summaryrefslogtreecommitdiff
path: root/odoo/api.py
diff options
context:
space:
mode:
Diffstat (limited to 'odoo/api.py')
-rw-r--r--odoo/api.py999
1 files changed, 999 insertions, 0 deletions
diff --git a/odoo/api.py b/odoo/api.py
new file mode 100644
index 00000000000..4741adedad8
--- /dev/null
+++ b/odoo/api.py
@@ -0,0 +1,999 @@
+# -*- coding: utf-8 -*-
+# Part of Odoo. See LICENSE file for full copyright and licensing details.
+
+""" This module provides the elements for managing two different API styles,
+ namely the "traditional" and "record" styles.
+
+ In the "traditional" style, parameters like the database cursor, user id,
+ context dictionary and record ids (usually denoted as ``cr``, ``uid``,
+ ``context``, ``ids``) are passed explicitly to all methods. In the "record"
+ style, those parameters are hidden into model instances, which gives it a
+ more object-oriented feel.
+
+ For instance, the statements::
+
+ model = self.pool.get(MODEL)
+ ids = model.search(cr, uid, DOMAIN, context=context)
+ for rec in model.browse(cr, uid, ids, context=context):
+ print rec.name
+ model.write(cr, uid, ids, VALUES, context=context)
+
+ may also be written as::
+
+ env = Environment(cr, uid, context) # cr, uid, context wrapped in env
+ model = env[MODEL] # retrieve an instance of MODEL
+ recs = model.search(DOMAIN) # search returns a recordset
+ for rec in recs: # iterate over the records
+ print rec.name
+ recs.write(VALUES) # update all records in recs
+
+ Methods written in the "traditional" style are automatically decorated,
+ following some heuristics based on parameter names.
+"""
+
+__all__ = [
+ 'Environment',
+ 'Meta', 'guess', 'noguess',
+ 'model', 'multi', 'one',
+ 'model_cr', 'model_cr_context',
+ 'cr', 'cr_context',
+ 'cr_uid', 'cr_uid_context',
+ 'cr_uid_id', 'cr_uid_id_context',
+ 'cr_uid_ids', 'cr_uid_ids_context',
+ 'cr_uid_records', 'cr_uid_records_context',
+ 'constrains', 'depends', 'onchange', 'returns',
+ 'call_kw',
+]
+
+import logging
+from collections import defaultdict, Mapping
+from contextlib import contextmanager
+from inspect import currentframe, getargspec
+from pprint import pformat
+from weakref import WeakSet
+
+from decorator import decorator
+from werkzeug.local import Local, release_local
+
+from openerp.tools import frozendict, classproperty
+
+_logger = logging.getLogger(__name__)
+
+# The following attributes are used, and reflected on wrapping methods:
+# - method._constrains: set by @constrains, specifies constraint dependencies
+# - method._depends: set by @depends, specifies compute dependencies
+# - method._returns: set by @returns, specifies return model
+# - method._onchange: set by @onchange, specifies onchange fields
+# - method.clear_cache: set by @ormcache, used to clear the cache
+#
+# On wrapping method only:
+# - method._api: decorator function, used for re-applying decorator
+# - method._orig: original method
+#
+
+WRAPPED_ATTRS = ('__module__', '__name__', '__doc__', '_constrains',
+ '_depends', '_onchange', '_returns', 'clear_cache')
+
+INHERITED_ATTRS = ('_returns',)
+
+
+class Params(object):
+ def __init__(self, args, kwargs):
+ self.args = args
+ self.kwargs = kwargs
+ def __str__(self):
+ params = []
+ for arg in self.args:
+ params.append(repr(arg))
+ for item in sorted(self.kwargs.iteritems()):
+ params.append("%s=%r" % item)
+ return ', '.join(params)
+
+
+class Meta(type):
+ """ Metaclass that automatically decorates traditional-style methods by
+ guessing their API. It also implements the inheritance of the
+ :func:`returns` decorators.
+ """
+
+ def __new__(meta, name, bases, attrs):
+ # dummy parent class to catch overridden methods decorated with 'returns'
+ parent = type.__new__(meta, name, bases, {})
+
+ for key, value in attrs.items():
+ if not key.startswith('__') and callable(value):
+ # make the method inherit from decorators
+ value = propagate(getattr(parent, key, None), value)
+
+ # guess calling convention if none is given
+ if not hasattr(value, '_api'):
+ try:
+ value = guess(value)
+ except TypeError:
+ pass
+
+ if (getattr(value, '_api', None) or '').startswith('cr'):
+ _logger.warning("Deprecated method %s.%s in module %s", name, key, attrs.get('__module__'))
+
+ attrs[key] = value
+
+ return type.__new__(meta, name, bases, attrs)
+
+
+def attrsetter(attr, value):
+ """ Return a function that sets ``attr`` on its argument and returns it. """
+ return lambda method: setattr(method, attr, value) or method
+
+def propagate(method1, method2):
+ """ Propagate decorators from ``method1`` to ``method2``, and return the
+ resulting method.
+ """
+ if method1:
+ for attr in INHERITED_ATTRS:
+ if hasattr(method1, attr) and not hasattr(method2, attr):
+ setattr(method2, attr, getattr(method1, attr))
+ return method2
+
+
+def constrains(*args):
+ """ Decorates a constraint checker. Each argument must be a field name
+ used in the check::
+
+ @api.one
+ @api.constrains('name', 'description')
+ def _check_description(self):
+ if self.name == self.description:
+ raise ValidationError("Fields name and description must be different")
+
+ Invoked on the records on which one of the named fields has been modified.
+
+ Should raise :class:`~openerp.exceptions.ValidationError` if the
+ validation failed.
+
+ .. warning::
+
+ ``@constrains`` only supports simple field names, dotted names
+ (fields of relational fields e.g. ``partner_id.customer``) are not
+ supported and will be ignored
+ """
+ return attrsetter('_constrains', args)
+
+
+def onchange(*args):
+ """ Return a decorator to decorate an onchange method for given fields.
+ Each argument must be a field name::
+
+ @api.onchange('partner_id')
+ def _onchange_partner(self):
+ self.message = "Dear %s" % (self.partner_id.name or "")
+
+ In the form views where the field appears, the method will be called
+ when one of the given fields is modified. The method is invoked on a
+ pseudo-record that contains the values present in the form. Field
+ assignments on that record are automatically sent back to the client.
+
+ The method may return a dictionary for changing field domains and pop up
+ a warning message, like in the old API::
+
+ return {
+ 'domain': {'other_id': [('partner_id', '=', partner_id)]},
+ 'warning': {'title': "Warning", 'message': "What is this?"},
+ }
+
+
+ .. warning::
+
+ ``@onchange`` only supports simple field names, dotted names
+ (fields of relational fields e.g. ``partner_id.tz``) are not
+ supported and will be ignored
+ """
+ return attrsetter('_onchange', args)
+
+
+def depends(*args):
+ """ Return a decorator that specifies the field dependencies of a "compute"
+ method (for new-style function fields). Each argument must be a string
+ that consists in a dot-separated sequence of field names::
+
+ pname = fields.Char(compute='_compute_pname')
+
+ @api.one
+ @api.depends('partner_id.name', 'partner_id.is_company')
+ def _compute_pname(self):
+ if self.partner_id.is_company:
+ self.pname = (self.partner_id.name or "").upper()
+ else:
+ self.pname = self.partner_id.name
+
+ One may also pass a single function as argument. In that case, the
+ dependencies are given by calling the function with the field's model.
+ """
+ if args and callable(args[0]):
+ args = args[0]
+ elif any('id' in arg.split('.') for arg in args):
+ raise NotImplementedError("Compute method cannot depend on field 'id'.")
+ return attrsetter('_depends', args)
+
+
+def returns(model, downgrade=None, upgrade=None):
+ """ Return a decorator for methods that return instances of ``model``.
+
+ :param model: a model name, or ``'self'`` for the current model
+
+ :param downgrade: a function ``downgrade(self, value, *args, **kwargs)``
+ to convert the record-style ``value`` to a traditional-style output
+
+ :param upgrade: a function ``upgrade(self, value, *args, **kwargs)``
+ to convert the traditional-style ``value`` to a record-style output
+
+ The arguments ``self``, ``*args`` and ``**kwargs`` are the ones passed
+ to the method in the record-style.
+
+ The decorator adapts the method output to the api style: ``id``, ``ids`` or
+ ``False`` for the traditional style, and recordset for the record style::
+
+ @model
+ @returns('res.partner')
+ def find_partner(self, arg):
+ ... # return some record
+
+ # output depends on call style: traditional vs record style
+ partner_id = model.find_partner(cr, uid, arg, context=context)
+
+ # recs = model.browse(cr, uid, ids, context)
+ partner_record = recs.find_partner(arg)
+
+ Note that the decorated method must satisfy that convention.
+
+ Those decorators are automatically *inherited*: a method that overrides
+ a decorated existing method will be decorated with the same
+ ``@returns(model)``.
+ """
+ return attrsetter('_returns', (model, downgrade, upgrade))
+
+
+def downgrade(method, value, self, args, kwargs):
+ """ Convert ``value`` returned by ``method`` on ``self`` to traditional style. """
+ spec = getattr(method, '_returns', None)
+ if not spec:
+ return value
+ _, convert, _ = spec
+ if convert and len(getargspec(convert).args) > 1:
+ return convert(self, value, *args, **kwargs)
+ elif convert:
+ return convert(value)
+ else:
+ return value.ids
+
+
+def aggregate(method, value, self):
+ """ Aggregate record-style ``value`` for a method decorated with ``@one``. """
+ spec = getattr(method, '_returns', None)
+ if spec:
+ # value is a list of instances, concatenate them
+ model, _, _ = spec
+ if model == 'self':
+ return sum(value, self.browse())
+ elif model:
+ return sum(value, self.env[model])
+ return value
+
+
+def split_context(method, args, kwargs):
+ """ Extract the context from a pair of positional and keyword arguments.
+ Return a triple ``context, args, kwargs``.
+ """
+ pos = len(getargspec(method).args) - 1
+ if pos < len(args):
+ return args[pos], args[:pos], kwargs
+ else:
+ return kwargs.pop('context', None), args, kwargs
+
+
+def model(method):
+ """ Decorate a record-style method where ``self`` is a recordset, but its
+ contents is not relevant, only the model is. Such a method::
+
+ @api.model
+ def method(self, args):
+ ...
+
+ may be called in both record and traditional styles, like::
+
+ # recs = model.browse(cr, uid, ids, context)
+ recs.method(args)
+
+ model.method(cr, uid, args, context=context)
+
+ Notice that no ``ids`` are passed to the method in the traditional style.
+ """
+ method._api = 'model'
+ return method
+
+
+def multi(method):
+ """ Decorate a record-style method where ``self`` is a recordset. The method
+ typically defines an operation on records. Such a method::
+
+ @api.multi
+ def method(self, args):
+ ...
+
+ may be called in both record and traditional styles, like::
+
+ # recs = model.browse(cr, uid, ids, context)
+ recs.method(args)
+
+ model.method(cr, uid, ids, args, context=context)
+ """
+ method._api = 'multi'
+ return method
+
+
+def one(method):
+ """ Decorate a record-style method where ``self`` is expected to be a
+ singleton instance. The decorated method automatically loops on records,
+ and makes a list with the results. In case the method is decorated with
+ :func:`returns`, it concatenates the resulting instances. Such a
+ method::
+
+ @api.one
+ def method(self, args):
+ return self.name
+
+ may be called in both record and traditional styles, like::
+
+ # recs = model.browse(cr, uid, ids, context)
+ names = recs.method(args)
+
+ names = model.method(cr, uid, ids, args, context=context)
+
+ .. deprecated:: 9.0
+
+ :func:`~.one` often makes the code less clear and behaves in ways
+ developers and readers may not expect.
+
+ It is strongly recommended to use :func:`~.multi` and either
+ iterate on the ``self`` recordset or ensure that the recordset
+ is a single record with :meth:`~openerp.models.Model.ensure_one`.
+ """
+ def loop(method, self, *args, **kwargs):
+ result = [method(rec, *args, **kwargs) for rec in self]
+ return aggregate(method, result, self)
+
+ wrapper = decorator(loop, method)
+ wrapper._api = 'one'
+ return wrapper
+
+
+def model_cr(method):
+ """ Decorate a record-style method where ``self`` is a recordset, but its
+ contents is not relevant, only the model is. Such a method::
+
+ @api.model_cr
+ def method(self, args):
+ ...
+
+ may be called in both record and traditional styles, like::
+
+ # recs = model.browse(cr, uid, ids, context)
+ recs.method(args)
+
+ model.method(cr, args)
+
+ Notice that no ``uid``, ``ids``, ``context`` are passed to the method in
+ the traditional style.
+ """
+ method._api = 'model_cr'
+ return method
+
+
+def model_cr_context(method):
+ """ Decorate a record-style method where ``self`` is a recordset, but its
+ contents is not relevant, only the model is. Such a method::
+
+ @api.model_cr_context
+ def method(self, args):
+ ...
+
+ may be called in both record and traditional styles, like::
+
+ # recs = model.browse(cr, uid, ids, context)
+ recs.method(args)
+
+ model.method(cr, args, context=context)
+
+ Notice that no ``uid``, ``ids`` are passed to the method in the
+ traditional style.
+ """
+ method._api = 'model_cr_context'
+ return method
+
+
+def cr(method):
+ """ Decorate a traditional-style method that takes ``cr`` as a parameter.
+ Such a method may be called in both record and traditional styles, like::
+
+ # recs = model.browse(cr, uid, ids, context)
+ recs.method(args)
+
+ model.method(cr, args)
+ """
+ method._api = 'cr'
+ return method
+
+
+def cr_context(method):
+ """ Decorate a traditional-style method that takes ``cr``, ``context`` as parameters. """
+ method._api = 'cr_context'
+ return method
+
+
+def cr_uid(method):
+ """ Decorate a traditional-style method that takes ``cr``, ``uid`` as parameters. """
+ method._api = 'cr_uid'
+ return method
+
+
+def cr_uid_context(method):
+ """ Decorate a traditional-style method that takes ``cr``, ``uid``, ``context`` as
+ parameters. Such a method may be called in both record and traditional
+ styles, like::
+
+ # recs = model.browse(cr, uid, ids, context)
+ recs.method(args)
+
+ model.method(cr, uid, args, context=context)
+ """
+ method._api = 'cr_uid_context'
+ return method
+
+
+def cr_uid_id(method):
+ """ Decorate a traditional-style method that takes ``cr``, ``uid``, ``id`` as
+ parameters. Such a method may be called in both record and traditional
+ styles. In the record style, the method automatically loops on records.
+ """
+ method._api = 'cr_uid_id'
+ return method
+
+
+def cr_uid_id_context(method):
+ """ Decorate a traditional-style method that takes ``cr``, ``uid``, ``id``,
+ ``context`` as parameters. Such a method::
+
+ @api.cr_uid_id
+ def method(self, cr, uid, id, args, context=None):
+ ...
+
+ may be called in both record and traditional styles, like::
+
+ # rec = model.browse(cr, uid, id, context)
+ rec.method(args)
+
+ model.method(cr, uid, id, args, context=context)
+ """
+ method._api = 'cr_uid_id_context'
+ return method
+
+
+def cr_uid_ids(method):
+ """ Decorate a traditional-style method that takes ``cr``, ``uid``, ``ids`` as
+ parameters. Such a method may be called in both record and traditional
+ styles.
+ """
+ method._api = 'cr_uid_ids'
+ return method
+
+
+def cr_uid_ids_context(method):
+ """ Decorate a traditional-style method that takes ``cr``, ``uid``, ``ids``,
+ ``context`` as parameters. Such a method::
+
+ @api.cr_uid_ids_context
+ def method(self, cr, uid, ids, args, context=None):
+ ...
+
+ may be called in both record and traditional styles, like::
+
+ # recs = model.browse(cr, uid, ids, context)
+ recs.method(args)
+
+ model.method(cr, uid, ids, args, context=context)
+
+ It is generally not necessary, see :func:`guess`.
+ """
+ method._api = 'cr_uid_ids_context'
+ return method
+
+
+def cr_uid_records(method):
+ """ Decorate a traditional-style method that takes ``cr``, ``uid``, a
+ recordset of model ``self`` as parameters. Such a method::
+
+ @api.cr_uid_records
+ def method(self, cr, uid, records, args):
+ ...
+
+ may be called in both record and traditional styles, like::
+
+ # records = model.browse(cr, uid, ids, context)
+ records.method(args)
+
+ model.method(cr, uid, records, args)
+ """
+ method._api = 'cr_uid_records'
+ return method
+
+
+def cr_uid_records_context(method):
+ """ Decorate a traditional-style method that takes ``cr``, ``uid``, a
+ recordset of model ``self``, ``context`` as parameters. Such a method::
+
+ @api.cr_uid_records_context
+ def method(self, cr, uid, records, args, context=None):
+ ...
+
+ may be called in both record and traditional styles, like::
+
+ # records = model.browse(cr, uid, ids, context)
+ records.method(args)
+
+ model.method(cr, uid, records, args, context=context)
+ """
+ method._api = 'cr_uid_records_context'
+ return method
+
+
+def v7(method_v7):
+ """ Decorate a method that supports the old-style api only. A new-style api
+ may be provided by redefining a method with the same name and decorated
+ with :func:`~.v8`::
+
+ @api.v7
+ def foo(self, cr, uid, ids, context=None):
+ ...
+
+ @api.v8
+ def foo(self):
+ ...
+
+ Special care must be taken if one method calls the other one, because
+ the method may be overridden! In that case, one should call the method
+ from the current class (say ``MyClass``), for instance::
+
+ @api.v7
+ def foo(self, cr, uid, ids, context=None):
+ # Beware: records.foo() may call an overriding of foo()
+ records = self.browse(cr, uid, ids, context)
+ return MyClass.foo(records)
+
+ Note that the wrapper method uses the docstring of the first method.
+ """
+ # retrieve method_v8 from the caller's frame
+ frame = currentframe().f_back
+ return frame.f_locals.get(method_v7.__name__, method_v7)
+
+
+def v8(method_v8):
+ """ Decorate a method that supports the new-style api only. An old-style api
+ may be provided by redefining a method with the same name and decorated
+ with :func:`~.v7`::
+
+ @api.v8
+ def foo(self):
+ ...
+
+ @api.v7
+ def foo(self, cr, uid, ids, context=None):
+ ...
+
+ Note that the wrapper method uses the docstring of the first method.
+ """
+ if method_v8.__name__ == 'read':
+ return multi(method_v8)
+ method_v8._api = 'v8'
+ return method_v8
+
+
+def noguess(method):
+ """ Decorate a method to prevent any effect from :func:`guess`. """
+ method._api = None
+ return method
+
+
+def guess(method):
+ """ Decorate ``method`` to make it callable in both traditional and record
+ styles. This decorator is applied automatically by the model's
+ metaclass, and has no effect on already-decorated methods.
+
+ The API style is determined by heuristics on the parameter names: ``cr``
+ or ``cursor`` for the cursor, ``uid`` or ``user`` for the user id,
+ ``id`` or ``ids`` for a list of record ids, and ``context`` for the
+ context dictionary. If a traditional API is recognized, one of the
+ decorators :func:`cr`, :func:`cr_context`, :func:`cr_uid`,
+ :func:`cr_uid_context`, :func:`cr_uid_id`, :func:`cr_uid_id_context`,
+ :func:`cr_uid_ids`, :func:`cr_uid_ids_context` is applied on the method.
+
+ Method calls are considered traditional style when their first parameter
+ is a database cursor.
+ """
+ if hasattr(method, '_api'):
+ return method
+
+ # introspection on argument names to determine api style
+ args, vname, kwname, defaults = getargspec(method)
+ names = tuple(args) + (None,) * 4
+
+ if names[0] == 'self':
+ if names[1] in ('cr', 'cursor'):
+ if names[2] in ('uid', 'user'):
+ if names[3] == 'ids':
+ if 'context' in names or kwname:
+ return cr_uid_ids_context(method)
+ else:
+ return cr_uid_ids(method)
+ elif names[3] == 'id' or names[3] == 'res_id':
+ if 'context' in names or kwname:
+ return cr_uid_id_context(method)
+ else:
+ return cr_uid_id(method)
+ elif 'context' in names or kwname:
+ return cr_uid_context(method)
+ else:
+ return cr_uid(method)
+ elif 'context' in names:
+ return cr_context(method)
+ else:
+ return cr(method)
+
+ # no wrapping by default
+ return noguess(method)
+
+
+def expected(decorator, func):
+ """ Decorate ``func`` with ``decorator`` if ``func`` is not wrapped yet. """
+ return decorator(func) if not hasattr(func, '_api') else func
+
+
+
+def call_kw_model(method, self, args, kwargs):
+ context, args, kwargs = split_context(method, args, kwargs)
+ recs = self.with_context(context or {})
+ _logger.debug("call %s.%s(%s)", recs, method.__name__, Params(args, kwargs))
+ result = method(recs, *args, **kwargs)
+ return downgrade(method, result, recs, args, kwargs)
+
+def call_kw_multi(method, self, args, kwargs):
+ ids, args = args[0], args[1:]
+ context, args, kwargs = split_context(method, args, kwargs)
+ recs = self.with_context(context or {}).browse(ids)
+ _logger.debug("call %s.%s(%s)", recs, method.__name__, Params(args, kwargs))
+ result = method(recs, *args, **kwargs)
+ return downgrade(method, result, recs, args, kwargs)
+
+def call_kw(model, name, args, kwargs):
+ """ Invoke the given method ``name`` on the recordset ``model``. """
+ method = getattr(type(model), name)
+ if getattr(method, '_api', None) == 'model':
+ return call_kw_model(method, model, args, kwargs)
+ else:
+ return call_kw_multi(method, model, args, kwargs)
+
+
+class Environment(Mapping):
+ """ An environment wraps data for ORM records:
+
+ - :attr:`cr`, the current database cursor;
+ - :attr:`uid`, the current user id;
+ - :attr:`context`, the current context dictionary.
+
+ It provides access to the registry by implementing a mapping from model
+ names to new api models. It also holds a cache for records, and a data
+ structure to manage recomputations.
+ """
+ _local = Local()
+
+ @classproperty
+ def envs(cls):
+ return cls._local.environments
+
+ @classmethod
+ @contextmanager
+ def manage(cls):
+ """ Context manager for a set of environments. """
+ if hasattr(cls._local, 'environments'):
+ yield
+ else:
+ try:
+ cls._local.environments = Environments()
+ yield
+ finally:
+ release_local(cls._local)
+
+ @classmethod
+ def reset(cls):
+ """ Clear the set of environments.
+ This may be useful when recreating a registry inside a transaction.
+ """
+ cls._local.environments = Environments()
+
+ def __new__(cls, cr, uid, context):
+ assert context is not None
+ args = (cr, uid, context)
+
+ # if env already exists, return it
+ env, envs = None, cls.envs
+ for env in envs:
+ if env.args == args:
+ return env
+
+ # otherwise create environment, and add it in the set
+ self = object.__new__(cls)
+ self.cr, self.uid, self.context = self.args = (cr, uid, frozendict(context))
+ self.registry = Registry(cr.dbname)
+ self.cache = defaultdict(dict) # {field: {id: value, ...}, ...}
+ self._protected = defaultdict(frozenset) # {field: ids, ...}
+ self.dirty = defaultdict(set) # {record: set(field_name), ...}
+ self.all = envs
+ envs.add(self)
+ return self
+
+ #
+ # Mapping methods
+ #
+
+ def __contains__(self, model_name):
+ """ Test whether the given model exists. """
+ return model_name in self.registry
+
+ def __getitem__(self, model_name):
+ """ Return an empty recordset from the given model. """
+ return self.registry[model_name]._browse((), self)
+
+ def __iter__(self):
+ """ Return an iterator on model names. """
+ return iter(self.registry)
+
+ def __len__(self):
+ """ Return the size of the model registry. """
+ return len(self.registry)
+
+ def __eq__(self, other):
+ return self is other
+
+ def __ne__(self, other):
+ return self is not other
+
+ def __hash__(self):
+ return object.__hash__(self)
+
+ def __call__(self, cr=None, user=None, context=None):
+ """ Return an environment based on ``self`` with modified parameters.
+
+ :param cr: optional database cursor to change the current cursor
+ :param user: optional user/user id to change the current user
+ :param context: optional context dictionary to change the current context
+ """
+ cr = self.cr if cr is None else cr
+ uid = self.uid if user is None else int(user)
+ context = self.context if context is None else context
+ return Environment(cr, uid, context)
+
+ def ref(self, xml_id, raise_if_not_found=True):
+ """ return the record corresponding to the given ``xml_id`` """
+ return self['ir.model.data'].xmlid_to_object(xml_id, raise_if_not_found=raise_if_not_found)
+
+ @property
+ def user(self):
+ """ return the current user (as an instance) """
+ return self(user=SUPERUSER_ID)['res.users'].browse(self.uid)
+
+ @property
+ def lang(self):
+ """ return the current language code """
+ return self.context.get('lang')
+
+ @contextmanager
+ def _do_in_mode(self, mode):
+ if self.all.mode:
+ yield
+ else:
+ try:
+ self.all.mode = mode
+ yield
+ finally:
+ self.all.mode = False
+ self.dirty.clear()
+
+ def do_in_draft(self):
+ """ Context-switch to draft mode, where all field updates are done in
+ cache only.
+ """
+ return self._do_in_mode(True)
+
+ @property
+ def in_draft(self):
+ """ Return whether we are in draft mode. """
+ return bool(self.all.mode)
+
+ def do_in_onchange(self):
+ """ Context-switch to 'onchange' draft mode, which is a specialized
+ draft mode used during execution of onchange methods.
+ """
+ return self._do_in_mode('onchange')
+
+ @property
+ def in_onchange(self):
+ """ Return whether we are in 'onchange' draft mode. """
+ return self.all.mode == 'onchange'
+
+ def invalidate(self, spec):
+ """ Invalidate some fields for some records in the cache of all
+ environments.
+
+ :param spec: what to invalidate, a list of `(field, ids)` pair,
+ where ``field`` is a field object, and ``ids`` is a list of record
+ ids or ``None`` (to invalidate all records).
+ """
+ if not spec:
+ return
+ for env in list(self.all):
+ c = env.cache
+ for field, ids in spec:
+ if ids is None:
+ if field in c:
+ del c[field]
+ else:
+ field_cache = c[field]
+ for id in ids:
+ field_cache.pop(id, None)
+
+ def invalidate_all(self):
+ """ Clear the cache of all environments. """
+ for env in list(self.all):
+ env.cache.clear()
+ env._protected.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 protected(self, field):
+ """ Return the recordset for which ``field`` should not be invalidated or recomputed. """
+ return self[field.model_name].browse(self._protected.get(field, ()))
+
+ @contextmanager
+ def protecting(self, fields, records):
+ """ Prevent the invalidation or recomputation of ``fields`` on ``records``. """
+ saved = {}
+ try:
+ for field in fields:
+ ids = saved[field] = self._protected[field]
+ self._protected[field] = ids.union(records._ids)
+ yield
+ finally:
+ self._protected.update(saved)
+
+ def field_todo(self, field):
+ """ Return a recordset with all records to recompute for ``field``. """
+ ids = {rid for recs in self.all.todo.get(field, ()) for rid in recs.ids}
+ return self[field.model_name].browse(ids)
+
+ def check_todo(self, field, record):
+ """ Check whether ``field`` must be recomputed on ``record``, and if so,
+ return the corresponding recordset to recompute.
+ """
+ for recs in self.all.todo.get(field, []):
+ if recs & record:
+ return recs
+
+ def add_todo(self, field, records):
+ """ Mark ``field`` to be recomputed on ``records``. """
+ recs_list = self.all.todo.setdefault(field, [])
+ for i, recs in enumerate(recs_list):
+ if recs.env == records.env:
+ recs_list[i] |= records
+ break
+ else:
+ recs_list.append(records)
+
+ def remove_todo(self, field, records):
+ """ Mark ``field`` as recomputed on ``records``. """
+ recs_list = [recs - records for recs in self.all.todo.pop(field, [])]
+ recs_list = filter(None, recs_list)
+ if recs_list:
+ self.all.todo[field] = recs_list
+
+ def has_todo(self):
+ """ Return whether some fields must be recomputed. """
+ return bool(self.all.todo)
+
+ def get_todo(self):
+ """ Return a pair ``(field, records)`` to recompute.
+ The field is such that none of its dependencies must be recomputed.
+ """
+ field = min(self.all.todo, key=self.registry.field_sequence)
+ return field, self.all.todo[field][0]
+
+ def check_cache(self):
+ """ Check the cache consistency. """
+ from openerp.fields import SpecialValue
+
+ # make a full copy of the cache, and invalidate it
+ cache_dump = dict(
+ (field, dict(field_cache))
+ for field, field_cache in self.cache.iteritems()
+ )
+ self.invalidate_all()
+
+ # re-fetch the records, and compare with their former cache
+ invalids = []
+ for field, field_dump in cache_dump.iteritems():
+ ids = filter(None, field_dump)
+ records = self[field.model_name].browse(ids)
+ for record in records:
+ try:
+ cached = field_dump[record.id]
+ cached = cached.get() if isinstance(cached, SpecialValue) else cached
+ value = field.convert_to_record(cached, record)
+ fetched = record[field.name]
+ if fetched != value:
+ info = {'cached': value, 'fetched': fetched}
+ invalids.append((field, record, info))
+ except (AccessError, MissingError):
+ pass
+
+ if invalids:
+ raise UserError('Invalid cache for fields\n' + pformat(invalids))
+
+ @property
+ def recompute(self):
+ return self.all.recompute
+
+ @contextmanager
+ def norecompute(self):
+ tmp = self.all.recompute
+ self.all.recompute = False
+ try:
+ yield
+ finally:
+ self.all.recompute = tmp
+
+
+class Environments(object):
+ """ A common object for all environments in a request. """
+ def __init__(self):
+ self.envs = WeakSet() # weak set of environments
+ self.todo = {} # recomputations {field: [records]}
+ self.mode = False # flag for draft/onchange
+ self.recompute = True
+
+ def add(self, env):
+ """ Add the environment ``env``. """
+ self.envs.add(env)
+
+ def __iter__(self):
+ """ Iterate over environments. """
+ return iter(self.envs)
+
+
+# keep those imports here in order to handle cyclic dependencies correctly
+from openerp import SUPERUSER_ID
+from openerp.exceptions import UserError, AccessError, MissingError
+from openerp.modules.registry import Registry