[IMP] fields: make attribute 'default' callable
[odoo/odoo.git] / openerp / fields.py
index 1af7283..4a9f4a2 100644 (file)
@@ -69,18 +69,6 @@ def resolve_all_mro(cls, name, reverse=False):
             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 = {}
@@ -261,7 +249,9 @@ class Field(object):
     _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 interfaced by 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)
@@ -282,7 +272,7 @@ class Field(object):
     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
@@ -322,6 +312,10 @@ class Field(object):
         attrs.update(self._attrs)       # necessary in case self is not in cls
 
         # initialize `self` with `attrs`
+        if 'default' in attrs and not callable(attrs['default']):
+            # make default callable
+            value = attrs['default']
+            attrs['default'] = lambda recs: value
         if attrs.get('compute'):
             # by default, computed fields are not stored, not copied and readonly
             attrs['store'] = attrs.get('store', False)
@@ -357,7 +351,7 @@ class Field(object):
 
     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
@@ -370,8 +364,8 @@ class Field(object):
             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_done = True
             self._setup(env)
 
     def _setup(self, env):
@@ -419,6 +413,7 @@ class Field(object):
         self.compute = self._compute_related
         self.inverse = self._inverse_related
         if field._description_searchable(env):
+            # 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.)
@@ -470,10 +465,6 @@ class Field(object):
         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
@@ -557,7 +548,20 @@ class Field(object):
         return False
 
     def _description_searchable(self, env):
-        return self._description_store(env) or bool(self.search)
+        if self.store:
+            column = env[self.model_name]._columns.get(self.name)
+            return bool(getattr(column, 'store', True)) or \
+                   bool(getattr(column, '_fnct_search', False))
+        return bool(self.search)
+
+    def _description_sortable(self, env):
+        if self.store:
+            column = env[self.model_name]._columns.get(self.name)
+            return bool(getattr(column, 'store', True))
+        if self.inherited:
+            # self is sortable if the inherited field is itself sortable
+            return self.related_field._description_sortable(env)
+        return False
 
     _description_manual = property(attrgetter('manual'))
     _description_depends = property(attrgetter('depends'))
@@ -592,10 +596,9 @@ class Field(object):
     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
 
+        # some columns are registry-dependent, like float fields (digits);
+        # duplicate them to avoid sharing between registries
         _logger.debug("Create fields._column for Field %s", self)
         args = {}
         for attr, prop in self.column_attrs:
@@ -609,6 +612,11 @@ class Field(object):
             args['relation'] = self.comodel_name
             return fields.property(**args)
 
+        if isinstance(self.column, fields.function):
+            # it is too tricky to recreate a function field, so for that case,
+            # we make a stupid (and possibly incorrect) copy of the column
+            return copy(self.column)
+
         return getattr(fields, self.type)(**args)
 
     # properties used by to_column() to create a column instance
@@ -769,9 +777,13 @@ class Field(object):
         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`. """
@@ -816,7 +828,10 @@ class Field(object):
 
     def determine_default(self, record):
         """ determine the default value of field `self` on `record` """
-        if self.compute:
+        if self.default:
+            value = self.default(record)
+            record._cache[self] = self.convert_to_cache(value, record)
+        elif self.compute:
             self._compute_value(record)
         else:
             record._cache[self] = SpecialValue(self.null(record.env))
@@ -899,6 +914,9 @@ class Integer(Field):
     type = 'integer'
 
     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):
@@ -926,9 +944,16 @@ class Float(Field):
     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.store:
+            column = env[self.model_name]._columns[self.name]
+            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'))
 
@@ -1164,12 +1189,11 @@ 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])
+    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`.
@@ -1330,13 +1354,11 @@ class Many2one(_Relational):
     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'))
@@ -1364,7 +1386,11 @@ class Many2one(_Relational):
             # 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
 
@@ -1434,6 +1460,7 @@ class _RelationalMulti(_Relational):
                         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])
@@ -1664,6 +1691,6 @@ class Id(Field):
 
 # 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