[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>
This commit is contained in:
Martin Trigaux
2019-08-19 11:44:34 +00:00
co-authored by Raphaël Collet
parent a248171d96
commit 7593b887df
21 changed files with 469 additions and 98 deletions
+214 -26
View File
@@ -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()
+21 -17
View File
@@ -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,')
],
}
@@ -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>
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
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
12 access_ir_model_data_group_erp_manager ir_model_data group_erp_manager model_ir_model_data group_erp_manager 1 1 1 1
13 access_ir_model_fields_group_erp_manager ir_model_fields group_erp_manager model_ir_model_fields group_erp_manager 1 1 1 1
14 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
15 access_ir_model_user ir_model_all model_ir_model base.group_user 1 0 0 0
16 access_ir_model_data_user ir_model_data user model_ir_model_data base.group_user 1 0 1 0
17 access_ir_model_fields_user ir_model_fields all model_ir_model_fields base.group_user 1 0 0 0
18 access_ir_model_fields_selection_user ir_model_fields_selection all model_ir_model_fields_selection base.group_user 1 0 0 0
19 access_ir_module_category_group_user ir_module_category group_user model_ir_module_category group_erp_manager 1 0 0 0
20 access_ir_module_module_group_user ir_module_module group_user model_ir_module_module group_system 1 1 1 1
21 access_ir_module_module_dependency_group_system ir_module_module_dependency group_system model_ir_module_module_dependency group_system 1 1 1 1
+55
View File
@@ -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')
+17
View File
@@ -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):
+68 -8
View File
@@ -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>
+5 -9
View File
@@ -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']])
+2 -2
View File
@@ -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'],
+1 -1
View File
@@ -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',
+9
View File
@@ -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')])
@@ -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',
])
@@ -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
1 id name model_id:id group_id:id perm_read perm_write perm_create perm_unlink
30 access_test_new_api_monetary_inherits access_test_new_api_monetary_inherits model_test_new_api_monetary_inherits 1 1 1 1
31 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
32 access_test_new_api_req_m2o access_test_new_api_req_m2o model_test_new_api_req_m2o 1 1 1 1
33 access_test_new_api_selection access_test_new_api_selection model_test_new_api_selection 1 1 1 1
34 access_test_new_api_attachment access_test_new_api_attachment model_test_new_api_attachment 1 1 1 1
35 access_test_new_api_attachment_host access_test_new_api_attachment_host model_test_new_api_attachment_host 1 1 1 1
36 access_test_new_api_model_image access_test_new_api_model_image model_test_new_api_model_image 1 1 1 1
+7
View File
@@ -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'
@@ -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):
@@ -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"
@@ -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 ""
@@ -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
+20 -4
View File
@@ -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
+1
View File
@@ -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()
+1 -16
View File
@@ -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