""" High-level objects for fields. """
-from copy import copy
from datetime import date, datetime
from functools import partial
from operator import attrgetter
+from types import NoneType
import logging
import pytz
import xmlrpclib
-from types import NoneType
-
from openerp.tools import float_round, ustr, html_sanitize
from openerp.tools import DEFAULT_SERVER_DATE_FORMAT as DATE_FORMAT
from openerp.tools import DEFAULT_SERVER_DATETIME_FORMAT as DATETIME_FORMAT
yield klass.__dict__[name]
-def default_compute(field, value):
- """ Return a compute function for the given default `value`; `value` is
- either a constant, or a unary function returning the default value.
- """
- name = field.name
- func = value if callable(value) else lambda rec: value
- def compute(recs):
- for rec in recs:
- rec[name] = func(rec)
- return compute
-
-
class MetaField(type):
""" Metaclass for field classes. """
by_type = {}
``one2many`` and computed fields, including property fields and
related fields)
+ :param string oldname: the previous name of this field, so that ORM can rename
+ it automatically at migration
+
.. _field-computed:
.. rubric:: Computed fields
: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
_free_attrs = None # list of semantic-free attribute names
automatic = False # whether the field is automatically created ("magic" field)
- _origin = None # the column or field interfaced by self, if any
+ inherited = False # whether the field is inherited (_inherits)
+ column = None # the column corresponding to the field
+ setup_done = False # whether the field has been set up
name = None # name of the field
type = None # type of the field (string)
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()
+ copy = True # whether the field is copied over by BaseModel.copy()
depends = () # collection of field dependencies
recursive = False # whether self depends on itself
compute = None # compute(recs) computes field on recs
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
+ default = None # default(recs) returns the default value
string = None # field label
help = None # field tooltip
self._attrs = {key: val for key, val in kwargs.iteritems() if val is not None}
self._free_attrs = []
- def copy(self, **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)
- return field
+ def new(self, **kwargs):
+ """ Return a field of the same type as `self`, with its own parameters. """
+ return type(self)(**kwargs)
def set_class_name(self, cls, name):
""" Assign the model class and field name of `self`. """
if attrs.get('related'):
# by default, related fields are not stored
attrs['store'] = attrs.get('store', False)
- if 'copy' in attrs:
- # attribute is copyable because there is also a copy() method
- attrs['copyable'] = attrs.pop('copy')
+
+ # fix for function fields overridden by regular columns
+ if not isinstance(attrs.get('column'), (NoneType, fields.function)):
+ attrs.pop('store', None)
for attr, value in attrs.iteritems():
if not hasattr(self, attr):
self._free_attrs.append(attr)
setattr(self, attr, value)
- if not self.string:
+ if not self.string and not self.related:
+ # related fields get their string from their parent field
self.string = name.replace('_', ' ').capitalize()
+ # determine self.default and cls._defaults in a consistent way
+ self._determine_default(cls, name)
+
self.reset()
+ def _determine_default(self, cls, name):
+ """ Retrieve the default value for `self` in the hierarchy of `cls`, and
+ determine `self.default` and `cls._defaults` accordingly.
+ """
+ self.default = None
+
+ # traverse the class hierarchy upwards, and take the first field
+ # definition with a default or _defaults for self
+ for klass in cls.__mro__:
+ 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):
+ 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]
+ 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(
+ func(recs), recs, validate=False,
+ )
+ cls._defaults[name] = value
+ return
+
def __str__(self):
return "%s.%s" % (self.model_name, self.name)
def reset(self):
""" Prepare `self` for a new setup. """
- self._setup_done = False
+ self.setup_done = False
# self._triggers is a set of pairs (field, path) that represents the
# computed fields that depend on `self`. When `self` is modified, it
# invalidates the cache of each `field`, and registers the records to
and other properties). This method is idempotent: it has no effect
if `self` has already been set up.
"""
- if not self._setup_done:
- self._setup_done = True
+ if not self.setup_done:
self._setup(env)
+ self.setup_done = True
def _setup(self, env):
""" Do the actual setup of `self`. """
# 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))
if isinstance(self.related, basestring):
self.related = tuple(self.related.split('.'))
- # determine the related field, and make sure it is set up
+ # determine the chain of fields, and make sure they are all set up
recs = env[self.model_name]
- for name in self.related[:-1]:
+ fields = []
+ for name in self.related:
+ recs._setup_fields()
+ field = recs._fields[name]
recs = recs[name]
- field = self.related_field = recs._fields[self.related[-1]]
- field.setup(env)
+ fields.append(field)
+
+ self.related_field = field
# check type consistency
if self.type != field.type:
self.depends = ('.'.join(self.related),)
self.compute = self._compute_related
self.inverse = self._inverse_related
- if field._description_searchable(env):
+ if field._description_searchable:
+ # allow searching on self only if the related field is searchable
self.search = self._search_related
# copy attributes from field to self (string, help, etc.)
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)
+
def _compute_related(self, records):
""" Compute the related field `self` on `records`. """
# when related_sudo, bypass access rights checks when reading values
return [('.'.join(self.related), operator, value)]
# properties used by _setup_related() to copy values from related field
+ _related_comodel_name = property(attrgetter('comodel_name'))
_related_string = property(attrgetter('string'))
_related_help = property(attrgetter('help'))
_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
#
def make_depends(deps):
return tuple(deps(recs) if callable(deps) else deps)
- # transform self.default into self.compute
- if self.default is not None and self.compute is None:
- self.compute = default_compute(self, self.default)
-
# convert compute into a callable and determine depends
if isinstance(self.compute, basestring):
# if the compute method has been overridden, concatenate all their _depends
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'])))
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_store = property(attrgetter('store'))
_description_manual = property(attrgetter('manual'))
_description_depends = property(attrgetter('depends'))
_description_related = property(attrgetter('related'))
_description_change_default = property(attrgetter('change_default'))
_description_deprecated = property(attrgetter('deprecated'))
+ @property
+ def _description_searchable(self):
+ return bool(self.store or self.search or (self.column and self.column._fnct_search))
+
+ @property
+ def _description_sortable(self):
+ return self.store or (self.inherited and self.related_field._description_sortable)
+
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
def to_column(self):
""" return a low-level field object corresponding to `self` """
- assert self.store
- if self._origin:
- assert isinstance(self._origin, fields._column)
- return self._origin
+ assert self.store or self.column
+ # determine column parameters
_logger.debug("Create fields._column for Field %s", self)
args = {}
for attr, prop in self.column_attrs:
# company-dependent fields are mapped to former property fields
args['type'] = self.type
args['relation'] = self.comodel_name
- return fields.property(**args)
+ self.column = fields.property(**args)
+ elif self.column:
+ # let the column provide a valid column for the given parameters
+ self.column = self.column.new(**args)
+ else:
+ # create a fresh new column of the right type
+ self.column = getattr(fields, self.type)(**args)
- return getattr(fields, self.type)(**args)
+ return self.column
# properties used by to_column() to create a column instance
- _column_copy = property(attrgetter('copyable'))
+ _column_copy = property(attrgetter('copy'))
_column_select = property(attrgetter('index'))
_column_manual = property(attrgetter('manual'))
_column_string = property(attrgetter('string'))
# normal record -> read or compute value for this field
self.determine_value(record)
else:
- # new record -> compute default value for this field
- record.add_default_value(self)
+ # draft record -> compute the value or let it be null
+ self.determine_draft_value(record)
# the result should be in cache now
return record._cache[self]
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:
def _compute_value(self, records):
""" Invoke the compute method on `records`. """
- # mark the computed fields failed in cache, so that access before
- # computation raises an exception
- exc = Warning("Field %s is accessed before being computed." % self)
+ # initialize the fields to their corresponding null value in cache
for field in self.computed_fields:
- records._cache[field] = FailedValue(exc)
+ records._cache[field] = field.null(records.env)
records.env.computed[field].update(records._ids)
self.compute(records)
for field in self.computed_fields:
with records.env.do_in_draft():
try:
self._compute_value(records)
- except MissingError:
- # some record is missing, retry on existing records only
- self._compute_value(records.exists())
+ except (AccessError, MissingError):
+ # some record is forbidden or missing, retry record by record
+ for record in records:
+ try:
+ self._compute_value(record)
+ except Exception as exc:
+ record._cache[self.name] = FailedValue(exc)
def determine_value(self, record):
""" Determine the value of `self` for `record`. """
env = record.env
- if self.store and not (self.depends and env.in_draft):
- # this is a stored field
+ if self.column and not (self.depends and env.in_draft):
+ # this is a stored field or an old-style function field
if self.depends:
# this is a stored computed field, check for recomputation
recs = record._recompute_check(self)
# this is a non-stored non-computed field
record._cache[self] = self.null(env)
- def determine_default(self, record):
- """ determine the default value of field `self` on `record` """
+ def determine_draft_value(self, record):
+ """ Determine the value of `self` for the given draft `record`. """
if self.compute:
self._compute_value(record)
else:
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):
+ # special case, when an integer field is used as inverse for a one2many
+ return value.get('id', False)
return int(value or 0)
def convert_to_read(self, value, use_name_get=True):
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)
+ def _setup_digits(self, env):
+ """ Setup the digits for `self` and its corresponding column """
+ self.digits = self._digits(env.cr) if callable(self._digits) else self._digits
+ if self.digits:
+ assert isinstance(self.digits, (tuple, list)) and len(self.digits) >= 2, \
+ "Float field %s with digits %r, expecting (total, decimal)" % (self, self.digits)
+ if self.column:
+ self.column.digits_change(env.cr)
+
def _setup_regular(self, env):
super(Float, self)._setup_regular(env)
- self.digits = self._digits(env.cr) if callable(self._digits) else self._digits
+ 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!
type = 'char'
size = None
+ def _setup(self, env):
+ super(Char, self)._setup(env)
+ assert isinstance(self.size, (NoneType, int)), \
+ "Char field %s with non-integer size %r" % (self, self.size)
+
_column_size = property(attrgetter('size'))
_related_size = property(attrgetter('size'))
_description_size = property(attrgetter('size'))
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
"""
selection = api.expected(api.model, selection)
super(Selection, self).__init__(selection=selection, string=string, **kwargs)
+ def _setup(self, env):
+ super(Selection, self)._setup(env)
+ assert self.selection is not None, "Field %s without selection" % self
+
def _setup_related(self, env):
super(Selection, self)._setup_related(env)
# selection must be computed on related 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])
+ def set_class_name(self, cls, name):
+ super(Selection, self).set_class_name(cls, name)
+ # determine selection (applying 'selection_add' extensions)
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)):
# We cannot use field.selection or field.selection_add here
# because those attributes are overridden by `set_class_name`.
name = "%s,%s" % (self.model_name, self.name)
translate = partial(
env['ir.translation']._get_source, name, 'selection', env.lang)
- return [(value, translate(label)) for value, label in selection]
+ return [(value, translate(label) if label else label) for value, label in selection]
else:
return selection
class Reference(Selection):
type = 'reference'
- size = 128
+ size = None
def __init__(self, selection=None, string=None, **kwargs):
super(Reference, self).__init__(selection=selection, string=string, **kwargs)
+ def _setup(self, env):
+ super(Reference, self)._setup(env)
+ assert isinstance(self.size, (NoneType, int)), \
+ "Reference field %s with non-integer size %r" % (self, self.size)
+
_related_size = property(attrgetter('size'))
_column_size = property(attrgetter('size'))
domain = None # domain for searching values
context = None # context for searching values
+ def _setup(self, env):
+ super(_Relational, self)._setup(env)
+ assert self.comodel_name in env.registry, \
+ "Field %s with unknown comodel_name %r" % (self, self.comodel_name)
+
+ @property
+ def _related_domain(self):
+ if callable(self.domain):
+ # will be called with another model than self's
+ return lambda recs: self.domain(recs.env[self.model_name])
+ else:
+ # maybe not correct if domain is a string...
+ return self.domain
+
+ _related_context = property(attrgetter('context'))
+
_description_relation = property(attrgetter('comodel_name'))
_description_context = property(attrgetter('context'))
def __init__(self, comodel_name=None, string=None, **kwargs):
super(Many2one, self).__init__(comodel_name=comodel_name, string=string, **kwargs)
- def _setup_regular(self, env):
- super(Many2one, self)._setup_regular(env)
-
- # self.inverse_fields is populated by the corresponding One2many field
-
+ def set_class_name(self, cls, name):
+ super(Many2one, self).set_class_name(cls, name)
# 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_auto_join = property(attrgetter('auto_join'))
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:
# evaluate name_get() as superuser, because the visibility of a
# many2one field value (id and name) depends on the current record's
# access rights, and not the value's access rights.
- return value.sudo().name_get()[0]
+ try:
+ return value.sudo().name_get()[0]
+ except MissingError:
+ # Should not happen, unless the foreign key is missing.
+ return False
else:
return value.id
def convert_to_display_name(self, value):
return ustr(value.display_name)
- def determine_default(self, record):
- super(Many2one, self).determine_default(record)
- if self.delegate:
- # special case: fields that implement inheritance between models
- value = record[self.name]
- if not value:
- # the default value cannot be null, use a new record instead
- 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,
result += result.new(command[2])
elif command[0] == 1:
result.browse(command[1]).update(command[2])
+ result += result.browse(command[1]) - result
elif command[0] == 2:
# note: the record will be deleted by write()
result -= result.browse(command[1])
# 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)
inverse_name = None # name of the inverse field
auto_join = False # whether joins are generated upon search
limit = None # optional limit to use upon read
- copyable = False # o2m are not copied by default
+ copy = False # o2m are not copied by default
def __init__(self, comodel_name=None, inverse_name=None, string=None, **kwargs):
super(One2many, self).__init__(
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.
def _setup_regular(self, env):
super(Many2many, self)._setup_regular(env)
- if self.store and not self.relation:
- model = env[self.model_name]
- column = model._columns[self.name]
- if not isinstance(column, fields.function):
- self.relation, self.column1, self.column2 = column._sql_names(model)
+ if not self.relation:
+ if isinstance(self.column, fields.many2many):
+ self.relation, self.column1, self.column2 = \
+ self.column._sql_names(env[self.model_name])
if self.relation:
m2m = env.registry._m2m
super(Id, self).__init__(type='integer', string=string, **kwargs)
def to_column(self):
- """ to_column() -> fields._column
-
- Whatever
- """
- return fields.integer('ID')
+ self.column = fields.integer('ID')
+ return self.column
def __get__(self, record, owner):
if record is None:
# imported here to avoid dependency cycle issues
from openerp import SUPERUSER_ID
-from .exceptions import Warning, MissingError
+from .exceptions import Warning, AccessError, MissingError
from .models import BaseModel, MAGIC_COLUMNS
from .osv import fields