[IMP] models: add check for common conversion error in field definitions
[odoo/odoo.git] / openerp / models.py
index 95b4ce1..ad91afe 100644 (file)
@@ -241,6 +241,11 @@ class MetaModel(api.Meta):
         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__:
@@ -516,7 +521,7 @@ class BaseModel(object):
         """
         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
@@ -700,50 +705,46 @@ class BaseModel(object):
                     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):
@@ -756,12 +757,8 @@ class BaseModel(object):
         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)
 
@@ -815,39 +812,17 @@ class BaseModel(object):
         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 = {}
 
@@ -2890,46 +2865,33 @@ class BaseModel(object):
     #
 
     @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):
@@ -2969,36 +2931,76 @@ class BaseModel(object):
     @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.
 
@@ -3006,16 +3008,14 @@ class BaseModel(object):
         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:
@@ -3024,14 +3024,15 @@ class BaseModel(object):
                 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
 
@@ -3607,38 +3608,67 @@ class BaseModel(object):
         :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
@@ -4666,6 +4696,8 @@ class BaseModel(object):
         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)
@@ -5221,13 +5253,14 @@ class BaseModel(object):
 
     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
@@ -5266,12 +5299,20 @@ class BaseModel(object):
             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`. """
@@ -5307,22 +5348,24 @@ class BaseModel(object):
         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
@@ -5724,7 +5767,7 @@ class BaseModel(object):
                     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),