diff --git a/odoo/addons/base/data/base_data.sql b/odoo/addons/base/data/base_data.sql index 367d3da72af..0b7b2357457 100644 --- a/odoo/addons/base/data/base_data.sql +++ b/odoo/addons/base/data/base_data.sql @@ -77,6 +77,7 @@ CREATE TABLE ir_module_module_dependency ( write_uid integer, -- references res_users on delete set null, name character varying(128), module_id integer REFERENCES ir_module_module ON DELETE cascade, + auto_install_required boolean DEFAULT true, primary key(id) ); diff --git a/odoo/addons/base/models/ir_module.py b/odoo/addons/base/models/ir_module.py index de11d836010..e2870d74b3c 100644 --- a/odoo/addons/base/models/ir_module.py +++ b/odoo/addons/base/models/ir_module.py @@ -382,7 +382,7 @@ class Module(models.Model): # - at least one dependency is 'to install' install_states = frozenset(('installed', 'to install', 'to upgrade')) def must_install(module): - states = set(dep.state for dep in module.dependencies_id) + states = {dep.state for dep in module.dependencies_id if dep.auto_install_required} return states <= install_states and 'to install' in states modules = self @@ -667,7 +667,7 @@ class Module(models.Model): 'license': terp.get('license', 'LGPL-3'), 'sequence': terp.get('sequence', 100), 'application': terp.get('application', False), - 'auto_install': terp.get('auto_install', False), + 'auto_install': terp.get('auto_install', False) is not False, 'icon': terp.get('icon', False), 'summary': terp.get('summary', ''), 'url': terp.get('url') or terp.get('live_test_url', ''), @@ -707,8 +707,7 @@ class Module(models.Model): updated_values = {} for key in values: old = getattr(mod, key) - updated = tools.ustr(values[key]) if isinstance(values[key], str) else values[key] - if (old or updated) and updated != old: + if (old or values[key]) and values[key] != old: updated_values[key] = values[key] if terp.get('installable', True) and mod.state == 'uninstallable': updated_values['state'] = 'uninstalled' @@ -724,7 +723,7 @@ class Module(models.Model): mod = self.create(dict(name=mod_name, state=state, **values)) res[1] += 1 - mod._update_dependencies(terp.get('depends', [])) + mod._update_dependencies(terp.get('depends', []), terp.get('auto_install')) mod._update_exclusions(terp.get('excludes', [])) mod._update_category(terp.get('category', 'Uncategorized')) @@ -835,13 +834,15 @@ class Module(models.Model): def get_apps_server(self): return tools.config.get('apps_server', 'https://apps.odoo.com/apps') - def _update_dependencies(self, depends=None): + def _update_dependencies(self, depends=None, auto_install_requirements=()): existing = set(dep.name for dep in self.dependencies_id) needed = set(depends or []) for dep in (needed - existing): self._cr.execute('INSERT INTO ir_module_module_dependency (module_id, name) values (%s, %s)', (self.id, dep)) for dep in (existing - needed): self._cr.execute('DELETE FROM ir_module_module_dependency WHERE module_id = %s and name = %s', (self.id, dep)) + self._cr.execute('UPDATE ir_module_module_dependency SET auto_install_required = (name = any(%s)) WHERE module_id = %s', + (list(auto_install_requirements or ()), self.id)) self.invalidate_cache(['dependencies_id'], self.ids) def _update_exclusions(self, excludes=None): @@ -913,6 +914,11 @@ class ModuleDependency(models.Model): depend_id = fields.Many2one('ir.module.module', 'Dependency', compute='_compute_depend') state = fields.Selection(DEP_STATES, string='Status', compute='_compute_state') + auto_install_required = fields.Boolean( + default=True, + help="Whether this dependency blocks automatic installation " + "of the dependent") + @api.multi @api.depends('name') def _compute_depend(self): diff --git a/odoo/modules/db.py b/odoo/modules/db.py index f7e91a01282..3a432f0fecb 100644 --- a/odoo/modules/db.py +++ b/odoo/modules/db.py @@ -56,7 +56,7 @@ def initialize(cr): info['author'], info['website'], i, info['name'], info['description'], category_id, - info['auto_install'], state, + info['auto_install'] is not False, state, info['web'], info['license'], info['application'], info['icon'], @@ -67,8 +67,11 @@ def initialize(cr): 'module_'+i, 'ir.module.module', 'base', id, True)) dependencies = info['depends'] for d in dependencies: - cr.execute('INSERT INTO ir_module_module_dependency \ - (module_id,name) VALUES (%s, %s)', (id, d)) + cr.execute( + 'INSERT INTO ir_module_module_dependency (module_id, name, auto_install_required)' + ' VALUES (%s, %s, %s)', + (id, d, d in (info['auto_install'] or ())) + ) # Install recursively all auto-installing modules while True: diff --git a/odoo/modules/loading.py b/odoo/modules/loading.py index 5fb56887948..6b924f09bf4 100644 --- a/odoo/modules/loading.py +++ b/odoo/modules/loading.py @@ -379,7 +379,22 @@ def load_modules(db, force_demo=False, status=None, update_module=False): _check_module_names(cr, itertools.chain(tools.config['init'], tools.config['update'])) - module_names = [k for k, v in tools.config['init'].items() if v] + # auto-install module second pass: recursive bit in db.py handles + # auto_install modules with no dependencies or where all + # dependencies are auto_install, couldn't get it to work with + # non-required auto_install deps so here + cr.execute(""" + SELECT m.name FROM ir_module_module m + WHERE m.auto_install + AND m.state != 'installed' + AND NOT EXISTS ( + SELECT 1 FROM ir_module_module_dependency d + JOIN ir_module_module mdep ON (d.name = mdep.name) + WHERE d.module_id = m.id + AND d.auto_install_required + AND mdep.state NOT IN ('installed', 'to install') + )""") + module_names = [k for k, v in tools.config['init'].items() if v] + [x[0] for x in cr.fetchall()] if module_names: modules = Module.search([('state', '=', 'uninstalled'), ('name', 'in', module_names)]) if modules: diff --git a/odoo/modules/module.py b/odoo/modules/module.py index 378812f1036..9160717bfb9 100644 --- a/odoo/modules/module.py +++ b/odoo/modules/module.py @@ -2,7 +2,7 @@ # Part of Odoo. See LICENSE file for full copyright and licensing details. import ast -import functools +import collections import imp import importlib import inspect @@ -341,9 +341,23 @@ def load_information_from_description_file(module, mod_path=None): readme_text = tools.file_open(readme_path[0]).read() info['description'] = readme_text - if 'active' in info: - # 'active' has been renamed 'auto_install' - info['auto_install'] = info['active'] + # auto_install is set to `False` if disabled, and a set of + # auto_install dependencies otherwise. That way, we can set + # auto_install: [] to always auto_install a module regardless of its + # dependencies + auto_install = info.get('auto_install', info.get('active', False)) + if isinstance(auto_install, collections.Iterable): + info['auto_install'] = set(auto_install) + non_dependencies = info['auto_install'].difference(info['depends']) + assert not non_dependencies,\ + "auto_install triggers must be dependencies, found " \ + "non-dependencies [%s] for module %s" % ( + ', '.join(non_dependencies), module + ) + elif auto_install: + info['auto_install'] = set(info['depends']) + else: + info['auto_install'] = False info['version'] = adapt_version(info['version']) return info