diff options
| author | Thibault Delavallée <tde@odoo.com> | 2017-10-20 11:39:28 +0200 |
|---|---|---|
| committer | Thibault Delavallée <tde@odoo.com> | 2017-11-27 11:15:00 +0100 |
| commit | c129b91b6dab7a236facf8a6eb28e2aecd6ee865 (patch) | |
| tree | cf14319e7b0f2805f4ed115768743634431514fb /odoo/addons/base/models/res_config.py | |
| parent | ee5d46ceb16cd05fb05c834bd742866c29968171 (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.py | 671 |
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) |
