diff --git a/odoo/addons/base/models/ir_model.py b/odoo/addons/base/models/ir_model.py index 99918b6445d..effbc16fac8 100644 --- a/odoo/addons/base/models/ir_model.py +++ b/odoo/addons/base/models/ir_model.py @@ -5,6 +5,7 @@ import dateutil import itertools import logging import time +from ast import literal_eval from collections import defaultdict, Mapping from operator import itemgetter @@ -335,10 +336,10 @@ class IrModelFields(models.Model): field_description = fields.Char(string='Field Label', default='', required=True, translate=True) help = fields.Text(string='Field Help', translate=True) ttype = fields.Selection(selection=FIELD_TYPES, string='Field Type', required=True) - selection = fields.Char(string='Selection Options', default="", - help="List of options for a selection field, " - "specified as a Python expression defining a list of (key, label) pairs. " - "For example: [('blue','Blue'),('yellow','Yellow')]") + selection = fields.Char(string="Selection Options (Deprecated)", + compute='_compute_selection', inverse='_inverse_selection') + selection_ids = fields.One2many("ir.model.fields.selection", "field_id", + string="Selection Options", copy=True) copied = fields.Boolean(string='Copied', help="Whether the value is copied when duplicating a record.") related = fields.Char(string='Related Field', help="The corresponding related field, if any. This must be a dot-separated list of field names.") @@ -384,6 +385,19 @@ class IrModelFields(models.Model): field = rec._related_field() rec.related_field_id = self._get(field.model_name, field.name) + @api.depends('selection_ids') + def _compute_selection(self): + for rec in self: + if rec.ttype in ('selection', 'reference'): + rec.selection = str(self.env['ir.model.fields.selection']._get_selection(rec.id)) + else: + rec.selection = False + + def _inverse_selection(self): + for rec in self: + selection = literal_eval(rec.selection or "[]") + self.env['ir.model.fields.selection']._update_selection(rec.model, rec.name, selection) + @api.depends() def _in_modules(self): installed_modules = self.env['ir.module.module'].search([('state', '=', 'installed')]) @@ -393,18 +407,6 @@ class IrModelFields(models.Model): module_names = set(xml_id.split('.')[0] for xml_id in xml_ids[field.id]) field.modules = ", ".join(sorted(installed_names & module_names)) - @api.model - def _check_selection(self, selection): - try: - items = safe_eval(selection) - if not (isinstance(items, (tuple, list)) and - all(isinstance(item, (tuple, list)) and len(item) == 2 for item in items)): - raise ValueError(selection) - except Exception: - _logger.info('Invalid selection list definition for fields.selection', exc_info=True) - raise UserError(_("The Selection Options expression is not a valid Pythonic expression. " - "Please provide an expression in the [('key','Label'), ...] format.")) - @api.constrains('domain') def _check_domain(self): for field in self: @@ -678,10 +680,6 @@ class IrModelFields(models.Model): if 'model_id' in vals: model_data = self.env['ir.model'].browse(vals['model_id']) vals['model'] = model_data.model - if vals.get('ttype') == 'selection': - if not vals.get('selection'): - raise UserError(_('For selection fields, the Selection Options must be given!')) - self._check_selection(vals['selection']) res = super(IrModelFields, self).create(vals) @@ -712,10 +710,6 @@ class IrModelFields(models.Model): patched_models = set() if vals and self: - # check selection if given - if vals.get('selection'): - self._check_selection(vals['selection']) - for item in self: if item.state != 'manual': raise UserError(_('Properties of base fields cannot be altered in this manner! ' @@ -908,7 +902,7 @@ class IrModelFields(models.Model): attrs['translate'] = bool(field_data['translate']) attrs['size'] = field_data['size'] or None elif field_data['ttype'] in ('selection', 'reference'): - attrs['selection'] = safe_eval(field_data['selection']) + attrs['selection'] = self.env['ir.model.fields.selection']._get_selection(field_data['id']) elif field_data['ttype'] == 'many2one': if not self.pool.loaded and field_data['relation'] not in self.env: return @@ -957,6 +951,196 @@ class IrModelFields(models.Model): model._add_field(name, field) +class IrModelSelection(models.Model): + _name = 'ir.model.fields.selection' + _order = 'sequence, id' + _description = "Fields Selection" + + field_id = fields.Many2one("ir.model.fields", + required=True, ondelete="cascade", index=True, + domain=[('ttype', 'in', ['selection', 'reference'])]) + value = fields.Char(required=True) + name = fields.Char(translate=True, required=True) + sequence = fields.Integer(default=1000) + + _sql_constraints = [ + ('selection_field_uniq', 'unique(field_id, value)', + 'Selections values must be unique per field'), + ] + + def _get_selection(self, field_id): + """ Return the given field's selection as a list of pairs (value, string). """ + self._cr.execute(""" + SELECT value, name + FROM ir_model_fields_selection + WHERE field_id=%s + ORDER BY sequence, id + """, (field_id,)) + return self._cr.fetchall() + + def _reflect_model(self, model): + """ Reflect the given model's fields' selections. """ + module = self._context.get('module') + model_name = model._name.replace('.', '_') + xml_id_pattern = '%s.selection__%s__%s__%s' + to_xmlids = [] + + def make_xml_id(field_name, value): + # the field value may contains exotic chars like spaces + sanitized_value = value.replace('.', '_').replace(' ', '_').lower() + return xml_id_pattern % (module, model_name, field_name, sanitized_value) + + # determine fields to reflect + fields_to_reflect = [ + field + for field in model._fields.values() + if field.type in ('selection', 'reference') + ] + + for field in fields_to_reflect: + # if selection is callable, make sure the reflection is empty + selection = field.selection if isinstance(field.selection, list) else [] + rows = self._update_selection(model._name, field.name, selection) + + # prepare update of XML ids below + if module: + for value, modules in field._selection_modules(model).items(): + if module in modules: + to_xmlids.append(dict( + xml_id=make_xml_id(field.name, value), + record=self.browse(rows[value]['id']), + )) + + # create/update XML ids + if to_xmlids: + self.env['ir.model.data']._update_xmlids(to_xmlids) + + def _update_selection(self, model_name, field_name, selection): + """ Set the selection of a field to the given list, and return the row + values of the given selection records. + """ + field_id = self.env['ir.model.fields']._get(model_name, field_name).id + + # selection rows {value: row} + cur_rows = self._existing_selection_data(model_name, field_name) + new_rows = { + value: dict(value=value, name=label, sequence=index) + for index, (value, label) in enumerate(selection) + } + + rows_to_insert = [] + rows_to_update = [] + rows_to_remove = [] + for value in new_rows.keys() | cur_rows.keys(): + new_row, cur_row = new_rows.get(value), cur_rows.get(value) + if new_row is None: + if not self._context.get(MODULE_UNINSTALL_FLAG) and not self._context.get('module'): + # removing a selection in the new list, at your own risks + _logger.warning("Removing selection value %s on %s.%s", + cur_row['value'], model_name, field_name) + rows_to_remove.append(cur_row['id']) + elif cur_row is None: + rows_to_insert.append(dict(new_row, field_id=field_id)) + elif any(new_row[key] != cur_row[key] for key in new_row): + rows_to_update.append(dict(new_row, id=cur_row['id'])) + + if rows_to_insert: + row_ids = query_insert(self.env.cr, self._table, rows_to_insert) + # update cur_rows for output + for row, row_id in zip(rows_to_insert, row_ids): + cur_rows[row['value']] = dict(row, id=row_id) + + for row in rows_to_update: + query_update(self.env.cr, self._table, row, ['id']) + + if rows_to_remove: + self.browse(rows_to_remove).unlink() + + return cur_rows + + def _existing_selection_data(self, model_name, field_name): + """ Return the selection data of the given model, by field and value, as + a dict {field_name: {value: row_values}}. + """ + query = """ + SELECT s.* + FROM ir_model_fields_selection s + JOIN ir_model_fields f ON s.field_id=f.id + WHERE f.model=%s and f.name=%s + """ + self._cr.execute(query, [model_name, field_name]) + return {row['value']: row for row in self._cr.dictfetchall()} + + @api.model_create_multi + def create(self, vals_list): + field_ids = {vals['field_id'] for vals in vals_list} + for field in self.env['ir.model.fields'].browse(field_ids): + if field.state != 'manual': + raise UserError(_('Properties of base fields cannot be altered in this manner! ' + 'Please modify them through Python code, ' + 'preferably through a custom addon!')) + recs = super().create(vals_list) + + # setup models; this re-initializes model in registry + self.pool.setup_models(self._cr) + + return recs + + def write(self, vals): + if ( + not self.env.user._is_admin() and + any(record.field_id.state != 'manual' for record in self) + ): + raise UserError(_('Properties of base fields cannot be altered in this manner! ' + 'Please modify them through Python code, ' + 'preferably through a custom addon!')) + + if 'value' in vals: + for selection in self: + if selection.value == vals['value']: + continue + if selection.field_id.store: + # replace the value by the new one in the field's corresponding column + query = "UPDATE {table} SET {field}=%s WHERE {field}=%s".format( + table=self.env[selection.field_id.model]._table, + field=selection.field_id.name, + ) + self.env.cr.execute(query, [vals['value'], selection.value]) + + result = super().write(vals) + + # setup models; this re-initializes model in registry + self.pool.setup_models(self._cr) + + return result + + def unlink(self): + # Prevent manual deletion of module columns + if ( + not self._context.get(MODULE_UNINSTALL_FLAG) and not self._context.get('module') + and any(selection.field_id.state != 'manual' for selection in self) + ): + raise UserError(_('Properties of base fields cannot be altered in this manner! ' + 'Please modify them through Python code, ' + 'preferably through a custom addon!')) + + for selection in self: + if selection.field_id.store: + # replace the value by NULL in the field's corresponding column + query = "UPDATE {table} SET {field}=NULL WHERE {field}=%s".format( + table=self.env[selection.field_id.model]._table, + field=selection.field_id.name, + ) + self.env.cr.execute(query, [selection.value]) + + result = super().unlink() + + # setup models; this re-initializes model in registry + self.pool.setup_models(self._cr) + + return result + + class IrModelConstraint(models.Model): """ This model tracks PostgreSQL foreign keys and constraints used by Odoo @@ -1590,6 +1774,7 @@ class IrModelData(models.Model): records_items = [] # [(model, id)] model_ids = [] field_ids = [] + selection_ids = [] constraint_ids = [] module_data = self.search([('module', 'in', modules_to_remove)], order='id DESC') @@ -1598,6 +1783,8 @@ class IrModelData(models.Model): model_ids.append(data.res_id) elif data.model == 'ir.model.fields': field_ids.append(data.res_id) + elif data.model == 'ir.model.fields.selection': + selection_ids.append(data.res_id) elif data.model == 'ir.model.constraint': constraint_ids.append(data.res_id) else: @@ -1664,8 +1851,9 @@ class IrModelData(models.Model): constraints = self.env['ir.model.constraint'].search([('module', 'in', modules.ids)]) constraints._module_data_uninstall() - # remove fields and relations + # remove fields, selections and relations delete(self.env['ir.model.fields'].browse(field_ids)) + delete(self.env['ir.model.fields.selection'].browse(selection_ids)) relations = self.env['ir.model.relation'].search([('module', 'in', modules.ids)]) relations._module_data_uninstall() diff --git a/odoo/addons/base/models/ir_translation.py b/odoo/addons/base/models/ir_translation.py index f553c847205..357f60e816c 100644 --- a/odoo/addons/base/models/ir_translation.py +++ b/odoo/addons/base/models/ir_translation.py @@ -15,7 +15,6 @@ _logger = logging.getLogger(__name__) TRANSLATION_TYPE = [ ('model', 'Model Field'), ('model_terms', 'Structured Model Field'), - ('selection', 'Selection'), ('code', 'Code'), ] @@ -122,16 +121,7 @@ class IrTranslationImport(object): WHERE EXCLUDED.value IS NOT NULL AND EXCLUDED.value != ''; """ % (self._model_table, self._table)) count += cr.rowcount - cr.execute(""" INSERT INTO %s(name, lang, res_id, src, type, value, module, state, comments) - SELECT name, lang, res_id, src, type, value, module, state, comments - FROM %s - WHERE type = 'selection' - AND noupdate IS NOT TRUE - ON CONFLICT (type, lang, name, md5(src)) WHERE type = 'selection' - DO UPDATE SET (name, lang, res_id, src, type, value, module, state, comments) = (EXCLUDED.name, EXCLUDED.lang, EXCLUDED.res_id, EXCLUDED.src, EXCLUDED.type, EXCLUDED.value, EXCLUDED.module, EXCLUDED.state, EXCLUDED.comments) - WHERE EXCLUDED.value IS NOT NULL AND EXCLUDED.value != ''; - """ % (self._model_table, self._table)) - count += cr.rowcount + cr.execute(""" INSERT INTO %s(name, lang, res_id, src, type, value, module, state, comments) SELECT name, lang, res_id, src, type, value, module, state, comments FROM %s @@ -204,8 +194,7 @@ class IrTranslation(models.Model): self._cr.execute("CREATE UNIQUE INDEX ir_translation_code_unique ON ir_translation (type, lang, md5(src)) WHERE type = 'code'") if not tools.index_exists(self._cr, 'ir_translation_model_unique'): self._cr.execute("CREATE UNIQUE INDEX ir_translation_model_unique ON ir_translation (type, lang, name, res_id) WHERE type = 'model'") - if not tools.index_exists(self._cr, 'ir_translation_selection_unique'): - self._cr.execute("CREATE UNIQUE INDEX ir_translation_selection_unique ON ir_translation (type, lang, name, md5(src)) WHERE type = 'selection'") + return res @api.model @@ -469,6 +458,19 @@ class IrTranslation(models.Model): fields = self.env['ir.model.fields'].sudo().search([('model', '=', model_name)]) return {field.name: field.help for field in fields} + @api.model + @tools.ormcache_context('model_name', 'field_name', keys=('lang',)) + def get_field_selection(self, model_name, field_name): + """ Return the translation of a field's selection in the context's language. + Note that the result contains the available translations only. + + :param model_name: the name of the field's model + :param field_name: the name of the field + :return: the fields' selection as a list + """ + field = self.env['ir.model.fields']._get(model_name, field_name) + return [(sel.value, sel.name) for sel in field.selection_ids] + def check(self, mode): """ Check access rights of operation ``mode`` on ``self`` for the current user. Raise an AccessError in case conditions are not met. @@ -802,6 +804,7 @@ class IrTranslation(models.Model): :return: action definition to open the list of available translations """ fields = self.env['ir.model.fields'].search([('model', '=', model_name)]) + selection_ids = [field.selection_ids.ids for field in fields if field.type == 'selection'] view = self.env.ref("base.view_translation_tree", False) or self.env['ir.ui.view'] return { 'name': _("Technical Translations"), @@ -810,12 +813,13 @@ class IrTranslation(models.Model): 'res_model': 'ir.translation', 'type': 'ir.actions.act_window', 'domain': [ - '|', - '&', ('type', '=', 'model'), + '&', + ('type', '=', 'model'), + '|', '&', ('res_id', 'in', fields.ids), ('name', 'like', 'ir.model.fields,'), - '&', ('type', '=', 'selection'), - ('name', 'like', model_name+','), + '&', ('res_id', 'in', selection_ids), + ('name', 'like', 'ir.model.fields.selection,') ], } diff --git a/odoo/addons/base/report/ir_model_templates.xml b/odoo/addons/base/report/ir_model_templates.xml index 0ca63091dcb..63fa21330ac 100644 --- a/odoo/addons/base/report/ir_model_templates.xml +++ b/odoo/addons/base/report/ir_model_templates.xml @@ -57,7 +57,13 @@

- relation =

- field =

-
- selection =
+
+ - selection = [ + + (, ), + ] + +
- size =
- ondelete =
- domain =
diff --git a/odoo/addons/base/security/ir.model.access.csv b/odoo/addons/base/security/ir.model.access.csv index 706f68e6f7d..e1d6785f006 100644 --- a/odoo/addons/base/security/ir.model.access.csv +++ b/odoo/addons/base/security/ir.model.access.csv @@ -11,9 +11,11 @@ "access_ir_model_access_group_erp_manager","ir_model_access_group_erp_manager","model_ir_model_access","group_erp_manager",1,1,1,1 "access_ir_model_data_group_erp_manager","ir_model_data group_erp_manager","model_ir_model_data","group_erp_manager",1,1,1,1 "access_ir_model_fields_group_erp_manager","ir_model_fields group_erp_manager","model_ir_model_fields","group_erp_manager",1,1,1,1 +"access_ir_model_fields_selection_group_erp_manager","ir_model_fields_selection group_erp_manager","model_ir_model_fields_selection","group_erp_manager",1,1,1,1 "access_ir_model_user","ir_model_all","model_ir_model",base.group_user,1,0,0,0 "access_ir_model_data_user","ir_model_data user","model_ir_model_data",base.group_user,1,0,1,0 "access_ir_model_fields_user","ir_model_fields all","model_ir_model_fields",base.group_user,1,0,0,0 +"access_ir_model_fields_selection_user","ir_model_fields_selection all","model_ir_model_fields_selection",base.group_user,1,0,0,0 "access_ir_module_category_group_user","ir_module_category group_user","model_ir_module_category","group_erp_manager",1,0,0,0 "access_ir_module_module_group_user","ir_module_module group_user","model_ir_module_module","group_system",1,1,1,1 "access_ir_module_module_dependency_group_system","ir_module_module_dependency group_system","model_ir_module_module_dependency","group_system",1,1,1,1 diff --git a/odoo/addons/base/tests/test_ir_actions.py b/odoo/addons/base/tests/test_ir_actions.py index cfabee877f1..46c4da5d744 100644 --- a/odoo/addons/base/tests/test_ir_actions.py +++ b/odoo/addons/base/tests/test_ir_actions.py @@ -421,3 +421,58 @@ class TestCustomFields(common.TransactionCase): custom_binary = self.env[self.MODEL]._fields['x_image'] self.assertTrue(custom_binary.attachment) + + def test_selection(self): + """ custom selection field """ + Model = self.env[self.MODEL] + model = self.env['ir.model'].search([('model', '=', self.MODEL)]) + field = self.env['ir.model.fields'].create({ + 'model_id': model.id, + 'name': 'x_sel', + 'field_description': "Custom Selection", + 'ttype': 'selection', + 'selection_ids': [ + (0, 0, {'value': 'foo', 'name': 'Foo', 'sequence': 0}), + (0, 0, {'value': 'bar', 'name': 'Bar', 'sequence': 1}), + ], + }) + + x_sel = Model._fields['x_sel'] + self.assertEqual(x_sel.type, 'selection') + self.assertEqual(x_sel.selection, [('foo', 'Foo'), ('bar', 'Bar')]) + + # add selection value 'baz' + field.selection_ids.create({ + 'field_id': field.id, 'value': 'baz', 'name': 'Baz', 'sequence': 2, + }) + x_sel = Model._fields['x_sel'] + self.assertEqual(x_sel.type, 'selection') + self.assertEqual(x_sel.selection, [('foo', 'Foo'), ('bar', 'Bar'), ('baz', 'Baz')]) + + # assign values to records + rec1 = Model.create({'name': 'Rec1', 'x_sel': 'foo'}) + rec2 = Model.create({'name': 'Rec2', 'x_sel': 'bar'}) + rec3 = Model.create({'name': 'Rec3', 'x_sel': 'baz'}) + self.assertEqual(rec1.x_sel, 'foo') + self.assertEqual(rec2.x_sel, 'bar') + self.assertEqual(rec3.x_sel, 'baz') + + # remove selection value 'foo' + field.selection_ids[0].unlink() + x_sel = Model._fields['x_sel'] + self.assertEqual(x_sel.type, 'selection') + self.assertEqual(x_sel.selection, [('bar', 'Bar'), ('baz', 'Baz')]) + + self.assertEqual(rec1.x_sel, False) + self.assertEqual(rec2.x_sel, 'bar') + self.assertEqual(rec3.x_sel, 'baz') + + # update selection value 'bar' + field.selection_ids[0].value = 'quux' + x_sel = Model._fields['x_sel'] + self.assertEqual(x_sel.type, 'selection') + self.assertEqual(x_sel.selection, [('quux', 'Bar'), ('baz', 'Baz')]) + + self.assertEqual(rec1.x_sel, False) + self.assertEqual(rec2.x_sel, 'quux') + self.assertEqual(rec3.x_sel, 'baz') diff --git a/odoo/addons/base/tests/test_translate.py b/odoo/addons/base/tests/test_translate.py index f04b1035b08..7caf01f0a45 100644 --- a/odoo/addons/base/tests/test_translate.py +++ b/odoo/addons/base/tests/test_translate.py @@ -479,6 +479,23 @@ class TestTranslationWrite(TransactionCase): ]) + def test_field_selection(self): + """ Test translations of field selections. """ + field = self.env['ir.model']._fields['state'] + self.assertEqual([key for key, _ in field.selection], ['manual', 'base']) + + ir_field = self.env['ir.model.fields']._get('ir.model', 'state') + ir_field = ir_field.with_context(lang='fr_FR') + ir_field.selection_ids[0].name = 'Custo' + ir_field.selection_ids[1].name = 'Pas touche!' + + fg = self.env['ir.model'].fields_get(['state']) + self.assertEqual(fg['state']['selection'], field.selection) + + fg = self.env['ir.model'].with_context(lang='fr_FR').fields_get(['state']) + self.assertEqual(fg['state']['selection'], + [('manual', 'Custo'), ('base', 'Pas touche!')]) + class TestXMLTranslation(TransactionCase): def setUp(self): diff --git a/odoo/addons/base/views/ir_model_views.xml b/odoo/addons/base/views/ir_model_views.xml index 4cf0c6ae3e7..958602dca48 100644 --- a/odoo/addons/base/views/ir_model_views.xml +++ b/odoo/addons/base/views/ir_model_views.xml @@ -75,10 +75,6 @@ - @@ -107,6 +103,16 @@ 'invisible': [('ttype','not in',['many2one','one2many','many2many'])]}"/> + + + + + + + + + @@ -260,10 +266,6 @@ - @@ -292,6 +294,16 @@ 'invisible': [('ttype','not in',['many2one','one2many','many2many'])]}"/> + + + + + + + + + @@ -387,6 +399,54 @@ + + + ir.model.fields.selection + +
+ + + + + + + + +
+
+
+ + ir.model.fields.selection + + + + + + + + + + + ir.model.fields.selection + + + + + + + + + + Fields Selection + ir.model.fields.selection + + + + ir.model.data diff --git a/odoo/addons/test_impex/tests/test_export.py b/odoo/addons/test_impex/tests/test_export.py index 4995f8a7e82..28a1c7472bd 100644 --- a/odoo/addons/test_impex/tests/test_export.py +++ b/odoo/addons/test_impex/tests/test_export.py @@ -286,15 +286,11 @@ class test_selection(CreatorCase): def test_localized_export(self): self.env['res.lang'].load_lang('fr_FR') - Translations = self.env['ir.translation'] - for source, value in self.translations_fr: - Translations.create({ - 'name': 'export.selection,value', - 'lang': 'fr_FR', - 'type': 'selection', - 'src': source, - 'value': value - }) + ir_field = self.env['ir.model.fields']._get('export.selection', 'value') + selection = ir_field.selection_ids + translations = dict(self.translations_fr) + for sel_fr, sel in zip(selection.with_context(lang='fr_FR'), selection): + sel_fr.name = translations.get(sel.name, sel_fr.name) self.assertEqual( self.export('2', context={'lang': 'fr_FR'}), [[u'titi']]) diff --git a/odoo/addons/test_impex/tests/test_load.py b/odoo/addons/test_impex/tests/test_load.py index e558608c449..aa3b0013676 100644 --- a/odoo/addons/test_impex/tests/test_load.py +++ b/odoo/addons/test_impex/tests/test_load.py @@ -441,7 +441,7 @@ class test_selection(ImporterCase): def test_imported_translated(self): self.add_translations( - 'export.selection,value', 'selection', 'fr_FR', *self.translations_fr) + 'ir.model.fields.selection,name', 'model', 'fr_FR', *self.translations_fr) result = self.import_(['value'], [ ['toto'], @@ -524,7 +524,7 @@ class test_selection_function(ImporterCase): """ Expects output of selection function returns translated labels """ self.add_translations( - 'export.selection,value', 'selection', 'fr_FR', *self.translations_fr) + 'ir.model.fields.selection,name', 'model', 'fr_FR', *self.translations_fr) result = self.import_(['value'], [ ['titi'], diff --git a/odoo/addons/test_inherit/__manifest__.py b/odoo/addons/test_inherit/__manifest__.py index c08117438a9..5387ca6c99e 100644 --- a/odoo/addons/test_inherit/__manifest__.py +++ b/odoo/addons/test_inherit/__manifest__.py @@ -4,7 +4,7 @@ 'version': '0.1', 'category': 'Tests', 'description': """A module to verify the inheritance.""", - 'depends': ['base'], + 'depends': ['base', 'test_new_api'], 'data': [ 'ir.model.access.csv', 'demo_data.xml', diff --git a/odoo/addons/test_inherit/models.py b/odoo/addons/test_inherit/models.py index 74d8b406705..e4b09e292c7 100644 --- a/odoo/addons/test_inherit/models.py +++ b/odoo/addons/test_inherit/models.py @@ -136,3 +136,12 @@ class Parent2(models.AbstractModel): @api.constrains('foo') def _check_foo(self): pass + + +# +# Extend a selection field +# +class Selection(models.Model): + _inherit = 'test_new_api.selection' + + state = fields.Selection(selection_add=[('bar', 'Bar'), ('baz', 'Baz')]) diff --git a/odoo/addons/test_inherit/tests/test_inherit.py b/odoo/addons/test_inherit/tests/test_inherit.py index 6b131d37c93..568ef5fb47b 100644 --- a/odoo/addons/test_inherit/tests/test_inherit.py +++ b/odoo/addons/test_inherit/tests/test_inherit.py @@ -130,3 +130,31 @@ class TestInherit(common.TransactionCase): # check properties memoized on model self.assertEqual(len(parent._constraint_methods), 1) self.assertEqual(len(child._constraint_methods), 1) + + +class TestXMLIDS(common.TransactionCase): + def test_xml_ids(self): + """ check XML ids of selection fields. """ + field = self.env['test_new_api.selection']._fields['state'] + self.assertEqual(field.selection, [('foo', 'Foo'), ('bar', 'Bar'), ('baz', 'Baz')]) + + ir_field = self.env['ir.model.fields']._get('test_new_api.selection', 'state') + xml_ids = ir_field._get_external_ids() + self.assertCountEqual(xml_ids.get(ir_field.id), [ + 'test_new_api.field_test_new_api_selection__state', + 'test_inherit.field_test_new_api_selection__state', + ]) + + foo, bar, baz = ir_field.selection_ids + xml_ids = (foo + bar + baz)._get_external_ids() + self.assertCountEqual(xml_ids.get(foo.id), [ + 'test_new_api.selection__test_new_api_selection__state__foo', + ]) + self.assertCountEqual(xml_ids.get(bar.id), [ + 'test_new_api.selection__test_new_api_selection__state__bar', + 'test_inherit.selection__test_new_api_selection__state__bar', + ]) + self.assertCountEqual(xml_ids.get(baz.id), [ + 'test_inherit.selection__test_new_api_selection__state__baz', + ]) + diff --git a/odoo/addons/test_new_api/ir.model.access.csv b/odoo/addons/test_new_api/ir.model.access.csv index 3db5381a4b9..5e9aceeab50 100644 --- a/odoo/addons/test_new_api/ir.model.access.csv +++ b/odoo/addons/test_new_api/ir.model.access.csv @@ -30,6 +30,7 @@ access_test_new_api_monetary_custom,access_test_new_api_monetary_custom,model_te access_test_new_api_monetary_inherits,access_test_new_api_monetary_inherits,model_test_new_api_monetary_inherits,,1,1,1,1 access_test_new_api_field_with_caps,access_test_new_api_field_with_caps,model_test_new_api_field_with_caps,,1,1,1,1 access_test_new_api_req_m2o,access_test_new_api_req_m2o,model_test_new_api_req_m2o,,1,1,1,1 +access_test_new_api_selection,access_test_new_api_selection,model_test_new_api_selection,,1,1,1,1 access_test_new_api_attachment,access_test_new_api_attachment,model_test_new_api_attachment,,1,1,1,1 access_test_new_api_attachment_host,access_test_new_api_attachment_host,model_test_new_api_attachment_host,,1,1,1,1 access_test_new_api_model_image,access_test_new_api_model_image,model_test_new_api_model_image,,1,1,1,1 diff --git a/odoo/addons/test_new_api/models.py b/odoo/addons/test_new_api/models.py index e045754bcc2..b4cf23290ba 100644 --- a/odoo/addons/test_new_api/models.py +++ b/odoo/addons/test_new_api/models.py @@ -543,6 +543,13 @@ class FieldWithCaps(models.Model): pArTneR_321_id = fields.Many2one('res.partner') +class Selection(models.Model): + _name = 'test_new_api.selection' + _description = "Selection" + + state = fields.Selection([('foo', 'Foo'), ('bar', 'Bar')]) + + class RequiredM2O(models.Model): _name = 'test_new_api.req_m2o' _description = 'Required Many2one' diff --git a/odoo/addons/test_new_api/tests/test_schema.py b/odoo/addons/test_new_api/tests/test_schema.py index 6e79da8f362..6ede0e833d5 100644 --- a/odoo/addons/test_new_api/tests/test_schema.py +++ b/odoo/addons/test_new_api/tests/test_schema.py @@ -44,6 +44,12 @@ class TestReflection(common.TransactionCase): relation = self.env['ir.model.relation'].search([('name', '=', field.relation)]) self.assertTrue(relation) self.assertIn(relation.model.model, [field.model_name, field.comodel_name]) + if field.type == 'selection': + selection = [(sel.value, sel.name) for sel in ir_field.selection_ids] + if isinstance(field.selection, list): + self.assertEqual(selection, field.selection) + else: + self.assertEqual(selection, []) class TestSchema(common.TransactionCase): diff --git a/odoo/addons/test_translation_import/i18n/fr.po b/odoo/addons/test_translation_import/i18n/fr.po index 829bdf3af4a..7d888109b76 100644 --- a/odoo/addons/test_translation_import/i18n/fr.po +++ b/odoo/addons/test_translation_import/i18n/fr.po @@ -24,7 +24,7 @@ msgid "1XBUO5PUYH2RYZSA1FTLRYS8SPCNU1UYXMEYMM25ASV7JC2KTJZQESZYRV9L8CGB" msgstr "1XBUO5PUYH2RYZSA1FTLRYS8SPCNU1UYXMEYMM25ASV7JC2KTJZQESZYRV9L8CGB in french" #. module: test_translation_import -#: selection:test.translation.import,import_type:0 +#: model:ir.model.fields.selection,name:test_translation_import.selection__test_translation_import__import_type__bar msgid "Bar Import Type" msgstr "Bar in french" @@ -36,7 +36,7 @@ msgid "Ijkl" msgstr "Ijkl in french" #. module: test_translation_import -#: selection:test.translation.import,import_type:0 +#: model:ir.model.fields.selection,name:test_translation_import.selection__test_translation_import__import_type__foo msgid "Foo Import Type" msgstr "Foo in french" diff --git a/odoo/addons/test_translation_import/i18n/test_translation_import.pot b/odoo/addons/test_translation_import/i18n/test_translation_import.pot index 8485bcf7bbe..2685bd6f2b5 100644 --- a/odoo/addons/test_translation_import/i18n/test_translation_import.pot +++ b/odoo/addons/test_translation_import/i18n/test_translation_import.pot @@ -36,7 +36,7 @@ msgid "Accounting" msgstr "" #. module: test_translation_import -#: selection:test.translation.import,import_type:0 +#: model:ir.model.fields.selection,name:test_translation_import.selection__test_translation_import__import_type__bar msgid "Bar Import Type" msgstr "" @@ -61,7 +61,7 @@ msgid "Efgh" msgstr "" #. module: test_translation_import -#: selection:test.translation.import,import_type:0 +#: model:ir.model.fields.selection,name:test_translation_import.selection__test_translation_import__import_type__foo msgid "Foo Import Type" msgstr "" diff --git a/odoo/addons/test_translation_import/tests/test_term_count.py b/odoo/addons/test_translation_import/tests/test_term_count.py index b2e84b67678..7526049f4e8 100644 --- a/odoo/addons/test_translation_import/tests/test_term_count.py +++ b/odoo/addons/test_translation_import/tests/test_term_count.py @@ -36,16 +36,6 @@ class TestTermCount(common.TransactionCase): field = self.env['ir.model.fields'].search([('model', '=', 'test.translation.import'), ('name', '=', 'name')]) self.assertEqual(translations[1].res_id, field.id) - translations = self.env['ir.translation'].search([ - ('lang', '=', 'fr_FR'), - ('type', '=', 'selection'), - ('module', '=', 'test_translation_import'), - ], order='src') - self.assertEqual(len(translations), 2) - self.assertEqual(translations[0].name, 'test.translation.import,import_type') - self.assertEqual(translations[0].res_id, 0) - - def test_count_term_module(self): """ Just make sure we have as many translation entries as we wanted and module deducted from file content diff --git a/odoo/fields.py b/odoo/fields.py index b8c964aaa79..daa28dff6e1 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -1980,6 +1980,25 @@ class Selection(Field): selection_add = field.args['selection_add'] self.selection = list(OrderedDict(self.selection + selection_add).items()) + def _selection_modules(self, model): + """ Return a mapping from selection values to modules defining each value. """ + if not isinstance(self.selection, list): + return {} + value_modules = defaultdict(set) + for field in reversed(resolve_mro(model, self.name, self._can_setup_from)): + module = field.args.get('_module') + if not module: + continue + if 'selection' in field.args: + value_modules.clear() + if isinstance(field.args['selection'], list): + for value, label in field.args['selection']: + value_modules[value].add(module) + if 'selection_add' in field.args: + for value, label in field.args['selection_add']: + value_modules[value].add(module) + return value_modules + def _description_selection(self, env): """ return the selection list (pairs (value, label)); labels are translated according to context language @@ -1992,10 +2011,7 @@ class Selection(Field): # translate selection labels if env.lang: - name = "%s,%s" % (self.model_name, self.name) - translate = partial( - env['ir.translation']._get_source, name, 'selection', env.lang) - return [(value, translate(label) if label else label) for value, label in selection] + return env['ir.translation'].get_field_selection(self.model_name, self.name) else: return selection diff --git a/odoo/models.py b/odoo/models.py index 2c4ad698b27..a2a538c9c0a 100644 --- a/odoo/models.py +++ b/odoo/models.py @@ -304,6 +304,7 @@ class BaseModel(MetaModel('DummyModel', (object,), {'_register': False})): """ self.env['ir.model']._reflect_model(self) self.env['ir.model.fields']._reflect_model(self) + self.env['ir.model.fields.selection']._reflect_model(self) self.env['ir.model.constraint']._reflect_model(self) self.invalidate_cache() diff --git a/odoo/tools/translate.py b/odoo/tools/translate.py index ae36cc3a2cf..4d7392bd25f 100644 --- a/odoo/tools/translate.py +++ b/odoo/tools/translate.py @@ -639,17 +639,7 @@ class PoFileReader: match = re.match(r'(selection):([\w.]+),([\w]+)', occurrence) if match: - type, model_name, field_name = match.groups() - yield { - 'type': type, - 'model': model_name, - 'name': model_name+','+field_name, - 'src': source, - 'value': translation, - 'comments': comments, - 'res_id': int(line_number), - 'module': module, - } + _logger.info("Skipped deprecated occurrence %s", occurrence) continue match = re.match(r'(sql_constraint|constraint):([\w.]+)', occurrence) @@ -944,11 +934,6 @@ def trans_generate(lang, modules, cr): continue field = field_model._fields[field_name] - if isinstance(getattr(field, 'selection', None), (list, tuple)): - name = "%s,%s" % (record.model, field_name) - for dummy, val in field.selection: - push_translation(module, 'selection', name, 0, val) - for field_name, field in record._fields.items(): if field.translate: name = model + "," + field_name