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-2013 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 ##############################################################################
23 """ Modules (also called addons) management.
34 import openerp.modules.db
35 import openerp.modules.graph
36 import openerp.modules.migration
37 import openerp.modules.registry
38 import openerp.osv as osv
39 import openerp.pooler as pooler
40 import openerp.tools as tools
41 from openerp import SUPERUSER_ID
43 from openerp.tools.translate import _
44 from openerp.modules.module import initialize_sys_path, \
45 load_openerp_module, init_module_models, adapt_version
47 _logger = logging.getLogger(__name__)
49 def open_openerp_namespace():
50 # See comment for open_openerp_namespace.
51 if openerp.conf.deprecation.open_openerp_namespace:
52 for k, v in list(sys.modules.items()):
53 if k.startswith('openerp.') and sys.modules.get(k[8:]) is None:
54 sys.modules[k[8:]] = v
57 def load_module_graph(cr, graph, status=None, perform_checks=True, skip_modules=None, report=None):
58 """Migrates+Updates or Installs all module nodes from ``graph``
59 :param graph: graph of module nodes to load
60 :param status: status dictionary for keeping track of progress
61 :param perform_checks: whether module descriptors should be checked for validity (prints warnings
63 :param skip_modules: optional list of module names (packages) which have previously been loaded and can be skipped
64 :return: list of modules that were installed or updated
66 def process_sql_file(cr, fp):
67 queries = fp.read().split(';')
69 new_query = ' '.join(query.split())
73 load_init_xml = lambda *args: _load_data(cr, *args, kind='init_xml')
74 load_update_xml = lambda *args: _load_data(cr, *args, kind='update_xml')
75 load_demo_xml = lambda *args: _load_data(cr, *args, kind='demo_xml')
76 load_data = lambda *args: _load_data(cr, *args, kind='data')
77 load_demo = lambda *args: _load_data(cr, *args, kind='demo')
79 def load_test(module_name, idref, mode):
82 threading.currentThread().testing = True
83 _load_data(cr, module_name, idref, mode, 'test')
87 'module %s: an exception occurred in a test', module_name)
90 threading.currentThread().testing = False
91 if tools.config.options['test_commit']:
96 def _load_data(cr, module_name, idref, mode, kind):
99 kind: data, demo, test, init_xml, update_xml, demo_xml.
101 noupdate is False, unless it is demo data or it is csv data in
105 for filename in package.data[kind]:
106 _logger.info("module %s: loading %s", module_name, filename)
107 _, ext = os.path.splitext(filename)
108 pathname = os.path.join(module_name, filename)
109 fp = tools.file_open(pathname)
111 if kind in ('demo', 'demo_xml'):
116 if kind in ('init', 'init_xml'):
118 tools.convert_csv_import(cr, module_name, pathname, fp.read(), idref, mode, noupdate)
120 process_sql_file(cr, fp)
122 tools.convert_yaml_import(cr, module_name, fp, kind, idref, mode, noupdate, report)
124 tools.convert_xml_import(cr, module_name, fp, idref, mode, noupdate, report)
126 pass # .js files are valid but ignored here.
128 _logger.warning("Can't load unknown file type %s.", filename)
135 processed_modules = []
137 pool = pooler.get_pool(cr.dbname)
138 migrations = openerp.modules.migration.MigrationManager(cr, graph)
139 _logger.info('loading %d modules...', len(graph))
141 # Query manual fields for all models at once and save them on the registry
142 # so the initialization code for each model does not have to do it
143 # one model at a time.
144 pool.fields_by_model = {}
145 cr.execute('SELECT * FROM ir_model_fields WHERE state=%s', ('manual',))
146 for field in cr.dictfetchall():
147 pool.fields_by_model.setdefault(field['model'], []).append(field)
149 # register, instantiate and initialize models for each modules
150 for index, package in enumerate(graph):
151 module_name = package.name
152 module_id = package.id
154 if skip_modules and module_name in skip_modules:
157 _logger.debug('module %s: loading objects', package.name)
158 migrations.migrate_module(package, 'pre')
159 load_openerp_module(package.name)
161 models = pool.load(cr, package)
163 loaded_modules.append(package.name)
164 if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'):
165 init_module_models(cr, package.name, models)
166 pool._init_modules.add(package.name)
167 status['progress'] = float(index) / len(graph)
169 # Can't put this line out of the loop: ir.module.module will be
170 # registered by init_module_models() above.
171 modobj = pool.get('ir.module.module')
174 modobj.check(cr, SUPERUSER_ID, [module_id])
179 if hasattr(package, 'init') or package.state == 'to install':
182 if hasattr(package, 'init') or hasattr(package, 'update') or package.state in ('to install', 'to upgrade'):
183 if package.state=='to upgrade':
184 # upgrading the module information
185 modobj.write(cr, SUPERUSER_ID, [module_id], modobj.get_values_from_terp(package.data))
186 load_init_xml(module_name, idref, mode)
187 load_update_xml(module_name, idref, mode)
188 load_data(module_name, idref, mode)
189 if hasattr(package, 'demo') or (package.dbdemo and package.state != 'installed'):
190 status['progress'] = (index + 0.75) / len(graph)
191 load_demo_xml(module_name, idref, mode)
192 load_demo(module_name, idref, mode)
193 cr.execute('update ir_module_module set demo=%s where id=%s', (True, module_id))
195 # launch tests only in demo mode, as most tests will depend
196 # on demo data. Other tests can be added into the regular
197 # 'data' section, but should probably not alter the data,
198 # as there is no rollback.
199 if tools.config.options['test_enable']:
200 report.record_result(load_test(module_name, idref, mode),
201 details=(dict(module=module_name,
202 msg="Exception during load of legacy "
203 "data-based tests (yml...)")))
204 # Run the `fast_suite` and `checks` tests given by the module.
205 if module_name == 'base':
206 # Also run the core tests after the database is created.
207 report.record_result(openerp.modules.module.run_unit_tests('openerp'),
208 details=dict(module='openerp',
209 msg="Failure or error in server core "
211 report.record_result(openerp.modules.module.run_unit_tests(module_name),
212 details=dict(module=module_name,
213 msg="Failure or error in unit tests, "
214 "check logs for more details"))
216 processed_modules.append(package.name)
218 migrations.migrate_module(package, 'post')
220 ver = adapt_version(package.data['version'])
221 # Set new modules and dependencies
222 modobj.write(cr, SUPERUSER_ID, [module_id], {'state': 'installed', 'latest_version': ver})
223 # Update translations for all installed languages
224 modobj.update_translations(cr, SUPERUSER_ID, [module_id], None, {'overwrite': openerp.tools.config["overwrite_existing_translations"]})
226 package.state = 'installed'
227 for kind in ('init', 'demo', 'update'):
228 if hasattr(package, kind):
229 delattr(package, kind)
233 # The query won't be valid for models created later (i.e. custom model
234 # created after the registry has been loaded), so empty its result.
235 pool.fields_by_model = None
239 return loaded_modules, processed_modules
241 def _check_module_names(cr, module_names):
242 mod_names = set(module_names)
243 if 'base' in mod_names:
244 # ignore dummy 'all' module
245 if 'all' in mod_names:
246 mod_names.remove('all')
248 cr.execute("SELECT count(id) AS count FROM ir_module_module WHERE name in %s", (tuple(mod_names),))
249 if cr.dictfetchone()['count'] != len(mod_names):
250 # find out what module name(s) are incorrect:
251 cr.execute("SELECT name FROM ir_module_module")
252 incorrect_names = mod_names.difference([x['name'] for x in cr.dictfetchall()])
253 _logger.warning('invalid module names, ignored: %s', ", ".join(incorrect_names))
255 def load_marked_modules(cr, graph, states, force, progressdict, report, loaded_modules, perform_checks):
256 """Loads modules marked with ``states``, adding them to ``graph`` and
257 ``loaded_modules`` and returns a list of installed/upgraded modules."""
258 processed_modules = []
260 cr.execute("SELECT name from ir_module_module WHERE state IN %s" ,(tuple(states),))
261 module_list = [name for (name,) in cr.fetchall() if name not in graph]
262 graph.add_modules(cr, module_list, force)
263 _logger.debug('Updating graph with %d more modules', len(module_list))
264 loaded, processed = load_module_graph(cr, graph, progressdict, report=report, skip_modules=loaded_modules, perform_checks=perform_checks)
265 processed_modules.extend(processed)
266 loaded_modules.extend(loaded)
267 if not processed: break
268 return processed_modules
271 def load_modules(db, force_demo=False, status=None, update_module=False):
272 # TODO status['progress'] reporting is broken: used twice (and reset each
273 # time to zero) in load_module_graph, not fine-grained enough.
274 # It should be a method exposed by the pool.
275 initialize_sys_path()
277 open_openerp_namespace()
285 if not openerp.modules.db.is_initialized(cr):
286 _logger.info("init db")
287 openerp.modules.db.initialize(cr)
288 tools.config["init"]["all"] = 1
289 tools.config['update']['all'] = 1
290 if not tools.config['without_demo']:
291 tools.config["demo"]['all'] = 1
293 # This is a brand new pool, just created in pooler.get_db_and_pool()
294 pool = pooler.get_pool(cr.dbname)
296 if 'base' in tools.config['update'] or 'all' in tools.config['update']:
297 cr.execute("update ir_module_module set state=%s where name=%s and state=%s", ('to upgrade', 'base', 'installed'))
299 # STEP 1: LOAD BASE (must be done before module dependencies can be computed for later steps)
300 graph = openerp.modules.graph.Graph()
301 graph.add_module(cr, 'base', force)
303 _logger.critical('module base cannot be loaded! (hint: verify addons-path)')
304 raise osv.osv.except_osv(_('Could not load base module'), _('module base cannot be loaded! (hint: verify addons-path)'))
306 # processed_modules: for cleanup step after install
307 # loaded_modules: to avoid double loading
308 report = pool._assertion_report
309 loaded_modules, processed_modules = load_module_graph(cr, graph, status, perform_checks=update_module, report=report)
311 if tools.config['load_language']:
312 for lang in tools.config['load_language'].split(','):
313 tools.load_language(cr, lang)
315 # STEP 2: Mark other modules to be loaded/updated
317 modobj = pool.get('ir.module.module')
318 if ('base' in tools.config['init']) or ('base' in tools.config['update']):
319 _logger.info('updating modules list')
320 modobj.update_list(cr, SUPERUSER_ID)
322 _check_module_names(cr, itertools.chain(tools.config['init'].keys(), tools.config['update'].keys()))
324 mods = [k for k in tools.config['init'] if tools.config['init'][k]]
326 ids = modobj.search(cr, SUPERUSER_ID, ['&', ('state', '=', 'uninstalled'), ('name', 'in', mods)])
328 modobj.button_install(cr, SUPERUSER_ID, ids)
330 mods = [k for k in tools.config['update'] if tools.config['update'][k]]
332 ids = modobj.search(cr, SUPERUSER_ID, ['&', ('state', '=', 'installed'), ('name', 'in', mods)])
334 modobj.button_upgrade(cr, SUPERUSER_ID, ids)
336 cr.execute("update ir_module_module set state=%s where name=%s", ('installed', 'base'))
339 # STEP 3: Load marked modules (skipping base which was done in STEP 1)
340 # IMPORTANT: this is done in two parts, first loading all installed or
341 # partially installed modules (i.e. installed/to upgrade), to
342 # offer a consistent system to the second part: installing
343 # newly selected modules.
344 # We include the modules 'to remove' in the first step, because
345 # they are part of the "currently installed" modules. They will
346 # be dropped in STEP 6 later, before restarting the loading
348 # IMPORTANT 2: We have to loop here until all relevant modules have been
349 # processed, because in some rare cases the dependencies have
350 # changed, and modules that depend on an uninstalled module
351 # will not be processed on the first pass.
352 # It's especially useful for migrations.
353 previously_processed = -1
354 while previously_processed < len(processed_modules):
355 previously_processed = len(processed_modules)
356 processed_modules += load_marked_modules(cr, graph,
357 ['installed', 'to upgrade', 'to remove'],
358 force, status, report, loaded_modules, update_module)
360 processed_modules += load_marked_modules(cr, graph,
361 ['to install'], force, status, report,
362 loaded_modules, update_module)
365 cr.execute('select model from ir_model where state=%s', ('manual',))
366 for model in cr.dictfetchall():
367 pool.get('ir.model').instanciate(cr, SUPERUSER_ID, model['model'], {})
369 # STEP 4: Finish and cleanup installations
370 if processed_modules:
371 cr.execute("""select model,name from ir_model where id NOT IN (select distinct model_id from ir_model_access)""")
372 for (model, name) in cr.fetchall():
373 model_obj = pool.get(model)
374 if model_obj and not model_obj.is_transient() and not isinstance(model_obj, openerp.osv.orm.AbstractModel):
375 _logger.warning('The model %s has no access rules, consider adding one. E.g. access_%s,access_%s,model_%s,,1,1,1,1',
376 model, model.replace('.', '_'), model.replace('.', '_'), model.replace('.', '_'))
378 # Temporary warning while we remove access rights on osv_memory objects, as they have
379 # been replaced by owner-only access rights
380 cr.execute("""select distinct mod.model, mod.name from ir_model_access acc, ir_model mod where acc.model_id = mod.id""")
381 for (model, name) in cr.fetchall():
382 model_obj = pool.get(model)
383 if model_obj and model_obj.is_transient():
384 _logger.warning('The transient model %s (%s) should not have explicit access rules!', model, name)
386 cr.execute("SELECT model from ir_model")
387 for (model,) in cr.fetchall():
388 obj = pool.get(model)
390 obj._check_removed_columns(cr, log=True)
392 _logger.warning("Model %s is declared but cannot be loaded! (Perhaps a module was partially removed or renamed)", model)
394 # Cleanup orphan records
395 pool.get('ir.model.data')._process_end(cr, SUPERUSER_ID, processed_modules)
397 for kind in ('init', 'demo', 'update'):
398 tools.config[kind] = {}
402 # STEP 5: Cleanup menus
403 # Remove menu items that are not referenced by any of other
404 # (child) menu item, ir_values, or ir_model_data.
405 # TODO: This code could be a method of ir_ui_menu. Remove menu without actions of children
408 cr.execute('''delete from
411 (id not IN (select parent_id from ir_ui_menu where parent_id is not null))
413 (id not IN (select res_id from ir_values where model='ir.ui.menu'))
415 (id not IN (select res_id from ir_model_data where model='ir.ui.menu'))''')
420 _logger.info('removed %d unused menus', cr.rowcount)
422 # STEP 6: Uninstall modules to remove
424 # Remove records referenced from ir_model_data for modules to be
425 # removed (and removed the references from ir_model_data).
426 cr.execute("SELECT id FROM ir_module_module WHERE state=%s", ('to remove',))
427 mod_ids_to_remove = [x[0] for x in cr.fetchall()]
428 if mod_ids_to_remove:
429 pool.get('ir.module.module').module_uninstall(cr, SUPERUSER_ID, mod_ids_to_remove)
430 # Recursive reload, should only happen once, because there should be no
431 # modules to remove next time
433 _logger.info('Reloading registry once more after uninstalling modules')
434 return pooler.restart_pool(cr.dbname, force_demo, status, update_module)
437 _logger.error('At least one test failed when loading the modules.')
439 _logger.info('Modules loaded.')
441 # STEP 7: call _register_hook on every model
442 for model in pool.models.values():
443 model._register_hook(cr)
449 # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: