summaryrefslogtreecommitdiff
path: root/openerp/fields.py
diff options
context:
space:
mode:
Diffstat (limited to 'openerp/fields.py')
-rw-r--r--openerp/fields.py376
1 files changed, 230 insertions, 146 deletions
diff --git a/openerp/fields.py b/openerp/fields.py
index 08270ae52f0..3c4744a89e0 100644
--- a/openerp/fields.py
+++ b/openerp/fields.py
@@ -40,7 +40,6 @@ DATETIME_LENGTH = len(datetime.now().strftime(DATETIME_FORMAT))
_logger = logging.getLogger(__name__)
-
class SpecialValue(object):
""" Encapsulates a value in the cache in place of a normal value. """
def __init__(self, value):
@@ -125,12 +124,21 @@ class Field(object):
:param default: the default value for the field; this is either a static
value, or a function taking a recordset and returning a value
- :param states: a dictionary mapping state values to lists of attribute-value
- pairs; possible attributes are: 'readonly', 'required', 'invisible'
+ :param states: a dictionary mapping state values to lists of UI attribute-value
+ pairs; possible attributes are: 'readonly', 'required', 'invisible'.
+ Note: Any state-based condition requires the ``state`` field value to be
+ available on the client-side UI. This is typically done by including it in
+ the relevant views, possibly made invisible if not relevant for the
+ end-user.
:param groups: comma-separated list of group xml ids (string); this
restricts the field access to the users of the given groups only
+ :param bool copy: whether the field value should be copied when the record
+ is duplicated (default: ``True`` for normal fields, ``False`` for
+ ``one2many`` and computed fields, including property fields and
+ related fields)
+
.. _field-computed:
.. rubric:: Computed fields
@@ -228,11 +236,12 @@ class Field(object):
.. rubric:: Incremental definition
- A field is defined as class attribute on a model class. If the model is
- extended (see :class:`BaseModel`), one can also extend the field
- definition by redefining a field with the same name and same type on the
- subclass. In that case, the attributes of the field are taken from the
- parent class and overridden by the ones given in subclasses.
+ A field is defined as class attribute on a model class. If the model
+ is extended (see :class:`~openerp.models.Model`), one can also extend
+ the field definition by redefining a field with the same name and same
+ type on the subclass. In that case, the attributes of the field are
+ taken from the parent class and overridden by the ones given in
+ subclasses.
For instance, the second class below only adds a tooltip on the field
``state``::
@@ -259,10 +268,11 @@ class Field(object):
relational = False # whether the field is a relational one
model_name = None # name of the model of this field
comodel_name = None # name of the model of values (if relational)
- inverse_field = None # inverse field (object), if it exists
+ inverse_fields = None # list of inverse fields (objects)
store = True # whether the field is stored in database
index = False # whether the field is indexed in database
+ manual = False # whether the field is a custom field
copyable = True # whether the field is copied over by BaseModel.copy()
depends = () # collection of field dependencies
recursive = False # whether self depends on itself
@@ -270,6 +280,7 @@ class Field(object):
inverse = None # inverse(recs) inverses field on recs
search = None # search(recs, operator, value) searches on self
related = None # sequence of field names, for related fields
+ related_sudo = True # whether related fields should be read as admin
company_dependent = False # whether `self` is company-dependent (property field)
default = None # default value
@@ -279,6 +290,8 @@ class Field(object):
required = False
states = None
groups = False # csv list of group xml ids
+ change_default = None # whether the field may trigger a "user-onchange"
+ deprecated = None # whether the field is ... deprecated
def __init__(self, string=None, **kwargs):
kwargs['string'] = string
@@ -286,7 +299,9 @@ class Field(object):
self._free_attrs = []
def copy(self, **kwargs):
- """ make a copy of `self`, possibly modified with parameters `kwargs` """
+ """ copy(item) -> test
+
+ make a copy of `self`, possibly modified with parameters `kwargs` """
field = copy(self)
field._attrs = {key: val for key, val in kwargs.iteritems() if val is not None}
field._free_attrs = list(self._free_attrs)
@@ -348,7 +363,7 @@ class Field(object):
# invalidates the cache of each `field`, and registers the records to
# recompute based on `path`. See method `modified` below for details.
self._triggers = set()
- self.inverse_field = None
+ self.inverse_fields = []
def setup(self, env):
""" Complete the setup of `self` (dependencies, recomputation triggers,
@@ -403,7 +418,8 @@ class Field(object):
self.depends = ('.'.join(self.related),)
self.compute = self._compute_related
self.inverse = self._inverse_related
- self.search = self._search_related
+ if field._description_searchable(env):
+ self.search = self._search_related
# copy attributes from field to self (string, help, etc.)
for attr, prop in self.related_attrs:
@@ -412,13 +428,16 @@ class Field(object):
def _compute_related(self, records):
""" Compute the related field `self` on `records`. """
- for record in records:
- # bypass access rights check when traversing the related path
- value = record.sudo() if record.id else record
- # traverse the intermediate fields, and keep at most one record
+ # when related_sudo, bypass access rights checks when reading values
+ others = records.sudo() if self.related_sudo else records
+ for record, other in zip(records, others):
+ if not record.id:
+ # draft record, do not switch to another environment
+ other = record
+ # traverse the intermediate fields; follow the first record at each step
for name in self.related[:-1]:
- value = value[name][:1]
- record[self.name] = value[self.related[-1]]
+ other = other[name][:1]
+ record[self.name] = other[self.related[-1]]
def _inverse_related(self, records):
""" Inverse the related field `self` on `records`. """
@@ -496,10 +515,10 @@ class Field(object):
#_logger.debug("Add trigger on %s to recompute %s", field, self)
field._triggers.add((self, '.'.join(path0 or ['id'])))
- # add trigger on inverse field, too
- if field.inverse_field:
- #_logger.debug("Add trigger on %s to recompute %s", field.inverse_field, self)
- field.inverse_field._triggers.add((self, '.'.join(path0 + [head])))
+ # add trigger on inverse fields, too
+ for invf in field.inverse_fields:
+ #_logger.debug("Add trigger on %s to recompute %s", invf, self)
+ invf._triggers.add((self, '.'.join(path0 + [head])))
# recursively traverse the dependency
if tail:
@@ -519,23 +538,28 @@ class Field(object):
def get_description(self, env):
""" Return a dictionary that describes the field `self`. """
desc = {'type': self.type}
- # determine 'store'
- if self.store:
- # if the corresponding column is a function field, check the column
- column = env[self.model_name]._columns.get(self.name)
- desc['store'] = bool(getattr(column, 'store', True))
- else:
- desc['store'] = False
- # determine other attributes
for attr, prop in self.description_attrs:
value = getattr(self, prop)
if callable(value):
value = value(env)
- if value:
+ if value is not None:
desc[attr] = value
+
return desc
# properties used by get_description()
+
+ def _description_store(self, env):
+ if self.store:
+ # if the corresponding column is a function field, check the column
+ column = env[self.model_name]._columns.get(self.name)
+ return bool(getattr(column, 'store', True))
+ return False
+
+ def _description_searchable(self, env):
+ return self._description_store(env) or bool(self.search)
+
+ _description_manual = property(attrgetter('manual'))
_description_depends = property(attrgetter('depends'))
_description_related = property(attrgetter('related'))
_description_company_dependent = property(attrgetter('company_dependent'))
@@ -543,6 +567,8 @@ class Field(object):
_description_required = property(attrgetter('required'))
_description_states = property(attrgetter('states'))
_description_groups = property(attrgetter('groups'))
+ _description_change_default = property(attrgetter('change_default'))
+ _description_deprecated = property(attrgetter('deprecated'))
def _description_string(self, env):
if self.string and env.lang:
@@ -588,12 +614,15 @@ class Field(object):
# properties used by to_column() to create a column instance
_column_copy = property(attrgetter('copyable'))
_column_select = property(attrgetter('index'))
+ _column_manual = property(attrgetter('manual'))
_column_string = property(attrgetter('string'))
_column_help = property(attrgetter('help'))
_column_readonly = property(attrgetter('readonly'))
_column_required = property(attrgetter('required'))
_column_states = property(attrgetter('states'))
_column_groups = property(attrgetter('groups'))
+ _column_change_default = property(attrgetter('change_default'))
+ _column_deprecated = property(attrgetter('deprecated'))
############################################################################
#
@@ -604,11 +633,13 @@ class Field(object):
""" return the null value for this field in the given environment """
return False
- def convert_to_cache(self, value, env, validate=True):
+ def convert_to_cache(self, value, record, validate=True):
""" convert `value` to the cache level in `env`; `value` may come from
an assignment, or have the format of methods :meth:`BaseModel.read`
or :meth:`BaseModel.write`
+ :param record: the target record for the assignment, or an empty recordset
+
:param bool validate: when True, field-specific validation of
`value` will be performed
"""
@@ -693,7 +724,7 @@ class Field(object):
record.ensure_one()
# adapt value to the cache level
- value = self.convert_to_cache(value, env)
+ value = self.convert_to_cache(value, record)
if env.in_draft or not record.id:
# determine dependent fields
@@ -702,8 +733,8 @@ class Field(object):
# set value in cache, inverse field, and mark record as dirty
record._cache[self] = value
if env.in_onchange:
- if self.inverse_field:
- self.inverse_field._update(value, record)
+ for invf in self.inverse_fields:
+ invf._update(value, record)
record._dirty = True
# determine more dependent fields, and invalidate them
@@ -853,10 +884,9 @@ class Field(object):
class Boolean(Field):
- """ Boolean field. """
type = 'boolean'
- def convert_to_cache(self, value, env, validate=True):
+ def convert_to_cache(self, value, record, validate=True):
return bool(value)
def convert_to_export(self, value, env):
@@ -866,10 +896,9 @@ class Boolean(Field):
class Integer(Field):
- """ Integer field. """
type = 'integer'
- def convert_to_cache(self, value, env, validate=True):
+ def convert_to_cache(self, value, record, validate=True):
return int(value or 0)
def convert_to_read(self, value, use_name_get=True):
@@ -885,11 +914,10 @@ class Integer(Field):
class Float(Field):
- """ Float field. The precision digits are given by the attribute
-
- :param digits: a pair (total, decimal), or a function taking a database
- cursor and returning a pair (total, decimal)
+ """ The precision digits are given by the attribute
+ :param digits: a pair (total, decimal), or a function taking a database
+ cursor and returning a pair (total, decimal)
"""
type = 'float'
_digits = None # digits argument passed to class initializer
@@ -909,7 +937,7 @@ class Float(Field):
_column_digits = property(lambda self: not callable(self._digits) and self._digits)
_column_digits_compute = property(lambda self: callable(self._digits) and self._digits)
- def convert_to_cache(self, value, env, validate=True):
+ def convert_to_cache(self, value, record, validate=True):
# apply rounding here, otherwise value in cache may be wrong!
if self.digits:
return float_round(float(value or 0.0), precision_digits=self.digits[1])
@@ -927,14 +955,11 @@ class _String(Field):
class Char(_String):
- """ Char field.
-
- :param size: the maximum size of values stored for that field (integer,
- optional)
-
- :param translate: whether the value of the field has translations
- (boolean, by default ``False``)
+ """ Basic string field, can be length-limited, usually displayed as a
+ single-line string in clients
+ :param int size: the maximum size of values stored for that field
+ :param bool translate: whether the values of this field can be translated
"""
type = 'char'
size = None
@@ -943,34 +968,41 @@ class Char(_String):
_related_size = property(attrgetter('size'))
_description_size = property(attrgetter('size'))
- def convert_to_cache(self, value, env, validate=True):
- return bool(value) and ustr(value)[:self.size]
-
+ def convert_to_cache(self, value, record, validate=True):
+ if value is None or value is False:
+ return False
+ return ustr(value)[:self.size]
class Text(_String):
- """ Text field. Very similar to :class:`Char`, but typically for longer
- contents.
-
- :param translate: whether the value of the field has translations
- (boolean, by default ``False``)
+ """ Text field. Very similar to :class:`~.Char` but used for longer
+ contents and displayed as a multiline text box
+ :param translate: whether the value of this field can be translated
"""
type = 'text'
- def convert_to_cache(self, value, env, validate=True):
- return bool(value) and ustr(value)
-
+ def convert_to_cache(self, value, record, validate=True):
+ if value is None or value is False:
+ return False
+ return ustr(value)
class Html(_String):
- """ Html field. """
type = 'html'
+ sanitize = True # whether value must be sanitized
- def convert_to_cache(self, value, env, validate=True):
- return bool(value) and html_sanitize(value)
+ _column_sanitize = property(attrgetter('sanitize'))
+ _related_sanitize = property(attrgetter('sanitize'))
+ _description_sanitize = property(attrgetter('sanitize'))
+
+ def convert_to_cache(self, value, record, validate=True):
+ if value is None or value is False:
+ return False
+ if validate and self.sanitize:
+ return html_sanitize(value)
+ return value
class Date(Field):
- """ Date field. """
type = 'date'
@staticmethod
@@ -1014,12 +1046,15 @@ class Date(Field):
""" Convert a :class:`date` value into the format expected by the ORM. """
return value.strftime(DATE_FORMAT)
- def convert_to_cache(self, value, env, validate=True):
+ def convert_to_cache(self, value, record, validate=True):
if not value:
return False
if isinstance(value, basestring):
- value = self.from_string(value)
- return value.strftime(DATE_FORMAT)
+ if validate:
+ # force parsing for validation
+ self.from_string(value)
+ return value[:DATE_LENGTH]
+ return self.to_string(value)
def convert_to_export(self, value, env):
if value and env.context.get('export_raw_data'):
@@ -1028,7 +1063,6 @@ class Date(Field):
class Datetime(Field):
- """ Datetime field. """
type = 'datetime'
@staticmethod
@@ -1079,12 +1113,18 @@ class Datetime(Field):
""" Convert a :class:`datetime` value into the format expected by the ORM. """
return value.strftime(DATETIME_FORMAT)
- def convert_to_cache(self, value, env, validate=True):
+ def convert_to_cache(self, value, record, validate=True):
if not value:
return False
if isinstance(value, basestring):
- value = self.from_string(value)
- return value.strftime(DATETIME_FORMAT)
+ if validate:
+ # force parsing for validation
+ self.from_string(value)
+ value = value[:DATETIME_LENGTH]
+ if len(value) == DATE_LENGTH:
+ value += " 00:00:00"
+ return value
+ return self.to_string(value)
def convert_to_export(self, value, env):
if value and env.context.get('export_raw_data'):
@@ -1093,23 +1133,24 @@ class Datetime(Field):
class Binary(Field):
- """ Binary field. """
type = 'binary'
class Selection(Field):
- """ Selection field.
-
- :param selection: specifies the possible values for this field.
- It is given as either a list of pairs (`value`, `string`), or a
- model method, or a method name.
-
- The attribute `selection` is mandatory except in the case of related
- fields (see :ref:`field-related`) or field extensions
- (see :ref:`field-incremental-definition`).
+ """
+ :param selection: specifies the possible values for this field.
+ It is given as either a list of pairs (`value`, `string`), or a
+ model method, or a method name.
+ :param selection_add: provides an extension of the selection in the case
+ of an overridden field. It is a list of pairs (`value`, `string`).
+
+ The attribute `selection` is mandatory except in the case of
+ :ref:`related fields <field-related>` or :ref:`field extensions
+ <field-incremental-definition>`.
"""
type = 'selection'
- selection = None # [(value, string), ...], model method or method name
+ selection = None # [(value, string), ...], function or method name
+ selection_add = None # [(value, string), ...]
def __init__(self, selection=None, string=None, **kwargs):
if callable(selection):
@@ -1123,6 +1164,23 @@ class Selection(Field):
field = self.related_field
self.selection = lambda model: field._description_selection(model.env)
+ def _setup_regular(self, env):
+ super(Selection, self)._setup_regular(env)
+ # determine selection (applying extensions)
+ cls = type(env[self.model_name])
+ selection = None
+ for field in resolve_all_mro(cls, self.name, reverse=True):
+ if isinstance(field, type(self)):
+ # We cannot use field.selection or field.selection_add here
+ # because those attributes are overridden by `set_class_name`.
+ if 'selection' in field._attrs:
+ selection = field._attrs['selection']
+ if 'selection_add' in field._attrs:
+ selection = selection + field._attrs['selection_add']
+ else:
+ selection = None
+ self.selection = selection
+
def _description_selection(self, env):
""" return the selection list (pairs (value, label)); labels are
translated according to context language
@@ -1159,10 +1217,10 @@ class Selection(Field):
selection = selection(env[self.model_name])
return [value for value, _ in selection]
- def convert_to_cache(self, value, env, validate=True):
+ def convert_to_cache(self, value, record, validate=True):
if not validate:
return value or False
- if value in self.get_values(env):
+ if value in self.get_values(record.env):
return value
elif not value:
return False
@@ -1179,16 +1237,6 @@ class Selection(Field):
class Reference(Selection):
- """ Reference field.
-
- :param selection: specifies the possible model names for this field.
- It is given as either a list of pairs (`value`, `string`), or a
- model method, or a method name.
-
- The attribute `selection` is mandatory except in the case of related
- fields (see :ref:`field-related`) or field extensions
- (see :ref:`field-incremental-definition`).
- """
type = 'reference'
size = 128
@@ -1199,14 +1247,14 @@ class Reference(Selection):
_column_size = property(attrgetter('size'))
- def convert_to_cache(self, value, env, validate=True):
+ def convert_to_cache(self, value, record, validate=True):
if isinstance(value, BaseModel):
- if ((not validate or value._name in self.get_values(env))
+ if ((not validate or value._name in self.get_values(record.env))
and len(value) <= 1):
- return value.with_env(env) or False
+ return value.with_env(record.env) or False
elif isinstance(value, basestring):
res_model, res_id = value.split(',')
- return env[res_model].browse(int(res_id))
+ return record.env[res_model].browse(int(res_id))
elif not value:
return False
raise ValueError("Wrong value for %s: %r" % (self, value))
@@ -1241,38 +1289,38 @@ class _Relational(Field):
return env[self.comodel_name]
def modified(self, records):
- # Invalidate cache for self.inverse_field, too. Note that recomputation
- # of fields that depend on self.inverse_field is already covered by the
+ # Invalidate cache for self.inverse_fields, too. Note that recomputation
+ # of fields that depend on self.inverse_fields is already covered by the
# triggers (see above).
spec = super(_Relational, self).modified(records)
- if self.inverse_field:
- spec.append((self.inverse_field, None))
+ for invf in self.inverse_fields:
+ spec.append((invf, None))
return spec
class Many2one(_Relational):
- """ Many2one field; the value of such a field is a recordset of size 0 (no
- record) or 1 (a single record).
+ """ The value of such a field is a recordset of size 0 (no
+ record) or 1 (a single record).
- :param comodel_name: name of the target model (string)
+ :param comodel_name: name of the target model (string)
- :param domain: an optional domain to set on candidate values on the
- client side (domain or string)
+ :param domain: an optional domain to set on candidate values on the
+ client side (domain or string)
- :param context: an optional context to use on the client side when
- handling that field (dictionary)
+ :param context: an optional context to use on the client side when
+ handling that field (dictionary)
- :param ondelete: what to do when the referred record is deleted;
- possible values are: ``'set null'``, ``'restrict'``, ``'cascade'``
+ :param ondelete: what to do when the referred record is deleted;
+ possible values are: ``'set null'``, ``'restrict'``, ``'cascade'``
- :param auto_join: whether JOINs are generated upon search through that
- field (boolean, by default ``False``)
+ :param auto_join: whether JOINs are generated upon search through that
+ field (boolean, by default ``False``)
- :param delegate: set it to ``True`` to make fields of the target model
- accessible from the current model (corresponds to ``_inherits``)
+ :param delegate: set it to ``True`` to make fields of the target model
+ accessible from the current model (corresponds to ``_inherits``)
- The attribute `comodel_name` is mandatory except in the case of related
- fields or field extensions.
+ The attribute `comodel_name` is mandatory except in the case of related
+ fields or field extensions.
"""
type = 'many2one'
ondelete = 'set null' # what to do when value is deleted
@@ -1285,7 +1333,7 @@ class Many2one(_Relational):
def _setup_regular(self, env):
super(Many2one, self)._setup_regular(env)
- # self.inverse_field is determined by the corresponding One2many field
+ # self.inverse_fields is populated by the corresponding One2many field
# determine self.delegate
self.delegate = self.name in env[self.model_name]._inherits.values()
@@ -1297,19 +1345,19 @@ class Many2one(_Relational):
""" Update the cached value of `self` for `records` with `value`. """
records._cache[self] = value
- def convert_to_cache(self, value, env, validate=True):
+ def convert_to_cache(self, value, record, validate=True):
if isinstance(value, (NoneType, int)):
- return env[self.comodel_name].browse(value)
+ return record.env[self.comodel_name].browse(value)
if isinstance(value, BaseModel):
if value._name == self.comodel_name and len(value) <= 1:
- return value.with_env(env)
+ return value.with_env(record.env)
raise ValueError("Wrong value for %s: %r" % (self, value))
elif isinstance(value, tuple):
- return env[self.comodel_name].browse(value[0])
+ return record.env[self.comodel_name].browse(value[0])
elif isinstance(value, dict):
- return env[self.comodel_name].new(value)
+ return record.env[self.comodel_name].new(value)
else:
- return env[self.comodel_name].browse(value)
+ return record.env[self.comodel_name].browse(value)
def convert_to_read(self, value, use_name_get=True):
if use_name_get and value:
@@ -1321,7 +1369,7 @@ class Many2one(_Relational):
return value.id
def convert_to_write(self, value, target=None, fnames=None):
- return bool(value) and (value.id or value._convert_to_write(value._cache))
+ return value.id
def convert_to_onchange(self, value):
return value.id
@@ -1342,35 +1390,57 @@ class Many2one(_Relational):
record[self.name] = record.env[self.comodel_name].new()
+class UnionUpdate(SpecialValue):
+ """ Placeholder for a value update; when this value is taken from the cache,
+ it returns ``record[field.name] | value`` and stores it in the cache.
+ """
+ def __init__(self, field, record, value):
+ self.args = (field, record, value)
+
+ def get(self):
+ field, record, value = self.args
+ # in order to read the current field's value, remove self from cache
+ del record._cache[field]
+ # read the current field's value, and update it in cache only
+ record._cache[field] = new_value = record[field.name] | value
+ return new_value
+
+
class _RelationalMulti(_Relational):
""" Abstract class for relational fields *2many. """
def _update(self, records, value):
""" Update the cached value of `self` for `records` with `value`. """
for record in records:
- record._cache[self] = record[self.name] | value
+ if self in record._cache:
+ record._cache[self] = record[self.name] | value
+ else:
+ record._cache[self] = UnionUpdate(self, record, value)
- def convert_to_cache(self, value, env, validate=True):
+ def convert_to_cache(self, value, record, validate=True):
if isinstance(value, BaseModel):
if value._name == self.comodel_name:
- return value.with_env(env)
+ return value.with_env(record.env)
elif isinstance(value, list):
# value is a list of record ids or commands
- result = env[self.comodel_name]
+ if not record.id:
+ record = record.browse() # new record has no value
+ result = record[self.name]
+ # modify result with the commands;
+ # beware to not introduce duplicates in result
for command in value:
if isinstance(command, (tuple, list)):
if command[0] == 0:
result += result.new(command[2])
elif command[0] == 1:
- record = result.browse(command[1])
- record.update(command[2])
- result += record
+ result.browse(command[1]).update(command[2])
elif command[0] == 2:
- pass
+ # note: the record will be deleted by write()
+ result -= result.browse(command[1])
elif command[0] == 3:
- pass
+ result -= result.browse(command[1])
elif command[0] == 4:
- result += result.browse(command[1])
+ result += result.browse(command[1]) - result
elif command[0] == 5:
result = result.browse()
elif command[0] == 6:
@@ -1378,10 +1448,10 @@ class _RelationalMulti(_Relational):
elif isinstance(command, dict):
result += result.new(command)
else:
- result += result.browse(command)
+ result += result.browse(command) - result
return result
elif not value:
- return self.null(env)
+ return self.null(record.env)
raise ValueError("Wrong value for %s: %s" % (self, value))
def convert_to_read(self, value, use_name_get=True):
@@ -1399,10 +1469,10 @@ class _RelationalMulti(_Relational):
add_existing = lambda id: result.append((4, id))
if fnames is None:
- # take all fields in cache, except the inverse of self
+ # take all fields in cache, except the inverses of self
fnames = set(value._fields) - set(MAGIC_COLUMNS)
- if self.inverse_field:
- fnames.discard(self.inverse_field.name)
+ for invf in self.inverse_fields:
+ fnames.discard(invf.name)
# add new and existing records
for record in value:
@@ -1424,6 +1494,15 @@ class _RelationalMulti(_Relational):
def convert_to_display_name(self, value):
raise NotImplementedError()
+ def _compute_related(self, records):
+ """ Compute the related field `self` on `records`. """
+ for record in records:
+ value = record
+ # traverse the intermediate fields, and keep at most one record
+ for name in self.related[:-1]:
+ value = value[name][:1]
+ record[self.name] = value[self.related[-1]]
+
class One2many(_RelationalMulti):
""" One2many field; the value of such a field is the recordset of all the
@@ -1469,8 +1548,8 @@ class One2many(_RelationalMulti):
if self.inverse_name:
# link self to its inverse field and vice-versa
invf = env[self.comodel_name]._fields[self.inverse_name]
- self.inverse_field = invf
- invf.inverse_field = self
+ self.inverse_fields.append(invf)
+ invf.inverse_fields.append(self)
_description_relation_field = property(attrgetter('inverse_name'))
@@ -1540,8 +1619,8 @@ class Many2many(_RelationalMulti):
# if inverse field has already been setup, it is present in m2m
invf = m2m.get((self.relation, self.column2, self.column1))
if invf:
- self.inverse_field = invf
- invf.inverse_field = self
+ self.inverse_fields.append(invf)
+ invf.inverse_fields.append(self)
else:
# add self in m2m, so that its inverse field can find it
m2m[(self.relation, self.column1, self.column2)] = self
@@ -1555,12 +1634,17 @@ class Many2many(_RelationalMulti):
class Id(Field):
""" Special case for field 'id'. """
store = True
+ #: Can't write this!
readonly = True
def __init__(self, string=None, **kwargs):
super(Id, self).__init__(type='integer', string=string, **kwargs)
def to_column(self):
+ """ to_column() -> fields._column
+
+ Whatever
+ """
return fields.integer('ID')
def __get__(self, record, owner):