summaryrefslogtreecommitdiff
path: root/odoo/addons/base
diff options
context:
space:
mode:
authorMartin Trigaux <mat@odoo.com>2019-01-25 15:04:42 +0000
committerMartin Trigaux <mat@odoo.com>2019-08-19 11:44:34 +0000
commit7593b887dff4fcd474ca72aead6825481b419d86 (patch)
tree28c5445d1440701de5c7534e1d8accef222798d8 /odoo/addons/base
parenta248171d96de3ad6d8e22c855886dfae61e145e2 (diff)
[REF] fields: use ir.model.fields.selection
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) <mat@odoo.com> Co-authored-by: Raphaƫl Collet <rco@odoo.com>
Diffstat (limited to 'odoo/addons/base')
-rw-r--r--odoo/addons/base/models/ir_model.py240
-rw-r--r--odoo/addons/base/models/ir_translation.py38
-rw-r--r--odoo/addons/base/report/ir_model_templates.xml8
-rw-r--r--odoo/addons/base/security/ir.model.access.csv2
-rw-r--r--odoo/addons/base/tests/test_ir_actions.py55
-rw-r--r--odoo/addons/base/tests/test_translate.py17
-rw-r--r--odoo/addons/base/views/ir_model_views.xml76
7 files changed, 384 insertions, 52 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 @@
<td colspan="4">
<p t-if="field.relation"><span>- relation = <span t-field="field.relation"/></span></p>
<p t-if="field.relation_field"><span>- field = <span t-field="field.relation_field"/></span></p>
- <div t-if="field.selection"><span>- selection = <span t-field="field.selection"/></span></div>
+ <div t-if="field.selection_ids">
+ <span>- selection = [
+ <t t-foreach="field.selection_ids" t-as="selection">
+ (<span t-field="selection.value"/>, <span t-field="selection.name"/>),
+ </t>]
+ </span>
+ </div>
<div t-if="field.size"><span>- size = <span t-field="field.size"/></span></div>
<div t-if="field.on_delete"><span>- ondelete = <span t-field="field.on_delete"/></span></div>
<div t-if="field.domain"><span>- domain = <span t-field="field.domain"/></span></div>
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 @@
<field name="translate"
attrs="{'readonly': [('ttype','not in',['char','text','html'])],
'invisible': [('ttype','not in',['char','text','html'])]}"/>
- <field name="selection" placeholder="[('blue', 'Blue'),('yellow', 'Yellow')]"
- attrs="{'required': [('ttype','in',['selection','reference'])],
- 'readonly': [('ttype','not in',['selection','reference'])],
- 'invisible': [('ttype','not in',['selection','reference'])]}"/>
<field name="size" groups="base.group_no_one"
attrs="{'readonly': [('ttype','not in',['char','reference'])],
'invisible': [('ttype','not in',['char','reference'])]}"/>
@@ -107,6 +103,16 @@
'invisible': [('ttype','not in',['many2one','one2many','many2many'])]}"/>
</group>
</group>
+ <group attrs="{'readonly': [('ttype','not in',['selection','reference'])],
+ 'invisible': [('ttype','not in',['selection','reference'])]}">
+ <field name="selection_ids">
+ <tree editable="bottom">
+ <field name="sequence" widget="handle"/>
+ <field name="value"/>
+ <field name="name"/>
+ </tree>
+ </field>
+ </group>
<group string="Advanced Properties" groups="base.group_no_one">
<field name="related"/>
<field name="depends" attrs="{'required': [('compute', 'not in', [False, ''])]}"/>
@@ -260,10 +266,6 @@
<field name="translate"
attrs="{'readonly': [('ttype','not in',['char','text','html'])],
'invisible': [('ttype','not in',['char','text','html'])]}"/>
- <field name="selection" placeholder="[('blue', 'Blue'),('yellow', 'Yellow')]"
- attrs="{'required': [('ttype','in',['selection','reference'])],
- 'readonly': [('ttype','not in',['selection','reference'])],
- 'invisible': [('ttype','not in',['selection','reference'])]}"/>
<field name="size" groups="base.group_no_one"
attrs="{'readonly': [('ttype','not in',['char','reference'])],
'invisible': [('ttype','not in',['char','reference'])]}"/>
@@ -292,6 +294,16 @@
'invisible': [('ttype','not in',['many2one','one2many','many2many'])]}"/>
</group>
</group>
+ <group attrs="{'readonly': [('ttype','not in',['selection','reference'])],
+ 'invisible': [('ttype','not in',['selection','reference'])]}">
+ <field name="selection_ids">
+ <tree editable="bottom">
+ <field name="sequence" widget="handle"/>
+ <field name="value"/>
+ <field name="name"/>
+ </tree>
+ </field>
+ </group>
<group string="Advanced Properties" groups="base.group_no_one">
<field name="related"/>
<field name="depends" attrs="{'required': [('compute', 'not in', [False, ''])]}"/>
@@ -387,6 +399,54 @@
</record>
<menuitem action="action_model_fields" id="ir_model_model_fields" parent="base.next_id_9"/>
+ <!-- ir model fields selection -->
+ <record id="view_model_fields_selection_form" model="ir.ui.view">
+ <field name="model">ir.model.fields.selection</field>
+ <field name="arch" type="xml">
+ <form string="Fields" duplicate="false">
+ <sheet>
+ <group>
+ <field name="field_id"/>
+ <field name="value" groups="base.group_no_one"/>
+ <field name="name"/>
+ <field name="sequence" groups="base.group_no_one"/>
+ </group>
+ </sheet>
+ </form>
+ </field>
+ </record>
+ <record id="view_model_fields_selection_tree" model="ir.ui.view">
+ <field name="model">ir.model.fields.selection</field>
+ <field name="arch" type="xml">
+ <tree>
+ <field name="sequence" widget="handle"/>
+ <field name="field_id"/>
+ <field name="value"/>
+ <field name="name"/>
+ </tree>
+ </field>
+ </record>
+ <record id="view_model_fields_selection_search" model="ir.ui.view">
+ <field name="model">ir.model.fields.selection</field>
+ <field name="arch" type="xml">
+ <search string="Fields">
+ <field name="field_id" string="Field"/>
+ <field name="name"
+ filter_domain="['|', ('name','ilike',self), ('value','ilike',self)]"
+ string="Selection" />
+ <filter name="group_by_field"
+ context="{'group_by':'field_id'}"
+ string="Field"/>
+ </search>
+ </field>
+ </record>
+ <record id="action_model_fields_selection" model="ir.actions.act_window">
+ <field name="name">Fields Selection</field>
+ <field name="res_model">ir.model.fields.selection</field>
+ <field name="view_id" ref="view_model_fields_selection_tree"/>
+ </record>
+ <menuitem action="action_model_fields_selection" id="ir_model_model_fields_selection" parent="base.next_id_9"/>
+
<!-- ir model data -->
<record model="ir.ui.view" id="view_model_data_form">
<field name="model">ir.model.data</field>