[IMP] Update translations
[odoo/odoo.git] / bin / addons / __init__.py
1 # -*- encoding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>). All Rights Reserved
6 #    $Id$
7 #
8 #    This program is free software: you can redistribute it and/or modify
9 #    it under the terms of the GNU General Public License as published by
10 #    the Free Software Foundation, either version 3 of the License, or
11 #    (at your option) any later version.
12 #
13 #    This program is distributed in the hope that it will be useful,
14 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
15 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16 #    GNU General Public License for more details.
17 #
18 #    You should have received a copy of the GNU General Public License
19 #    along with this program.  If not, see <http://www.gnu.org/licenses/>.
20 #
21 ##############################################################################
22
23 import os, sys, imp
24 from os.path import join as opj
25 import itertools
26 import zipimport
27
28 import osv
29 import tools
30 import tools.osutil
31 import pooler
32
33
34 import netsvc
35 from osv import fields
36
37 import zipfile
38 import release
39
40 import re
41 import base64
42 from zipfile import PyZipFile, ZIP_DEFLATED
43 from cStringIO import StringIO
44
45
46 logger = netsvc.Logger()
47
48 _ad = os.path.abspath(opj(tools.config['root_path'], 'addons'))     # default addons path (base)
49 ad = os.path.abspath(tools.config['addons_path'])           # alternate addons path
50
51 sys.path.insert(1, _ad)
52 if ad != _ad:
53     sys.path.insert(1, ad)
54
55 # Modules already loaded
56 loaded = []
57
58
59 class Graph(dict):
60
61     def addNode(self, name, deps):
62         max_depth, father = 0, None
63         for n in [Node(x, self) for x in deps]:
64             if n.depth >= max_depth:
65                 father = n
66                 max_depth = n.depth
67         if father:
68             father.addChild(name)
69         else:
70             Node(name, self)
71     
72     def update_from_db(self, cr):
73         # update the graph with values from the database (if exist)
74         ## First, we set the default values for each package in graph
75         additional_data = dict.fromkeys(self.keys(), {'id': 0, 'state': 'uninstalled', 'dbdemo': False, 'installed_version': None})
76         ## Then we get the values from the database
77         cr.execute('SELECT name, id, state, demo AS dbdemo, latest_version AS installed_version'
78                    '  FROM ir_module_module'
79                    ' WHERE name in (%s)' % (','.join(['%s'] * len(self))),
80                     additional_data.keys()
81                    )
82
83         ## and we update the default values with values from the database
84         additional_data.update(dict([(x.pop('name'), x) for x in cr.dictfetchall()]))
85
86         for package in self.values():
87             for k, v in additional_data[package.name].items():
88                 setattr(package, k, v)
89
90
91
92     def __iter__(self):
93         level = 0
94         done = set(self.keys())
95         while done:
96             level_modules = [(name, module) for name, module in self.items() if module.depth==level]
97             for name, module in level_modules:
98                 done.remove(name)
99                 yield module
100             level += 1
101
102 class Singleton(object):
103     def __new__(cls, name, graph):
104         if name in graph:
105             inst = graph[name]
106         else:
107             inst = object.__new__(cls)
108             inst.name = name
109             graph[name] = inst
110         return inst
111
112
113 class Node(Singleton):
114
115     def __init__(self, name, graph):
116         self.graph = graph
117         if not hasattr(self, 'children'):
118             self.children = []
119         if not hasattr(self, 'depth'):
120             self.depth = 0
121
122     def addChild(self, name):
123         node = Node(name, self.graph)
124         node.depth = self.depth + 1
125         if node not in self.children:
126             self.children.append(node)
127         for attr in ('init', 'update', 'demo'):
128             if hasattr(self, attr):
129                 setattr(node, attr, True)
130         self.children.sort(lambda x, y: cmp(x.name, y.name))
131
132     def __setattr__(self, name, value):
133         super(Singleton, self).__setattr__(name, value)
134         if name in ('init', 'update', 'demo'):
135             tools.config[name][self.name] = 1
136             for child in self.children:
137                 setattr(child, name, value)
138         if name == 'depth':
139             for child in self.children:
140                 setattr(child, name, value + 1)
141
142     def __iter__(self):
143         return itertools.chain(iter(self.children), *map(iter, self.children))
144     
145     def __str__(self):
146         return self._pprint()
147
148     def _pprint(self, depth=0):
149         s = '%s\n' % self.name
150         for c in self.children:
151             s += '%s`-> %s' % ('   ' * depth, c._pprint(depth+1))
152         return s
153
154
155 def get_module_path(module, downloaded=False):
156     """Return the path of the given module."""
157     if os.path.exists(opj(ad, module)) or os.path.exists(opj(ad, '%s.zip' % module)):
158         return opj(ad, module)
159
160     if os.path.exists(opj(_ad, module)) or os.path.exists(opj(_ad, '%s.zip' % module)):
161         return opj(_ad, module)
162     if downloaded:
163         return opj(_ad, module)
164     logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: module not found' % (module,))
165     return False
166
167
168 def get_module_filetree(module, dir='.'):
169     path = get_module_path(module)
170     if not path:
171         return False
172
173     dir = os.path.normpath(dir)
174     if dir == '.':
175         dir = ''
176     if dir.startswith('..') or (dir and dir[0] == '/'):
177         raise Exception('Cannot access file outside the module')
178
179     if not os.path.isdir(path):
180         # zipmodule
181         zip = zipfile.ZipFile(path + ".zip")
182         files = ['/'.join(f.split('/')[1:]) for f in zip.namelist()]
183     else:
184         files = tools.osutil.listdir(path, True)
185
186     tree = {}
187     for f in files:
188         if not f.startswith(dir):
189             continue
190
191         if dir:
192             f = f[len(dir)+int(not dir.endswith('/')):]
193         lst = f.split(os.sep)
194         current = tree
195         while len(lst) != 1:
196             current = current.setdefault(lst.pop(0), {})
197         current[lst.pop(0)] = None
198
199     return tree
200
201 def get_module_as_zip_from_module_directory(module_directory, b64enc=True, src=True):
202     """Compress a module directory
203
204     @param module_directory: The module directory
205     @param base64enc: if True the function will encode the zip file with base64
206     @param src: Integrate the source files
207
208     @return: a stream to store in a file-like object
209     """
210
211     RE_exclude = re.compile('(?:^\..+\.swp$)|(?:\.py[oc]$)|(?:\.bak$)|(?:\.~.~$)', re.I)
212
213     def _zippy(archive, path, src=True):
214         path = os.path.abspath(path)
215         base = os.path.basename(path)
216         for f in tools.osutil.listdir(path, True):
217             bf = os.path.basename(f)
218             if not RE_exclude.search(bf) and (src or bf == '__terp__.py' or not bf.endswith('.py')):
219                 archive.write(os.path.join(path, f), os.path.join(base, f))
220
221     archname = StringIO()
222     archive = PyZipFile(archname, "w", ZIP_DEFLATED)
223     archive.writepy(module_directory)
224     _zippy(archive, module_directory, src=src)
225     archive.close()
226     val = archname.getvalue()
227     archname.close()
228
229     if b64enc:
230         val = base64.encodestring(val)
231
232     return val
233
234 def get_module_as_zip(modulename, b64enc=True, src=True):
235     """Generate a module as zip file with the source or not and can do a base64 encoding
236
237     @param modulename: The module name
238     @param b64enc: if True the function will encode the zip file with base64
239     @param src: Integrate the source files
240
241     @return: a stream to store in a file-like object
242     """
243
244     ap = get_module_path(str(modulename))
245     if not ap:
246         raise Exception('Unable to find path for module %s' % modulename)
247
248     ap = ap.encode('utf8')
249     if os.path.isfile(ap + '.zip'):
250         val = file(ap + '.zip', 'rb').read()
251         if b64enc:
252             val = base64.encodestring(val)
253     else:
254         val = get_module_as_zip_from_module_directory(ap, b64enc, src)
255
256     return val
257
258
259 def get_module_resource(module, *args):
260     """Return the full path of a resource of the given module.
261
262     @param module: the module
263     @param args: the resource path components
264
265     @return: absolute path to the resource
266     """
267     a = get_module_path(module)
268     return a and opj(a, *args) or False
269
270
271 def get_modules():
272     """Returns the list of module names
273     """
274     def listdir(dir):
275         def clean(name):
276             name = os.path.basename(name)
277             if name[-4:] == '.zip':
278                 name = name[:-4]
279             return name
280
281         def is_really_module(name):
282             name = opj(dir, name)
283             return os.path.isdir(name) or zipfile.is_zipfile(name)
284         return map(clean, filter(is_really_module, os.listdir(dir)))
285
286     return list(set(listdir(ad) + listdir(_ad)))
287
288 def get_modules_with_version():
289     modules = get_modules()
290     res = {}
291     for module in modules:
292         terp = get_module_resource(module, '__terp__.py')
293         try:
294             info = eval(tools.file_open(terp).read())
295             res[module] = "%s.%s" % (release.major_version, info['version'])
296         except Exception, e:
297             continue
298     return res
299
300 def create_graph(cr, module_list, force=None):
301     graph = Graph()
302     upgrade_graph(graph, cr, module_list, force)
303     return graph
304
305 def upgrade_graph(graph, cr, module_list, force=None):
306     if force is None:
307         force = []
308     packages = []
309     len_graph = len(graph)
310     for module in module_list:
311         mod_path = get_module_path(module)
312         terp_file = get_module_resource(module, '__terp__.py')
313         if not mod_path or not terp_file:
314             cr.execute("update ir_module_module set state=%s where name=%s", ('uninstallable', module))
315             continue
316
317         if os.path.isfile(terp_file) or zipfile.is_zipfile(mod_path+'.zip'):
318             try:
319                 info = eval(tools.file_open(terp_file).read())
320             except:
321                 logger.notifyChannel('init', netsvc.LOG_ERROR, 'module %s: eval file %s' % (module, terp_file))
322                 raise
323             if info.get('installable', True):
324                 packages.append((module, info.get('depends', []), info))
325
326     dependencies = dict([(p, deps) for p, deps, data in packages])
327     current, later = set([p for p, dep, data in packages]), set()
328     while packages and current > later:
329         package, deps, data = packages[0]
330
331         # if all dependencies of 'package' are already in the graph, add 'package' in the graph
332         if reduce(lambda x, y: x and y in graph, deps, True):
333             if not package in current:
334                 packages.pop(0)
335                 continue
336             later.clear()
337             current.remove(package)
338             graph.addNode(package, deps)
339             node = Node(package, graph)
340             node.data = data
341             for kind in ('init', 'demo', 'update'):
342                 if package in tools.config[kind] or 'all' in tools.config[kind] or kind in force:
343                     setattr(node, kind, True)
344         else:
345             later.add(package)
346             packages.append((package, deps, data))
347         packages.pop(0)
348
349     graph.update_from_db(cr)
350
351     for package in later:
352         unmet_deps = filter(lambda p: p not in graph, dependencies[package])
353         logger.notifyChannel('init', netsvc.LOG_ERROR, 'module %s: Unmet dependencies: %s' % (package, ', '.join(unmet_deps)))
354     
355     result = len(graph) - len_graph
356     if result != len(module_list):
357         logger.notifyChannel('init', netsvc.LOG_WARNING, 'Not all modules have loaded.')
358     return result
359
360
361 def init_module_objects(cr, module_name, obj_list):
362     logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: creating or updating database tables' % module_name)
363     todo = []
364     for obj in obj_list:
365         result = obj._auto_init(cr, {'module': module_name})
366         if result:
367             todo += result
368         if hasattr(obj, 'init'):
369             obj.init(cr)
370         cr.commit()
371     todo.sort()
372     for t in todo:
373         t[1](cr, *t[2])
374     cr.commit()
375
376
377 def register_class(m):
378     """
379     Register module named m, if not already registered
380     """
381
382     def log(e):
383         mt = isinstance(e, zipimport.ZipImportError) and 'zip ' or ''
384         msg = "Couldn't load %smodule %s" % (mt, m)
385         logger.notifyChannel('init', netsvc.LOG_CRITICAL, msg)
386         logger.notifyChannel('init', netsvc.LOG_CRITICAL, e)
387
388     global loaded
389     if m in loaded:
390         return
391     logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: registering objects' % m)
392     mod_path = get_module_path(m)
393
394     try:
395         zip_mod_path = mod_path + '.zip'
396         if not os.path.isfile(zip_mod_path):
397             fm = imp.find_module(m, [ad, _ad])
398             try:
399                 imp.load_module(m, *fm)
400             finally:
401                 if fm[0]:
402                     fm[0].close()
403         else:
404             zimp = zipimport.zipimporter(zip_mod_path)
405             zimp.load_module(m)
406     except Exception, e:
407         log(e)
408         raise
409     else:
410         loaded.append(m)
411
412
413 class MigrationManager(object):
414     """
415         This class manage the migration of modules
416         Migrations files must be python files containing a "migrate(cr, installed_version)" function.
417         Theses files must respect a directory tree structure: A 'migrations' folder which containt a
418         folder by version. Version can be 'module' version or 'server.module' version (in this case,
419         the files will only be processed by this version of the server). Python file names must start
420         by 'pre' or 'post' and will be executed, respectively, before and after the module initialisation
421         Example:
422
423             <moduledir>
424             `-- migrations
425                 |-- 1.0
426                 |   |-- pre-update_table_x.py
427                 |   |-- pre-update_table_y.py
428                 |   |-- post-clean-data.py
429                 |   `-- README.txt              # not processed
430                 |-- 5.0.1.1                     # files in this folder will be executed only on a 5.0 server
431                 |   |-- pre-delete_table_z.py
432                 |   `-- post-clean-data.py
433                 `-- foo.py                      # not processed
434
435         This similar structure is generated by the maintenance module with the migrations files get by
436         the maintenance contract
437
438     """
439     def __init__(self, cr, graph):
440         self.cr = cr
441         self.graph = graph
442         self.migrations = {}
443         self._get_files()
444
445     def _get_files(self):
446
447         """
448         import addons.base.maintenance.utils as maintenance_utils
449         maintenance_utils.update_migrations_files(self.cr)
450         #"""
451
452         for pkg in self.graph:
453             self.migrations[pkg.name] = {}
454             if not (hasattr(pkg, 'update') or pkg.state == 'to upgrade'):
455                 continue
456
457             self.migrations[pkg.name]['module'] = get_module_filetree(pkg.name, 'migrations') or {}
458             self.migrations[pkg.name]['maintenance'] = get_module_filetree('base', 'maintenance/migrations/' + pkg.name) or {}
459
460     def migrate_module(self, pkg, stage):
461         assert stage in ('pre', 'post')
462         stageformat = {'pre': '[>%s]',
463                        'post': '[%s>]',
464                       }
465
466         if not (hasattr(pkg, 'update') or pkg.state == 'to upgrade'):
467             return
468
469         def convert_version(version):
470             if version.startswith(release.major_version) and version != release.major_version:
471                 return version  # the version number already containt the server version
472             return "%s.%s" % (release.major_version, version)
473
474         def _get_migration_versions(pkg):
475             def __get_dir(tree):
476                 return [d for d in tree if tree[d] is not None]
477
478             versions = list(set(
479                 __get_dir(self.migrations[pkg.name]['module']) +
480                 __get_dir(self.migrations[pkg.name]['maintenance'])
481             ))
482             versions.sort(key=lambda k: parse_version(convert_version(k)))
483             return versions
484
485         def _get_migration_files(pkg, version, stage):
486             """ return a list of tuple (module, file)
487             """
488             m = self.migrations[pkg.name]
489             lst = []
490
491             mapping = {'module': opj(pkg.name, 'migrations'),
492                        'maintenance': opj('base', 'maintenance', 'migrations', pkg.name),
493                       }
494
495             for x in mapping.keys():
496                 if version in m[x]:
497                     for f in m[x][version]:
498                         if m[x][version][f] is not None:
499                             continue
500                         if not f.startswith(stage + '-'):
501                             continue
502                         lst.append(opj(mapping[x], version, f))
503             lst.sort()
504             return lst
505
506         def mergedict(a, b):
507             a = a.copy()
508             a.update(b)
509             return a
510
511         from tools.parse_version import parse_version
512
513         parsed_installed_version = parse_version(pkg.installed_version or '')
514         current_version = parse_version(convert_version(pkg.data.get('version', '0')))
515         
516         versions = _get_migration_versions(pkg)
517
518         for version in versions:
519             if parsed_installed_version < parse_version(convert_version(version)) <= current_version:
520
521                 strfmt = {'addon': pkg.name,
522                           'stage': stage,
523                           'version': stageformat[stage] % version,
524                           }
525
526                 for pyfile in _get_migration_files(pkg, version, stage):
527                     name, ext = os.path.splitext(os.path.basename(pyfile))
528                     if ext.lower() != '.py':
529                         continue
530                     mod = fp = fp2 = None
531                     try:
532                         fp = tools.file_open(pyfile)
533
534                         # imp.load_source need a real file object, so we create
535                         # one from the file-like object we get from file_open
536                         fp2 = os.tmpfile()
537                         fp2.write(fp.read())
538                         fp2.seek(0)
539                         try:
540                             mod = imp.load_source(name, pyfile, fp2)
541                             logger.notifyChannel('migration', netsvc.LOG_INFO, 'module %(addon)s: Running migration %(version)s %(name)s' % mergedict({'name': mod.__name__}, strfmt))
542                             mod.migrate(self.cr, pkg.installed_version)
543                         except ImportError:
544                             logger.notifyChannel('migration', netsvc.LOG_ERROR, 'module %(addon)s: Unable to load %(stage)s-migration file %(file)s' % mergedict({'file': pyfile}, strfmt))
545                             raise
546                         except AttributeError:
547                             logger.notifyChannel('migration', netsvc.LOG_ERROR, 'module %(addon)s: Each %(stage)s-migration file must have a "migrate(cr, installed_version)" function' % strfmt)
548                         except:
549                             raise
550                     finally:
551                         if fp:
552                             fp.close()
553                         if fp2:
554                             fp2.close()
555                         if mod:
556                             del mod
557
558
559 def load_module_graph(cr, graph, status=None, perform_checks=True, **kwargs):
560     # **kwargs is passed directly to convert_xml_import
561     if not status:
562         status = {}
563
564     status = status.copy()
565     package_todo = []
566     statusi = 0
567     pool = pooler.get_pool(cr.dbname)
568
569     migrations = MigrationManager(cr, graph)
570
571     has_updates = False
572     modobj = None
573
574     for package in graph:
575         logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: loading objects' % package.name)
576         migrations.migrate_module(package, 'pre')
577         register_class(package.name)
578         modules = pool.instanciate(package.name, cr)
579         if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'):
580             init_module_objects(cr, package.name, modules)
581         cr.commit()
582
583     for package in graph:
584         status['progress'] = (float(statusi)+0.1) / len(graph)
585         m = package.name
586         mid = package.id
587
588
589         if modobj is None:
590             modobj = pool.get('ir.module.module')
591
592         if modobj and perform_checks:
593             modobj.check(cr, 1, [mid])
594
595         idref = {}
596         status['progress'] = (float(statusi)+0.4) / len(graph)
597         
598         mode = 'update'
599         if hasattr(package, 'init') or package.state == 'to install':
600             mode = 'init'
601
602         if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'):
603             has_updates = True
604             for kind in ('init', 'update'):
605                 for filename in package.data.get('%s_xml' % kind, []):
606                     logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: loading %s' % (m, filename))
607                     name, ext = os.path.splitext(filename)
608                     fp = tools.file_open(opj(m, filename))
609                     if ext == '.csv':
610                         tools.convert_csv_import(cr, m, os.path.basename(filename), fp.read(), idref, mode=mode)
611                     elif ext == '.sql':
612                         queries = fp.read().split(';')
613                         for query in queries:
614                             new_query = ' '.join(query.split())
615                             if new_query:
616                                 cr.execute(new_query)
617                     else:
618                         tools.convert_xml_import(cr, m, fp, idref, mode=mode, **kwargs)
619                     fp.close()
620             if hasattr(package, 'demo') or (package.dbdemo and package.state != 'installed'):
621                 status['progress'] = (float(statusi)+0.75) / len(graph)
622                 for xml in package.data.get('demo_xml', []):
623                     name, ext = os.path.splitext(xml)
624                     logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: loading %s' % (m, xml))
625                     fp = tools.file_open(opj(m, xml))
626                     if ext == '.csv':
627                         tools.convert_csv_import(cr, m, os.path.basename(xml), fp.read(), idref, mode=mode, noupdate=True)
628                     else:
629                         tools.convert_xml_import(cr, m, fp, idref, mode=mode, noupdate=True, **kwargs)
630                     fp.close()
631                 cr.execute('update ir_module_module set demo=%s where id=%s', (True, mid))
632             package_todo.append(package.name)
633
634             migrations.migrate_module(package, 'post')
635             
636             if modobj:
637                 ver = release.major_version + '.' + package.data.get('version', '1.0')
638                 # Set new modules and dependencies
639                 modobj.write(cr, 1, [mid], {'state': 'installed', 'latest_version': ver})
640                 cr.commit()
641                 # Update translations for all installed languages
642                 modobj.update_translations(cr, 1, [mid], None)
643                 cr.commit()
644             
645             package.state = 'installed'
646             for kind in ('init', 'demo', 'update'):
647                 if hasattr(package, kind):
648                     delattr(package, kind)
649
650         statusi += 1
651     
652     cr.execute('select model from ir_model where state=%s', ('manual',))
653     for model in cr.dictfetchall():
654         pool.get('ir.model').instanciate(cr, 1, model['model'], {})
655
656     pool.get('ir.model.data')._process_end(cr, 1, package_todo)
657     cr.commit()
658     
659     return has_updates
660
661 def load_modules(db, force_demo=False, status=None, update_module=False):
662     if not status:
663         status = {}
664
665     cr = db.cursor()
666     force = []
667     if force_demo:
668         force.append('demo')
669     pool = pooler.get_pool(cr.dbname)
670     try:
671         report = tools.assertion_report()
672         STATES_TO_LOAD = ['installed', 'to upgrade']
673         graph = create_graph(cr, ['base'], force)
674         has_updates = False
675         if update_module:
676             has_updates = load_module_graph(cr, graph, status, perform_checks=False, report=report)
677
678             modobj = pool.get('ir.module.module')
679             logger.notifyChannel('init', netsvc.LOG_INFO, 'updating modules list')
680             if ('base' in tools.config['init']) or ('base' in tools.config['update']):
681                 modobj.update_list(cr, 1)
682
683             mods = [k for k in tools.config['init'] if tools.config['init'][k]]
684             if mods:
685                 ids = modobj.search(cr, 1, ['&', ('state', '=', 'uninstalled'), ('name', 'in', mods)])
686                 if ids:
687                     modobj.button_install(cr, 1, ids)
688
689             mods = [k for k in tools.config['update'] if tools.config['update'][k]]
690             if mods:
691                 ids = modobj.search(cr, 1, ['&', ('state', '=', 'installed'), ('name', 'in', mods)])
692                 if ids:
693                     modobj.button_upgrade(cr, 1, ids)
694
695             cr.execute("update ir_module_module set state=%s where name=%s", ('installed', 'base'))
696             
697             STATES_TO_LOAD += ['to install']
698         
699         loop_guardrail = 0
700         while True:
701             loop_guardrail += 1
702             if loop_guardrail > 100:
703                 raise ProgrammingError()
704             cr.execute("SELECT name from ir_module_module WHERE state in (%s)" % ','.join(['%s']*len(STATES_TO_LOAD)), STATES_TO_LOAD)
705
706             module_list = [name for (name,) in cr.fetchall() if name not in graph]
707             if not module_list:
708                 break
709
710             new_modules_in_graph = upgrade_graph(graph, cr, module_list, force)
711             if new_modules_in_graph == 0:
712                 # nothing to load
713                 break
714             
715             logger.notifyChannel('init', netsvc.LOG_DEBUG, 'Updating graph with %d more modules' % (len(module_list)))
716             r = load_module_graph(cr, graph, status, report=report)
717             has_updates = has_updates or r
718
719         if has_updates:
720             cr.execute("""select model,name from ir_model where id not in (select model_id from ir_model_access)""")
721             for (model, name) in cr.fetchall():
722                 logger.notifyChannel('init', netsvc.LOG_WARNING, 'object %s (%s) has no access rules!' % (model, name))
723
724             cr.execute("SELECT model from ir_model")
725             for (model,) in cr.fetchall():
726                 obj = pool.get(model)
727                 if obj:
728                     obj._check_removed_columns(cr, log=True)
729
730         if report.get_report():
731             logger.notifyChannel('init', netsvc.LOG_INFO, report)
732
733         for kind in ('init', 'demo', 'update'):
734             tools.config[kind] = {}
735
736         cr.commit()
737         if update_module:
738             cr.execute("select id,name from ir_module_module where state=%s", ('to remove',))
739             for mod_id, mod_name in cr.fetchall():
740                 cr.execute('select model,res_id from ir_model_data where noupdate=%s and module=%s order by id desc', (False, mod_name,))
741                 for rmod, rid in cr.fetchall():
742                     uid = 1
743                     pool.get(rmod).unlink(cr, uid, [rid])
744                 cr.execute('delete from ir_model_data where noupdate=%s and module=%s', (False, mod_name,))
745                 cr.commit()
746             #
747             # TODO: remove menu without actions of children
748             #
749             while True:
750                 cr.execute('''delete from
751                         ir_ui_menu
752                     where
753                         (id not in (select parent_id from ir_ui_menu where parent_id is not null))
754                     and
755                         (id not in (select res_id from ir_values where model='ir.ui.menu'))
756                     and
757                         (id not in (select res_id from ir_model_data where model='ir.ui.menu'))''')
758                 cr.commit()
759                 if not cr.rowcount:
760                     break
761                 else:
762                     logger.notifyChannel('init', netsvc.LOG_INFO, 'removed %d unused menus' % (cr.rowcount,))
763
764             cr.execute("update ir_module_module set state=%s where state=%s", ('uninstalled', 'to remove',))
765             cr.commit()
766     finally:
767         cr.close()
768
769
770 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: