summaryrefslogtreecommitdiff
path: root/odoo/addons/base/models/res_config.py
diff options
context:
space:
mode:
authorThibault Delavallée <tde@odoo.com>2017-10-20 11:39:28 +0200
committerThibault Delavallée <tde@odoo.com>2017-11-27 11:15:00 +0100
commitc129b91b6dab7a236facf8a6eb28e2aecd6ee865 (patch)
treecf14319e7b0f2805f4ed115768743634431514fb /odoo/addons/base/models/res_config.py
parentee5d46ceb16cd05fb05c834bd742866c29968171 (diff)
[MOV] base: move res_* models into models/
Diffstat (limited to 'odoo/addons/base/models/res_config.py')
-rw-r--r--odoo/addons/base/models/res_config.py671
1 files changed, 671 insertions, 0 deletions
diff --git a/odoo/addons/base/models/res_config.py b/odoo/addons/base/models/res_config.py
new file mode 100644
index 00000000000..81e62bdf2d8
--- /dev/null
+++ b/odoo/addons/base/models/res_config.py
@@ -0,0 +1,671 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+import json
+import logging
+import re
+
+from operator import attrgetter, add
+from lxml import etree
+
+from odoo import api, models, registry, SUPERUSER_ID, _
+from odoo.exceptions import AccessError, RedirectWarning, UserError
+from odoo.tools import ustr
+
+_logger = logging.getLogger(__name__)
+
+
+class ResConfigModuleInstallationMixin(object):
+ @api.model
+ def _install_modules(self, modules):
+ """ Install the requested modules.
+
+ :param modules: a list of tuples (module_name, module_record)
+ :return: the next action to execute
+ """
+ to_install_modules = self.env['ir.module.module']
+ to_install_missing_names = []
+
+ for name, module in modules:
+ if not module:
+ to_install_missing_names.append(name)
+ elif module.state == 'uninstalled':
+ to_install_modules += module
+ result = None
+ if to_install_modules:
+ result = to_install_modules.button_immediate_install()
+ #FIXME: if result is not none, the corresponding todo will be skipped because it was just marked done
+ if to_install_missing_names:
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'apps',
+ 'params': {'modules': to_install_missing_names},
+ }
+
+ return result
+
+
+class ResConfigConfigurable(models.TransientModel):
+ ''' Base classes for new-style configuration items
+
+ Configuration items should inherit from this class, implement
+ the execute method (and optionally the cancel one) and have
+ their view inherit from the related res_config_view_base view.
+ '''
+ _name = 'res.config'
+
+ @api.multi
+ def start(self):
+ # pylint: disable=next-method-called
+ return self.next()
+
+ @api.multi
+ def next(self):
+ """
+ Reload the settings page
+ """
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'reload',
+ }
+
+ @api.multi
+ def execute(self):
+ """ Method called when the user clicks on the ``Next`` button.
+
+ Execute *must* be overloaded unless ``action_next`` is overloaded
+ (which is something you generally don't need to do).
+
+ If ``execute`` returns an action dictionary, that action is executed
+ rather than just going to the next configuration item.
+ """
+ raise NotImplementedError(
+ 'Configuration items need to implement execute')
+
+ @api.multi
+ def cancel(self):
+ """ Method called when the user click on the ``Skip`` button.
+
+ ``cancel`` should be overloaded instead of ``action_skip``. As with
+ ``execute``, if it returns an action dictionary that action is
+ executed in stead of the default (going to the next configuration item)
+
+ The default implementation is a NOOP.
+
+ ``cancel`` is also called by the default implementation of
+ ``action_cancel``.
+ """
+ pass
+
+ @api.multi
+ def action_next(self):
+ """ Action handler for the ``next`` event.
+
+ Sets the status of the todo the event was sent from to
+ ``done``, calls ``execute`` and -- unless ``execute`` returned
+ an action dictionary -- executes the action provided by calling
+ ``next``.
+ """
+ # pylint: disable=next-method-called
+ return self.execute() or self.next()
+
+ @api.multi
+ def action_skip(self):
+ """ Action handler for the ``skip`` event.
+
+ Sets the status of the todo the event was sent from to
+ ``skip``, calls ``cancel`` and -- unless ``cancel`` returned
+ an action dictionary -- executes the action provided by calling
+ ``next``.
+ """
+ # pylint: disable=next-method-called
+ return self.cancel() or self.next()
+
+ @api.multi
+ def action_cancel(self):
+ """ Action handler for the ``cancel`` event. That event isn't
+ generated by the res.config.view.base inheritable view, the
+ inherited view has to overload one of the buttons (or add one
+ more).
+
+ Sets the status of the todo the event was sent from to
+ ``cancel``, calls ``cancel`` and -- unless ``cancel`` returned
+ an action dictionary -- executes the action provided by calling
+ ``next``.
+ """
+ # pylint: disable=next-method-called
+ return self.cancel() or self.next()
+
+
+class ResConfigInstaller(models.TransientModel, ResConfigModuleInstallationMixin):
+ """ New-style configuration base specialized for addons selection
+ and installation.
+
+ Basic usage
+ -----------
+
+ Subclasses can simply define a number of boolean fields. The field names
+ should be the names of the addons to install (when selected). Upon action
+ execution, selected boolean fields (and those only) will be interpreted as
+ addons to install, and batch-installed.
+
+ Additional addons
+ -----------------
+
+ It is also possible to require the installation of an additional
+ addon set when a specific preset of addons has been marked for
+ installation (in the basic usage only, additionals can't depend on
+ one another).
+
+ These additionals are defined through the ``_install_if``
+ property. This property is a mapping of a collection of addons (by
+ name) to a collection of addons (by name) [#]_, and if all the *key*
+ addons are selected for installation, then the *value* ones will
+ be selected as well. For example::
+
+ _install_if = {
+ ('sale','crm'): ['sale_crm'],
+ }
+
+ This will install the ``sale_crm`` addon if and only if both the
+ ``sale`` and ``crm`` addons are selected for installation.
+
+ You can define as many additionals as you wish, and additionals
+ can overlap in key and value. For instance::
+
+ _install_if = {
+ ('sale','crm'): ['sale_crm'],
+ ('sale','project'): ['sale_service'],
+ }
+
+ will install both ``sale_crm`` and ``sale_service`` if all of
+ ``sale``, ``crm`` and ``project`` are selected for installation.
+
+ Hook methods
+ ------------
+
+ Subclasses might also need to express dependencies more complex
+ than that provided by additionals. In this case, it's possible to
+ define methods of the form ``_if_%(name)s`` where ``name`` is the
+ name of a boolean field. If the field is selected, then the
+ corresponding module will be marked for installation *and* the
+ hook method will be executed.
+
+ Hook methods take the usual set of parameters (cr, uid, ids,
+ context) and can return a collection of additional addons to
+ install (if they return anything, otherwise they should not return
+ anything, though returning any "falsy" value such as None or an
+ empty collection will have the same effect).
+
+ Complete control
+ ----------------
+
+ The last hook is to simply overload the ``modules_to_install``
+ method, which implements all the mechanisms above. This method
+ takes the usual set of parameters (cr, uid, ids, context) and
+ returns a ``set`` of addons to install (addons selected by the
+ above methods minus addons from the *basic* set which are already
+ installed) [#]_ so an overloader can simply manipulate the ``set``
+ returned by ``ResConfigInstaller.modules_to_install`` to add or
+ remove addons.
+
+ Skipping the installer
+ ----------------------
+
+ Unless it is removed from the view, installers have a *skip*
+ button which invokes ``action_skip`` (and the ``cancel`` hook from
+ ``res.config``). Hooks and additionals *are not run* when skipping
+ installation, even for already installed addons.
+
+ Again, setup your hooks accordingly.
+
+ .. [#] note that since a mapping key needs to be hashable, it's
+ possible to use a tuple or a frozenset, but not a list or a
+ regular set
+
+ .. [#] because the already-installed modules are only pruned at
+ the very end of ``modules_to_install``, additionals and
+ hooks depending on them *are guaranteed to execute*. Setup
+ your hooks accordingly.
+ """
+ _name = 'res.config.installer'
+ _inherit = 'res.config'
+
+ _install_if = {}
+
+ def already_installed(self):
+ """ For each module, check if it's already installed and if it
+ is return its name
+
+ :returns: a list of the already installed modules in this
+ installer
+ :rtype: [str]
+ """
+ return [m.name for m in self._already_installed()]
+
+ def _already_installed(self):
+ """ For each module (boolean fields in a res.config.installer),
+ check if it's already installed (either 'to install', 'to upgrade'
+ or 'installed') and if it is return the module's record
+
+ :returns: a list of all installed modules in this installer
+ :rtype: recordset (collection of Record)
+ """
+ selectable = [name for name, field in self._fields.items()
+ if field.type == 'boolean']
+ return self.env['ir.module.module'].search([('name', 'in', selectable),
+ ('state', 'in', ['to install', 'installed', 'to upgrade'])])
+
+ def modules_to_install(self):
+ """ selects all modules to install:
+
+ * checked boolean fields
+ * return values of hook methods. Hook methods are of the form
+ ``_if_%(addon_name)s``, and are called if the corresponding
+ addon is marked for installation. They take the arguments
+ cr, uid, ids and context, and return an iterable of addon
+ names
+ * additionals, additionals are setup through the ``_install_if``
+ class variable. ``_install_if`` is a dict of {iterable:iterable}
+ where key and value are iterables of addon names.
+
+ If all the addons in the key are selected for installation
+ (warning: addons added through hooks don't count), then the
+ addons in the value are added to the set of modules to install
+ * not already installed
+ """
+ base = set(module_name
+ for installer in self.read()
+ for module_name, to_install in installer.items()
+ if self._fields[module_name].type == 'boolean' and to_install)
+
+ hooks_results = set()
+ for module in base:
+ hook = getattr(self, '_if_%s'% module, None)
+ if hook:
+ hooks_results.update(hook() or set())
+
+ additionals = set(module
+ for requirements, consequences in self._install_if.items()
+ if base.issuperset(requirements)
+ for module in consequences)
+
+ return (base | hooks_results | additionals) - set(self.already_installed())
+
+ @api.model
+ def default_get(self, fields_list):
+ ''' If an addon is already installed, check it by default
+ '''
+ defaults = super(ResConfigInstaller, self).default_get(fields_list)
+ return dict(defaults, **dict.fromkeys(self.already_installed(), True))
+
+ @api.model
+ def fields_get(self, fields=None, attributes=None):
+ """ If an addon is already installed, set it to readonly as
+ res.config.installer doesn't handle uninstallations of already
+ installed addons
+ """
+ fields = super(ResConfigInstaller, self).fields_get(fields, attributes=attributes)
+
+ for name in self.already_installed():
+ if name not in fields:
+ continue
+ fields[name].update(
+ readonly=True,
+ help= ustr(fields[name].get('help', '')) +
+ _('\n\nThis addon is already installed on your system'))
+ return fields
+
+ @api.multi
+ def execute(self):
+ to_install = list(self.modules_to_install())
+ _logger.info('Selecting addons %s to install', to_install)
+
+ IrModule = self.env['ir.module.module']
+ modules = []
+ for name in to_install:
+ module = IrModule.search([('name', '=', name)], limit=1)
+ modules.append((name, module))
+
+ return self._install_modules(modules)
+
+
+class ResConfigSettings(models.TransientModel, ResConfigModuleInstallationMixin):
+ """ Base configuration wizard for application settings. It provides support for setting
+ default values, assigning groups to employee users, and installing modules.
+ To make such a 'settings' wizard, define a model like::
+
+ class MyConfigWizard(models.TransientModel):
+ _name = 'my.settings'
+ _inherit = 'res.config.settings'
+
+ default_foo = fields.type(..., default_model='my.model'),
+ group_bar = fields.Boolean(..., group='base.group_user', implied_group='my.group'),
+ module_baz = fields.Boolean(...),
+ other_field = fields.type(...),
+
+ The method ``execute`` provides some support based on a naming convention:
+
+ * For a field like 'default_XXX', ``execute`` sets the (global) default value of
+ the field 'XXX' in the model named by ``default_model`` to the field's value.
+
+ * For a boolean field like 'group_XXX', ``execute`` adds/removes 'implied_group'
+ to/from the implied groups of 'group', depending on the field's value.
+ By default 'group' is the group Employee. Groups are given by their xml id.
+ The attribute 'group' may contain several xml ids, separated by commas.
+
+ * For a selection field like 'group_XXX' composed of 2 integers values ('0' and '1'),
+ ``execute`` adds/removes 'implied_group' to/from the implied groups of 'group',
+ depending on the field's value.
+ By default 'group' is the group Employee. Groups are given by their xml id.
+ The attribute 'group' may contain several xml ids, separated by commas.
+
+ * For a boolean field like 'module_XXX', ``execute`` triggers the immediate
+ installation of the module named 'XXX' if the field has value ``True``.
+
+ * For a selection field like 'module_XXX' composed of 2 integers values ('0' and '1'),
+ ``execute`` triggers the immediate installation of the module named 'XXX'
+ if the field has the integer value ``1``.
+
+ * For the other fields, the method ``execute`` invokes all methods with a name
+ that starts with 'set_'; such methods can be defined to implement the effect
+ of those fields.
+
+ The method ``default_get`` retrieves values that reflect the current status of the
+ fields like 'default_XXX', 'group_XXX' and 'module_XXX'. It also invokes all methods
+ with a name that starts with 'get_default_'; such methods can be defined to provide
+ current values for other fields.
+ """
+ _name = 'res.config.settings'
+
+ @api.multi
+ def copy(self, values):
+ raise UserError(_("Cannot duplicate configuration!"), "")
+
+ # TODO: Find replacement for 'onchange' attribute in view with dynamic
+ # api.onchange(...) and migrate the onchange_module(...) accordingly.
+ @api.model
+ def fields_view_get(self, view_id=None, view_type='form',
+ toolbar=False, submenu=False):
+ ret_val = super(ResConfigSettings, self).fields_view_get(
+ view_id=view_id, view_type=view_type,
+ toolbar=toolbar, submenu=submenu)
+
+ can_install_modules = self.env['ir.module.module'].check_access_rights(
+ 'write', raise_exception=False)
+
+ doc = etree.XML(ret_val['arch'])
+
+ for field in ret_val['fields']:
+ if not field.startswith("module_"):
+ continue
+ for node in doc.xpath("//field[@name='%s']" % field):
+ if not can_install_modules:
+ node.set("readonly", "1")
+ modifiers = json.loads(node.get("modifiers"))
+ modifiers['readonly'] = True
+ node.set("modifiers", json.dumps(modifiers))
+ if 'on_change' not in node.attrib:
+ node.set("on_change",
+ "onchange_module(%s, '%s')" % (field, field))
+
+ ret_val['arch'] = etree.tostring(doc, encoding='unicode')
+ return ret_val
+
+ @api.multi
+ def onchange_module(self, field_value, module_name):
+ ModuleSudo = self.env['ir.module.module'].sudo()
+ modules = ModuleSudo.search(
+ [('name', '=', module_name.replace("module_", '')),
+ ('state', 'in', ['to install', 'installed', 'to upgrade'])])
+
+ if modules and not field_value:
+ deps = modules.sudo().downstream_dependencies()
+ dep_names = (deps | modules).mapped('shortdesc')
+ message = '\n'.join(dep_names)
+ return {
+ 'warning': {
+ 'title': _('Warning!'),
+ 'message': _('Disabling this option will also uninstall the following modules \n%s') % message,
+ }
+ }
+ return {}
+
+ @api.model
+ def _get_classified_fields(self):
+ """ return a dictionary with the fields classified by category::
+
+ { 'default': [('default_foo', 'model', 'foo'), ...],
+ 'group': [('group_bar', [browse_group], browse_implied_group), ...],
+ 'module': [('module_baz', browse_module), ...],
+ 'other': ['other_field', ...],
+ }
+ """
+ IrModule = self.env['ir.module.module']
+ Groups = self.env['res.groups']
+ ref = self.env.ref
+
+ defaults, groups, modules, others = [], [], [], []
+ for name, field in self._fields.items():
+ if name.startswith('default_') and hasattr(field, 'default_model'):
+ defaults.append((name, field.default_model, name[8:]))
+ elif name.startswith('group_') and field.type in ('boolean', 'selection') and \
+ hasattr(field, 'implied_group'):
+ field_group_xmlids = getattr(field, 'group', 'base.group_user').split(',')
+ field_groups = Groups.concat(*(ref(it) for it in field_group_xmlids))
+ groups.append((name, field_groups, ref(field.implied_group)))
+ elif name.startswith('module_') and field.type in ('boolean', 'selection'):
+ module = IrModule.sudo().search([('name', '=', name[7:])], limit=1)
+ modules.append((name, module))
+ else:
+ others.append(name)
+
+ return {'default': defaults, 'group': groups, 'module': modules, 'other': others}
+
+ def get_values(self):
+ """
+ Return values for the fields other that `default`, `group` and `module`
+ """
+ return {}
+
+ @api.model
+ def default_get(self, fields):
+ IrDefault = self.env['ir.default']
+ classified = self._get_classified_fields()
+
+ res = super(ResConfigSettings, self).default_get(fields)
+
+ # defaults: take the corresponding default value they set
+ for name, model, field in classified['default']:
+ value = IrDefault.get(model, field)
+ if value is not None:
+ res[name] = value
+
+ # groups: which groups are implied by the group Employee
+ for name, groups, implied_group in classified['group']:
+ res[name] = all(implied_group in group.implied_ids for group in groups)
+ if self._fields[name].type == 'selection':
+ res[name] = int(res[name])
+
+ # modules: which modules are installed/to install
+ for name, module in classified['module']:
+ res[name] = module.state in ('installed', 'to install', 'to upgrade')
+ if self._fields[name].type == 'selection':
+ res[name] = int(res[name])
+
+ # other fields: call the method 'get_values'
+ # The other methods that start with `get_default_` are deprecated
+ for method in dir(self):
+ if method.startswith('get_default_'):
+ _logger.warning(_('Methods that start with `get_default_` are deprecated. Override `get_values` instead(Method %s)') % method)
+ res.update(self.get_values())
+
+ return res
+
+ def set_values(self):
+ """
+ Set values for the fields other that `default`, `group` and `module`
+ """
+ pass
+
+ @api.multi
+ def execute(self):
+ self.ensure_one()
+ if not self.env.user._is_superuser() and not self.env.user.has_group('base.group_system'):
+ raise AccessError(_("Only administrators can change the settings"))
+
+ self = self.with_context(active_test=False)
+ classified = self._get_classified_fields()
+
+ # default values fields
+ IrDefault = self.env['ir.default'].sudo()
+ for name, model, field in classified['default']:
+ if isinstance(self[name], models.BaseModel):
+ if self._fields[name].type == 'many2one':
+ value = self[name].id
+ else:
+ value = self[name].ids
+ else:
+ value = self[name]
+ IrDefault.set(model, field, value)
+
+ # group fields: modify group / implied groups
+ for name, groups, implied_group in classified['group']:
+ if self[name]:
+ groups.write({'implied_ids': [(4, implied_group.id)]})
+ else:
+ groups.write({'implied_ids': [(3, implied_group.id)]})
+ implied_group.write({'users': [(3, user.id) for user in groups.mapped('users')]})
+
+ # other fields: execute method 'set_values'
+ # Methods that start with `set_` are now deprecated
+ for method in dir(self):
+ if method.startswith('set_') and method is not 'set_values':
+ _logger.warning(_('Methods that start with `set_` are deprecated. Override `set_values` instead (Method %s)') % method)
+ self.set_values()
+
+ # module fields: install/uninstall the selected modules
+ to_install = []
+ to_uninstall_modules = self.env['ir.module.module']
+ lm = len('module_')
+ for name, module in classified['module']:
+ if self[name]:
+ to_install.append((name[lm:], module))
+ else:
+ if module and module.state in ('installed', 'to upgrade'):
+ to_uninstall_modules += module
+
+ if to_uninstall_modules:
+ to_uninstall_modules.button_immediate_uninstall()
+
+ self._install_modules(to_install)
+
+ if to_install or to_uninstall_modules:
+ # After the uninstall/install calls, the registry and environments
+ # are no longer valid. So we reset the environment.
+ self.env.reset()
+ self = self.env()[self._name]
+
+ # pylint: disable=next-method-called
+ config = self.env['res.config'].next() or {}
+ if config.get('type') not in ('ir.actions.act_window_close',):
+ return config
+
+ # force client-side reload (update user menu and current view)
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'reload',
+ }
+
+ @api.multi
+ def cancel(self):
+ # ignore the current record, and send the action to reopen the view
+ actions = self.env['ir.actions.act_window'].search([('res_model', '=', self._name)], limit=1)
+ if actions:
+ return actions.read()[0]
+ return {}
+
+ @api.multi
+ def name_get(self):
+ """ Override name_get method to return an appropriate configuration wizard
+ name, and not the generated name."""
+ action = self.env['ir.actions.act_window'].search([('res_model', '=', self._name)], limit=1)
+ name = action.name or self._name
+ return [(record.id, name) for record in self]
+
+ @api.model
+ def get_option_path(self, menu_xml_id):
+ """
+ Fetch the path to a specified configuration view and the action id to access it.
+
+ :param string menu_xml_id: the xml id of the menuitem where the view is located,
+ structured as follows: module_name.menuitem_xml_id (e.g.: "sales_team.menu_sale_config")
+ :return tuple:
+ - t[0]: string: full path to the menuitem (e.g.: "Settings/Configuration/Sales")
+ - t[1]: int or long: id of the menuitem's action
+ """
+ ir_ui_menu = self.env.ref(menu_xml_id)
+ return (ir_ui_menu.complete_name, ir_ui_menu.action.id)
+
+ @api.model
+ def get_option_name(self, full_field_name):
+ """
+ Fetch the human readable name of a specified configuration option.
+
+ :param string full_field_name: the full name of the field, structured as follows:
+ model_name.field_name (e.g.: "sale.config.settings.fetchmail_lead")
+ :return string: human readable name of the field (e.g.: "Create leads from incoming mails")
+ """
+ model_name, field_name = full_field_name.rsplit('.', 1)
+ return self.env[model_name].fields_get([field_name])[field_name]['string']
+
+ @api.model_cr_context
+ def get_config_warning(self, msg):
+ """
+ Helper: return a Warning exception with the given message where the %(field:xxx)s
+ and/or %(menu:yyy)s are replaced by the human readable field's name and/or menuitem's
+ full path.
+
+ Usage:
+ ------
+ Just include in your error message %(field:model_name.field_name)s to obtain the human
+ readable field's name, and/or %(menu:module_name.menuitem_xml_id)s to obtain the menuitem's
+ full path.
+
+ Example of use:
+ ---------------
+ from odoo.addons.base.models.res_config import get_warning_config
+ raise get_warning_config(cr, _("Error: this action is prohibited. You should check the field %(field:sale.config.settings.fetchmail_lead)s in %(menu:sales_team.menu_sale_config)s."), context=context)
+
+ This will return an exception containing the following message:
+ Error: this action is prohibited. You should check the field Create leads from incoming mails in Settings/Configuration/Sales.
+
+ What if there is another substitution in the message already?
+ -------------------------------------------------------------
+ You could have a situation where the error message you want to upgrade already contains a substitution. Example:
+ Cannot find any account journal of %s type for this company.\n\nYou can create one in the menu: \nConfiguration\Journals\Journals.
+ What you want to do here is simply to replace the path by %menu:account.menu_account_config)s, and leave the rest alone.
+ In order to do that, you can use the double percent (%%) to escape your new substitution, like so:
+ Cannot find any account journal of %s type for this company.\n\nYou can create one in the %%(menu:account.menu_account_config)s.
+ """
+ self = self.sudo()
+
+ # Process the message
+ # 1/ find the menu and/or field references, put them in a list
+ regex_path = r'%\(((?:menu|field):[a-z_\.]*)\)s'
+ references = re.findall(regex_path, msg, flags=re.I)
+
+ # 2/ fetch the menu and/or field replacement values (full path and
+ # human readable field's name) and the action_id if any
+ values = {}
+ action_id = None
+ for item in references:
+ ref_type, ref = item.split(':')
+ if ref_type == 'menu':
+ values[item], action_id = self.get_option_path(ref)
+ elif ref_type == 'field':
+ values[item] = self.get_option_name(ref)
+
+ # 3/ substitute and return the result
+ if (action_id):
+ return RedirectWarning(msg % values, action_id, _('Go to the configuration panel'))
+ return UserError(msg % values)