[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:
co-authored by
Raphaël Collet
parent
a248171d96
commit
7593b887df
@@ -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()
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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')
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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']])
|
||||
|
||||
@@ -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'],
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
|
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user