[FIX] fields: in to_column(), returning self.column is generally not correct

Consider the following example:

    class Foo(models.Model):
        _name = 'foo'
        _columns = {
            'state': fields.selection([('a', 'A')]),
        }

    class Bar(models.Model):
        _inherit = 'foo'
        state = fields.Selection(selection_add=[('b', 'B')])

The attribute 'column' of the field does not have the full selection list,
therefore the column object cannot not be reused, even a copy of it.  The
solution is to systematically recreate the column from the field's final
specification, except for function fields that have no sensible way for being
recreated.
This commit is contained in:
Raphael Collet 2014-10-08 10:47:25 +02:00
parent 10142d7dc7
commit 619a844428
6 changed files with 55 additions and 42 deletions

View File

@ -8,7 +8,10 @@
'maintainer': 'OpenERP SA', 'maintainer': 'OpenERP SA',
'website': 'http://www.openerp.com', 'website': 'http://www.openerp.com',
'depends': ['base'], 'depends': ['base'],
'data': ['ir.model.access.csv'], 'data': [
'ir.model.access.csv',
'demo_data.xml',
],
'installable': True, 'installable': True,
'auto_install': False, 'auto_install': False,
} }

View File

@ -0,0 +1,23 @@
<openerp>
<data>
<record id="mother_a" model="test.inherit.mother">
<field name="name">Mother A</field>
<field name="state">a</field>
</record>
<record id="mother_b" model="test.inherit.mother">
<field name="name">Mother B</field>
<field name="state">b</field>
</record>
<record id="mother_c" model="test.inherit.mother">
<field name="name">Mother C</field>
<field name="state">c</field>
</record>
<record id="mother_d" model="test.inherit.mother">
<field name="name">Mother D</field>
<field name="state">d</field>
</record>
</data>
</openerp>

View File

@ -8,10 +8,10 @@ class mother(models.Model):
_columns = { _columns = {
# check interoperability of field inheritance with old-style fields # check interoperability of field inheritance with old-style fields
'name': osv.fields.char('Name', required=True), 'name': osv.fields.char('Name', required=True),
'state': osv.fields.selection([('a', 'A'), ('b', 'B')], string='State'),
} }
surname = fields.Char(compute='_compute_surname') surname = fields.Char(compute='_compute_surname')
state = fields.Selection([('a', 'A'), ('b', 'B')])
@api.one @api.one
@api.depends('name') @api.depends('name')

View File

@ -43,10 +43,12 @@ class test_inherits(common.TransactionCase):
def test_selection_extension(self): def test_selection_extension(self):
""" check that attribute selection_add=... extends selection on fields. """ """ check that attribute selection_add=... extends selection on fields. """
mother = self.env['test.inherit.mother'] mother = self.env['test.inherit.mother']
field = mother._fields['state']
# the extra values are added # the extra values are added, both in the field and the column
self.assertEqual(field.selection, [('a', 'A'), ('b', 'B'), ('c', 'C'), ('d', 'D')]) self.assertEqual(mother._fields['state'].selection,
[('a', 'A'), ('b', 'B'), ('c', 'C'), ('d', 'D')])
self.assertEqual(mother._columns['state'].selection,
[('a', 'A'), ('b', 'B'), ('c', 'C'), ('d', 'D')])
# vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4: # vim:expandtab:smartindent:tabstop=4:softtabstop=4:shiftwidth=4:

View File

@ -592,11 +592,9 @@ class Field(object):
def to_column(self): def to_column(self):
""" return a low-level field object corresponding to `self` """ """ return a low-level field object corresponding to `self` """
assert self.store assert self.store
if self.column:
# some columns are registry-dependent, like float fields (digits);
# duplicate them to avoid sharing between registries
return copy(self.column)
# some columns are registry-dependent, like float fields (digits);
# duplicate them to avoid sharing between registries
_logger.debug("Create fields._column for Field %s", self) _logger.debug("Create fields._column for Field %s", self)
args = {} args = {}
for attr, prop in self.column_attrs: for attr, prop in self.column_attrs:
@ -610,6 +608,11 @@ class Field(object):
args['relation'] = self.comodel_name args['relation'] = self.comodel_name
return fields.property(**args) return fields.property(**args)
if isinstance(self.column, fields.function):
# it is too tricky to recreate a function field, so for that case,
# we make a stupid (and possibly incorrect) copy of the column
return copy(self.column)
return getattr(fields, self.type)(**args) return getattr(fields, self.type)(**args)
# properties used by to_column() to create a column instance # properties used by to_column() to create a column instance
@ -1182,12 +1185,11 @@ class Selection(Field):
field = self.related_field field = self.related_field
self.selection = lambda model: field._description_selection(model.env) self.selection = lambda model: field._description_selection(model.env)
def _setup_regular(self, env): def set_class_name(self, cls, name):
super(Selection, self)._setup_regular(env) super(Selection, self).set_class_name(cls, name)
# determine selection (applying extensions) # determine selection (applying 'selection_add' extensions)
cls = type(env[self.model_name])
selection = None selection = None
for field in resolve_all_mro(cls, self.name, reverse=True): for field in resolve_all_mro(cls, name, reverse=True):
if isinstance(field, type(self)): if isinstance(field, type(self)):
# We cannot use field.selection or field.selection_add here # We cannot use field.selection or field.selection_add here
# because those attributes are overridden by `set_class_name`. # because those attributes are overridden by `set_class_name`.
@ -1348,13 +1350,11 @@ class Many2one(_Relational):
def __init__(self, comodel_name=None, string=None, **kwargs): def __init__(self, comodel_name=None, string=None, **kwargs):
super(Many2one, self).__init__(comodel_name=comodel_name, string=string, **kwargs) super(Many2one, self).__init__(comodel_name=comodel_name, string=string, **kwargs)
def _setup_regular(self, env): def set_class_name(self, cls, name):
super(Many2one, self)._setup_regular(env) super(Many2one, self).set_class_name(cls, name)
# self.inverse_fields is populated by the corresponding One2many field
# determine self.delegate # determine self.delegate
self.delegate = self.name in env[self.model_name]._inherits.values() if not self.delegate:
self.delegate = name in cls._inherits.values()
_column_ondelete = property(attrgetter('ondelete')) _column_ondelete = property(attrgetter('ondelete'))
_column_auto_join = property(attrgetter('auto_join')) _column_auto_join = property(attrgetter('auto_join'))

View File

@ -2271,28 +2271,13 @@ class BaseModel(object):
if val is not False: if val is not False:
cr.execute(update_query, (ss[1](val), key)) cr.execute(update_query, (ss[1](val), key))
def _check_selection_field_value(self, cr, uid, field, value, context=None): @api.model
"""Raise except_orm if value is not among the valid values for the selection field""" def _check_selection_field_value(self, field, value):
if self._columns[field]._type == 'reference': """ Check whether value is among the valid values for the given
val_model, val_id_str = value.split(',', 1) selection/reference field, and raise an exception if not.
val_id = False """
try: field = self._fields[field]
val_id = long(val_id_str) field.convert_to_cache(value, self)
except ValueError:
pass
if not val_id:
raise except_orm(_('ValidateError'),
_('Invalid value for reference field "%s.%s" (last part must be a non-zero integer): "%s"') % (self._table, field, value))
val = val_model
else:
val = value
if isinstance(self._columns[field].selection, (tuple, list)):
if val in dict(self._columns[field].selection):
return
elif val in dict(self._columns[field].selection(self, cr, uid, context=context)):
return
raise except_orm(_('ValidateError'),
_('The value "%s" for the field "%s.%s" is not in the selection') % (value, self._name, field))
def _check_removed_columns(self, cr, log=False): def _check_removed_columns(self, cr, log=False):
# iterate on the database columns to drop the NOT NULL constraints # iterate on the database columns to drop the NOT NULL constraints