:param related: sequence of field names
- The value of some attributes from related fields are automatically taken
- from the source field, when it makes sense. Examples are the attributes
- `string` or `selection` on selection fields.
+ Some field attributes are automatically copied from the source field if
+ they are not redefined: `string`, `help`, `readonly`, `required` (only
+ if all fields in the sequence are required), `groups`, `digits`, `size`,
+ `translate`, `sanitize`, `selection`, `comodel_name`, `domain`,
+ `context`. All semantic-free attributes are copied from the source
+ field.
By default, the values of related fields are not stored to the database.
Add the attribute ``store=True`` to make it stored, just like computed
# traverse the class hierarchy upwards, and take the first field
# definition with a default or _defaults for self
for klass in cls.__mro__:
- field = klass.__dict__.get(name, self)
- if not isinstance(field, type(self)):
- return # klass contains another value overridden by self
+ if name in klass.__dict__:
+ field = klass.__dict__[name]
+ if not isinstance(field, type(self)):
+ # klass contains another value overridden by self
+ return
- if 'default' in field._attrs:
- # take the default in field, and adapt it for cls._defaults
- value = field._attrs['default']
- if callable(value):
- self.default = value
- cls._defaults[name] = lambda model, cr, uid, context: \
- self.convert_to_write(value(model.browse(cr, uid, [], context)))
- else:
- self.default = lambda recs: value
- cls._defaults[name] = value
- return
+ if 'default' in field._attrs:
+ # take the default in field, and adapt it for cls._defaults
+ value = field._attrs['default']
+ if callable(value):
+ from openerp import api
+ self.default = value
+ cls._defaults[name] = api.model(
+ lambda recs: self.convert_to_write(value(recs))
+ )
+ else:
+ self.default = lambda recs: value
+ cls._defaults[name] = value
+ return
defaults = klass.__dict__.get('_defaults') or {}
if name in defaults:
# take the value from _defaults, and adapt it for self.default
value = defaults[name]
- value_func = value if callable(value) else lambda *args: value
+ if callable(value):
+ func = lambda recs: value(recs._model, recs._cr, recs._uid, recs._context)
+ else:
+ func = lambda recs: value
self.default = lambda recs: self.convert_to_cache(
- value_func(recs._model, recs._cr, recs._uid, recs._context),
- recs, validate=False,
+ func(recs), recs, validate=False,
)
cls._defaults[name] = value
return
# put invalidation triggers on model dependencies
for dep_model_name, field_names in model._depends.iteritems():
dep_model = env[dep_model_name]
+ dep_model._setup_fields()
for field_name in field_names:
field = dep_model._fields[field_name]
field._triggers.add((self, None))
recs = env[self.model_name]
fields = []
for name in self.related:
+ recs._setup_fields()
field = recs._fields[name]
- field.setup(env)
recs = recs[name]
fields.append(field)
if not getattr(self, attr):
setattr(self, attr, getattr(field, prop))
+ for attr in field._free_attrs:
+ if attr not in self._free_attrs:
+ self._free_attrs.append(attr)
+ setattr(self, attr, getattr(field, attr))
+
+ # special case for states: copy it only for inherited fields
+ if not self.states and self.inherited:
+ self.states = field.states
+
# special case for required: check if all fields are required
if not self.store and not self.required:
self.required = all(field.required for field in fields)
_related_readonly = property(attrgetter('readonly'))
_related_groups = property(attrgetter('groups'))
+ @property
+ def base_field(self):
+ """ Return the base field of an inherited field, or `self`. """
+ return self.related_field if self.inherited else self
+
#
# Setup of non-related fields
#
env = model.env
head, tail = path1[0], path1[1:]
+ model._setup_fields()
if head == '*':
# special case: add triggers on all fields of model (except self)
fields = set(model._fields.itervalues()) - set([self])
self.recursive = True
continue
- field.setup(env)
-
#_logger.debug("Add trigger on %s to recompute %s", field, self)
field._triggers.add((self, '.'.join(path0 or ['id'])))
def _description_string(self, env):
if self.string and env.lang:
- name = "%s,%s" % (self.model_name, self.name)
+ field = self.base_field
+ name = "%s,%s" % (field.model_name, field.name)
trans = env['ir.translation']._get_source(name, 'field', env.lang)
return trans or self.string
return self.string
if env.in_onchange:
for invf in self.inverse_fields:
invf._update(value, record)
- record._dirty = True
+ record._set_dirty(self.name)
# determine more dependent fields, and invalidate them
if self.relational:
class Integer(Field):
type = 'integer'
+ group_operator = None # operator for aggregating values
+
+ _related_group_operator = property(attrgetter('group_operator'))
+
+ _column_group_operator = property(attrgetter('group_operator'))
def convert_to_cache(self, value, record, validate=True):
if isinstance(value, dict):
type = 'float'
_digits = None # digits argument passed to class initializer
digits = None # digits as computed by setup()
+ group_operator = None # operator for aggregating values
def __init__(self, string=None, digits=None, **kwargs):
super(Float, self).__init__(string=string, _digits=digits, **kwargs)
self._setup_digits(env)
_related_digits = property(attrgetter('digits'))
+ _related_group_operator = property(attrgetter('group_operator'))
_description_digits = property(attrgetter('digits'))
_column_digits = property(lambda self: not callable(self._digits) and self._digits)
_column_digits_compute = property(lambda self: callable(self._digits) and self._digits)
+ _column_group_operator = property(attrgetter('group_operator'))
def convert_to_cache(self, value, record, validate=True):
# apply rounding here, otherwise value in cache may be wrong!
return ustr(value)[:self.size]
class Text(_String):
- """ Text field. Very similar to :class:`~.Char` but used for longer
- contents and displayed as a multiline text box
+ """ Very similar to :class:`~.Char` but used for longer contents, does not
+ have a size and usually displayed as a multiline text box.
:param translate: whether the value of this field can be translated
"""
records._cache[self] = value
def convert_to_cache(self, value, record, validate=True):
- if isinstance(value, (NoneType, int)):
+ if isinstance(value, (NoneType, int, long)):
return record.env[self.comodel_name].browse(value)
if isinstance(value, BaseModel):
if value._name == self.comodel_name and len(value) <= 1:
elif isinstance(value, dict):
return record.env[self.comodel_name].new(value)
else:
- return record.env[self.comodel_name].browse(value)
+ return self.null(record.env)
def convert_to_read(self, value, use_name_get=True):
if use_name_get and value:
# add new and existing records
for record in value:
- if not record.id or record._dirty:
- values = dict((k, v) for k, v in record._cache.iteritems() if k in fnames)
+ if not record.id:
+ values = {k: v for k, v in record._cache.iteritems() if k in fnames}
values = record._convert_to_write(values)
- if not record.id:
- result.append((0, 0, values))
- else:
- result.append((1, record.id, values))
+ result.append((0, 0, values))
+ elif record._is_dirty():
+ values = {k: record._cache[k] for k in record._get_dirty() if k in fnames}
+ values = record._convert_to_write(values)
+ result.append((1, record.id, values))
else:
add_existing(record.id)
if self.inverse_name:
# link self to its inverse field and vice-versa
- invf = env[self.comodel_name]._fields[self.inverse_name]
+ comodel = env[self.comodel_name]
+ comodel._setup_fields()
+ invf = comodel._fields[self.inverse_name]
# In some rare cases, a `One2many` field can link to `Int` field
# (res_model/res_id pattern). Only inverse the field if this is
# a `Many2one` field.