1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2013 OpenERP (<http://www.openerp.com>).
7 # This program is free software: you can redistribute it and/or modify
8 # it under the terms of the GNU Affero General Public License as
9 # published by the Free Software Foundation, either version 3 of the
10 # License, or (at your option) any later version.
12 # This program is distributed in the hope that it will be useful,
13 # but WITHOUT ANY WARRANTY; without even the implied warranty of
14 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 # GNU Affero General Public License for more details.
17 # You should have received a copy of the GNU Affero General Public License
18 # along with this program. If not, see <http://www.gnu.org/licenses/>.
20 ##############################################################################
22 """ This module provides the elements for managing two different API styles,
23 namely the "traditional" and "record" styles.
25 In the "traditional" style, parameters like the database cursor, user id,
26 context dictionary and record ids (usually denoted as ``cr``, ``uid``,
27 ``context``, ``ids``) are passed explicitly to all methods. In the "record"
28 style, those parameters are hidden into model instances, which gives it a
29 more object-oriented feel.
31 For instance, the statements::
33 model = self.pool.get(MODEL)
34 ids = model.search(cr, uid, DOMAIN, context=context)
35 for rec in model.browse(cr, uid, ids, context=context):
37 model.write(cr, uid, ids, VALUES, context=context)
39 may also be written as::
41 env = Env(cr, uid, context) # cr, uid, context wrapped in env
42 recs = env[MODEL] # retrieve an instance of MODEL
43 recs = recs.search(DOMAIN) # search returns a recordset
44 for rec in recs: # iterate over the records
46 recs.write(VALUES) # update all records in recs
48 Methods written in the "traditional" style are automatically decorated,
49 following some heuristics based on parameter names.
54 'Meta', 'guess', 'noguess',
55 'model', 'multi', 'one',
56 'cr', 'cr_context', 'cr_uid', 'cr_uid_context',
57 'cr_uid_id', 'cr_uid_id_context', 'cr_uid_ids', 'cr_uid_ids_context',
58 'constrains', 'depends', 'onchange', 'returns',
63 from inspect import currentframe, getargspec
64 from collections import defaultdict, MutableMapping
65 from contextlib import contextmanager
66 from pprint import pformat
67 from weakref import WeakSet
68 from werkzeug.local import Local, release_local
70 from openerp.tools import frozendict
72 _logger = logging.getLogger(__name__)
74 # The following attributes are used, and reflected on wrapping methods:
75 # - method._api: decorator function, used for re-applying decorator
76 # - method._constrains: set by @constrains, specifies constraint dependencies
77 # - method._depends: set by @depends, specifies compute dependencies
78 # - method._returns: set by @returns, specifies return model
79 # - method._onchange: set by @onchange, specifies onchange fields
80 # - method.clear_cache: set by @ormcache, used to clear the cache
82 # On wrapping method only:
83 # - method._orig: original method
86 WRAPPED_ATTRS = ('__module__', '__name__', '__doc__', '_api', '_constrains',
87 '_depends', '_onchange', '_returns', 'clear_cache')
89 INHERITED_ATTRS = ('_returns',)
93 """ Metaclass that automatically decorates traditional-style methods by
94 guessing their API. It also implements the inheritance of the
95 :func:`returns` decorators.
98 def __new__(meta, name, bases, attrs):
99 # dummy parent class to catch overridden methods decorated with 'returns'
100 parent = type.__new__(meta, name, bases, {})
102 for key, value in attrs.items():
103 if not key.startswith('__') and callable(value):
104 # make the method inherit from decorators
105 value = propagate(getattr(parent, key, None), value)
107 # guess calling convention if none is given
108 if not hasattr(value, '_api'):
116 return type.__new__(meta, name, bases, attrs)
119 identity = lambda x: x
121 def decorate(method, attr, value):
122 """ Decorate `method` or its original method. """
123 # decorate the original method, and re-apply the api decorator, if any
124 orig = getattr(method, '_orig', method)
125 setattr(orig, attr, value)
126 return getattr(method, '_api', identity)(orig)
128 def propagate(from_method, to_method):
129 """ Propagate decorators from `from_method` to `to_method`, and return the
133 for attr in INHERITED_ATTRS:
134 if hasattr(from_method, attr) and not hasattr(to_method, attr):
135 to_method = decorate(to_method, attr, getattr(from_method, attr))
139 def constrains(*args):
140 """ Decorates a constraint checker. Each argument must be a field name
144 @api.constrains('name', 'description')
145 def _check_description(self):
146 if self.name == self.description:
147 raise ValidationError("Fields name and description must be different")
149 Invoked on the records on which one of the named fields has been modified.
151 Should raise :class:`~openerp.exceptions.ValidationError` if the
154 return lambda method: decorate(method, '_constrains', args)
158 """ Return a decorator to decorate an onchange method for given fields.
159 Each argument must be a field name::
161 @api.onchange('partner_id')
162 def _onchange_partner(self):
163 self.message = "Dear %s" % (self.partner_id.name or "")
165 In the form views where the field appears, the method will be called
166 when one of the given fields is modified. The method is invoked on a
167 pseudo-record that contains the values present in the form. Field
168 assignments on that record are automatically sent back to the client.
170 return lambda method: decorate(method, '_onchange', args)
174 """ Return a decorator that specifies the field dependencies of a "compute"
175 method (for new-style function fields). Each argument must be a string
176 that consists in a dot-separated sequence of field names::
178 pname = fields.Char(compute='_compute_pname')
181 @api.depends('partner_id.name', 'partner_id.is_company')
182 def _compute_pname(self):
183 if self.partner_id.is_company:
184 self.pname = (self.partner_id.name or "").upper()
186 self.pname = self.partner_id.name
188 One may also pass a single function as argument. In that case, the
189 dependencies are given by calling the function with the field's model.
191 if args and callable(args[0]):
193 return lambda method: decorate(method, '_depends', args)
196 def returns(model, downgrade=None):
197 """ Return a decorator for methods that return instances of `model`.
199 :param model: a model name, or ``'self'`` for the current model
201 :param downgrade: a function `downgrade(value)` to convert the
202 record-style `value` to a traditional-style output
204 The decorator adapts the method output to the api style: `id`, `ids` or
205 ``False`` for the traditional style, and recordset for the record style::
208 @returns('res.partner')
209 def find_partner(self, arg):
210 ... # return some record
212 # output depends on call style: traditional vs record style
213 partner_id = model.find_partner(cr, uid, arg, context=context)
215 # recs = model.browse(cr, uid, ids, context)
216 partner_record = recs.find_partner(arg)
218 Note that the decorated method must satisfy that convention.
220 Those decorators are automatically *inherited*: a method that overrides
221 a decorated existing method will be decorated with the same
224 return lambda method: decorate(method, '_returns', (model, downgrade))
227 def make_wrapper(method, old_api, new_api):
228 """ Return a wrapper method for `method`. """
229 def wrapper(self, *args, **kwargs):
230 # avoid hasattr(self, '_ids') because __getattr__() is overridden
231 if '_ids' in self.__dict__:
232 return new_api(self, *args, **kwargs)
234 return old_api(self, *args, **kwargs)
236 # propagate specific openerp attributes from method to wrapper
237 for attr in WRAPPED_ATTRS:
238 if hasattr(method, attr):
239 setattr(wrapper, attr, getattr(method, attr))
240 wrapper._orig = method
245 def get_downgrade(method):
246 """ Return a function `downgrade(value)` that adapts `value` from
247 record-style to traditional-style, following the convention of `method`.
249 spec = getattr(method, '_returns', None)
251 model, downgrade = spec
252 return downgrade or (lambda value: value.ids)
254 return lambda value: value
257 def get_upgrade(method):
258 """ Return a function `upgrade(self, value)` that adapts `value` from
259 traditional-style to record-style, following the convention of `method`.
261 spec = getattr(method, '_returns', None)
263 model, downgrade = spec
265 return lambda self, value: self.browse(value)
267 return lambda self, value: self.env[model].browse(value)
269 return lambda self, value: value
272 def get_aggregate(method):
273 """ Return a function `aggregate(self, value)` that aggregates record-style
274 `value` for a method decorated with ``@one``.
276 spec = getattr(method, '_returns', None)
278 # value is a list of instances, concatenate them
279 model, downgrade = spec
281 return lambda self, value: sum(value, self.browse())
283 return lambda self, value: sum(value, self.env[model].browse())
285 return lambda self, value: value
288 def get_context_split(method):
289 """ Return a function `split` that extracts the context from a pair of
290 positional and keyword arguments::
292 context, args, kwargs = split(args, kwargs)
294 pos = len(getargspec(method).args) - 1
296 def split(args, kwargs):
298 return args[pos], args[:pos], kwargs
300 return kwargs.pop('context', None), args, kwargs
306 """ Decorate a record-style method where `self` is a recordset, but its
307 contents is not relevant, only the model is. Such a method::
310 def method(self, args):
313 may be called in both record and traditional styles, like::
315 # recs = model.browse(cr, uid, ids, context)
318 model.method(cr, uid, args, context=context)
320 Notice that no `ids` are passed to the method in the traditional style.
323 split = get_context_split(method)
324 downgrade = get_downgrade(method)
326 def old_api(self, cr, uid, *args, **kwargs):
327 context, args, kwargs = split(args, kwargs)
328 recs = self.browse(cr, uid, [], context)
329 result = method(recs, *args, **kwargs)
330 return downgrade(result)
332 return make_wrapper(method, old_api, method)
336 """ Decorate a record-style method where `self` is a recordset. The method
337 typically defines an operation on records. Such a method::
340 def method(self, args):
343 may be called in both record and traditional styles, like::
345 # recs = model.browse(cr, uid, ids, context)
348 model.method(cr, uid, ids, args, context=context)
351 split = get_context_split(method)
352 downgrade = get_downgrade(method)
354 def old_api(self, cr, uid, ids, *args, **kwargs):
355 context, args, kwargs = split(args, kwargs)
356 recs = self.browse(cr, uid, ids, context)
357 result = method(recs, *args, **kwargs)
358 return downgrade(result)
360 return make_wrapper(method, old_api, method)
364 """ Decorate a record-style method where `self` is expected to be a
365 singleton instance. The decorated method automatically loops on records,
366 and makes a list with the results. In case the method is decorated with
367 @returns, it concatenates the resulting instances. Such a method::
370 def method(self, args):
373 may be called in both record and traditional styles, like::
375 # recs = model.browse(cr, uid, ids, context)
376 names = recs.method(args)
378 names = model.method(cr, uid, ids, args, context=context)
381 split = get_context_split(method)
382 downgrade = get_downgrade(method)
383 aggregate = get_aggregate(method)
385 def old_api(self, cr, uid, ids, *args, **kwargs):
386 context, args, kwargs = split(args, kwargs)
387 recs = self.browse(cr, uid, ids, context)
388 result = new_api(recs, *args, **kwargs)
389 return downgrade(result)
391 def new_api(self, *args, **kwargs):
392 result = [method(rec, *args, **kwargs) for rec in self]
393 return aggregate(self, result)
395 return make_wrapper(method, old_api, new_api)
399 """ Decorate a traditional-style method that takes `cr` as a parameter.
400 Such a method may be called in both record and traditional styles, like::
402 # recs = model.browse(cr, uid, ids, context)
405 model.method(cr, args)
408 upgrade = get_upgrade(method)
410 def new_api(self, *args, **kwargs):
411 cr, uid, context = self.env.args
412 result = method(self._model, cr, *args, **kwargs)
413 return upgrade(self, result)
415 return make_wrapper(method, method, new_api)
418 def cr_context(method):
419 """ Decorate a traditional-style method that takes `cr`, `context` as parameters. """
420 method._api = cr_context
421 upgrade = get_upgrade(method)
423 def new_api(self, *args, **kwargs):
424 cr, uid, context = self.env.args
425 kwargs['context'] = context
426 result = method(self._model, cr, *args, **kwargs)
427 return upgrade(self, result)
429 return make_wrapper(method, method, new_api)
433 """ Decorate a traditional-style method that takes `cr`, `uid` as parameters. """
435 upgrade = get_upgrade(method)
437 def new_api(self, *args, **kwargs):
438 cr, uid, context = self.env.args
439 result = method(self._model, cr, uid, *args, **kwargs)
440 return upgrade(self, result)
442 return make_wrapper(method, method, new_api)
445 def cr_uid_context(method):
446 """ Decorate a traditional-style method that takes `cr`, `uid`, `context` as
447 parameters. Such a method may be called in both record and traditional
450 # recs = model.browse(cr, uid, ids, context)
453 model.method(cr, uid, args, context=context)
455 method._api = cr_uid_context
456 upgrade = get_upgrade(method)
458 def new_api(self, *args, **kwargs):
459 cr, uid, context = self.env.args
460 kwargs['context'] = context
461 result = method(self._model, cr, uid, *args, **kwargs)
462 return upgrade(self, result)
464 return make_wrapper(method, method, new_api)
467 def cr_uid_id(method):
468 """ Decorate a traditional-style method that takes `cr`, `uid`, `id` as
469 parameters. Such a method may be called in both record and traditional
470 styles. In the record style, the method automatically loops on records.
472 method._api = cr_uid_id
473 upgrade = get_upgrade(method)
475 def new_api(self, *args, **kwargs):
476 cr, uid, context = self.env.args
477 result = [method(self._model, cr, uid, id, *args, **kwargs) for id in self.ids]
478 return upgrade(self, result)
480 return make_wrapper(method, method, new_api)
483 def cr_uid_id_context(method):
484 """ Decorate a traditional-style method that takes `cr`, `uid`, `id`,
485 `context` as parameters. Such a method::
488 def method(self, cr, uid, id, args, context=None):
491 may be called in both record and traditional styles, like::
493 # rec = model.browse(cr, uid, id, context)
496 model.method(cr, uid, id, args, context=context)
498 method._api = cr_uid_id_context
499 upgrade = get_upgrade(method)
501 def new_api(self, *args, **kwargs):
502 cr, uid, context = self.env.args
503 kwargs['context'] = context
504 result = [method(self._model, cr, uid, id, *args, **kwargs) for id in self.ids]
505 return upgrade(self, result)
507 return make_wrapper(method, method, new_api)
510 def cr_uid_ids(method):
511 """ Decorate a traditional-style method that takes `cr`, `uid`, `ids` as
512 parameters. Such a method may be called in both record and traditional
515 method._api = cr_uid_ids
516 upgrade = get_upgrade(method)
518 def new_api(self, *args, **kwargs):
519 cr, uid, context = self.env.args
520 result = method(self._model, cr, uid, self.ids, *args, **kwargs)
521 return upgrade(self, result)
523 return make_wrapper(method, method, new_api)
526 def cr_uid_ids_context(method):
527 """ Decorate a traditional-style method that takes `cr`, `uid`, `ids`,
528 `context` as parameters. Such a method::
530 @api.cr_uid_ids_context
531 def method(self, cr, uid, ids, args, context=None):
534 may be called in both record and traditional styles, like::
536 # recs = model.browse(cr, uid, ids, context)
539 model.method(cr, uid, ids, args, context=context)
541 It is generally not necessary, see :func:`guess`.
543 method._api = cr_uid_ids_context
544 upgrade = get_upgrade(method)
546 def new_api(self, *args, **kwargs):
547 cr, uid, context = self.env.args
548 kwargs['context'] = context
549 result = method(self._model, cr, uid, self.ids, *args, **kwargs)
550 return upgrade(self, result)
552 return make_wrapper(method, method, new_api)
556 """ Decorate a method that supports the old-style api only. A new-style api
557 may be provided by redefining a method with the same name and decorated
561 def foo(self, cr, uid, ids, context=None):
568 Note that the wrapper method uses the docstring of the first method.
570 # retrieve method_v8 from the caller's frame
571 frame = currentframe().f_back
572 method = frame.f_locals.get(method_v7.__name__)
573 method_v8 = getattr(method, '_v8', method)
575 wrapper = make_wrapper(method_v7, method_v7, method_v8)
576 wrapper._v7 = method_v7
577 wrapper._v8 = method_v8
582 """ Decorate a method that supports the new-style api only. An old-style api
583 may be provided by redefining a method with the same name and decorated
591 def foo(self, cr, uid, ids, context=None):
594 Note that the wrapper method uses the docstring of the first method.
596 # retrieve method_v7 from the caller's frame
597 frame = currentframe().f_back
598 method = frame.f_locals.get(method_v8.__name__)
599 method_v7 = getattr(method, '_v7', method)
601 wrapper = make_wrapper(method_v8, method_v7, method_v8)
602 wrapper._v7 = method_v7
603 wrapper._v8 = method_v8
608 """ Decorate a method to prevent any effect from :func:`guess`. """
614 """ Decorate `method` to make it callable in both traditional and record
615 styles. This decorator is applied automatically by the model's
616 metaclass, and has no effect on already-decorated methods.
618 The API style is determined by heuristics on the parameter names: ``cr``
619 or ``cursor`` for the cursor, ``uid`` or ``user`` for the user id,
620 ``id`` or ``ids`` for a list of record ids, and ``context`` for the
621 context dictionary. If a traditional API is recognized, one of the
622 decorators :func:`cr`, :func:`cr_context`, :func:`cr_uid`,
623 :func:`cr_uid_context`, :func:`cr_uid_id`, :func:`cr_uid_id_context`,
624 :func:`cr_uid_ids`, :func:`cr_uid_ids_context` is applied on the method.
626 Method calls are considered traditional style when their first parameter
627 is a database cursor.
629 if hasattr(method, '_api'):
632 # introspection on argument names to determine api style
633 args, vname, kwname, defaults = getargspec(method)
634 names = tuple(args) + (None,) * 4
636 if names[0] == 'self':
637 if names[1] in ('cr', 'cursor'):
638 if names[2] in ('uid', 'user'):
639 if names[3] == 'ids':
640 if 'context' in names or kwname:
641 return cr_uid_ids_context(method)
643 return cr_uid_ids(method)
644 elif names[3] == 'id':
645 if 'context' in names or kwname:
646 return cr_uid_id_context(method)
648 return cr_uid_id(method)
649 elif 'context' in names or kwname:
650 return cr_uid_context(method)
652 return cr_uid(method)
653 elif 'context' in names:
654 return cr_context(method)
658 # no wrapping by default
659 return noguess(method)
662 def expected(decorator, func):
663 """ Decorate `func` with `decorator` if `func` is not wrapped yet. """
664 return decorator(func) if not hasattr(func, '_orig') else func
668 class Environment(object):
669 """ An environment wraps data for ORM records:
671 - :attr:`cr`, the current database cursor;
672 - :attr:`uid`, the current user id;
673 - :attr:`context`, the current context dictionary.
675 It also provides access to the registry, a cache for records, and a data
676 structure to manage recomputations.
683 """ Context manager for a set of environments. """
684 if hasattr(cls._local, 'environments'):
688 cls._local.environments = WeakSet()
691 release_local(cls._local)
693 def __new__(cls, cr, uid, context):
694 assert context is not None
695 args = (cr, uid, context)
697 # if env already exists, return it
698 env, envs = None, cls._local.environments
703 # otherwise create environment, and add it in the set
704 self = object.__new__(cls)
705 self.cr, self.uid, self.context = self.args = (cr, uid, frozendict(context))
706 self.registry = RegistryManager.get(cr.dbname)
707 self.cache = defaultdict(dict) # {field: {id: value, ...}, ...}
708 self.prefetch = defaultdict(set) # {model_name: set(id), ...}
709 self.computed = defaultdict(set) # {field: set(id), ...}
710 self.dirty = set() # set(record)
711 self.todo = {} # {field: records, ...}
712 self.mode = env.mode if env else Mode()
717 def __getitem__(self, model_name):
718 """ return a given model """
719 return self.registry[model_name]._browse(self, ())
721 def __call__(self, cr=None, user=None, context=None):
722 """ Return an environment based on `self` with modified parameters.
724 :param cr: optional database cursor to change the current cursor
725 :param user: optional user/user id to change the current user
726 :param context: optional context dictionary to change the current context
728 cr = self.cr if cr is None else cr
729 uid = self.uid if user is None else int(user)
730 context = self.context if context is None else context
731 return Environment(cr, uid, context)
733 def ref(self, xml_id, raise_if_not_found=True):
734 """ return the record corresponding to the given `xml_id` """
735 return self['ir.model.data'].xmlid_to_object(xml_id, raise_if_not_found=raise_if_not_found)
739 """ return the current user (as an instance) """
740 return self(user=SUPERUSER_ID)['res.users'].browse(self.uid)
744 """ return the current language code """
745 return self.context.get('lang')
748 def _do_in_mode(self, mode):
753 self.mode.value = mode
756 self.mode.value = False
759 def do_in_draft(self):
760 """ Context-switch to draft mode, where all field updates are done in
763 return self._do_in_mode(True)
767 """ Return whether we are in draft mode. """
768 return bool(self.mode.value)
770 def do_in_onchange(self):
771 """ Context-switch to 'onchange' draft mode, which is a specialized
772 draft mode used during execution of onchange methods.
774 return self._do_in_mode('onchange')
777 def in_onchange(self):
778 """ Return whether we are in 'onchange' draft mode. """
779 return self.mode.value == 'onchange'
781 def invalidate(self, spec):
782 """ Invalidate some fields for some records in the cache of all
785 :param spec: what to invalidate, a list of `(field, ids)` pair,
786 where `field` is a field object, and `ids` is a list of record
787 ids or ``None`` (to invalidate all records).
791 for env in list(iter(self.all)):
793 for field, ids in spec:
798 field_cache = c[field]
800 field_cache.pop(id, None)
802 def invalidate_all(self):
803 """ Clear the cache of all environments. """
804 for env in list(iter(self.all)):
810 def check_cache(self):
811 """ Check the cache consistency. """
812 # make a full copy of the cache, and invalidate it
814 (field, dict(field_cache))
815 for field, field_cache in self.cache.iteritems()
817 self.invalidate_all()
819 # re-fetch the records, and compare with their former cache
821 for field, field_dump in cache_dump.iteritems():
822 ids = filter(None, field_dump)
823 records = self[field.model_name].browse(ids)
824 for record in records:
826 cached = field_dump[record.id]
827 fetched = record[field.name]
828 if fetched != cached:
829 info = {'cached': cached, 'fetched': fetched}
830 invalids.append((field, record, info))
831 except (AccessError, MissingError):
835 raise Warning('Invalid cache for fields\n' + pformat(invalids))
839 """ A mode flag shared among environments. """
840 value = False # False, True (draft) or 'onchange' (onchange draft)
843 # keep those imports here in order to handle cyclic dependencies correctly
844 from openerp import SUPERUSER_ID
845 from openerp.exceptions import Warning, AccessError, MissingError
846 from openerp.modules.registry import RegistryManager