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
|