From 7593b887dff4fcd474ca72aead6825481b419d86 Mon Sep 17 00:00:00 2001 From: Martin Trigaux Date: Fri, 25 Jan 2019 15:04:42 +0000 Subject: [PATCH] [REF] fields: use ir.model.fields.selection MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The selection values of a selection field are now stored in database in the model ir.model.fields.selection This will allow to have a modular approche on selections and each selection is now linked to the module that declared it. Previously to this change, the selections were linked to the field, meaning uninstalling a module had no impact on the selections stored on database. With this change, the selections will now be translated in the correct module (having an external id) and the records having a used selection will now be reset to null. closes odoo/odoo#30228 Signed-off-by: Martin Trigaux (mat) Co-authored-by: Raphaƫl Collet --- odoo/addons/base/models/ir_model.py | 240 ++++++++++++++++-- odoo/addons/base/models/ir_translation.py | 38 +-- .../addons/base/report/ir_model_templates.xml | 8 +- odoo/addons/base/security/ir.model.access.csv | 2 + odoo/addons/base/tests/test_ir_actions.py | 55 ++++ odoo/addons/base/tests/test_translate.py | 17 ++ odoo/addons/base/views/ir_model_views.xml | 76 +++++- odoo/addons/test_impex/tests/test_export.py | 14 +- odoo/addons/test_impex/tests/test_load.py | 4 +- odoo/addons/test_inherit/__manifest__.py | 2 +- odoo/addons/test_inherit/models.py | 9 + .../addons/test_inherit/tests/test_inherit.py | 28 ++ odoo/addons/test_new_api/ir.model.access.csv | 1 + odoo/addons/test_new_api/models.py | 7 + odoo/addons/test_new_api/tests/test_schema.py | 6 + .../addons/test_translation_import/i18n/fr.po | 4 +- .../i18n/test_translation_import.pot | 4 +- .../tests/test_term_count.py | 10 - odoo/fields.py | 24 +- odoo/models.py | 1 + odoo/tools/translate.py | 17 +- 21 files changed, 469 insertions(+), 98 deletions(-) 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