From a0dd81bfab7078ee0eb5889f449eb3e3bf7391a5 Mon Sep 17 00:00:00 2001 From: Richard Mathot Date: Thu, 16 Nov 2017 17:39:28 +0100 Subject: [PATCH 1/6] [IMP] base: add native support for ir.config_parameter in res.config Res config currently have some native behavior to support features like default_XX (setting global default value), group_XX (adding implied groups) and module_XX (installing or removing modules). This commit add support of config_parameter attribute on other fields that automatically links a config parameter to the field. It allows to hide the set and get of config parameter and their value conversion as everything is now handled by the base settings class. Thanks to @rco-odoo for his help and review when working on this feature. --- odoo/addons/base/models/res_config.py | 74 ++++++++++++++++++++++++--- 1 file changed, 67 insertions(+), 7 deletions(-) diff --git a/odoo/addons/base/models/res_config.py b/odoo/addons/base/models/res_config.py index 81e62bdf2d8..f0b2622c355 100644 --- a/odoo/addons/base/models/res_config.py +++ b/odoo/addons/base/models/res_config.py @@ -4,10 +4,9 @@ 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 import api, models, _ from odoo.exceptions import AccessError, RedirectWarning, UserError from odoo.tools import ustr @@ -341,6 +340,7 @@ class ResConfigSettings(models.TransientModel, ResConfigModuleInstallationMixin) default_foo = fields.type(..., default_model='my.model'), group_bar = fields.Boolean(..., group='base.group_user', implied_group='my.group'), module_baz = fields.Boolean(...), + config_qux = fields.Char(..., config_parameter='my.parameter') other_field = fields.type(...), The method ``execute`` provides some support based on a naming convention: @@ -366,14 +366,18 @@ class ResConfigSettings(models.TransientModel, ResConfigModuleInstallationMixin) ``execute`` triggers the immediate installation of the module named 'XXX' if the field has the integer value ``1``. + * For a field with no specific prefix BUT an attribute 'config_parameter', + ``execute``` will save its value in an ir.config.parameter (global setting for the + database). + * 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. + fields like 'default_XXX', 'group_XXX', 'module_XXX' and config_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' @@ -437,6 +441,7 @@ class ResConfigSettings(models.TransientModel, ResConfigModuleInstallationMixin) { 'default': [('default_foo', 'model', 'foo'), ...], 'group': [('group_bar', [browse_group], browse_implied_group), ...], 'module': [('module_baz', browse_module), ...], + 'config': [('config_qux', 'my.parameter'), ...], 'other': ['other_field', ...], } """ @@ -444,7 +449,7 @@ class ResConfigSettings(models.TransientModel, ResConfigModuleInstallationMixin) Groups = self.env['res.groups'] ref = self.env.ref - defaults, groups, modules, others = [], [], [], [] + defaults, groups, modules, configs, 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:])) @@ -456,10 +461,18 @@ class ResConfigSettings(models.TransientModel, ResConfigModuleInstallationMixin) elif name.startswith('module_') and field.type in ('boolean', 'selection'): module = IrModule.sudo().search([('name', '=', name[7:])], limit=1) modules.append((name, module)) + elif hasattr(field, 'config_parameter'): + if field.type in ('boolean', 'char', 'selection', 'many2one', 'integer', 'float'): + configs.append((name, field.config_parameter)) + else: + msg = "Error with field %s !" \ + "Automatic ir.config_parameter getting/setting is not implemented for %s's." \ + % (name, field.type) + raise Exception(msg) else: others.append(name) - return {'default': defaults, 'group': groups, 'module': modules, 'other': others} + return {'default': defaults, 'group': groups, 'module': modules, 'config': configs, 'other': others} def get_values(self): """ @@ -470,6 +483,7 @@ class ResConfigSettings(models.TransientModel, ResConfigModuleInstallationMixin) @api.model def default_get(self, fields): IrDefault = self.env['ir.default'] + IrConfigParameter = self.env['ir.config_parameter'].sudo() classified = self._get_classified_fields() res = super(ResConfigSettings, self).default_get(fields) @@ -492,6 +506,36 @@ class ResConfigSettings(models.TransientModel, ResConfigModuleInstallationMixin) if self._fields[name].type == 'selection': res[name] = int(res[name]) + # config: get & convert stored ir.config_parameter (or default) + WARNING_MESSAGE = "Error when converting value %r of field %s for ir.config.parameter %r" + for name, icp in classified['config']: + field = self._fields[name] + value = IrConfigParameter.get_param(icp, field.default(self) if field.default else False) + if value is not False: + if field.type == 'many2one': + try: + # Special case when value is the id of a deleted record, we do not want to + # block the settings screen + value = self.env[field.comodel_name].browse(int(value)).exists().id + except ValueError: + _logger.warning(WARNING_MESSAGE, value, field, icp) + value = False + elif field.type == 'integer': + try: + value = int(value) + except ValueError: + _logger.warning(WARNING_MESSAGE, value, field, icp) + value = 0 + elif field.type == 'float': + try: + value = float(value) + except ValueError: + _logger.warning(WARNING_MESSAGE, value, field, icp) + value = 0.0 + elif field.type == 'boolean': + value = value.lower() == 'true' + res[name] = value + # other fields: call the method 'get_values' # The other methods that start with `get_default_` are deprecated for method in dir(self): @@ -536,6 +580,22 @@ class ResConfigSettings(models.TransientModel, ResConfigModuleInstallationMixin) groups.write({'implied_ids': [(3, implied_group.id)]}) implied_group.write({'users': [(3, user.id) for user in groups.mapped('users')]}) + # config fields: store ir.config_parameters + IrConfigParameter = self.env['ir.config_parameter'].sudo() + for name, icp in classified['config']: + field = self._fields[name] + value = self[name] + if field.type == 'char': + # storing developer keys as ir.config_parameter may lead to nasty + # bugs when users leave spaces around them + value = (value or "").strip() or False + elif field.type in ('integer', 'float'): + value = repr(value) if value else False + elif field.type == 'many2one': + # value is a (possibly empty) recordset + value = value.id + IrConfigParameter.set_param(icp, value) + # other fields: execute method 'set_values' # Methods that start with `set_` are now deprecated for method in dir(self): From 098788b4070c4448ded7f17c1bda01db84af90f6 Mon Sep 17 00:00:00 2001 From: Richard Mathot Date: Thu, 16 Nov 2017 17:39:36 +0100 Subject: [PATCH 2/6] [IMP] base: order config parameters by key This convenience ordering helps finding parameters. --- odoo/addons/base/models/ir_config_parameter.py | 1 + 1 file changed, 1 insertion(+) diff --git a/odoo/addons/base/models/ir_config_parameter.py b/odoo/addons/base/models/ir_config_parameter.py index 781b0a62c81..c53946a9d22 100644 --- a/odoo/addons/base/models/ir_config_parameter.py +++ b/odoo/addons/base/models/ir_config_parameter.py @@ -27,6 +27,7 @@ class IrConfigParameter(models.Model): """Per-database storage of configuration key-value pairs.""" _name = 'ir.config_parameter' _rec_name = 'key' + _order = 'key' key = fields.Char(required=True, index=True) value = fields.Text(required=True) From a18bd499acf518f070318aeabd81f8eb94faad01 Mon Sep 17 00:00:00 2001 From: Richard Mathot Date: Thu, 16 Nov 2017 17:39:41 +0100 Subject: [PATCH 3/6] [REF] various: convert res.config.settings to use ir.config_parameter integration This commit updates the various res config of Odoo addons to use the newly-introduced config_parameter field attribute. --- addons/auth_signup/controllers/main.py | 2 +- .../auth_signup/models/res_config_settings.py | 31 ++------------ addons/auth_signup/models/res_partner.py | 2 +- addons/auth_signup/models/res_users.py | 2 +- .../base_setup/models/res_config_settings.py | 16 ++------ addons/crm/models/res_config_settings.py | 4 +- .../models/res_config_settings.py | 12 +----- .../models/res_config_settings.py | 11 +---- .../hr_expense/models/res_config_settings.py | 5 +-- .../models/res_config_settings.py | 16 +------- addons/mail/models/res_config_settings.py | 10 ++--- addons/mrp/models/res_config_settings.py | 15 +------ addons/purchase/models/res_config_settings.py | 3 +- addons/sale/models/res_config_settings.py | 40 +++++-------------- .../sale_stock/models/res_config_settings.py | 12 +----- addons/stock/models/res_config_settings.py | 12 ++---- addons/website/models/res_config_settings.py | 29 +++----------- addons/website_forum/data/forum_data.xml | 2 +- .../models/res_config_settings.py | 13 ++---- addons/website_sale/views/templates.xml | 2 +- .../models/res_config_settings.py | 15 +------ 21 files changed, 51 insertions(+), 203 deletions(-) diff --git a/addons/auth_signup/controllers/main.py b/addons/auth_signup/controllers/main.py index 1d1b0476337..b8bdb9787b3 100644 --- a/addons/auth_signup/controllers/main.py +++ b/addons/auth_signup/controllers/main.py @@ -93,7 +93,7 @@ class AuthSignupHome(Home): get_param = request.env['ir.config_parameter'].sudo().get_param return { - 'signup_enabled': get_param('auth_signup.allow_uninvited') == 'True', + 'signup_enabled': get_param('auth_signup.invitation_scope', 'b2b') == 'b2c', 'reset_password_enabled': get_param('auth_signup.reset_password') == 'True', } diff --git a/addons/auth_signup/models/res_config_settings.py b/addons/auth_signup/models/res_config_settings.py index 7350121137b..9cfceecaecd 100644 --- a/addons/auth_signup/models/res_config_settings.py +++ b/addons/auth_signup/models/res_config_settings.py @@ -9,36 +9,13 @@ from odoo import api, fields, models class ResConfigSettings(models.TransientModel): _inherit = 'res.config.settings' - auth_signup_reset_password = fields.Boolean(string='Enable password reset from Login page') + auth_signup_reset_password = fields.Boolean(string='Enable password reset from Login page', config_parameter='auth_signup.reset_password') auth_signup_uninvited = fields.Selection([ ('b2b', 'On invitation (B2B)'), ('b2c', 'Free sign up (B2C)'), - ], string='Customer Account') - auth_signup_template_user_id = fields.Many2one('res.users', string='Template user for new users created through signup') - - @api.model - def get_values(self): - res = super(ResConfigSettings, self).get_values() - get_param = self.env['ir.config_parameter'].sudo().get_param - auth_signup_template_user_id = literal_eval(get_param('auth_signup.template_user_id', default='False')) - if auth_signup_template_user_id and not self.env['res.users'].sudo().browse(auth_signup_template_user_id).exists(): - auth_signup_template_user_id = False - # the value of the parameter is a nonempty string - res.update( - auth_signup_reset_password=get_param('auth_signup.reset_password', 'False').lower() == 'true', - auth_signup_uninvited='b2c' if get_param('auth_signup.allow_uninvited', 'False').lower() == 'true' else 'b2b', - auth_signup_template_user_id=auth_signup_template_user_id, - ) - return res - - @api.multi - def set_values(self): - super(ResConfigSettings, self).set_values() - set_param = self.env['ir.config_parameter'].sudo().set_param - # we store the repr of the values, since the value of the parameter is a required string - set_param('auth_signup.reset_password', repr(self.auth_signup_reset_password)) - set_param('auth_signup.allow_uninvited', repr(self.auth_signup_uninvited == 'b2c')) - set_param('auth_signup.template_user_id', repr(self.auth_signup_template_user_id.id)) + ], string='Customer Account', default='b2b', config_parameter='auth_signup.invitation_scope') + auth_signup_template_user_id = fields.Many2one('res.users', string='Template user for new users created through signup', + config_parameter='auth_signup.template_user_id') @api.multi def open_template_user(self): diff --git a/addons/auth_signup/models/res_partner.py b/addons/auth_signup/models/res_partner.py index ecf63246580..9fbcd27872f 100644 --- a/addons/auth_signup/models/res_partner.py +++ b/addons/auth_signup/models/res_partner.py @@ -102,7 +102,7 @@ class ResPartner(models.Model): """ res = defaultdict(dict) - allow_signup = self.env['ir.config_parameter'].sudo().get_param('auth_signup.allow_uninvited', 'False').lower() == 'true' + allow_signup = self.env['ir.config_parameter'].sudo().get_param('auth_signup.invitation_scope', 'b2b') == 'b2c' for partner in self: if allow_signup and not partner.user_ids: partner.signup_prepare() diff --git a/addons/auth_signup/models/res_users.py b/addons/auth_signup/models/res_users.py index ceab0701772..c91cf78a3fc 100644 --- a/addons/auth_signup/models/res_users.py +++ b/addons/auth_signup/models/res_users.py @@ -84,7 +84,7 @@ class ResUsers(models.Model): # check that uninvited users may sign up if 'partner_id' not in values: - if not literal_eval(get_param('auth_signup.allow_uninvited', 'False')): + if get_param('auth_signup.invitation_scope', 'b2b') != 'b2c': raise SignupError(_('Signup is not allowed for uninvited users')) assert values.get('login'), "Signup: no login given for new user" diff --git a/addons/base_setup/models/res_config_settings.py b/addons/base_setup/models/res_config_settings.py index e925bac6a05..2d8d43ffe0c 100644 --- a/addons/base_setup/models/res_config_settings.py +++ b/addons/base_setup/models/res_config_settings.py @@ -11,8 +11,8 @@ class ResConfigSettings(models.TransientModel): group_multi_company = fields.Boolean("Manage multiple companies", implied_group='base.group_multi_company') company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.user.company_id) - default_user_rights = fields.Boolean("Default Access Rights") - default_external_email_server = fields.Boolean("External Email Servers") + default_user_rights = fields.Boolean("Default Access Rights", config_parameter='base_setup.default_user_rights') + default_external_email_server = fields.Boolean("External Email Servers", config_parameter='base_setup.default_external_email_server') module_base_import = fields.Boolean("Allow users to import data from CSV/XLS/XLSX/ODS files") module_google_calendar = fields.Boolean( string='Allow the users to synchronize their calendar with Google Calendar') @@ -28,7 +28,7 @@ class ResConfigSettings(models.TransientModel): help="Share your partners to all companies defined in your instance.\n" " * Checked : Partners are visible for every companies, even if a company is defined on the partner.\n" " * Unchecked : Each company can see only its partner (partners where company is defined). Partners not related to a company are visible for all companies.") - default_custom_report_footer = fields.Boolean("Default Custom Report Footer") + default_custom_report_footer = fields.Boolean("Default Custom Report Footer", config_parameter='base_setup.default_custom_report_footer') report_footer = fields.Text(related="company_id.report_footer", string='Custom Report Footer', help="Footer text displayed at the bottom of all reports.") group_multi_currency = fields.Boolean(string='Multi-Currencies', implied_group='base.group_multi_currency', @@ -39,14 +39,7 @@ class ResConfigSettings(models.TransientModel): @api.model def get_values(self): res = super(ResConfigSettings, self).get_values() - params = self.env['ir.config_parameter'].sudo() - default_external_email_server = params.get_param('base_setup.default_external_email_server', default=False) - default_user_rights = params.get_param('base_setup.default_user_rights', default=False) - default_custom_report_footer = params.get_param('base_setup.default_custom_report_footer', default=False) res.update( - default_external_email_server=default_external_email_server, - default_user_rights=default_user_rights, - default_custom_report_footer=default_custom_report_footer, company_share_partner=not self.env.ref('base.res_partner_rule').active, ) return res @@ -54,9 +47,6 @@ class ResConfigSettings(models.TransientModel): @api.multi def set_values(self): super(ResConfigSettings, self).set_values() - self.env['ir.config_parameter'].sudo().set_param("base_setup.default_external_email_server", self.default_external_email_server) - self.env['ir.config_parameter'].sudo().set_param("base_setup.default_user_rights", self.default_user_rights) - self.env['ir.config_parameter'].sudo().set_param("base_setup.default_custom_report_footer", self.default_custom_report_footer) self.env.ref('base.res_partner_rule').write({'active': not self.company_share_partner}) @api.multi diff --git a/addons/crm/models/res_config_settings.py b/addons/crm/models/res_config_settings.py index 52d8f220d1f..a944c9d6b71 100644 --- a/addons/crm/models/res_config_settings.py +++ b/addons/crm/models/res_config_settings.py @@ -8,7 +8,7 @@ class ResConfigSettings(models.TransientModel): _inherit = 'res.config.settings' crm_alias_prefix = fields.Char('Default Alias Name for Leads') - generate_lead_from_alias = fields.Boolean('Manual Assignation of Emails') + generate_lead_from_alias = fields.Boolean('Manual Assignation of Emails', config_parameter='crm.generate_lead_from_alias') group_use_lead = fields.Boolean(string="Leads", implied_group='crm.group_use_lead') module_crm_phone_validation = fields.Boolean("Phone Validation") module_web_clearbit = fields.Boolean("Customer Autocomplete") @@ -41,14 +41,12 @@ class ResConfigSettings(models.TransientModel): alias = self._find_default_lead_alias_id() res.update( crm_alias_prefix=alias.alias_name if alias else False, - generate_lead_from_alias=self.env['ir.config_parameter'].sudo().get_param('crm.generate_lead_from_alias') ) return res @api.multi def set_values(self): super(ResConfigSettings, self).set_values() - self.env['ir.config_parameter'].sudo().set_param('crm.generate_lead_from_alias', self.generate_lead_from_alias) alias = self._find_default_lead_alias_id() if alias: alias.write({'alias_name': self.crm_alias_prefix}) diff --git a/addons/google_calendar/models/res_config_settings.py b/addons/google_calendar/models/res_config_settings.py index 1ac55dbc58d..1086ce02f38 100644 --- a/addons/google_calendar/models/res_config_settings.py +++ b/addons/google_calendar/models/res_config_settings.py @@ -7,23 +7,15 @@ from odoo import api, fields, models class ResConfigSettings(models.TransientModel): _inherit = 'res.config.settings' - cal_client_id = fields.Char("Client_id") - cal_client_secret = fields.Char("Client_key") + cal_client_id = fields.Char("Client_id", config_parameter='google_calendar_client_id', default='') + cal_client_secret = fields.Char("Client_key", config_parameter='google_calendar_client_secret', default='') server_uri = fields.Char('URI for tuto') - def set_values(self): - super(ResConfigSettings, self).set_values() - set_param = self.env['ir.config_parameter'].set_param - set_param('google_calendar_client_id', (self.cal_client_id or '').strip()) - set_param('google_calendar_client_secret', (self.cal_client_secret or '').strip()) - @api.model def get_values(self): res = super(ResConfigSettings, self).get_values() get_param = self.env['ir.config_parameter'].sudo().get_param res.update( - cal_client_id=get_param('google_calendar_client_id', default=''), - cal_client_secret=get_param('google_calendar_client_secret', default=''), server_uri="%s/google_account/authentication" % get_param('web.base.url', default="http://yourcompany.odoo.com"), ) return res diff --git a/addons/google_drive/models/res_config_settings.py b/addons/google_drive/models/res_config_settings.py index 0358f615832..a4414fe7f32 100644 --- a/addons/google_drive/models/res_config_settings.py +++ b/addons/google_drive/models/res_config_settings.py @@ -7,7 +7,7 @@ from odoo import api, fields, models class ResConfigSettings(models.TransientModel): _inherit = "res.config.settings" - google_drive_authorization_code = fields.Char(string='Authorization Code') + google_drive_authorization_code = fields.Char(string='Authorization Code', config_parameter='google_drive_authorization_code') google_drive_uri = fields.Char(compute='_compute_drive_uri', string='URI', help="The URL to generate the authorization code from Google") @api.depends('google_drive_authorization_code') @@ -16,14 +16,6 @@ class ResConfigSettings(models.TransientModel): for config in self: config.google_drive_uri = google_drive_uri - @api.model - def get_values(self): - res = super(ResConfigSettings, self).get_values() - res.update( - google_drive_authorization_code=self.env['ir.config_parameter'].sudo().get_param('google_drive_authorization_code'), - ) - return res - def set_values(self): super(ResConfigSettings, self).set_values() params = self.env['ir.config_parameter'].sudo() @@ -31,5 +23,4 @@ class ResConfigSettings(models.TransientModel): refresh_token = False if authorization_code and authorization_code != params.get_param('google_drive_authorization_code'): refresh_token = self.env['google.service'].generate_refresh_token('drive', authorization_code) - params.set_param('google_drive_authorization_code', authorization_code) params.set_param('google_drive_refresh_token', refresh_token) diff --git a/addons/hr_expense/models/res_config_settings.py b/addons/hr_expense/models/res_config_settings.py index b1bce213f79..a142834e17a 100644 --- a/addons/hr_expense/models/res_config_settings.py +++ b/addons/hr_expense/models/res_config_settings.py @@ -7,7 +7,8 @@ class ResConfigSettings(models.TransientModel): _inherit = 'res.config.settings' expense_alias_prefix = fields.Char('Default Alias Name for Expenses') - use_mailgateway = fields.Boolean(string='Let your employees record expenses by email') + use_mailgateway = fields.Boolean(string='Let your employees record expenses by email', + config_parameter='hr_expense.use_mailgateway') module_sale_management = fields.Boolean(string="Customer Billing") @api.model @@ -15,7 +16,6 @@ class ResConfigSettings(models.TransientModel): res = super(ResConfigSettings, self).get_values() res.update( expense_alias_prefix=self.env.ref('hr_expense.mail_alias_expense').alias_name, - use_mailgateway=self.env['ir.config_parameter'].sudo().get_param('hr_expense.use_mailgateway'), ) return res @@ -23,7 +23,6 @@ class ResConfigSettings(models.TransientModel): def set_values(self): super(ResConfigSettings, self).set_values() self.env.ref('hr_expense.mail_alias_expense').write({'alias_name': self.expense_alias_prefix}) - self.env['ir.config_parameter'].sudo().set_param('hr_expense.use_mailgateway', self.use_mailgateway) @api.onchange('use_mailgateway') def _onchange_use_mailgateway(self): diff --git a/addons/l10n_be_hr_payroll_fleet/models/res_config_settings.py b/addons/l10n_be_hr_payroll_fleet/models/res_config_settings.py index 5ff6bb0f7dd..fd33cfebef5 100644 --- a/addons/l10n_be_hr_payroll_fleet/models/res_config_settings.py +++ b/addons/l10n_be_hr_payroll_fleet/models/res_config_settings.py @@ -1,22 +1,10 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -from odoo import api, fields, models +from odoo import fields, models class ResConfigSettings(models.TransientModel): _inherit = ['res.config.settings'] - max_unused_cars = fields.Integer(string='Maximum unused cars') - - def set_values(self): - super(ResConfigSettings, self).set_values() - Param = self.env['ir.config_parameter'].sudo() - Param.set_param("l10n_be_hr_payroll_fleet.max_unused_cars", self.max_unused_cars) - - @api.model - def get_values(self): - res = super(ResConfigSettings, self).get_values() - params = self.env['ir.config_parameter'].sudo() - res.update(max_unused_cars=int(params.get_param('l10n_be_hr_payroll_fleet.max_unused_cars', default=1000))) - return res + max_unused_cars = fields.Integer(string='Maximum unused cars', default=1000, config_parameter='l10n_be_hr_payroll_fleet.max_unused_cars') diff --git a/addons/mail/models/res_config_settings.py b/addons/mail/models/res_config_settings.py index 19d70b0973d..881f7c20184 100644 --- a/addons/mail/models/res_config_settings.py +++ b/addons/mail/models/res_config_settings.py @@ -15,22 +15,22 @@ class ResConfigSettings(models.TransientModel): fail_counter = fields.Integer('Fail Mail', readonly=True) alias_domain = fields.Char('Alias Domain', help="If you have setup a catch-all email domain redirected to " - "the Odoo server, enter the domain name here.") + "the Odoo server, enter the domain name here.", config_parameter='mail.catchall.domain') @api.model def get_values(self): res = super(ResConfigSettings, self).get_values() - previous_date = datetime.datetime.now() - datetime.timedelta(days=30) - - alias_domain = self.env["ir.config_parameter"].get_param("mail.catchall.domain", default=None) - if alias_domain is None: + alias_domain = res.get('alias_domain') + if not alias_domain: domain = self.env["ir.config_parameter"].get_param("web.base.url") try: alias_domain = urls.url_parse(domain).host except Exception: pass + previous_date = datetime.datetime.now() - datetime.timedelta(days=30) + res.update( fail_counter=self.env['mail.mail'].sudo().search_count([ ('date', '>=', previous_date.strftime(tools.DEFAULT_SERVER_DATETIME_FORMAT)), diff --git a/addons/mrp/models/res_config_settings.py b/addons/mrp/models/res_config_settings.py index 0d278e9f947..15628babc75 100644 --- a/addons/mrp/models/res_config_settings.py +++ b/addons/mrp/models/res_config_settings.py @@ -8,7 +8,7 @@ class ResConfigSettings(models.TransientModel): _inherit = 'res.config.settings' manufacturing_lead = fields.Float(related='company_id.manufacturing_lead', string="Manufacturing Lead Time") - use_manufacturing_lead = fields.Boolean(string="Default Manufacturing Lead Time", oldname='default_use_manufacturing_lead') + use_manufacturing_lead = fields.Boolean(string="Default Manufacturing Lead Time", config_parameter='mrp.use_manufacturing_lead', oldname='default_use_manufacturing_lead') module_mrp_byproduct = fields.Boolean("By-Products") module_mrp_mps = fields.Boolean("Master Production Schedule") module_mrp_plm = fields.Boolean("Product Lifecycle Management (PLM)") @@ -19,19 +19,6 @@ class ResConfigSettings(models.TransientModel): group_mrp_routings = fields.Boolean("MRP Work Orders", implied_group='mrp.group_mrp_routings') - @api.model - def get_values(self): - res = super(ResConfigSettings, self).get_values() - res.update( - use_manufacturing_lead=self.env['ir.config_parameter'].sudo().get_param('mrp.use_manufacturing_lead') - ) - return res - - @api.multi - def set_values(self): - super(ResConfigSettings, self).set_values() - self.env['ir.config_parameter'].sudo().set_param('mrp.use_manufacturing_lead', self.use_manufacturing_lead) - @api.onchange('use_manufacturing_lead') def _onchange_use_manufacturing_lead(self): if not self.use_manufacturing_lead: diff --git a/addons/purchase/models/res_config_settings.py b/addons/purchase/models/res_config_settings.py index 70265059bc2..865c02603e3 100644 --- a/addons/purchase/models/res_config_settings.py +++ b/addons/purchase/models/res_config_settings.py @@ -33,6 +33,7 @@ class ResConfigSettings(models.TransientModel): use_po_lead = fields.Boolean( string="Security Lead Time for Purchase", oldname='default_new_po_lead', + config_parameter='purchase.use_po_lead', help="Margin of error for vendor lead times. When the system generates Purchase Orders for reordering products,they will be scheduled that many days earlier to cope with unexpected vendor delays.") @api.onchange('use_po_lead') @@ -45,7 +46,6 @@ class ResConfigSettings(models.TransientModel): res = super(ResConfigSettings, self).get_values() res.update( is_installed_sale=self.env['ir.module.module'].search([('name', '=', 'sale'), ('state', '=', 'installed')]).id, - use_po_lead=self.env['ir.config_parameter'].sudo().get_param('purchase.use_po_lead') ) return res @@ -53,4 +53,3 @@ class ResConfigSettings(models.TransientModel): super(ResConfigSettings, self).set_values() self.po_lock = 'lock' if self.lock_confirmed_po else 'edit' self.po_double_validation = 'two_step' if self.po_order_approval else 'one_step' - self.env['ir.config_parameter'].sudo().set_param('purchase.use_po_lead', self.use_po_lead) diff --git a/addons/sale/models/res_config_settings.py b/addons/sale/models/res_config_settings.py index 9e5493354a1..99d3bf6d291 100644 --- a/addons/sale/models/res_config_settings.py +++ b/addons/sale/models/res_config_settings.py @@ -12,7 +12,8 @@ class ResConfigSettings(models.TransientModel): sale_note = fields.Text(related='company_id.sale_note', string="Terms & Conditions") use_sale_note = fields.Boolean( string='Default Terms & Conditions', - oldname='default_use_sale_note') + oldname='default_use_sale_note', + config_parameter='sale.use_sale_note') group_discount_per_so_line = fields.Boolean("Discounts", implied_group='sale.group_discount_per_so_line') module_sale_margin = fields.Boolean("Margins") group_sale_layout = fields.Boolean("Sections on Sales Orders", implied_group='sale.group_sale_layout') @@ -20,7 +21,8 @@ class ResConfigSettings(models.TransientModel): portal_confirmation = fields.Boolean('Online Signature & Payment') portal_confirmation_options = fields.Selection([ ('sign', 'Signature'), - ('pay', 'Payment')], string="Online Signature & Payment options") + ('pay', 'Payment')], string="Online Signature & Payment options", + config_parameter='sale.sale_portal_confirmation_options') module_sale_payment = fields.Boolean("Sale Payment", help='Technical field implied by user choice of online_confirmation') module_website_quote = fields.Boolean("Quotations Templates") group_sale_delivery_address = fields.Boolean("Customer Addresses", implied_group='sale.group_delivery_invoice_address') @@ -33,7 +35,7 @@ class ResConfigSettings(models.TransientModel): ('fixed', 'A single sales price per product'), ('percentage', 'Multiple prices per product (e.g. customer segments, currencies)'), ('formula', 'Price computed from formulas (discounts, margins, roundings)') - ], string="Pricelists") + ], string="Pricelists", config_parameter='sale.sale_pricelist_setting') group_show_price_subtotal = fields.Boolean( "Show subtotal", implied_group='sale.group_show_price_subtotal', @@ -47,7 +49,8 @@ class ResConfigSettings(models.TransientModel): sale_show_tax = fields.Selection([ ('subtotal', 'Tax-Excluded Prices'), ('total', 'Tax-Included Prices')], string="Tax Display", - default='subtotal', required=True) + required=True, default='subtotal', + config_parameter='sale.sale_show_tax') default_invoice_policy = fields.Selection([ ('order', 'Invoice what is ordered'), ('delivery', 'Invoice what is delivered') @@ -58,15 +61,16 @@ class ResConfigSettings(models.TransientModel): 'product.product', 'Deposit Product', domain="[('type', '=', 'service')]", + config_parameter='sale.default_deposit_product_id', oldname='deposit_product_id_setting', help='Default product used for payment advances') - auto_done_setting = fields.Boolean("Lock Confirmed Sales") + auto_done_setting = fields.Boolean("Lock Confirmed Sales", config_parameter='sale.auto_done_setting') module_website_sale_digital = fields.Boolean("Sell digital products - provide downloadable content on your customer portal") auth_signup_uninvited = fields.Selection([ ('b2b', 'On invitation (B2B)'), ('b2c', 'Free sign up (B2C)'), - ], string='Customer Account') + ], string='Customer Account', default='b2b', config_parameter='auth_signup.invitation_scope') module_delivery = fields.Boolean("Shipping Costs") module_delivery_dhl = fields.Boolean("DHL Connector") @@ -137,32 +141,10 @@ class ResConfigSettings(models.TransientModel): res = super(ResConfigSettings, self).get_values() ICPSudo = self.env['ir.config_parameter'].sudo() sale_pricelist_setting = ICPSudo.get_param('sale.sale_pricelist_setting') - sale_portal_confirmation_options = ICPSudo.get_param('sale.sale_portal_confirmation_options', default='none') - default_deposit_product_id = literal_eval(ICPSudo.get_param('sale.default_deposit_product_id', default='False')) - if default_deposit_product_id and not self.env['product.product'].browse(default_deposit_product_id).exists(): - default_deposit_product_id = False res.update( - auth_signup_uninvited='b2c' if ICPSudo.get_param('auth_signup.allow_uninvited', 'False').lower() == 'true' else 'b2b', - use_sale_note=ICPSudo.get_param('sale.use_sale_note', default=False), - auto_done_setting=ICPSudo.get_param('sale.auto_done_setting'), - default_deposit_product_id=default_deposit_product_id, - sale_show_tax=ICPSudo.get_param('sale.sale_show_tax', default='subtotal'), multi_sales_price=sale_pricelist_setting in ['percentage', 'formula'], multi_sales_price_method=sale_pricelist_setting in ['percentage', 'formula'] and sale_pricelist_setting or False, sale_pricelist_setting=sale_pricelist_setting, - portal_confirmation=sale_portal_confirmation_options in ('pay', 'sign'), - portal_confirmation_options=sale_portal_confirmation_options if sale_portal_confirmation_options in ('pay', 'sign') else False, + portal_confirmation=ICPSudo.get_param('sale.sale_portal_confirmation_options', default=False) in ('pay', 'sign'), ) return res - - @api.multi - def set_values(self): - super(ResConfigSettings, self).set_values() - ICPSudo = self.env['ir.config_parameter'].sudo() - ICPSudo.set_param('auth_signup.allow_uninvited', repr(self.auth_signup_uninvited == 'b2c')) - ICPSudo.set_param("sale.use_sale_note", self.use_sale_note) - ICPSudo.set_param("sale.auto_done_setting", self.auto_done_setting) - ICPSudo.set_param("sale.default_deposit_product_id", self.default_deposit_product_id.id) - ICPSudo.set_param('sale.sale_pricelist_setting', self.sale_pricelist_setting) - ICPSudo.set_param('sale.sale_show_tax', self.sale_show_tax) - ICPSudo.set_param('sale.sale_portal_confirmation_options', self.portal_confirmation_options if self.portal_confirmation_options in ('pay', 'sign') else 'none') diff --git a/addons/sale_stock/models/res_config_settings.py b/addons/sale_stock/models/res_config_settings.py index fba1d57e4f2..0fed29fc8a7 100644 --- a/addons/sale_stock/models/res_config_settings.py +++ b/addons/sale_stock/models/res_config_settings.py @@ -14,6 +14,7 @@ class ResConfigSettings(models.TransientModel): group_display_incoterm = fields.Boolean("Incoterms", implied_group='sale_stock.group_display_incoterm') use_security_lead = fields.Boolean( string="Security Lead Time for Sales", + config_parameter='sale_stock.use_security_lead', oldname='default_new_security_lead', help="Margin of error for dates promised to customers. Products will be scheduled for delivery that many days earlier than the actual promised date, to cope with unexpected delays in the supply chain.") default_picking_policy = fields.Selection([ @@ -25,14 +26,3 @@ class ResConfigSettings(models.TransientModel): def _onchange_use_security_lead(self): if not self.use_security_lead: self.security_lead = 0.0 - - def get_values(self): - res = super(ResConfigSettings, self).get_values() - res.update( - use_security_lead=self.env['ir.config_parameter'].sudo().get_param('sale_stock.use_security_lead') - ) - return res - - def set_values(self): - super(ResConfigSettings, self).set_values() - self.env['ir.config_parameter'].sudo().set_param('sale_stock.use_security_lead', self.use_security_lead) diff --git a/addons/stock/models/res_config_settings.py b/addons/stock/models/res_config_settings.py index 68819f5ef63..34dbb7c02a9 100644 --- a/addons/stock/models/res_config_settings.py +++ b/addons/stock/models/res_config_settings.py @@ -28,6 +28,7 @@ class ResConfigSettings(models.TransientModel): use_propagation_minimum_delta = fields.Boolean( string="No Rescheduling Propagation", oldname='default_new_propagation_minimum_delta', + config_parameter='stock.use_propagation_minimum_delta', help="Rescheduling applies to any chain of operations (e.g. Make To Order, Pick Pack Ship). In the case of MTO sales, a vendor delay (updated incoming date) impacts the expected delivery date to the customer. \n This option allows to not propagate the rescheduling if the change is not critical.") module_stock_picking_batch = fields.Boolean("Batch Pickings", oldname="module_stock_picking_wave") module_stock_barcode = fields.Boolean("Barcode Scanner") @@ -61,20 +62,13 @@ class ResConfigSettings(models.TransientModel): if self.group_stock_adv_location and not self.group_stock_multi_locations: self.group_stock_multi_locations = True - @api.model - def get_values(self): - res = super(ResConfigSettings, self).get_values() - res.update( - use_propagation_minimum_delta=self.env['ir.config_parameter'].sudo().get_param('stock.use_propagation_minimum_delta') - ) - return res - @api.multi def set_values(self): super(ResConfigSettings, self).set_values() + if not self.user_has_groups('stock.group_stock_manager'): return - self.env['ir.config_parameter'].sudo().set_param('stock.use_propagation_minimum_delta', self.use_propagation_minimum_delta) + """ If we are not in multiple locations, we can deactivate the internal operation types of the warehouses, so they won't appear in the dashboard. Otherwise, activate them. diff --git a/addons/website/models/res_config_settings.py b/addons/website/models/res_config_settings.py index 2cef7af3dce..20b6f0703a0 100644 --- a/addons/website/models/res_config_settings.py +++ b/addons/website/models/res_config_settings.py @@ -33,14 +33,14 @@ class ResConfigSettings(models.TransientModel): favicon = fields.Binary('Favicon', related='website_id.favicon') # Set as global config parameter since methods using it are not website-aware. To be changed # when multi-website is implemented - google_maps_api_key = fields.Char(string='Google Maps API Key') - has_google_analytics = fields.Boolean("Google Analytics") - has_google_analytics_dashboard = fields.Boolean("Google Analytics in Dashboard") - has_google_maps = fields.Boolean("Google Maps") + google_maps_api_key = fields.Char(string='Google Maps API Key', config_parameter='google_maps_api_key', default='') + has_google_analytics = fields.Boolean("Google Analytics", config_parameter='website.has_google_analytics') + has_google_analytics_dashboard = fields.Boolean("Google Analytics in Dashboard", config_parameter='website.has_google_analytics_dashboard') + has_google_maps = fields.Boolean("Google Maps", config_parameter='website.has_google_maps') auth_signup_uninvited = fields.Selection([ ('b2b', 'On invitation (B2B)'), ('b2c', 'Free sign up (B2C)'), - ], string='Customer Account') + ], string='Customer Account', default='b2b', config_parameter='auth_signup.invitation_scope') @api.onchange('has_google_analytics') def onchange_has_google_analytics(self): @@ -67,29 +67,10 @@ class ResConfigSettings(models.TransientModel): for config in self: config.language_count = len(self.language_ids) - @api.model - def get_values(self): - res = super(ResConfigSettings, self).get_values() - get_param = self.env['ir.config_parameter'].sudo().get_param - res.update( - auth_signup_uninvited='b2c' if get_param('auth_signup.allow_uninvited', 'False').lower() == 'true' else 'b2b', - has_google_analytics=get_param('website.has_google_analytics'), - has_google_analytics_dashboard=get_param('website.has_google_analytics_dashboard'), - has_google_maps=get_param('website.has_google_maps'), - google_maps_api_key=get_param('google_maps_api_key', default=''), - ) - return res - def set_values(self): if not self.user_has_groups('website.group_website_designer'): raise AccessDenied() super(ResConfigSettings, self).set_values() - set_param = self.env['ir.config_parameter'].sudo().set_param - set_param('auth_signup.allow_uninvited', repr(self.auth_signup_uninvited == 'b2c')) - set_param('website.has_google_analytics', self.has_google_analytics) - set_param('website.has_google_analytics_dashboard', self.has_google_analytics_dashboard) - set_param('website.has_google_maps', self.has_google_maps) - set_param('google_maps_api_key', (self.google_maps_api_key or '').strip()) @api.multi def open_template_user(self): diff --git a/addons/website_forum/data/forum_data.xml b/addons/website_forum/data/forum_data.xml index 939fac46191..4331e2f04a4 100644 --- a/addons/website_forum/data/forum_data.xml +++ b/addons/website_forum/data/forum_data.xml @@ -13,7 +13,7 @@ - + diff --git a/addons/website_sale/models/res_config_settings.py b/addons/website_sale/models/res_config_settings.py index 8f8d19378e9..ddc93dda214 100644 --- a/addons/website_sale/models/res_config_settings.py +++ b/addons/website_sale/models/res_config_settings.py @@ -50,8 +50,9 @@ class ResConfigSettings(models.TransientModel): module_l10n_eu_service = fields.Boolean(string="EU Digital Goods VAT") cart_recovery_mail_template = fields.Many2one('mail.template', string='Cart Recovery Email', - default=_default_recovery_mail_template, domain="[('model', '=', 'sale.order')]") - cart_abandoned_delay = fields.Float("Abandoned Delay", default=1.0, help="number of hours after which the cart is considered abandoned") + default=_default_recovery_mail_template, config_parameter='website_sale.cart_recovery_mail_template_id', domain="[('model', '=', 'sale.order')]") + cart_abandoned_delay = fields.Float("Abandoned Delay", help="number of hours after which the cart is considered abandoned", + default=1.0, config_parameter='website_sale.cart_abandoned_delay') @api.model def get_values(self): @@ -64,15 +65,9 @@ class ResConfigSettings(models.TransientModel): if self.env['ir.module.module'].search([('name', '=', 'website_sale_delivery')], limit=1).state in ('installed', 'to install', 'to upgrade'): sale_delivery_settings = 'website' - cart_recovery_mail_template = literal_eval(params.get_param('website_sale.cart_recovery_mail_template_id', default='False')) - if cart_recovery_mail_template and not self.env['mail.template'].browse(cart_recovery_mail_template).exists(): - cart_recovery_mail_template = self._default_recovery_mail_template() - res.update( automatic_invoice=params.get_param('website_sale.automatic_invoice', default=False), sale_delivery_settings=sale_delivery_settings, - cart_recovery_mail_template=cart_recovery_mail_template, - cart_abandoned_delay=float(params.get_param('website_sale.cart_abandoned_delay', '1.0')) ) return res @@ -80,8 +75,6 @@ class ResConfigSettings(models.TransientModel): super(ResConfigSettings, self).set_values() value = self.module_account_invoicing and self.default_invoice_policy == 'order' and self.automatic_invoice self.env['ir.config_parameter'].sudo().set_param('website_sale.automatic_invoice', value) - self.env['ir.config_parameter'].sudo().set_param('website_sale.cart_recovery_mail_template_id', self.cart_recovery_mail_template.id) - self.env['ir.config_parameter'].sudo().set_param('website_sale.cart_abandoned_delay', self.cart_abandoned_delay) @api.onchange('sale_delivery_settings') def _onchange_sale_delivery_settings(self): diff --git a/addons/website_sale/views/templates.xml b/addons/website_sale/views/templates.xml index 46af4a27002..5f477365e36 100644 --- a/addons/website_sale/views/templates.xml +++ b/addons/website_sale/views/templates.xml @@ -1476,7 +1476,7 @@ - +

Sign Up Now to Follow Your Order diff --git a/addons/website_slides/models/res_config_settings.py b/addons/website_slides/models/res_config_settings.py index 65c8604bc53..7021191249e 100644 --- a/addons/website_slides/models/res_config_settings.py +++ b/addons/website_slides/models/res_config_settings.py @@ -7,17 +7,4 @@ from odoo import api, fields, models class ResConfigSettings(models.TransientModel): _inherit = "res.config.settings" - website_slide_google_app_key = fields.Char(string='Google Doc Key') - - @api.model - def get_values(self): - res = super(ResConfigSettings, self).get_values() - res.update( - website_slide_google_app_key=self.env['ir.config_parameter'].sudo().get_param('website_slides.google_app_key'), - ) - return res - - @api.multi - def set_values(self): - super(ResConfigSettings, self).set_values() - self.env['ir.config_parameter'].sudo().set_param('website_slides.google_app_key', self.website_slide_google_app_key) + website_slide_google_app_key = fields.Char(string='Google Doc Key', config_parameter='website_slides.google_app_key') From c434d7f947a9ba8b45ca3cbab16b796a6d3bb0fc Mon Sep 17 00:00:00 2001 From: Raphael Collet Date: Wed, 10 Jan 2018 11:51:41 +0100 Subject: [PATCH 4/6] [IMP] base: make `res.config.settings` strict about field definitions This way we avoid wrongly configured fields and triggering a behavior without knowing it. --- odoo/addons/base/models/res_config.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/odoo/addons/base/models/res_config.py b/odoo/addons/base/models/res_config.py index f0b2622c355..60f42b16c1c 100644 --- a/odoo/addons/base/models/res_config.py +++ b/odoo/addons/base/models/res_config.py @@ -451,24 +451,27 @@ class ResConfigSettings(models.TransientModel, ResConfigModuleInstallationMixin) defaults, groups, modules, configs, others = [], [], [], [], [] for name, field in self._fields.items(): - if name.startswith('default_') and hasattr(field, 'default_model'): + if name.startswith('default_'): + if not hasattr(field, 'default_model'): + raise Exception("Field %s without attribute 'default_model'" % field) defaults.append((name, field.default_model, name[8:])) - elif name.startswith('group_') and field.type in ('boolean', 'selection') and \ - hasattr(field, 'implied_group'): + elif name.startswith('group_'): + if field.type not in ('boolean', 'selection'): + raise Exception("Field %s must have type 'boolean' or 'selection'" % field) + if not hasattr(field, 'implied_group'): + raise Exception("Field %s without attribute 'implied_group'" % field) 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'): + elif name.startswith('module_'): + if field.type not in ('boolean', 'selection'): + raise Exception("Field %s must have type 'boolean' or 'selection'" % field) module = IrModule.sudo().search([('name', '=', name[7:])], limit=1) modules.append((name, module)) elif hasattr(field, 'config_parameter'): - if field.type in ('boolean', 'char', 'selection', 'many2one', 'integer', 'float'): - configs.append((name, field.config_parameter)) - else: - msg = "Error with field %s !" \ - "Automatic ir.config_parameter getting/setting is not implemented for %s's." \ - % (name, field.type) - raise Exception(msg) + if field.type not in ('boolean', 'integer', 'float', 'char', 'selection', 'many2one'): + raise Exception("Field %s must have type 'boolean', 'integer', 'float', 'char', 'selection' or 'many2one'" % field) + configs.append((name, field.config_parameter)) else: others.append(name) From faf6165e037bcb1afee8a4881f3a166c86fdee59 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thibault=20Delavall=C3=A9e?= Date: Tue, 16 Jan 2018 10:17:43 +0100 Subject: [PATCH 5/6] [REF][FIX] base_setup, sale, website: update config field name for config_parameter fields Those fields are prefixed by default_ which now strictly means they should be linked to a default value for a given model. Their name is updated so that it does not conflict with the strict verification done recently for settings. --- addons/base_setup/models/res_config_settings.py | 10 ++++++++-- addons/base_setup/views/res_config_settings_views.xml | 8 ++++---- addons/mail/views/res_config_settings_views.xml | 8 ++++---- addons/sale/models/res_config_settings.py | 4 ++-- addons/sale/views/res_config_settings_views.xml | 2 +- addons/website/models/res_config_settings.py | 9 +++++++-- addons/website/views/res_config_settings_views.xml | 4 ++-- 7 files changed, 28 insertions(+), 17 deletions(-) diff --git a/addons/base_setup/models/res_config_settings.py b/addons/base_setup/models/res_config_settings.py index 2d8d43ffe0c..c511b44fb73 100644 --- a/addons/base_setup/models/res_config_settings.py +++ b/addons/base_setup/models/res_config_settings.py @@ -11,8 +11,14 @@ class ResConfigSettings(models.TransientModel): group_multi_company = fields.Boolean("Manage multiple companies", implied_group='base.group_multi_company') company_id = fields.Many2one('res.company', string='Company', required=True, default=lambda self: self.env.user.company_id) - default_user_rights = fields.Boolean("Default Access Rights", config_parameter='base_setup.default_user_rights') - default_external_email_server = fields.Boolean("External Email Servers", config_parameter='base_setup.default_external_email_server') + user_default_rights = fields.Boolean( + "Default Access Rights", + config_parameter='base_setup.default_user_rights', + oldname='default_user_rights') + external_email_server_default = fields.Boolean( + "External Email Servers", + config_parameter='base_setup.default_external_email_server', + oldname='default_external_email_server') module_base_import = fields.Boolean("Allow users to import data from CSV/XLS/XLSX/ODS files") module_google_calendar = fields.Boolean( string='Allow the users to synchronize their calendar with Google Calendar') diff --git a/addons/base_setup/views/res_config_settings_views.xml b/addons/base_setup/views/res_config_settings_views.xml index 12b60ef5914..a58f3e0d808 100644 --- a/addons/base_setup/views/res_config_settings_views.xml +++ b/addons/base_setup/views/res_config_settings_views.xml @@ -50,17 +50,17 @@

Users

-
+
- +
-