Skip to content
Snippets Groups Projects
fields.py 62.5 KiB
Newer Older
Raphael Collet's avatar
Raphael Collet committed
# -*- coding: utf-8 -*-
##############################################################################
#
#    OpenERP, Open Source Management Solution
#    Copyright (C) 2013-2014 OpenERP (<http://www.openerp.com>).
#
#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU Affero General Public License as
#    published by the Free Software Foundation, either version 3 of the
#    License, or (at your option) any later version.
#
#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU Affero General Public License for more details.
#
#    You should have received a copy of the GNU Affero General Public License
#    along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
##############################################################################

""" High-level objects for fields. """

from copy import copy
from datetime import date, datetime
from functools import partial
from operator import attrgetter
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

DATE_LENGTH = len(date.today().strftime(DATE_FORMAT))
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):
        self.value = value
    def get(self):
        return self.value

class FailedValue(SpecialValue):
    """ Special value that encapsulates an exception instead of a value. """
    def __init__(self, exception):
        self.exception = exception
    def get(self):
        raise self.exception

def _check_value(value):
    """ Return `value`, or call its getter if `value` is a :class:`SpecialValue`. """
    return value.get() if isinstance(value, SpecialValue) else value


def resolve_all_mro(cls, name, reverse=False):
    """ Return the (successively overridden) values of attribute `name` in `cls`
        in mro order, or inverse mro order if `reverse` is true.
    """
    klasses = reversed(cls.__mro__) if reverse else cls.__mro__
    for klass in klasses:
        if name in klass.__dict__:
            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 = {}

    def __init__(cls, name, bases, attrs):
        super(MetaField, cls).__init__(name, bases, attrs)
        if cls.type:
            cls.by_type[cls.type] = cls

        # compute class attributes to avoid calling dir() on fields
        cls.column_attrs = []
        cls.related_attrs = []
        cls.description_attrs = []
        for attr in dir(cls):
            if attr.startswith('_column_'):
                cls.column_attrs.append((attr[8:], attr))
            elif attr.startswith('_related_'):
                cls.related_attrs.append((attr[9:], attr))
            elif attr.startswith('_description_'):
                cls.description_attrs.append((attr[13:], attr))


class Field(object):
    """ The field descriptor contains the field definition, and manages accesses
        and assignments of the corresponding field on records. The following
        attributes may be provided when instanciating a field:

        :param string: the label of the field seen by users (string); if not
            set, the ORM takes the field name in the class (capitalized).

        :param help: the tooltip of the field seen by users (string)

        :param readonly: whether the field is readonly (boolean, by default ``False``)

        :param required: whether the value of the field is required (boolean, by
            default ``False``)

        :param index: whether the field is indexed in database (boolean, by
            default ``False``)

        :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 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.
Raphael Collet's avatar
Raphael Collet committed

        :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)

Raphael Collet's avatar
Raphael Collet committed
        .. _field-computed:

        .. rubric:: Computed fields

        One can define a field whose value is computed instead of simply being
        read from the database. The attributes that are specific to computed
        fields are given below. To define such a field, simply provide a value
        for the attribute `compute`.

        :param compute: name of a method that computes the field

        :param inverse: name of a method that inverses the field (optional)

        :param search: name of a method that implement search on the field (optional)

        :param store: whether the field is stored in database (boolean, by
            default ``False`` on computed fields)

        The methods given for `compute`, `inverse` and `search` are model
        methods. Their signature is shown in the following example::

            upper = fields.Char(compute='_compute_upper',
                                inverse='_inverse_upper',
                                search='_search_upper')

            @api.depends('name')
            def _compute_upper(self):
                for rec in self:
                    self.upper = self.name.upper() if self.name else False

            def _inverse_upper(self):
                for rec in self:
                    self.name = self.upper.lower() if self.upper else False

            def _search_upper(self, operator, value):
                if operator == 'like':
                    operator = 'ilike'
                return [('name', operator, value)]

        The compute method has to assign the field on all records of the invoked
        recordset. The decorator :meth:`openerp.api.depends` must be applied on
        the compute method to specify the field dependencies; those dependencies
        are used to determine when to recompute the field; recomputation is
        automatic and guarantees cache/database consistency. Note that the same
        method can be used for several fields, you simply have to assign all the
        given fields in the method; the method will be invoked once for all
        those fields.

        By default, a computed field is not stored to the database, and is
        computed on-the-fly. Adding the attribute ``store=True`` will store the
        field's values in the database. The advantage of a stored field is that
        searching on that field is done by the database itself. The disadvantage
        is that it requires database updates when the field must be recomputed.

        The inverse method, as its name says, does the inverse of the compute
        method: the invoked records have a value for the field, and you must
        apply the necessary changes on the field dependencies such that the
        computation gives the expected value. Note that a computed field without
        an inverse method is readonly by default.

        The search method is invoked when processing domains before doing an
        actual search on the model. It must return a domain equivalent to the
        condition: `field operator value`.

        .. _field-related:

        .. rubric:: Related fields

        The value of a related field is given by following a sequence of
        relational fields and reading a field on the reached model. The complete
        sequence of fields to traverse is specified by the attribute

        :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.

        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
        fields. Related fields are automatically recomputed when their
        dependencies are modified.

        .. _field-company-dependent:

        .. rubric:: Company-dependent fields

        Formerly known as 'property' fields, the value of those fields depends
        on the company. In other words, users that belong to different companies
        may see different values for the field on a given record.

        :param company_dependent: whether the field is company-dependent (boolean)

        .. _field-incremental-definition:

        .. 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.

        For instance, the second class below only adds a tooltip on the field
        ``state``::

            class First(models.Model):
                _name = 'foo'
                state = fields.Selection([...], required=True)

            class Second(models.Model):
                _inherit = 'foo'
                state = fields.Selection(help="Blah blah blah")

    """
    __metaclass__ = MetaField

    _attrs = None               # dictionary with all field attributes
    _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

    name = None                 # name of the field
    type = None                 # type of the field (string)
    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

    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
Raphael Collet's avatar
Raphael Collet committed
    copyable = 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
    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
    company_dependent = False   # whether `self` is company-dependent (property field)
    default = None              # default value

    string = None               # field label
    help = None                 # field tooltip
    readonly = False
    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
Raphael Collet's avatar
Raphael Collet committed

    def __init__(self, string=None, **kwargs):
        kwargs['string'] = string
        self._attrs = {key: val for key, val in kwargs.iteritems() if val is not None}
        self._free_attrs = []

    def copy(self, **kwargs):
        """ 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 set_class_name(self, cls, name):
        """ Assign the model class and field name of `self`. """
        self.model_name = cls._name
        self.name = name

        # determine all inherited field attributes
        attrs = {}
        for field in resolve_all_mro(cls, name, reverse=True):
            if isinstance(field, type(self)):
                attrs.update(field._attrs)
            else:
                attrs.clear()
        attrs.update(self._attrs)       # necessary in case self is not in cls

        # initialize `self` with `attrs`
        if attrs.get('compute'):
            # by default, computed fields are not stored, not copied and readonly
            attrs['store'] = attrs.get('store', False)
            attrs['copy'] = attrs.get('copy', False)
            attrs['readonly'] = attrs.get('readonly', not attrs.get('inverse'))
        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')

        for attr, value in attrs.iteritems():
            if not hasattr(self, attr):
                self._free_attrs.append(attr)
            setattr(self, attr, value)

        if not self.string:
            self.string = name.replace('_', ' ').capitalize()

        self.reset()

    def __str__(self):
        return "%s.%s" % (self.model_name, self.name)

    def __repr__(self):
        return "%s.%s" % (self.model_name, self.name)

    ############################################################################
    #
    # Field setup
    #

    def reset(self):
        """ Prepare `self` for a new setup. """
        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
        # recompute based on `path`. See method `modified` below for details.
        self._triggers = set()
        self.inverse_field = None

    def setup(self, env):
        """ Complete the setup of `self` (dependencies, recomputation triggers,
            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
            self._setup(env)

    def _setup(self, env):
        """ Do the actual setup of `self`. """
        if self.related:
            self._setup_related(env)
        else:
            self._setup_regular(env)

        # put invalidation/recomputation triggers on field dependencies
        model = env[self.model_name]
Raphael Collet's avatar
Raphael Collet committed
        for path in self.depends:
            self._setup_dependency([], model, path.split('.'))

        # put invalidation triggers on model dependencies
        for dep_model_name, field_names in model._depends.iteritems():
            dep_model = env[dep_model_name]
            for field_name in field_names:
                field = dep_model._fields[field_name]
                field._triggers.add((self, None))
Raphael Collet's avatar
Raphael Collet committed

    #
    # Setup of related fields
    #

    def _setup_related(self, env):
        """ Setup the attributes of a related field. """
        # fix the type of self.related if necessary
        if isinstance(self.related, basestring):
            self.related = tuple(self.related.split('.'))

        # determine the related field, and make sure it is set up
        recs = env[self.model_name]
        for name in self.related[:-1]:
            recs = recs[name]
        field = self.related_field = recs._fields[self.related[-1]]
        field.setup(env)

        # check type consistency
        if self.type != field.type:
            raise Warning("Type of related field %s is inconsistent with %s" % (self, field))

        # determine dependencies, compute, inverse, and search
        self.depends = ('.'.join(self.related),)
        self.compute = self._compute_related
        self.inverse = self._inverse_related
        if field._description_searchable(env):
            self.search = self._search_related
Raphael Collet's avatar
Raphael Collet committed

        # copy attributes from field to self (string, help, etc.)
        for attr, prop in self.related_attrs:
            if not getattr(self, attr):
                setattr(self, attr, getattr(field, prop))

    def _compute_related(self, records):
        """ Compute the related field `self` on `records`. """
        for record, sudo_record in zip(records, records.sudo()):
Raphael Collet's avatar
Raphael Collet committed
            # bypass access rights check when traversing the related path
            value = sudo_record if record.id else record
Raphael Collet's avatar
Raphael Collet committed
            # 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]]

    def _inverse_related(self, records):
        """ Inverse the related field `self` on `records`. """
        for record in records:
            other = record
            # traverse the intermediate fields, and keep at most one record
            for name in self.related[:-1]:
                other = other[name][:1]
            if other:
                other[self.related[-1]] = record[self.name]

    def _search_related(self, records, operator, value):
        """ Determine the domain to search on field `self`. """
        return [('.'.join(self.related), operator, value)]

    # properties used by _setup_related() to copy values from related field
    _related_string = property(attrgetter('string'))
    _related_help = property(attrgetter('help'))
    _related_readonly = property(attrgetter('readonly'))
    _related_groups = property(attrgetter('groups'))

    #
    # Setup of non-related fields
    #

    def _setup_regular(self, env):
        """ Setup the attributes of a non-related field. """
        recs = env[self.model_name]

        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
            self.depends = ()
            for method in resolve_all_mro(type(recs), self.compute, reverse=True):
                self.depends += make_depends(getattr(method, '_depends', ()))
            self.compute = getattr(type(recs), self.compute)
        else:
            self.depends = make_depends(getattr(self.compute, '_depends', ()))

        # convert inverse and search into callables
        if isinstance(self.inverse, basestring):
            self.inverse = getattr(type(recs), self.inverse)
        if isinstance(self.search, basestring):
            self.search = getattr(type(recs), self.search)

    def _setup_dependency(self, path0, model, path1):
        """ Make `self` depend on `model`; `path0 + path1` is a dependency of
            `self`, and `path0` is the sequence of field names from `self.model`
            to `model`.
        """
        env = model.env
        head, tail = path1[0], path1[1:]

        if head == '*':
            # special case: add triggers on all fields of model (except self)
            fields = set(model._fields.itervalues()) - set([self])
        else:
            fields = [model._fields[head]]

        for field in fields:
            if field == self:
                _logger.debug("Field %s is recursively defined", 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'])))

            # 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])))

            # recursively traverse the dependency
            if tail:
                comodel = env[field.comodel_name]
                self._setup_dependency(path0 + [head], comodel, tail)

    @property
    def dependents(self):
        """ Return the computed fields that depend on `self`. """
        return (field for field, path in self._triggers)

    ############################################################################
    #
    # Field description
    #

    def get_description(self, env):
        """ Return a dictionary that describes the field `self`. """
        desc = {'type': self.type}
        for attr, prop in self.description_attrs:
            value = getattr(self, prop)
            if callable(value):
                value = value(env)
            if value is not None:
Raphael Collet's avatar
Raphael Collet committed
                desc[attr] = value
Raphael Collet's avatar
Raphael Collet committed
        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)

Raphael Collet's avatar
Raphael Collet committed
    _description_depends = property(attrgetter('depends'))
    _description_related = property(attrgetter('related'))
    _description_company_dependent = property(attrgetter('company_dependent'))
    _description_readonly = property(attrgetter('readonly'))
    _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'))
Raphael Collet's avatar
Raphael Collet committed

    def _description_string(self, env):
        if self.string and env.lang:
            name = "%s,%s" % (self.model_name, self.name)
            trans = env['ir.translation']._get_source(name, 'field', env.lang)
            return trans or self.string
        return self.string

    def _description_help(self, env):
        if self.help and env.lang:
            name = "%s,%s" % (self.model_name, self.name)
            trans = env['ir.translation']._get_source(name, 'help', env.lang)
            return trans or self.help
        return self.help

    ############################################################################
    #
    # Conversion to column instance
    #

    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

        _logger.debug("Create fields._column for Field %s", self)
        args = {}
        for attr, prop in self.column_attrs:
            args[attr] = getattr(self, prop)
        for attr in self._free_attrs:
            args[attr] = getattr(self, attr)

        if self.company_dependent:
            # company-dependent fields are mapped to former property fields
            args['type'] = self.type
            args['relation'] = self.comodel_name
            return fields.property(**args)

        return getattr(fields, self.type)(**args)

    # properties used by to_column() to create a column instance
    _column_copy = property(attrgetter('copyable'))
    _column_select = property(attrgetter('index'))
    _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'))
Raphael Collet's avatar
Raphael Collet committed

    ############################################################################
    #
    # Conversion of values
    #

    def null(self, env):
        """ return the null value for this field in the given environment """
        return False

    def convert_to_cache(self, value, record, validate=True):
Raphael Collet's avatar
Raphael Collet committed
        """ 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
Raphael Collet's avatar
Raphael Collet committed
        """
        return value

    def convert_to_read(self, value, use_name_get=True):
        """ convert `value` from the cache to a value as returned by method
            :meth:`BaseModel.read`

            :param bool use_name_get: when True, value's diplay name will
                be computed using :meth:`BaseModel.name_get`, if relevant
                for the field
        return False if value is None else value
Raphael Collet's avatar
Raphael Collet committed

    def convert_to_write(self, value, target=None, fnames=None):
        """ convert `value` from the cache to a valid value for method
            :meth:`BaseModel.write`.

            :param target: optional, the record to be modified with this value
            :param fnames: for relational fields only, an optional collection of
                field names to convert
        """
        return self.convert_to_read(value)

    def convert_to_onchange(self, value):
        """ convert `value` from the cache to a valid value for an onchange
            method v7.
        """
        return self.convert_to_write(value)

    def convert_to_export(self, value, env):
        """ convert `value` from the cache to a valid value for export. The
            parameter `env` is given for managing translations.
        """
        if env.context.get('export_raw_data'):
            return value
        return bool(value) and ustr(value)

    def convert_to_display_name(self, value):
        """ convert `value` from the cache to a suitable display name. """
        return ustr(value)

    ############################################################################
    #
    # Descriptor methods
    #

    def __get__(self, record, owner):
        """ return the value of field `self` on `record` """
        if record is None:
            return self         # the field is accessed through the owner class

        if not record:
            # null record -> return the null value for this field
            return self.null(record.env)

        # only a single record may be accessed
        record.ensure_one()

        try:
            return record._cache[self]
        except KeyError:
            pass

        # cache miss, retrieve value
        if record.id:
            # 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)

        # the result should be in cache now
        return record._cache[self]

    def __set__(self, record, value):
        """ set the value of field `self` on `record` """
        env = record.env

        # only a single record may be updated
        record.ensure_one()

        # adapt value to the cache level
        value = self.convert_to_cache(value, record)
Raphael Collet's avatar
Raphael Collet committed

        if env.in_draft or not record.id:
            # determine dependent fields
            spec = self.modified_draft(record)

            # 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)
                record._dirty = True

            # determine more dependent fields, and invalidate them
            if self.relational:
                spec += self.modified_draft(record)
            env.invalidate(spec)

        else:
            # simply write to the database, and update cache
            record.write({self.name: self.convert_to_write(value)})
            record._cache[self] = value

    ############################################################################
    #
    # Computation of field values
    #

    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)
        for field in self.computed_fields:
            records._cache[field] = FailedValue(exc)
            records.env.computed[field].update(records._ids)
        self.compute(records)
        for field in self.computed_fields:
            records.env.computed[field].difference_update(records._ids)

    def compute_value(self, records):
        """ Invoke the compute method on `records`; the results are in cache. """
        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())

    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.depends:
                # this is a stored computed field, check for recomputation
                recs = record._recompute_check(self)
                if recs:
                    # recompute the value (only in cache)
                    self.compute_value(recs)
                    # HACK: if result is in the wrong cache, copy values
                    if recs.env != env:
                        for source, target in zip(recs, recs.with_env(env)):
                            try:
                                values = target._convert_to_cache({
                                    f.name: source[f.name] for f in self.computed_fields
Raphael Collet's avatar
Raphael Collet committed
                            except MissingError as e:
                                values = FailedValue(e)
                            target._cache.update(values)
                    # the result is saved to database by BaseModel.recompute()
                    return

            # read the field from database
            record._prefetch_field(self)

        elif self.compute:
            # this is either a non-stored computed field, or a stored computed
            # field in draft mode
            if self.recursive:
                self.compute_value(record)
            else:
                recs = record._in_cache_without(self)
                self.compute_value(recs)

        else:
            # 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` """
        if self.compute:
            self._compute_value(record)
        else:
            record._cache[self] = SpecialValue(self.null(record.env))

    def determine_inverse(self, records):
        """ Given the value of `self` on `records`, inverse the computation. """
        if self.inverse:
            self.inverse(records)

    def determine_domain(self, records, operator, value):
        """ Return a domain representing a condition on `self`. """
        if self.search:
            return self.search(records, operator, value)
        else:
            return [(self.name, operator, value)]

    ############################################################################
    #
    # Notification when fields are modified
    #

    def modified(self, records):
        """ Notify that field `self` has been modified on `records`: prepare the
            fields/records to recompute, and return a spec indicating what to
            invalidate.
        """
        # invalidate the fields that depend on self, and prepare recomputation
        spec = [(self, records._ids)]
        for field, path in self._triggers:
Raphael Collet's avatar
Raphael Collet committed
                # don't move this line to function top, see log
                env = records.env(user=SUPERUSER_ID, context={'active_test': False})
                target = env[field.model_name].search([(path, 'in', records.ids)])
                if target:
                    spec.append((field, target._ids))
                    target.with_env(records.env)._recompute_todo(field)
            else:
                spec.append((field, None))

        return spec

    def modified_draft(self, records):
        """ Same as :meth:`modified`, but in draft mode. """
        env = records.env

        # invalidate the fields on the records in cache that depend on
        # `records`, except fields currently being computed
        spec = []
        for field, path in self._triggers:
            target = env[field.model_name]
            computed = target.browse(env.computed[field])
            if path == 'id':
                target = records - computed
            elif path:
                target = (target.browse(env.cache[field]) - computed).filtered(
                    lambda rec: rec._mapped_cache(path) & records
                )
Raphael Collet's avatar
Raphael Collet committed
            else:
                target = target.browse(env.cache[field]) - computed
Raphael Collet's avatar
Raphael Collet committed
            if target:
                spec.append((field, target._ids))

        return spec


class Boolean(Field):
    """ Boolean field. """
    type = 'boolean'

    def convert_to_cache(self, value, record, validate=True):
Raphael Collet's avatar
Raphael Collet committed
        return bool(value)

    def convert_to_export(self, value, env):
        if env.context.get('export_raw_data'):
            return value
        return ustr(value)


class Integer(Field):
    """ Integer field. """
    type = 'integer'

    def convert_to_cache(self, value, record, validate=True):
Raphael Collet's avatar
Raphael Collet committed
        return int(value or 0)

    def convert_to_read(self, value, use_name_get=True):
        # Integer values greater than 2^31-1 are not supported in pure XMLRPC,
        # so we have to pass them as floats :-(
        if value and value > xmlrpclib.MAXINT:
            return float(value)
        return value

    def _update(self, records, value):
        # special case, when an integer field is used as inverse for a one2many
        records._cache[self] = value.id or 0


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)

    """
    type = 'float'
    _digits = None              # digits argument passed to class initializer
    digits = None               # digits as computed by setup()

    def __init__(self, string=None, digits=None, **kwargs):
        super(Float, self).__init__(string=string, _digits=digits, **kwargs)

    def _setup_regular(self, env):
        super(Float, self)._setup_regular(env)
        self.digits = self._digits(env.cr) if callable(self._digits) else self._digits

    _related_digits = property(attrgetter('digits'))

    _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)

    def convert_to_cache(self, value, record, validate=True):
Raphael Collet's avatar
Raphael Collet committed
        # 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])
        else:
            return float(value or 0.0)


class _String(Field):
    """ Abstract class for string fields. """
    translate = False

    _column_translate = property(attrgetter('translate'))
    _related_translate = property(attrgetter('translate'))
    _description_translate = property(attrgetter('translate'))


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``)

    """
    type = 'char'
    size = None

    _column_size = property(attrgetter('size'))
    _related_size = property(attrgetter('size'))
    _description_size = property(attrgetter('size'))

    def convert_to_cache(self, value, record, validate=True):
Raphael Collet's avatar
Raphael Collet committed
        return bool(value) and 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``)

    """
    type = 'text'

    def convert_to_cache(self, value, record, validate=True):
Raphael Collet's avatar
Raphael Collet committed
        return bool(value) and ustr(value)


class Html(_String):
    """ Html field. """
    type = 'html'

    def convert_to_cache(self, value, record, validate=True):
Raphael Collet's avatar
Raphael Collet committed
        return bool(value) and html_sanitize(value)


class Date(Field):
    """ Date field. """
    type = 'date'

    @staticmethod
    def today(*args):