1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2004-2009 Tiny SPRL (<http://tiny.be>).
6 # Copyright (C) 2010 OpenERP s.a. (<http://openerp.com>).
8 # This program is free software: you can redistribute it and/or modify
9 # it under the terms of the GNU Affero General Public License as
10 # published by the Free Software Foundation, either version 3 of the
11 # License, or (at your option) any later version.
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 Affero General Public License for more details.
18 # You should have received a copy of the GNU Affero General Public License
19 # along with this program. If not, see <http://www.gnu.org/licenses/>.
21 ##############################################################################
24 from os.path import join as opj
31 from tools.safe_eval import safe_eval as eval
42 from zipfile import PyZipFile, ZIP_DEFLATED
43 from cStringIO import StringIO
48 logger = netsvc.Logger()
50 _ad = os.path.abspath(opj(tools.config['root_path'], 'addons')) # default addons path (base)
51 ad_paths= map(lambda m: os.path.abspath(m.strip()),tools.config['addons_path'].split(','))
53 sys.path.insert(1, _ad)
58 sys.path.insert(ad_cnt, adp)
61 ad_paths.append(_ad) # for get_module_path
63 # Modules already loaded
68 def addNode(self, name, deps):
69 max_depth, father = 0, None
70 for n in [Node(x, self) for x in deps]:
71 if n.depth >= max_depth:
79 def update_from_db(self, cr):
82 # update the graph with values from the database (if exist)
83 ## First, we set the default values for each package in graph
84 additional_data = dict.fromkeys(self.keys(), {'id': 0, 'state': 'uninstalled', 'dbdemo': False, 'installed_version': None})
85 ## Then we get the values from the database
86 cr.execute('SELECT name, id, state, demo AS dbdemo, latest_version AS installed_version'
87 ' FROM ir_module_module'
88 ' WHERE name IN %s',(tuple(additional_data),)
91 ## and we update the default values with values from the database
92 additional_data.update(dict([(x.pop('name'), x) for x in cr.dictfetchall()]))
94 for package in self.values():
95 for k, v in additional_data[package.name].items():
96 setattr(package, k, v)
100 done = set(self.keys())
102 level_modules = [(name, module) for name, module in self.items() if module.depth==level]
103 for name, module in level_modules:
108 class Singleton(object):
109 def __new__(cls, name, graph):
113 inst = object.__new__(cls)
119 class Node(Singleton):
121 def __init__(self, name, graph):
123 if not hasattr(self, 'children'):
125 if not hasattr(self, 'depth'):
128 def addChild(self, name):
129 node = Node(name, self.graph)
130 node.depth = self.depth + 1
131 if node not in self.children:
132 self.children.append(node)
133 for attr in ('init', 'update', 'demo'):
134 if hasattr(self, attr):
135 setattr(node, attr, True)
136 self.children.sort(lambda x, y: cmp(x.name, y.name))
138 def __setattr__(self, name, value):
139 super(Singleton, self).__setattr__(name, value)
140 if name in ('init', 'update', 'demo'):
141 tools.config[name][self.name] = 1
142 for child in self.children:
143 setattr(child, name, value)
145 for child in self.children:
146 setattr(child, name, value + 1)
149 return itertools.chain(iter(self.children), *map(iter, self.children))
152 return self._pprint()
154 def _pprint(self, depth=0):
155 s = '%s\n' % self.name
156 for c in self.children:
157 s += '%s`-> %s' % (' ' * depth, c._pprint(depth+1))
161 def get_module_path(module, downloaded=False):
162 """Return the path of the given module."""
164 if os.path.exists(opj(adp, module)) or os.path.exists(opj(adp, '%s.zip' % module)):
165 return opj(adp, module)
168 return opj(_ad, module)
169 logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: module not found' % (module,))
173 def get_module_filetree(module, dir='.'):
174 path = get_module_path(module)
178 dir = os.path.normpath(dir)
181 if dir.startswith('..') or (dir and dir[0] == '/'):
182 raise Exception('Cannot access file outside the module')
184 if not os.path.isdir(path):
186 zip = zipfile.ZipFile(path + ".zip")
187 files = ['/'.join(f.split('/')[1:]) for f in zip.namelist()]
189 files = tools.osutil.listdir(path, True)
193 if not f.startswith(dir):
197 f = f[len(dir)+int(not dir.endswith('/')):]
198 lst = f.split(os.sep)
201 current = current.setdefault(lst.pop(0), {})
202 current[lst.pop(0)] = None
206 def get_module_as_zip_from_module_directory(module_directory, b64enc=True, src=True):
207 """Compress a module directory
209 @param module_directory: The module directory
210 @param base64enc: if True the function will encode the zip file with base64
211 @param src: Integrate the source files
213 @return: a stream to store in a file-like object
216 RE_exclude = re.compile('(?:^\..+\.swp$)|(?:\.py[oc]$)|(?:\.bak$)|(?:\.~.~$)', re.I)
218 def _zippy(archive, path, src=True):
219 path = os.path.abspath(path)
220 base = os.path.basename(path)
221 for f in tools.osutil.listdir(path, True):
222 bf = os.path.basename(f)
223 if not RE_exclude.search(bf) and (src or bf in ('__openerp__.py', '__terp__.py') or not bf.endswith('.py')):
224 archive.write(os.path.join(path, f), os.path.join(base, f))
226 archname = StringIO()
227 archive = PyZipFile(archname, "w", ZIP_DEFLATED)
228 archive.writepy(module_directory)
229 _zippy(archive, module_directory, src=src)
231 val = archname.getvalue()
235 val = base64.encodestring(val)
239 def get_module_as_zip(modulename, b64enc=True, src=True):
240 """Generate a module as zip file with the source or not and can do a base64 encoding
242 @param modulename: The module name
243 @param b64enc: if True the function will encode the zip file with base64
244 @param src: Integrate the source files
246 @return: a stream to store in a file-like object
249 ap = get_module_path(str(modulename))
251 raise Exception('Unable to find path for module %s' % modulename)
253 ap = ap.encode('utf8')
254 if os.path.isfile(ap + '.zip'):
255 val = file(ap + '.zip', 'rb').read()
257 val = base64.encodestring(val)
259 val = get_module_as_zip_from_module_directory(ap, b64enc, src)
264 def get_module_resource(module, *args):
265 """Return the full path of a resource of the given module.
267 @param module: the module
268 @param args: the resource path components
270 @return: absolute path to the resource
272 a = get_module_path(module)
273 return a and opj(a, *args) or False
277 """Returns the list of module names
281 name = os.path.basename(name)
282 if name[-4:] == '.zip':
286 def is_really_module(name):
287 name = opj(dir, name)
288 return os.path.isdir(name) or zipfile.is_zipfile(name)
289 return map(clean, filter(is_really_module, os.listdir(dir)))
293 plist.extend(listdir(ad))
294 return list(set(plist))
296 def load_information_from_description_file(module):
298 :param module: The name of the module (sale, purchase, ...)
300 for filename in ['__openerp__.py', '__terp__.py']:
301 description_file = get_module_resource(module, filename)
302 if os.path.isfile(description_file):
303 return eval(tools.file_open(description_file).read())
305 #TODO: refactor the logger in this file to follow the logging guidelines
307 logging.getLogger('addons').debug('The module %s does not contain a description file:'\
308 '__openerp__.py or __terp__.py (deprecated)', module)
311 def get_modules_with_version():
312 modules = get_modules()
314 for module in modules:
316 info = load_information_from_description_file(module)
317 res[module] = "%s.%s" % (release.major_version, info['version'])
322 def create_graph(cr, module_list, force=None):
324 upgrade_graph(graph, cr, module_list, force)
327 def upgrade_graph(graph, cr, module_list, force=None):
331 len_graph = len(graph)
332 for module in module_list:
333 mod_path = get_module_path(module)
334 terp_file = get_module_resource(module, '__openerp__.py')
335 if not terp_file or not os.path.isfile(terp_file):
336 terp_file = get_module_resource(module, '__terp__.py')
338 if not mod_path or not terp_file:
339 logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: not found, skipped' % (module))
342 if os.path.isfile(terp_file) or zipfile.is_zipfile(mod_path+'.zip'):
344 info = eval(tools.file_open(terp_file).read())
346 logger.notifyChannel('init', netsvc.LOG_ERROR, 'module %s: eval file %s' % (module, terp_file))
348 if info.get('installable', True):
349 packages.append((module, info.get('depends', []), info))
351 logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: not installable, skipped' % (module))
353 dependencies = dict([(p, deps) for p, deps, data in packages])
354 current, later = set([p for p, dep, data in packages]), set()
356 while packages and current > later:
357 package, deps, data = packages[0]
359 # if all dependencies of 'package' are already in the graph, add 'package' in the graph
360 if reduce(lambda x, y: x and y in graph, deps, True):
361 if not package in current:
365 current.remove(package)
366 graph.addNode(package, deps)
367 node = Node(package, graph)
369 for kind in ('init', 'demo', 'update'):
370 if package in tools.config[kind] or 'all' in tools.config[kind] or kind in force:
371 setattr(node, kind, True)
374 packages.append((package, deps, data))
377 graph.update_from_db(cr)
379 for package in later:
380 unmet_deps = filter(lambda p: p not in graph, dependencies[package])
381 logger.notifyChannel('init', netsvc.LOG_ERROR, 'module %s: Unmet dependencies: %s' % (package, ', '.join(unmet_deps)))
383 result = len(graph) - len_graph
384 if result != len(module_list):
385 logger.notifyChannel('init', netsvc.LOG_WARNING, 'Not all modules have loaded.')
389 def init_module_objects(cr, module_name, obj_list):
390 logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: creating or updating database tables' % module_name)
394 result = obj._auto_init(cr, {'module': module_name})
399 if hasattr(obj, 'init'):
408 def register_class(m):
410 Register module named m, if not already registered
414 mt = isinstance(e, zipimport.ZipImportError) and 'zip ' or ''
415 msg = "Couldn't load %smodule %s" % (mt, m)
416 logger.notifyChannel('init', netsvc.LOG_CRITICAL, msg)
417 logger.notifyChannel('init', netsvc.LOG_CRITICAL, e)
422 logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: registering objects' % m)
423 mod_path = get_module_path(m)
426 zip_mod_path = mod_path + '.zip'
427 if not os.path.isfile(zip_mod_path):
428 fm = imp.find_module(m, ad_paths)
430 imp.load_module(m, *fm)
435 zimp = zipimport.zipimporter(zip_mod_path)
444 class MigrationManager(object):
446 This class manage the migration of modules
447 Migrations files must be python files containing a "migrate(cr, installed_version)" function.
448 Theses files must respect a directory tree structure: A 'migrations' folder which containt a
449 folder by version. Version can be 'module' version or 'server.module' version (in this case,
450 the files will only be processed by this version of the server). Python file names must start
451 by 'pre' or 'post' and will be executed, respectively, before and after the module initialisation
457 | |-- pre-update_table_x.py
458 | |-- pre-update_table_y.py
459 | |-- post-clean-data.py
460 | `-- README.txt # not processed
461 |-- 5.0.1.1 # files in this folder will be executed only on a 5.0 server
462 | |-- pre-delete_table_z.py
463 | `-- post-clean-data.py
464 `-- foo.py # not processed
466 This similar structure is generated by the maintenance module with the migrations files get by
467 the maintenance contract
470 def __init__(self, cr, graph):
476 def _get_files(self):
479 import addons.base.maintenance.utils as maintenance_utils
480 maintenance_utils.update_migrations_files(self.cr)
483 for pkg in self.graph:
484 self.migrations[pkg.name] = {}
485 if not (hasattr(pkg, 'update') or pkg.state == 'to upgrade'):
488 self.migrations[pkg.name]['module'] = get_module_filetree(pkg.name, 'migrations') or {}
489 self.migrations[pkg.name]['maintenance'] = get_module_filetree('base', 'maintenance/migrations/' + pkg.name) or {}
491 def migrate_module(self, pkg, stage):
492 assert stage in ('pre', 'post')
493 stageformat = {'pre': '[>%s]',
497 if not (hasattr(pkg, 'update') or pkg.state == 'to upgrade'):
500 def convert_version(version):
501 if version.startswith(release.major_version) and version != release.major_version:
502 return version # the version number already containt the server version
503 return "%s.%s" % (release.major_version, version)
505 def _get_migration_versions(pkg):
507 return [d for d in tree if tree[d] is not None]
510 __get_dir(self.migrations[pkg.name]['module']) +
511 __get_dir(self.migrations[pkg.name]['maintenance'])
513 versions.sort(key=lambda k: parse_version(convert_version(k)))
516 def _get_migration_files(pkg, version, stage):
517 """ return a list of tuple (module, file)
519 m = self.migrations[pkg.name]
522 mapping = {'module': opj(pkg.name, 'migrations'),
523 'maintenance': opj('base', 'maintenance', 'migrations', pkg.name),
526 for x in mapping.keys():
528 for f in m[x][version]:
529 if m[x][version][f] is not None:
531 if not f.startswith(stage + '-'):
533 lst.append(opj(mapping[x], version, f))
542 from tools.parse_version import parse_version
544 parsed_installed_version = parse_version(pkg.installed_version or '')
545 current_version = parse_version(convert_version(pkg.data.get('version', '0')))
547 versions = _get_migration_versions(pkg)
549 for version in versions:
550 if parsed_installed_version < parse_version(convert_version(version)) <= current_version:
552 strfmt = {'addon': pkg.name,
554 'version': stageformat[stage] % version,
557 for pyfile in _get_migration_files(pkg, version, stage):
558 name, ext = os.path.splitext(os.path.basename(pyfile))
559 if ext.lower() != '.py':
561 mod = fp = fp2 = None
563 fp = tools.file_open(pyfile)
565 # imp.load_source need a real file object, so we create
566 # one from the file-like object we get from file_open
571 mod = imp.load_source(name, pyfile, fp2)
572 logger.notifyChannel('migration', netsvc.LOG_INFO, 'module %(addon)s: Running migration %(version)s %(name)s' % mergedict({'name': mod.__name__}, strfmt))
573 mod.migrate(self.cr, pkg.installed_version)
575 logger.notifyChannel('migration', netsvc.LOG_ERROR, 'module %(addon)s: Unable to load %(stage)s-migration file %(file)s' % mergedict({'file': pyfile}, strfmt))
577 except AttributeError:
578 logger.notifyChannel('migration', netsvc.LOG_ERROR, 'module %(addon)s: Each %(stage)s-migration file must have a "migrate(cr, installed_version)" function' % strfmt)
589 log = logging.getLogger('init')
591 def load_module_graph(cr, graph, status=None, perform_checks=True, **kwargs):
593 def process_sql_file(cr, fp):
594 queries = fp.read().split(';')
595 for query in queries:
596 new_query = ' '.join(query.split())
598 cr.execute(new_query)
600 def load_init_update_xml(cr, m, idref, mode, kind):
601 for filename in package.data.get('%s_xml' % kind, []):
602 logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: loading %s' % (m, filename))
603 _, ext = os.path.splitext(filename)
604 fp = tools.file_open(opj(m, filename))
606 noupdate = (kind == 'init')
607 tools.convert_csv_import(cr, m, os.path.basename(filename), fp.read(), idref, mode=mode, noupdate=noupdate)
609 process_sql_file(cr, fp)
611 tools.convert_yaml_import(cr, m, fp, idref, mode=mode, **kwargs)
613 tools.convert_xml_import(cr, m, fp, idref, mode=mode, **kwargs)
616 def load_demo_xml(cr, m, idref, mode):
617 for xml in package.data.get('demo_xml', []):
618 name, ext = os.path.splitext(xml)
619 logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: loading %s' % (m, xml))
620 fp = tools.file_open(opj(m, xml))
622 tools.convert_csv_import(cr, m, os.path.basename(xml), fp.read(), idref, mode=mode, noupdate=True)
624 tools.convert_yaml_import(cr, m, fp, idref, mode=mode, noupdate=True, **kwargs)
626 tools.convert_xml_import(cr, m, fp, idref, mode=mode, noupdate=True, **kwargs)
629 def load_data(cr, module_name, id_map, mode):
630 _load_data(cr, module_name, id_map, mode, 'data')
632 def load_demo(cr, module_name, id_map, mode):
633 _load_data(cr, module_name, id_map, mode, 'demo')
635 def load_test(cr, module_name, id_map, mode):
637 if not tools.config.options['test_disable']:
639 _load_data(cr, module_name, id_map, mode, 'test')
641 logger.notifyChannel('ERROR', netsvc.LOG_TEST, e)
644 if tools.config.options['test_commit']:
649 def _load_data(cr, module_name, id_map, mode, kind):
650 noupdate = (kind == 'demo')
651 for filename in package.data.get(kind, []):
652 _, ext = os.path.splitext(filename)
653 log.info("module %s: loading %s", module_name, filename)
654 pathname = os.path.join(module_name, filename)
655 file = tools.file_open(pathname)
656 # TODO manage .csv file with noupdate == (kind == 'init')
658 process_sql_file(cr, file)
660 noupdate = (kind == 'init')
661 tools.convert_csv_import(cr, module_name, pathname, file.read(), id_map, mode, noupdate)
663 tools.convert_yaml_import(cr, module_name, file, id_map, mode, noupdate)
665 tools.convert_xml_import(cr, module_name, file, id_map, mode, noupdate)
668 # **kwargs is passed directly to convert_xml_import
672 status = status.copy()
675 pool = pooler.get_pool(cr.dbname)
677 migrations = MigrationManager(cr, graph)
682 logger.notifyChannel('init', netsvc.LOG_DEBUG, 'loading %d packages..' % len(graph))
684 for package in graph:
685 logger.notifyChannel('init', netsvc.LOG_INFO, 'module %s: loading objects' % package.name)
686 migrations.migrate_module(package, 'pre')
687 register_class(package.name)
688 modules = pool.instanciate(package.name, cr)
689 if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'):
690 init_module_objects(cr, package.name, modules)
693 for package in graph:
694 status['progress'] = (float(statusi)+0.1) / len(graph)
699 modobj = pool.get('ir.module.module')
701 if modobj and perform_checks:
702 modobj.check(cr, 1, [mid])
705 status['progress'] = (float(statusi)+0.4) / len(graph)
708 if hasattr(package, 'init') or package.state == 'to install':
711 if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'):
713 for kind in ('init', 'update'):
714 if package.state=='to upgrade':
715 # upgrading the module information
716 modobj.write(cr, 1, [mid], modobj.get_values_from_terp(package.data))
717 load_init_update_xml(cr, m, idref, mode, kind)
718 load_data(cr, m, idref, mode)
719 if hasattr(package, 'demo') or (package.dbdemo and package.state != 'installed'):
720 status['progress'] = (float(statusi)+0.75) / len(graph)
721 load_demo_xml(cr, m, idref, mode)
722 load_demo(cr, m, idref, mode)
723 cr.execute('update ir_module_module set demo=%s where id=%s', (True, mid))
725 # launch tests only in demo mode, as most tests will depend
726 # on demo data. Other tests can be added into the regular
727 # 'data' section, but should probably not alter the data,
728 # as there is no rollback.
729 load_test(cr, m, idref, mode)
731 package_todo.append(package.name)
733 migrations.migrate_module(package, 'post')
736 ver = release.major_version + '.' + package.data.get('version', '1.0')
737 # Set new modules and dependencies
738 modobj.write(cr, 1, [mid], {'state': 'installed', 'latest_version': ver})
740 # Update translations for all installed languages
741 modobj.update_translations(cr, 1, [mid], None)
744 package.state = 'installed'
745 for kind in ('init', 'demo', 'update'):
746 if hasattr(package, kind):
747 delattr(package, kind)
751 cr.execute('select model from ir_model where state=%s', ('manual',))
752 for model in cr.dictfetchall():
753 pool.get('ir.model').instanciate(cr, 1, model['model'], {})
755 pool.get('ir.model.data')._process_end(cr, 1, package_todo)
760 def load_modules(db, force_demo=False, status=None, update_module=False):
762 def check_module_name(cr, mods, state):
764 id = modobj.search(cr, 1, ['&', ('state', '=', state), ('name', '=', mod)])
766 getattr(modobj, states[state])(cr, 1, id)
768 logger.notifyChannel('init', netsvc.LOG_WARNING, 'module %s: invalid module name!' % (mod))
774 cr.execute("SELECT relname FROM pg_class WHERE relkind='r' AND relname='ir_module_module'")
775 if len(cr.fetchall())==0:
776 logger.notifyChannel("init", netsvc.LOG_INFO, "init db")
778 tools.config["init"]["all"] = 1
779 tools.config['update']['all'] = 1
780 if not tools.config['without_demo']:
781 tools.config["demo"]['all'] = 1
785 pool = pooler.get_pool(cr.dbname)
787 report = tools.assertion_report()
788 # NOTE: Try to also load the modules that have been marked as uninstallable previously...
789 STATES_TO_LOAD = ['installed', 'to upgrade', 'uninstallable']
790 graph = create_graph(cr, ['base'], force)
792 logger.notifyChannel('init', netsvc.LOG_CRITICAL, 'module base cannot be loaded! (hint: verify addons-path)')
793 raise osv.osv.except_osv('Could not load base module', 'module base cannot be loaded! (hint: verify addons-path)')
794 has_updates = load_module_graph(cr, graph, status, perform_checks=(not update_module), report=report)
797 modobj = pool.get('ir.module.module')
798 states = {'installed': 'button_upgrade', 'uninstalled': 'button_install'}
799 logger.notifyChannel('init', netsvc.LOG_INFO, 'updating modules list')
800 if ('base' in tools.config['init']) or ('base' in tools.config['update']):
801 modobj.update_list(cr, 1)
803 mods = [k for k in tools.config['init'] if tools.config['init'][k]]
804 check_module_name(cr, mods, 'uninstalled')
806 mods = [k for k in tools.config['update'] if tools.config['update'][k]]
807 check_module_name(cr, mods, 'installed')
809 cr.execute("update ir_module_module set state=%s where name=%s", ('installed', 'base'))
811 STATES_TO_LOAD += ['to install']
816 if loop_guardrail > 100:
817 raise ValueError('Possible recursive module tree detected, aborting.')
818 cr.execute("SELECT name from ir_module_module WHERE state IN %s" ,(tuple(STATES_TO_LOAD),))
820 module_list = [name for (name,) in cr.fetchall() if name not in graph]
824 new_modules_in_graph = upgrade_graph(graph, cr, module_list, force)
825 if new_modules_in_graph == 0:
829 logger.notifyChannel('init', netsvc.LOG_DEBUG, 'Updating graph with %d more modules' % (len(module_list)))
830 r = load_module_graph(cr, graph, status, report=report)
831 has_updates = has_updates or r
834 cr.execute("""select model,name from ir_model where id NOT IN (select distinct model_id from ir_model_access)""")
835 for (model, name) in cr.fetchall():
836 model_obj = pool.get(model)
837 if not isinstance(model_obj, osv.osv.osv_memory):
838 logger.notifyChannel('init', netsvc.LOG_WARNING, 'object %s (%s) has no access rules!' % (model, name))
840 # Temporary warning while we remove access rights on osv_memory objects, as they have
841 # been replaced by owner-only access rights
842 cr.execute("""select distinct mod.model, mod.name from ir_model_access acc, ir_model mod where acc.model_id = mod.id""")
843 for (model, name) in cr.fetchall():
844 model_obj = pool.get(model)
845 if isinstance(model_obj, osv.osv.osv_memory):
846 logger.notifyChannel('init', netsvc.LOG_WARNING, 'In-memory object %s (%s) should not have explicit access rules!' % (model, name))
848 cr.execute("SELECT model from ir_model")
849 for (model,) in cr.fetchall():
850 obj = pool.get(model)
852 obj._check_removed_columns(cr, log=True)
854 if report.get_report():
855 logger.notifyChannel('init', netsvc.LOG_INFO, report)
857 for kind in ('init', 'demo', 'update'):
858 tools.config[kind] = {}
862 cr.execute("select id,name from ir_module_module where state=%s", ('to remove',))
863 for mod_id, mod_name in cr.fetchall():
864 cr.execute('select model,res_id from ir_model_data where noupdate=%s and module=%s order by id desc', (False, mod_name,))
865 for rmod, rid in cr.fetchall():
867 rmod_module= pool.get(rmod)
869 rmod_module.unlink(cr, uid, [rid])
871 logger.notifyChannel('init', netsvc.LOG_ERROR, 'Could not locate %s to remove res=%d' % (rmod,rid))
872 cr.execute('delete from ir_model_data where noupdate=%s and module=%s', (False, mod_name,))
875 # TODO: remove menu without actions of children
878 cr.execute('''delete from
881 (id not IN (select parent_id from ir_ui_menu where parent_id is not null))
883 (id not IN (select res_id from ir_values where model='ir.ui.menu'))
885 (id not IN (select res_id from ir_model_data where model='ir.ui.menu'))''')
890 logger.notifyChannel('init', netsvc.LOG_INFO, 'removed %d unused menus' % (cr.rowcount,))
892 cr.execute("update ir_module_module set state=%s where state=%s", ('uninstalled', 'to remove',))
898 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: