if not self._custom:
self.module_to_models.setdefault(self._module, []).append(self)
+ # check for new-api conversion error: leave comma after field definition
+ for key, val in attrs.iteritems():
+ if type(val) is tuple and len(val) == 1 and isinstance(val[0], Field):
+ _logger.error("Trailing comma after field definition: %s.%s", self, key)
+
# transform columns into new-style fields (enables field inheritance)
for name, column in self._columns.iteritems():
if name in self.__dict__:
"""
def add(name, field):
""" add `field` with the given `name` if it does not exist yet """
- if name not in cls._columns and name not in cls._fields:
+ if name not in cls._fields:
cls._add_field(name, field)
# cyclic import
pool._store_function[model].sort(key=lambda x: x[4])
@classmethod
- def _init_manual_fields(cls, pool, cr):
+ def _init_manual_fields(cls, cr):
# Check whether the query is already done
- if pool.fields_by_model is not None:
- manual_fields = pool.fields_by_model.get(cls._name, [])
+ if cls.pool.fields_by_model is not None:
+ manual_fields = cls.pool.fields_by_model.get(cls._name, [])
else:
cr.execute('SELECT * FROM ir_model_fields WHERE model=%s AND state=%s', (cls._name, 'manual'))
manual_fields = cr.dictfetchall()
for field in manual_fields:
- if field['name'] in cls._columns:
+ if field['name'] in cls._fields:
continue
attrs = {
+ 'manual': True,
'string': field['field_description'],
'required': bool(field['required']),
'readonly': bool(field['readonly']),
- 'domain': eval(field['domain']) if field['domain'] else None,
- 'size': field['size'] or None,
- 'ondelete': field['on_delete'],
- 'translate': (field['translate']),
- 'manual': True,
- '_prefetch': False,
- #'select': int(field['select_level'])
}
- if field['serialization_field_id']:
- cr.execute('SELECT name FROM ir_model_fields WHERE id=%s', (field['serialization_field_id'],))
- attrs.update({'serialization_field': cr.fetchone()[0], 'type': field['ttype']})
- if field['ttype'] in ['many2one', 'one2many', 'many2many']:
- attrs.update({'relation': field['relation']})
- cls._columns[field['name']] = fields.sparse(**attrs)
- elif field['ttype'] == 'selection':
- cls._columns[field['name']] = fields.selection(eval(field['selection']), **attrs)
- elif field['ttype'] == 'reference':
- cls._columns[field['name']] = fields.reference(selection=eval(field['selection']), **attrs)
+ # FIXME: ignore field['serialization_field_id']
+ if field['ttype'] in ('char', 'text', 'html'):
+ attrs['translate'] = bool(field['translate'])
+ attrs['size'] = field['size'] or None
+ elif field['ttype'] in ('selection', 'reference'):
+ attrs['selection'] = eval(field['selection'])
elif field['ttype'] == 'many2one':
- cls._columns[field['name']] = fields.many2one(field['relation'], **attrs)
+ attrs['comodel_name'] = field['relation']
+ attrs['ondelete'] = field['on_delete']
+ attrs['domain'] = eval(field['domain']) if field['domain'] else None
elif field['ttype'] == 'one2many':
- cls._columns[field['name']] = fields.one2many(field['relation'], field['relation_field'], **attrs)
+ attrs['comodel_name'] = field['relation']
+ attrs['inverse_name'] = field['relation_field']
+ attrs['domain'] = eval(field['domain']) if field['domain'] else None
elif field['ttype'] == 'many2many':
+ attrs['comodel_name'] = field['relation']
_rel1 = field['relation'].replace('.', '_')
_rel2 = field['model'].replace('.', '_')
- _rel_name = 'x_%s_%s_%s_rel' % (_rel1, _rel2, field['name'])
- cls._columns[field['name']] = fields.many2many(field['relation'], _rel_name, 'id1', 'id2', **attrs)
- else:
- cls._columns[field['name']] = getattr(fields, field['ttype'])(**attrs)
+ attrs['relation'] = 'x_%s_%s_%s_rel' % (_rel1, _rel2, field['name'])
+ attrs['column1'] = 'id1'
+ attrs['column2'] = 'id2'
+ attrs['domain'] = eval(field['domain']) if field['domain'] else None
+ cls._add_field(field['name'], Field.by_type[field['ttype']](**attrs))
@classmethod
def _init_constraints_onchanges(cls):
cls._onchange_methods = defaultdict(list)
for attr, func in getmembers(cls, callable):
if hasattr(func, '_constrains'):
- if not all(name in cls._fields for name in func._constrains):
- _logger.warning("@constrains%r parameters must be field names", func._constrains)
cls._constraint_methods.append(func)
if hasattr(func, '_onchange'):
- if not all(name in cls._fields for name in func._onchange):
- _logger.warning("@onchange%r parameters must be field names", func._onchange)
for name in func._onchange:
cls._onchange_methods[name].append(func)
cls._fields = {}
above = cls.__bases__[0]
for attr, field in getmembers(above, Field.__instancecheck__):
- if not field.inherited:
- cls._add_field(attr, field.new())
+ cls._add_field(attr, field.new())
# introduce magic fields
cls._add_magic_fields()
# register stuff about low-level function fields and custom fields
cls._init_function_fields(pool, cr)
- cls._init_manual_fields(pool, cr)
-
- # process _inherits
- cls._inherits_check()
- cls._inherits_reload()
# register constraints and onchange methods
cls._init_constraints_onchanges()
- # check defaults
- for k in cls._defaults:
- assert k in cls._fields, \
- "Model %s has a default for nonexiting field %s" % (cls._name, k)
-
- # restart columns
- for column in cls._columns.itervalues():
- column.restart()
-
- # validate rec_name
- if cls._rec_name:
- assert cls._rec_name in cls._fields, \
- "Invalid rec_name %s for model %s" % (cls._rec_name, cls._name)
- elif 'name' in cls._fields:
- cls._rec_name = 'name'
-
# prepare ormcache, which must be shared by all instances of the model
cls._ormcache = {}
#
@classmethod
- def _inherits_reload_src(cls):
- """ Recompute the _inherit_fields mapping on each _inherits'd child model."""
- for model in cls.pool.values():
- if cls._name in model._inherits:
- model._inherits_reload()
-
- @classmethod
def _inherits_reload(cls):
- """ Recompute the _inherit_fields mapping.
-
- This will also call itself on each inherits'd child model.
+ """ Recompute the _inherit_fields mapping, and inherited fields. """
+ struct = {}
+ fields = {}
+ for parent_model, parent_field in cls._inherits.iteritems():
+ parent = cls.pool[parent_model]
+ # old-api struct for _inherit_fields
+ for name, column in parent._columns.iteritems():
+ struct[name] = (parent_model, parent_field, column, parent_model)
+ for name, source in parent._inherit_fields.iteritems():
+ struct[name] = (parent_model, parent_field, source[2], source[3])
+ # new-api fields for _fields
+ for name, field in parent._fields.iteritems():
+ fields[name] = field.new(
+ inherited=True,
+ related=(parent_field, name),
+ related_sudo=False,
+ )
- """
- res = {}
- for table in cls._inherits:
- other = cls.pool[table]
- for col in other._columns.keys():
- res[col] = (table, cls._inherits[table], other._columns[col], table)
- for col in other._inherit_fields.keys():
- res[col] = (table, cls._inherits[table], other._inherit_fields[col][2], other._inherit_fields[col][3])
- cls._inherit_fields = res
+ # old-api stuff
+ cls._inherit_fields = struct
cls._all_columns = cls._get_column_infos()
- # interface columns with new-style fields
- for attr, column in cls._columns.items():
- if attr not in cls._fields:
- cls._add_field(attr, column.to_field())
-
- # interface inherited fields with new-style fields (note that the
- # reverse order is for being consistent with _all_columns above)
- for parent_model, parent_field in reversed(cls._inherits.items()):
- for attr, field in cls.pool[parent_model]._fields.iteritems():
- if attr not in cls._fields:
- cls._add_field(attr, field.new(
- inherited=True,
- related=(parent_field, attr),
- related_sudo=False,
- ))
-
- cls._inherits_reload_src()
+ # add inherited fields that are not redefined locally
+ for name, field in fields.iteritems():
+ if name not in cls._fields:
+ cls._add_field(name, field)
@classmethod
def _get_column_infos(cls):
@api.model
def _prepare_setup_fields(self):
""" Prepare the setup of fields once the models have been loaded. """
- for field in self._fields.itervalues():
- field.reset()
+ type(self)._setup_done = False
+ for name, field in self._fields.items():
+ if field.inherited:
+ del self._fields[name]
+ else:
+ field.reset()
@api.model
- def _setup_fields(self, partial=False):
+ def _setup_fields(self):
""" Setup the fields (dependency triggers, etc). """
- for field in self._fields.itervalues():
- try:
- field.setup(self.env)
- except Exception:
- if not partial:
- raise
+ cls = type(self)
+ if cls._setup_done:
+ return
+ cls._setup_done = True
+
+ # first make sure that parent models are all set up
+ for parent in self._inherits:
+ self.env[parent]._setup_fields()
+
+ # retrieve custom fields
+ if not self._context.get('_setup_fields_partial'):
+ cls._init_manual_fields(self._cr)
- # update columns (fields may have changed), and column_infos
- for name, field in self._fields.iteritems():
+ # retrieve inherited fields
+ cls._inherits_check()
+ cls._inherits_reload()
+
+ # set up fields
+ for field in cls._fields.itervalues():
+ field.setup(self.env)
+
+ # update columns (fields may have changed)
+ for name, field in cls._fields.iteritems():
if field.column:
- self._columns[name] = field.to_column()
- self._inherits_reload()
+ cls._columns[name] = field.to_column()
# group fields by compute to determine field.computed_fields
fields_by_compute = defaultdict(list)
- for field in self._fields.itervalues():
+ for field in cls._fields.itervalues():
if field.compute:
field.computed_fields = fields_by_compute[field.compute]
field.computed_fields.append(field)
else:
field.computed_fields = []
- def fields_get(self, cr, user, allfields=None, context=None, write_access=True):
- """ fields_get([fields])
+ # check constraints
+ for func in cls._constraint_methods:
+ if not all(name in cls._fields for name in func._constrains):
+ _logger.warning("@constrains%r parameters must be field names", func._constrains)
+ for name in cls._onchange_methods:
+ if name not in cls._fields:
+ func = cls._onchange_methods[name]
+ _logger.warning("@onchange%r parameters must be field names", func._onchange)
+
+ # check defaults
+ for name in cls._defaults:
+ assert name in cls._fields, \
+ "Model %s has a default for nonexiting field %s" % (cls._name, name)
+
+ # validate rec_name
+ if cls._rec_name:
+ assert cls._rec_name in cls._fields, \
+ "Invalid rec_name %s for model %s" % (cls._rec_name, cls._name)
+ elif 'name' in cls._fields:
+ cls._rec_name = 'name'
+ elif 'x_name' in cls._fields:
+ cls._rec_name = 'x_name'
+
+ def fields_get(self, cr, user, allfields=None, context=None, write_access=True, attributes=None):
+ """ fields_get([fields][, attributes])
Return the definition of each field.
dictionaries. The _inherits'd fields are included. The string, help,
and selection (if present) attributes are translated.
- :param cr: database cursor
- :param user: current user id
- :param allfields: list of fields
- :param context: context arguments, like lang, time zone
- :return: dictionary of field dictionaries, each one describing a field of the business object
- :raise AccessError: * if user has no create/write rights on the requested object
-
+ :param allfields: list of fields to document, all if empty or not provided
+ :param attributes: list of description attributes to return for each field, all if empty or not provided
"""
recs = self.browse(cr, user, [], context)
+ has_access = functools.partial(recs.check_access_rights, raise_exception=False)
+ readonly = not (has_access('write') or has_access('create'))
+
res = {}
for fname, field in self._fields.iteritems():
if allfields and fname not in allfields:
continue
if field.groups and not recs.user_has_groups(field.groups):
continue
- res[fname] = field.get_description(recs.env)
- # if user cannot create or modify records, make all fields readonly
- has_access = functools.partial(recs.check_access_rights, raise_exception=False)
- if not (has_access('write') or has_access('create')):
- for description in res.itervalues():
+ description = field.get_description(recs.env)
+ if readonly:
description['readonly'] = True
description['states'] = {}
+ if attributes:
+ description = {k: v for k, v in description.iteritems()
+ if k in attributes}
+ res[fname] = description
return res
:raise ValidateError: if user tries to enter invalid value for a field that is not in selection
:raise UserError: if a loop would be created in a hierarchy of objects a result of the operation (such as setting an object as its own parent)
- .. _openerp/models/relationals/format:
-
- .. note:: Relational fields use a special "commands" format to manipulate their values
-
- This format is a list of command triplets executed sequentially,
- possible command triplets are:
-
- ``(0, _, values: dict)``
- links to a new record created from the provided values
- ``(1, id, values: dict)``
- updates the already-linked record of id ``id`` with the
- provided ``values``
- ``(2, id, _)``
- unlinks and deletes the linked record of id ``id``
- ``(3, id, _)``
- unlinks the linked record of id ``id`` without deleting it
- ``(4, id, _)``
- links to an existing record of id ``id``
- ``(5, _, _)``
- unlinks all records in the relation, equivalent to using
- the command ``3`` on every linked record
- ``(6, _, ids)``
- replaces the existing list of linked records by the provoded
- ones, equivalent to using ``5`` then ``4`` for each id in
- ``ids``)
-
- (in command triplets, ``_`` values are ignored and can be
- anything, generally ``0`` or ``False``)
-
- Any command can be used on :class:`~openerp.fields.Many2many`,
- only ``0``, ``1`` and ``2`` can be used on
- :class:`~openerp.fields.One2many`.
+ * For numeric fields (:class:`~openerp.fields.Integer`,
+ :class:`~openerp.fields.Float`) the value should be of the
+ corresponding type
+ * For :class:`~openerp.fields.Boolean`, the value should be a
+ :class:`python:bool`
+ * For :class:`~openerp.fields.Selection`, the value should match the
+ selection values (generally :class:`python:str`, sometimes
+ :class:`python:int`)
+ * For :class:`~openerp.fields.Many2one`, the value should be the
+ database identifier of the record to set
+ * Other non-relational fields use a string for value
+
+ .. danger::
+
+ for historical and compatibility reasons,
+ :class:`~openerp.fields.Date` and
+ :class:`~openerp.fields.Datetime` fields use strings as values
+ (written and read) rather than :class:`~python:datetime.date` or
+ :class:`~python:datetime.datetime`. These date strings are
+ UTC-only and formatted according to
+ :const:`openerp.tools.misc.DEFAULT_SERVER_DATE_FORMAT` and
+ :const:`openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT`
+ * .. _openerp/models/relationals/format:
+
+ :class:`~openerp.fields.One2many` and
+ :class:`~openerp.fields.Many2many` use a special "commands" format to
+ manipulate the set of records stored in/associated with the field.
+
+ This format is a list of triplets executed sequentially, where each
+ triplet is a command to execute on the set of records. Not all
+ commands apply in all situations. Possible commands are:
+
+ ``(0, _, values)``
+ adds a new record created from the provided ``value`` dict.
+ ``(1, id, values)``
+ updates an existing record of id ``id`` with the values in
+ ``values``. Can not be used in :meth:`~.create`.
+ ``(2, id, _)``
+ removes the record of id ``id`` from the set, then deletes it
+ (from the database). Can not be used in :meth:`~.create`.
+ ``(3, id, _)``
+ removes the record of id ``id`` from the set, but does not
+ delete it. Can not be used on
+ :class:`~openerp.fields.One2many`. Can not be used in
+ :meth:`~.create`.
+ ``(4, id, _)``
+ adds an existing record of id ``id`` to the set. Can not be
+ used on :class:`~openerp.fields.One2many`.
+ ``(5, _, _)``
+ removes all records from the set, equivalent to using the
+ command ``3`` on every record explicitly. Can not be used on
+ :class:`~openerp.fields.One2many`. Can not be used in
+ :meth:`~.create`.
+ ``(6, _, ids)``
+ replaces all existing records in the set by the ``ids`` list,
+ equivalent to using the command ``5`` followed by a command
+ ``4`` for each ``id`` in ``ids``. Can not be used on
+ :class:`~openerp.fields.One2many`.
+
+ .. note:: Values marked as ``_`` in the list above are ignored and
+ can be anything, generally ``0`` or ``False``.
"""
if not self:
return True
trans_obj = self.pool.get('ir.translation')
for field_name, field in self._fields.iteritems():
+ if not field.copy:
+ continue
# removing the lang to compare untranslated values
context_wo_lang = dict(context, lang=None)
old_record, new_record = self.browse(cr, uid, [old_id, new_id], context=context_wo_lang)
def _mapped_func(self, func):
""" Apply function `func` on all records in `self`, and return the
- result as a list or a recordset (if `func` return recordsets).
+ result as a list or a recordset (if `func` returns recordsets).
"""
- vals = [func(rec) for rec in self]
- val0 = vals[0] if vals else func(self)
- if isinstance(val0, BaseModel):
- return reduce(operator.or_, vals, val0)
- return vals
+ if self:
+ vals = [func(rec) for rec in self]
+ return reduce(operator.or_, vals) if isinstance(vals[0], BaseModel) else vals
+ else:
+ vals = func(self)
+ return vals if isinstance(vals, BaseModel) else []
def mapped(self, func):
""" Apply `func` on all records in `self`, and return the result as a
func = lambda rec: filter(None, rec.mapped(name))
return self.browse([rec.id for rec in self if func(rec)])
- def sorted(self, key=None):
- """ Return the recordset `self` ordered by `key` """
+ def sorted(self, key=None, reverse=False):
+ """ Return the recordset `self` ordered by `key`.
+
+ :param key: either a function of one argument that returns a
+ comparison key for each record, or ``None``, in which case
+ records are ordered according the default model's order
+
+ :param reverse: if ``True``, return the result in reverse order
+ """
if key is None:
- return self.search([('id', 'in', self.ids)])
+ recs = self.search([('id', 'in', self.ids)])
+ return self.browse(reversed(recs._ids)) if reverse else recs
else:
- return self.browse(map(int, sorted(self, key=key)))
+ return self.browse(map(int, sorted(self, key=key, reverse=reverse)))
def update(self, values):
""" Update record `self[0]` with `values`. """
return record
#
- # Dirty flag, to mark records modified (in draft mode)
+ # Dirty flags, to mark record fields modified (in draft mode)
#
- @property
- def _dirty(self):
+ def _is_dirty(self):
""" Return whether any record in `self` is dirty. """
dirty = self.env.dirty
return any(record in dirty for record in self)
- @_dirty.setter
- def _dirty(self, value):
- """ Mark the records in `self` as dirty. """
- if value:
- map(self.env.dirty.add, self)
- else:
- map(self.env.dirty.discard, self)
+ def _get_dirty(self):
+ """ Return the list of field names for which `self` is dirty. """
+ dirty = self.env.dirty
+ return list(dirty.get(self, ()))
+
+ def _set_dirty(self, field_name):
+ """ Mark the records in `self` as dirty for the given `field_name`. """
+ dirty = self.env.dirty
+ for record in self:
+ dirty[record].add(field_name)
#
# "Dunder" methods
field = self._fields[name]
newval = record[name]
if field.type in ('one2many', 'many2many'):
- if newval != oldval or newval._dirty:
+ if newval != oldval or newval._is_dirty():
# put new value in result
result['value'][name] = field.convert_to_write(
newval, record._origin, subfields.get(name),