1 # -*- coding: utf-8 -*-
2 ##############################################################################
4 # OpenERP, Open Source Management Solution
5 # Copyright (C) 2013-2014 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',
64 from inspect import currentframe, getargspec
65 from collections import defaultdict, MutableMapping
66 from contextlib import contextmanager
67 from pprint import pformat
68 from weakref import WeakSet
69 from werkzeug.local import Local, release_local
71 from openerp.tools import frozendict
73 _logger = logging.getLogger(__name__)
75 # The following attributes are used, and reflected on wrapping methods:
76 # - method._api: decorator function, used for re-applying decorator
77 # - method._constrains: set by @constrains, specifies constraint dependencies
78 # - method._depends: set by @depends, specifies compute dependencies
79 # - method._returns: set by @returns, specifies return model
80 # - method._onchange: set by @onchange, specifies onchange fields
81 # - method.clear_cache: set by @ormcache, used to clear the cache
83 # On wrapping method only:
84 # - method._orig: original method
87 WRAPPED_ATTRS = ('__module__', '__name__', '__doc__', '_api', '_constrains',
88 '_depends', '_onchange', '_returns', 'clear_cache')
90 INHERITED_ATTRS = ('_returns',)
94 """ Metaclass that automatically decorates traditional-style methods by
95 guessing their API. It also implements the inheritance of the
96 :func:`returns` decorators.
99 def __new__(meta, name, bases, attrs):
100 # dummy parent class to catch overridden methods decorated with 'returns'
101 parent = type.__new__(meta, name, bases, {})
103 for key, value in attrs.items():
104 if not key.startswith('__') and callable(value):
105 # make the method inherit from decorators
106 value = propagate(getattr(parent, key, None), value)
108 # guess calling convention if none is given
109 if not hasattr(value, '_api'):
117 return type.__new__(meta, name, bases, attrs)
120 identity = lambda x: x
122 def decorate(method, attr, value):
123 """ Decorate `method` or its original method. """
124 # decorate the original method, and re-apply the api decorator, if any
125 orig = getattr(method, '_orig', method)
126 setattr(orig, attr, value)
127 return getattr(method, '_api', identity)(orig)
129 def propagate(from_method, to_method):
130 """ Propagate decorators from `from_method` to `to_method`, and return the
134 for attr in INHERITED_ATTRS:
135 if hasattr(from_method, attr) and not hasattr(to_method, attr):
136 to_method = decorate(to_method, attr, getattr(from_method, attr))
140 def constrains(*args):
141 """ Decorates a constraint checker. Each argument must be a field name
145 @api.constrains('name', 'description')
146 def _check_description(self):
147 if self.name == self.description:
148 raise ValidationError("Fields name and description must be different")
150 Invoked on the records on which one of the named fields has been modified.
152 Should raise :class:`~openerp.exceptions.ValidationError` if the
155 return lambda method: decorate(method, '_constrains', args)
159 """ Return a decorator to decorate an onchange method for given fields.
160 Each argument must be a field name::
162 @api.onchange('partner_id')
163 def _onchange_partner(self):
164 self.message = "Dear %s" % (self.partner_id.name or "")
166 In the form views where the field appears, the method will be called
167 when one of the given fields is modified. The method is invoked on a
168 pseudo-record that contains the values present in the form. Field
169 assignments on that record are automatically sent back to the client.
171 return lambda method: decorate(method, '_onchange', args)
175 """ Return a decorator that specifies the field dependencies of a "compute"
176 method (for new-style function fields). Each argument must be a string
177 that consists in a dot-separated sequence of field names::
179 pname = fields.Char(compute='_compute_pname')
182 @api.depends('partner_id.name', 'partner_id.is_company')
183 def _compute_pname(self):
184 if self.partner_id.is_company:
185 self.pname = (self.partner_id.name or "").upper()
187 self.pname = self.partner_id.name
189 One may also pass a single function as argument. In that case, the
190 dependencies are given by calling the function with the field's model.
192 if args and callable(args[0]):
194 elif any('id' in arg.split('.') for arg in args):
195 raise NotImplementedError("Compute method cannot depend on field 'id'.")
196 return lambda method: decorate(method, '_depends', args)
199 def returns(model, downgrade=None):
200 """ Return a decorator for methods that return instances of `model`.
202 :param model: a model name, or ``'self'`` for the current model
204 :param downgrade: a function `downgrade(value)` to convert the
205 record-style `value` to a traditional-style output
207 The decorator adapts the method output to the api style: `id`, `ids` or
208 ``False`` for the traditional style, and recordset for the record style::
211 @returns('res.partner')
212 def find_partner(self, arg):
213 ... # return some record
215 # output depends on call style: traditional vs record style
216 partner_id = model.find_partner(cr, uid, arg, context=context)
218 # recs = model.browse(cr, uid, ids, context)
219 partner_record = recs.find_partner(arg)
221 Note that the decorated method must satisfy that convention.
223 Those decorators are automatically *inherited*: a method that overrides
224 a decorated existing method will be decorated with the same
227 return lambda method: decorate(method, '_returns', (model, downgrade))
230 def make_wrapper(method, old_api, new_api):
231 """ Return a wrapper method for `method`. """
232 def wrapper(self, *args, **kwargs):
233 # avoid hasattr(self, '_ids') because __getattr__() is overridden
234 if '_ids' in self.__dict__:
235 return new_api(self, *args, **kwargs)
237 return old_api(self, *args, **kwargs)
239 # propagate specific openerp attributes from method to wrapper
240 for attr in WRAPPED_ATTRS:
241 if hasattr(method, attr):
242 setattr(wrapper, attr, getattr(method, attr))
243 wrapper._orig = method
248 def get_downgrade(method):
249 """ Return a function `downgrade(value)` that adapts `value` from
250 record-style to traditional-style, following the convention of `method`.
252 spec = getattr(method, '_returns', None)
254 model, downgrade = spec
255 return downgrade or (lambda value: value.ids)
257 return lambda value: value
260 def get_upgrade(method):
261 """ Return a function `upgrade(self, value)` that adapts `value` from
262 traditional-style to record-style, following the convention of `method`.
264 spec = getattr(method, '_returns', None)
266 model, downgrade = spec
268 return lambda self, value: self.browse(value)
270 return lambda self, value: self.env[model].browse(value)
272 return lambda self, value: value
275 def get_aggregate(method):
276 """ Return a function `aggregate(self, value)` that aggregates record-style
277 `value` for a method decorated with ``@one``.
279 spec = getattr(method, '_returns', None)
281 # value is a list of instances, concatenate them
282 model, downgrade = spec
284 return lambda self, value: sum(value, self.browse())
286 return lambda self, value: sum(value, self.env[model].browse())
288 return lambda self, value: value
291 def get_context_split(method):
292 """ Return a function `split` that extracts the context from a pair of
293 positional and keyword arguments::
295 context, args, kwargs = split(args, kwargs)
297 pos = len(getargspec(method).args) - 1
299 def split(args, kwargs):
301 return args[pos], args[:pos], kwargs
303 return kwargs.pop('context', None), args, kwargs
309 """ Decorate a record-style method where `self` is a recordset, but its
310 contents is not relevant, only the model is. Such a method::
313 def method(self, args):
316 may be called in both record and traditional styles, like::
318 # recs = model.browse(cr, uid, ids, context)
321 model.method(cr, uid, args, context=context)
323 Notice that no `ids` are passed to the method in the traditional style.
326 split = get_context_split(method)
327 downgrade = get_downgrade(method)
329 def old_api(self, cr, uid, *args, **kwargs):
330 context, args, kwargs = split(args, kwargs)
331 recs = self.browse(cr, uid, [], context)
332 result = method(recs, *args, **kwargs)
333 return downgrade(result)
335 return make_wrapper(method, old_api, method)
339 """ Decorate a record-style method where `self` is a recordset. The method
340 typically defines an operation on records. Such a method::
343 def method(self, args):
346 may be called in both record and traditional styles, like::
348 # recs = model.browse(cr, uid, ids, context)
351 model.method(cr, uid, ids, args, context=context)
354 split = get_context_split(method)
355 downgrade = get_downgrade(method)
357 def old_api(self, cr, uid, ids, *args, **kwargs):
358 context, args, kwargs = split(args, kwargs)
359 recs = self.browse(cr, uid, ids, context)
360 result = method(recs, *args, **kwargs)
361 return downgrade(result)
363 return make_wrapper(method, old_api, method)
367 """ Decorate a record-style method where `self` is expected to be a
368 singleton instance. The decorated method automatically loops on records,
369 and makes a list with the results. In case the method is decorated with
370 @returns, it concatenates the resulting instances. Such a method::
373 def method(self, args):
376 may be called in both record and traditional styles, like::
378 # recs = model.browse(cr, uid, ids, context)
379 names = recs.method(args)
381 names = model.method(cr, uid, ids, args, context=context)
384 split = get_context_split(method)
385 downgrade = get_downgrade(method)
386 aggregate = get_aggregate(method)
388 def old_api(self, cr, uid, ids, *args, **kwargs):
389 context, args, kwargs = split(args, kwargs)
390 recs = self.browse(cr, uid, ids, context)
391 result = new_api(recs, *args, **kwargs)
392 return downgrade(result)
394 def new_api(self, *args, **kwargs):
395 result = [method(rec, *args, **kwargs) for rec in self]
396 return aggregate(self, result)
398 return make_wrapper(method, old_api, new_api)
402 """ Decorate a traditional-style method that takes `cr` as a parameter.
403 Such a method may be called in both record and traditional styles, like::
405 # recs = model.browse(cr, uid, ids, context)
408 model.method(cr, args)
411 upgrade = get_upgrade(method)
413 def new_api(self, *args, **kwargs):
414 cr, uid, context = self.env.args
415 result = method(self._model, cr, *args, **kwargs)
416 return upgrade(self, result)
418 return make_wrapper(method, method, new_api)
421 def cr_context(method):
422 """ Decorate a traditional-style method that takes `cr`, `context` as parameters. """
423 method._api = cr_context
424 upgrade = get_upgrade(method)
426 def new_api(self, *args, **kwargs):
427 cr, uid, context = self.env.args
428 kwargs['context'] = context
429 result = method(self._model, cr, *args, **kwargs)
430 return upgrade(self, result)
432 return make_wrapper(method, method, new_api)
436 """ Decorate a traditional-style method that takes `cr`, `uid` as parameters. """
438 upgrade = get_upgrade(method)
440 def new_api(self, *args, **kwargs):
441 cr, uid, context = self.env.args
442 result = method(self._model, cr, uid, *args, **kwargs)
443 return upgrade(self, result)
445 return make_wrapper(method, method, new_api)
448 def cr_uid_context(method):
449 """ Decorate a traditional-style method that takes `cr`, `uid`, `context` as
450 parameters. Such a method may be called in both record and traditional
453 # recs = model.browse(cr, uid, ids, context)
456 model.method(cr, uid, args, context=context)
458 method._api = cr_uid_context
459 upgrade = get_upgrade(method)
461 def new_api(self, *args, **kwargs):
462 cr, uid, context = self.env.args
463 kwargs['context'] = context
464 result = method(self._model, cr, uid, *args, **kwargs)
465 return upgrade(self, result)
467 return make_wrapper(method, method, new_api)
470 def cr_uid_id(method):
471 """ Decorate a traditional-style method that takes `cr`, `uid`, `id` as
472 parameters. Such a method may be called in both record and traditional
473 styles. In the record style, the method automatically loops on records.
475 method._api = cr_uid_id
476 upgrade = get_upgrade(method)
478 def new_api(self, *args, **kwargs):
479 cr, uid, context = self.env.args
480 result = [method(self._model, cr, uid, id, *args, **kwargs) for id in self.ids]
481 return upgrade(self, result)
483 return make_wrapper(method, method, new_api)
486 def cr_uid_id_context(method):
487 """ Decorate a traditional-style method that takes `cr`, `uid`, `id`,
488 `context` as parameters. Such a method::
491 def method(self, cr, uid, id, args, context=None):
494 may be called in both record and traditional styles, like::
496 # rec = model.browse(cr, uid, id, context)
499 model.method(cr, uid, id, args, context=context)
501 method._api = cr_uid_id_context
502 upgrade = get_upgrade(method)
504 def new_api(self, *args, **kwargs):
505 cr, uid, context = self.env.args
506 kwargs['context'] = context
507 result = [method(self._model, cr, uid, id, *args, **kwargs) for id in self.ids]
508 return upgrade(self, result)
510 return make_wrapper(method, method, new_api)
513 def cr_uid_ids(method):
514 """ Decorate a traditional-style method that takes `cr`, `uid`, `ids` as
515 parameters. Such a method may be called in both record and traditional
518 method._api = cr_uid_ids
519 upgrade = get_upgrade(method)
521 def new_api(self, *args, **kwargs):
522 cr, uid, context = self.env.args
523 result = method(self._model, cr, uid, self.ids, *args, **kwargs)
524 return upgrade(self, result)
526 return make_wrapper(method, method, new_api)
529 def cr_uid_ids_context(method):
530 """ Decorate a traditional-style method that takes `cr`, `uid`, `ids`,
531 `context` as parameters. Such a method::
533 @api.cr_uid_ids_context
534 def method(self, cr, uid, ids, args, context=None):
537 may be called in both record and traditional styles, like::
539 # recs = model.browse(cr, uid, ids, context)
542 model.method(cr, uid, ids, args, context=context)
544 It is generally not necessary, see :func:`guess`.
546 method._api = cr_uid_ids_context
547 upgrade = get_upgrade(method)
549 def new_api(self, *args, **kwargs):
550 cr, uid, context = self.env.args
551 kwargs['context'] = context
552 result = method(self._model, cr, uid, self.ids, *args, **kwargs)
553 return upgrade(self, result)
555 return make_wrapper(method, method, new_api)
559 """ Decorate a method that supports the old-style api only. A new-style api
560 may be provided by redefining a method with the same name and decorated
564 def foo(self, cr, uid, ids, context=None):
571 Note that the wrapper method uses the docstring of the first method.
573 # retrieve method_v8 from the caller's frame
574 frame = currentframe().f_back
575 method = frame.f_locals.get(method_v7.__name__)
576 method_v8 = getattr(method, '_v8', method)
578 wrapper = make_wrapper(method_v7, method_v7, method_v8)
579 wrapper._v7 = method_v7
580 wrapper._v8 = method_v8
585 """ Decorate a method that supports the new-style api only. An old-style api
586 may be provided by redefining a method with the same name and decorated
594 def foo(self, cr, uid, ids, context=None):
597 Note that the wrapper method uses the docstring of the first method.
599 # retrieve method_v7 from the caller's frame
600 frame = currentframe().f_back
601 method = frame.f_locals.get(method_v8.__name__)
602 method_v7 = getattr(method, '_v7', method)
604 wrapper = make_wrapper(method_v8, method_v7, method_v8)
605 wrapper._v7 = method_v7
606 wrapper._v8 = method_v8
611 """ Decorate a method to prevent any effect from :func:`guess`. """
617 """ Decorate `method` to make it callable in both traditional and record
618 styles. This decorator is applied automatically by the model's
619 metaclass, and has no effect on already-decorated methods.
621 The API style is determined by heuristics on the parameter names: ``cr``
622 or ``cursor`` for the cursor, ``uid`` or ``user`` for the user id,
623 ``id`` or ``ids`` for a list of record ids, and ``context`` for the
624 context dictionary. If a traditional API is recognized, one of the
625 decorators :func:`cr`, :func:`cr_context`, :func:`cr_uid`,
626 :func:`cr_uid_context`, :func:`cr_uid_id`, :func:`cr_uid_id_context`,
627 :func:`cr_uid_ids`, :func:`cr_uid_ids_context` is applied on the method.
629 Method calls are considered traditional style when their first parameter
630 is a database cursor.
632 if hasattr(method, '_api'):
635 # introspection on argument names to determine api style
636 args, vname, kwname, defaults = getargspec(method)
637 names = tuple(args) + (None,) * 4
639 if names[0] == 'self':
640 if names[1] in ('cr', 'cursor'):
641 if names[2] in ('uid', 'user'):
642 if names[3] == 'ids':
643 if 'context' in names or kwname:
644 return cr_uid_ids_context(method)
646 return cr_uid_ids(method)
647 elif names[3] == 'id':
648 if 'context' in names or kwname:
649 return cr_uid_id_context(method)
651 return cr_uid_id(method)
652 elif 'context' in names or kwname:
653 return cr_uid_context(method)
655 return cr_uid(method)
656 elif 'context' in names:
657 return cr_context(method)
661 # no wrapping by default
662 return noguess(method)
665 def expected(decorator, func):
666 """ Decorate `func` with `decorator` if `func` is not wrapped yet. """
667 return decorator(func) if not hasattr(func, '_orig') else func
671 class Environment(object):
672 """ An environment wraps data for ORM records:
674 - :attr:`cr`, the current database cursor;
675 - :attr:`uid`, the current user id;
676 - :attr:`context`, the current context dictionary.
678 It also provides access to the registry, a cache for records, and a data
679 structure to manage recomputations.
686 """ Context manager for a set of environments. """
687 if hasattr(cls._local, 'environments'):
691 cls._local.environments = Environments()
694 release_local(cls._local)
698 """ Clear the set of environments.
699 This may be useful when recreating a registry inside a transaction.
701 cls._local.environments = Environments()
703 def __new__(cls, cr, uid, context):
704 assert context is not None
705 args = (cr, uid, context)
707 # if env already exists, return it
708 env, envs = None, cls._local.environments
713 # otherwise create environment, and add it in the set
714 self = object.__new__(cls)
715 self.cr, self.uid, self.context = self.args = (cr, uid, frozendict(context))
716 self.registry = RegistryManager.get(cr.dbname)
717 self.cache = defaultdict(dict) # {field: {id: value, ...}, ...}
718 self.prefetch = defaultdict(set) # {model_name: set(id), ...}
719 self.computed = defaultdict(set) # {field: set(id), ...}
720 self.dirty = defaultdict(set) # {record: set(field_name), ...}
725 def __getitem__(self, model_name):
726 """ return a given model """
727 return self.registry[model_name]._browse(self, ())
729 def __call__(self, cr=None, user=None, context=None):
730 """ Return an environment based on `self` with modified parameters.
732 :param cr: optional database cursor to change the current cursor
733 :param user: optional user/user id to change the current user
734 :param context: optional context dictionary to change the current context
736 cr = self.cr if cr is None else cr
737 uid = self.uid if user is None else int(user)
738 context = self.context if context is None else context
739 return Environment(cr, uid, context)
741 def ref(self, xml_id, raise_if_not_found=True):
742 """ return the record corresponding to the given `xml_id` """
743 return self['ir.model.data'].xmlid_to_object(xml_id, raise_if_not_found=raise_if_not_found)
747 """ return the current user (as an instance) """
748 return self(user=SUPERUSER_ID)['res.users'].browse(self.uid)
752 """ return the current language code """
753 return self.context.get('lang')
756 def _do_in_mode(self, mode):
764 self.all.mode = False
767 def do_in_draft(self):
768 """ Context-switch to draft mode, where all field updates are done in
771 return self._do_in_mode(True)
775 """ Return whether we are in draft mode. """
776 return bool(self.all.mode)
778 def do_in_onchange(self):
779 """ Context-switch to 'onchange' draft mode, which is a specialized
780 draft mode used during execution of onchange methods.
782 return self._do_in_mode('onchange')
785 def in_onchange(self):
786 """ Return whether we are in 'onchange' draft mode. """
787 return self.all.mode == 'onchange'
789 def invalidate(self, spec):
790 """ Invalidate some fields for some records in the cache of all
793 :param spec: what to invalidate, a list of `(field, ids)` pair,
794 where `field` is a field object, and `ids` is a list of record
795 ids or ``None`` (to invalidate all records).
799 for env in list(self.all):
801 for field, ids in spec:
806 field_cache = c[field]
808 field_cache.pop(id, None)
810 def invalidate_all(self):
811 """ Clear the cache of all environments. """
812 for env in list(self.all):
819 """ Clear all record caches, and discard all fields to recompute.
820 This may be useful when recovering from a failed ORM operation.
822 self.invalidate_all()
823 self.all.todo.clear()
826 def clear_upon_failure(self):
827 """ Context manager that clears the environments (caches and fields to
828 recompute) upon exception.
836 def field_todo(self, field):
837 """ Check whether `field` must be recomputed, and returns a recordset
838 with all records to recompute for `field`.
840 if field in self.all.todo:
841 return reduce(operator.or_, self.all.todo[field])
843 def check_todo(self, field, record):
844 """ Check whether `field` must be recomputed on `record`, and if so,
845 returns the corresponding recordset to recompute.
847 for recs in self.all.todo.get(field, []):
851 def add_todo(self, field, records):
852 """ Mark `field` to be recomputed on `records`. """
853 recs_list = self.all.todo.setdefault(field, [])
854 recs_list.append(records)
856 def remove_todo(self, field, records):
857 """ Mark `field` as recomputed on `records`. """
858 recs_list = [recs - records for recs in self.all.todo.pop(field, [])]
859 recs_list = filter(None, recs_list)
861 self.all.todo[field] = recs_list
864 """ Return whether some fields must be recomputed. """
865 return bool(self.all.todo)
868 """ Return a pair `(field, records)` to recompute. """
869 for field, recs_list in self.all.todo.iteritems():
870 return field, recs_list[0]
872 def check_cache(self):
873 """ Check the cache consistency. """
874 # make a full copy of the cache, and invalidate it
876 (field, dict(field_cache))
877 for field, field_cache in self.cache.iteritems()
879 self.invalidate_all()
881 # re-fetch the records, and compare with their former cache
883 for field, field_dump in cache_dump.iteritems():
884 ids = filter(None, field_dump)
885 records = self[field.model_name].browse(ids)
886 for record in records:
888 cached = field_dump[record.id]
889 fetched = record[field.name]
890 if fetched != cached:
891 info = {'cached': cached, 'fetched': fetched}
892 invalids.append((field, record, info))
893 except (AccessError, MissingError):
897 raise Warning('Invalid cache for fields\n' + pformat(invalids))
900 class Environments(object):
901 """ A common object for all environments in a request. """
903 self.envs = WeakSet() # weak set of environments
904 self.todo = {} # recomputations {field: [records]}
905 self.mode = False # flag for draft/onchange
908 """ Add the environment `env`. """
912 """ Iterate over environments. """
913 return iter(self.envs)
916 # keep those imports here in order to handle cyclic dependencies correctly
917 from openerp import SUPERUSER_ID
918 from openerp.exceptions import Warning, AccessError, MissingError
919 from openerp.modules.registry import RegistryManager