Merge pull request #3645 from odoo-dev/8.0-fix2manyupdateonchanges-chs
authorRaphael Collet <rco@openerp.com>
Tue, 25 Nov 2014 11:42:36 +0000 (12:42 +0100)
committerRaphael Collet <rco@openerp.com>
Tue, 25 Nov 2014 11:42:36 +0000 (12:42 +0100)
[FIX] api: avoid to return all fields *2many in onchanges

1  2 
openerp/api.py
openerp/fields.py
openerp/models.py

diff --combined openerp/api.py
@@@ -2,7 -2,7 +2,7 @@@
  ##############################################################################
  #
  #    OpenERP, Open Source Management Solution
- #    Copyright (C) 2013 OpenERP (<http://www.openerp.com>).
+ #    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
@@@ -717,7 -717,7 +717,7 @@@ class Environment(object)
          self.cache = defaultdict(dict)      # {field: {id: value, ...}, ...}
          self.prefetch = defaultdict(set)    # {model_name: set(id), ...}
          self.computed = defaultdict(set)    # {field: set(id), ...}
-         self.dirty = set()                  # set(record)
+         self.dirty = defaultdict(set)       # {record: set(field_name), ...}
          self.all = envs
          envs.add(self)
          return self
              env.computed.clear()
              env.dirty.clear()
  
 +    def clear(self):
 +        """ Clear all record caches, and discard all fields to recompute.
 +            This may be useful when recovering from a failed ORM operation.
 +        """
 +        self.invalidate_all()
 +        self.all.todo.clear()
 +
 +    @contextmanager
 +    def clear_upon_failure(self):
 +        """ Context manager that clears the environments (caches and fields to
 +            recompute) upon exception.
 +        """
 +        try:
 +            yield
 +        except Exception:
 +            self.clear()
 +            raise
 +
      def field_todo(self, field):
          """ Check whether `field` must be recomputed, and returns a recordset
              with all records to recompute for `field`.
diff --combined openerp/fields.py
@@@ -791,7 -791,7 +791,7 @@@ class Field(object)
              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:
@@@ -1057,8 -1057,8 +1057,8 @@@ class Char(_String)
          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
      """
@@@ -1577,13 -1577,14 +1577,14 @@@ class _RelationalMulti(_Relational)
  
          # 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)
  
diff --combined openerp/models.py
@@@ -2991,11 -2991,9 +2991,11 @@@ class BaseModel(object)
                  "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):
 -        """ fields_get([fields])
 +    def fields_get(self, cr, user, allfields=None, context=None, write_access=True, attributes=None):
 +        """ fields_get([fields][, attributes])
  
          Return the definition of each field.
  
          dictionaries. The _inherits'd fields are included. The string, help,
          and selection (if present) attributes are translated.
  
 -        :param cr: database cursor
 -        :param user: current user id
 -        :param allfields: list of fields
 -        :param context: context arguments, like lang, time zone
 -        :return: dictionary of field dictionaries, each one describing a field of the business object
 -        :raise AccessError: * if user has no create/write rights on the requested object
 -
 +        :param allfields: list of fields to document, all if empty or not provided
 +        :param attributes: list of description attributes to return for each field, all if empty or not provided
          """
          recs = self.browse(cr, user, [], context)
  
 +        has_access = functools.partial(recs.check_access_rights, raise_exception=False)
 +        readonly = not (has_access('write') or has_access('create'))
 +
          res = {}
          for fname, field in self._fields.iteritems():
              if allfields and fname not in allfields:
                  continue
              if field.groups and not recs.user_has_groups(field.groups):
                  continue
 -            res[fname] = field.get_description(recs.env)
  
 -        # if user cannot create or modify records, make all fields readonly
 -        has_access = functools.partial(recs.check_access_rights, raise_exception=False)
 -        if not (has_access('write') or has_access('create')):
 -            for description in res.itervalues():
 +            description = field.get_description(recs.env)
 +            if readonly:
                  description['readonly'] = True
                  description['states'] = {}
 +            if attributes:
 +                description = {k: v for k, v in description.iteritems()
 +                               if k in attributes}
 +            res[fname] = description
  
          return res
  
          :raise ValidateError: if user tries to enter invalid value for a field that is not in selection
          :raise UserError: if a loop would be created in a hierarchy of objects a result of the operation (such as setting an object as its own parent)
  
 -        .. _openerp/models/relationals/format:
 -
 -        .. note:: Relational fields use a special "commands" format to manipulate their values
 -
 -            This format is a list of command triplets executed sequentially,
 -            possible command triplets are:
 -
 -            ``(0, _, values: dict)``
 -                links to a new record created from the provided values
 -            ``(1, id, values: dict)``
 -                updates the already-linked record of id ``id`` with the
 -                provided ``values``
 -            ``(2, id, _)``
 -                unlinks and deletes the linked record of id ``id``
 -            ``(3, id, _)``
 -                unlinks the linked record of id ``id`` without deleting it
 -            ``(4, id, _)``
 -                links to an existing record of id ``id``
 -            ``(5, _, _)``
 -                unlinks all records in the relation, equivalent to using
 -                the command ``3`` on every linked record
 -            ``(6, _, ids)``
 -                replaces the existing list of linked records by the provoded
 -                ones, equivalent to using ``5`` then ``4`` for each id in
 -                ``ids``)
 -
 -            (in command triplets, ``_`` values are ignored and can be
 -            anything, generally ``0`` or ``False``)
 -
 -            Any command can be used on :class:`~openerp.fields.Many2many`,
 -            only ``0``, ``1`` and ``2`` can be used on
 -            :class:`~openerp.fields.One2many`.
 +        * For numeric fields (:class:`~openerp.fields.Integer`,
 +          :class:`~openerp.fields.Float`) the value should be of the
 +          corresponding type
 +        * For :class:`~openerp.fields.Boolean`, the value should be a
 +          :class:`python:bool`
 +        * For :class:`~openerp.fields.Selection`, the value should match the
 +          selection values (generally :class:`python:str`, sometimes
 +          :class:`python:int`)
 +        * For :class:`~openerp.fields.Many2one`, the value should be the
 +          database identifier of the record to set
 +        * Other non-relational fields use a string for value
 +
 +          .. danger::
 +
 +              for historical and compatibility reasons,
 +              :class:`~openerp.fields.Date` and
 +              :class:`~openerp.fields.Datetime` fields use strings as values
 +              (written and read) rather than :class:`~python:datetime.date` or
 +              :class:`~python:datetime.datetime`. These date strings are
 +              UTC-only and formatted according to
 +              :const:`openerp.tools.misc.DEFAULT_SERVER_DATE_FORMAT` and
 +              :const:`openerp.tools.misc.DEFAULT_SERVER_DATETIME_FORMAT`
 +        * .. _openerp/models/relationals/format:
 +
 +          :class:`~openerp.fields.One2many` and
 +          :class:`~openerp.fields.Many2many` use a special "commands" format to
 +          manipulate the set of records stored in/associated with the field.
 +
 +          This format is a list of triplets executed sequentially, where each
 +          triplet is a command to execute on the set of records. Not all
 +          commands apply in all situations. Possible commands are:
 +
 +          ``(0, _, values)``
 +              adds a new record created from the provided ``value`` dict.
 +          ``(1, id, values)``
 +              updates an existing record of id ``id`` with the values in
 +              ``values``. Can not be used in :meth:`~.create`.
 +          ``(2, id, _)``
 +              removes the record of id ``id`` from the set, then deletes it
 +              (from the database). Can not be used in :meth:`~.create`.
 +          ``(3, id, _)``
 +              removes the record of id ``id`` from the set, but does not
 +              delete it. Can not be used on
 +              :class:`~openerp.fields.One2many`. Can not be used in
 +              :meth:`~.create`.
 +          ``(4, id, _)``
 +              adds an existing record of id ``id`` to the set. Can not be
 +              used on :class:`~openerp.fields.One2many`.
 +          ``(5, _, _)``
 +              removes all records from the set, equivalent to using the
 +              command ``3`` on every record explicitly. Can not be used on
 +              :class:`~openerp.fields.One2many`. Can not be used in
 +              :meth:`~.create`.
 +          ``(6, _, ids)``
 +              replaces all existing records in the set by the ``ids`` list,
 +              equivalent to using the command ``5`` followed by a command
 +              ``4`` for each ``id`` in ``ids``. Can not be used on
 +              :class:`~openerp.fields.One2many`.
 +
 +          .. note:: Values marked as ``_`` in the list above are ignored and
 +                    can be anything, generally ``0`` or ``False``.
          """
          if not self:
              return True
          return record
  
      #
-     # Dirty flag, to mark records modified (in draft mode)
+     # Dirty flags, to mark record fields modified (in draft mode)
      #
  
-     @property
-     def _dirty(self):
+     def _is_dirty(self):
          """ Return whether any record in `self` is dirty. """
          dirty = self.env.dirty
          return any(record in dirty for record in self)
  
-     @_dirty.setter
-     def _dirty(self, value):
-         """ Mark the records in `self` as dirty. """
-         if value:
-             map(self.env.dirty.add, self)
-         else:
-             map(self.env.dirty.discard, self)
+     def _get_dirty(self):
+         """ Return the list of field names for which `self` is dirty. """
+         dirty = self.env.dirty
+         return list(dirty.get(self, ()))
+     def _set_dirty(self, field_name):
+         """ Mark the records in `self` as dirty for the given `field_name`. """
+         dirty = self.env.dirty
+         for record in self:
+             dirty[record].add(field_name)
  
      #
      # "Dunder" methods
                      field = self._fields[name]
                      newval = record[name]
                      if field.type in ('one2many', 'many2many'):
-                         if newval != oldval or newval._dirty:
+                         if newval != oldval or newval._is_dirty():
                              # put new value in result
                              result['value'][name] = field.convert_to_write(
                                  newval, record._origin, subfields.get(name),