da9e62f1549c7df9475df31e6f885d7806ac83d6
[odoo/odoo.git] / openerp / api.py
1 # -*- coding: utf-8 -*-
2 ##############################################################################
3 #
4 #    OpenERP, Open Source Management Solution
5 #    Copyright (C) 2013 OpenERP (<http://www.openerp.com>).
6 #
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.
11 #
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.
16 #
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/>.
19 #
20 ##############################################################################
21
22 """ This module provides the elements for managing two different API styles,
23     namely the "traditional" and "record" styles.
24
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.
30
31     For instance, the statements::
32
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):
36             print rec.name
37         model.write(cr, uid, ids, VALUES, context=context)
38
39     may also be written as::
40
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
45             print rec.name
46         recs.write(VALUES)                  # update all records in recs
47
48     Methods written in the "traditional" style are automatically decorated,
49     following some heuristics based on parameter names.
50 """
51
52 __all__ = [
53     'Environment',
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',
59 ]
60
61 import logging
62 import operator
63
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
70
71 from openerp.tools import frozendict
72
73 _logger = logging.getLogger(__name__)
74
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
82 #
83 # On wrapping method only:
84 #  - method._orig: original method
85 #
86
87 WRAPPED_ATTRS = ('__module__', '__name__', '__doc__', '_api', '_constrains',
88                  '_depends', '_onchange', '_returns', 'clear_cache')
89
90 INHERITED_ATTRS = ('_returns',)
91
92
93 class Meta(type):
94     """ Metaclass that automatically decorates traditional-style methods by
95         guessing their API. It also implements the inheritance of the
96         :func:`returns` decorators.
97     """
98
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, {})
102
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)
107
108                 # guess calling convention if none is given
109                 if not hasattr(value, '_api'):
110                     try:
111                         value = guess(value)
112                     except TypeError:
113                         pass
114
115                 attrs[key] = value
116
117         return type.__new__(meta, name, bases, attrs)
118
119
120 identity = lambda x: x
121
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)
128
129 def propagate(from_method, to_method):
130     """ Propagate decorators from `from_method` to `to_method`, and return the
131         resulting method.
132     """
133     if from_method:
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))
137     return to_method
138
139
140 def constrains(*args):
141     """ Decorates a constraint checker. Each argument must be a field name
142     used in the check::
143
144         @api.one
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")
149
150     Invoked on the records on which one of the named fields has been modified.
151
152     Should raise :class:`~openerp.exceptions.ValidationError` if the
153     validation failed.
154     """
155     return lambda method: decorate(method, '_constrains', args)
156
157
158 def onchange(*args):
159     """ Return a decorator to decorate an onchange method for given fields.
160         Each argument must be a field name::
161
162             @api.onchange('partner_id')
163             def _onchange_partner(self):
164                 self.message = "Dear %s" % (self.partner_id.name or "")
165
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.
170     """
171     return lambda method: decorate(method, '_onchange', args)
172
173
174 def depends(*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::
178
179             pname = fields.Char(compute='_compute_pname')
180
181             @api.one
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()
186                 else:
187                     self.pname = self.partner_id.name
188
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.
191     """
192     if args and callable(args[0]):
193         args = 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)
197
198
199 def returns(model, downgrade=None):
200     """ Return a decorator for methods that return instances of `model`.
201
202         :param model: a model name, or ``'self'`` for the current model
203
204         :param downgrade: a function `downgrade(value)` to convert the
205             record-style `value` to a traditional-style output
206
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::
209
210             @model
211             @returns('res.partner')
212             def find_partner(self, arg):
213                 ...     # return some record
214
215             # output depends on call style: traditional vs record style
216             partner_id = model.find_partner(cr, uid, arg, context=context)
217
218             # recs = model.browse(cr, uid, ids, context)
219             partner_record = recs.find_partner(arg)
220
221         Note that the decorated method must satisfy that convention.
222
223         Those decorators are automatically *inherited*: a method that overrides
224         a decorated existing method will be decorated with the same
225         ``@returns(model)``.
226     """
227     return lambda method: decorate(method, '_returns', (model, downgrade))
228
229
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)
236         else:
237             return old_api(self, *args, **kwargs)
238
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
244
245     return wrapper
246
247
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`.
251     """
252     spec = getattr(method, '_returns', None)
253     if spec:
254         model, downgrade = spec
255         return downgrade or (lambda value: value.ids)
256     else:
257         return lambda value: value
258
259
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`.
263     """
264     spec = getattr(method, '_returns', None)
265     if spec:
266         model, downgrade = spec
267         if model == 'self':
268             return lambda self, value: self.browse(value)
269         else:
270             return lambda self, value: self.env[model].browse(value)
271     else:
272         return lambda self, value: value
273
274
275 def get_aggregate(method):
276     """ Return a function `aggregate(self, value)` that aggregates record-style
277         `value` for a method decorated with ``@one``.
278     """
279     spec = getattr(method, '_returns', None)
280     if spec:
281         # value is a list of instances, concatenate them
282         model, downgrade = spec
283         if model == 'self':
284             return lambda self, value: sum(value, self.browse())
285         else:
286             return lambda self, value: sum(value, self.env[model].browse())
287     else:
288         return lambda self, value: value
289
290
291 def get_context_split(method):
292     """ Return a function `split` that extracts the context from a pair of
293         positional and keyword arguments::
294
295             context, args, kwargs = split(args, kwargs)
296     """
297     pos = len(getargspec(method).args) - 1
298
299     def split(args, kwargs):
300         if pos < len(args):
301             return args[pos], args[:pos], kwargs
302         else:
303             return kwargs.pop('context', None), args, kwargs
304
305     return split
306
307
308 def model(method):
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::
311
312             @api.model
313             def method(self, args):
314                 ...
315
316         may be called in both record and traditional styles, like::
317
318             # recs = model.browse(cr, uid, ids, context)
319             recs.method(args)
320
321             model.method(cr, uid, args, context=context)
322
323         Notice that no `ids` are passed to the method in the traditional style.
324     """
325     method._api = model
326     split = get_context_split(method)
327     downgrade = get_downgrade(method)
328
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)
334
335     return make_wrapper(method, old_api, method)
336
337
338 def multi(method):
339     """ Decorate a record-style method where `self` is a recordset. The method
340         typically defines an operation on records. Such a method::
341
342             @api.multi
343             def method(self, args):
344                 ...
345
346         may be called in both record and traditional styles, like::
347
348             # recs = model.browse(cr, uid, ids, context)
349             recs.method(args)
350
351             model.method(cr, uid, ids, args, context=context)
352     """
353     method._api = multi
354     split = get_context_split(method)
355     downgrade = get_downgrade(method)
356
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)
362
363     return make_wrapper(method, old_api, method)
364
365
366 def one(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::
371
372             @api.one
373             def method(self, args):
374                 return self.name
375
376         may be called in both record and traditional styles, like::
377
378             # recs = model.browse(cr, uid, ids, context)
379             names = recs.method(args)
380
381             names = model.method(cr, uid, ids, args, context=context)
382     """
383     method._api = one
384     split = get_context_split(method)
385     downgrade = get_downgrade(method)
386     aggregate = get_aggregate(method)
387
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)
393
394     def new_api(self, *args, **kwargs):
395         result = [method(rec, *args, **kwargs) for rec in self]
396         return aggregate(self, result)
397
398     return make_wrapper(method, old_api, new_api)
399
400
401 def cr(method):
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::
404
405             # recs = model.browse(cr, uid, ids, context)
406             recs.method(args)
407
408             model.method(cr, args)
409     """
410     method._api = cr
411     upgrade = get_upgrade(method)
412
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)
417
418     return make_wrapper(method, method, new_api)
419
420
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)
425
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)
431
432     return make_wrapper(method, method, new_api)
433
434
435 def cr_uid(method):
436     """ Decorate a traditional-style method that takes `cr`, `uid` as parameters. """
437     method._api = cr_uid
438     upgrade = get_upgrade(method)
439
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)
444
445     return make_wrapper(method, method, new_api)
446
447
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
451         styles, like::
452
453             # recs = model.browse(cr, uid, ids, context)
454             recs.method(args)
455
456             model.method(cr, uid, args, context=context)
457     """
458     method._api = cr_uid_context
459     upgrade = get_upgrade(method)
460
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)
466
467     return make_wrapper(method, method, new_api)
468
469
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.
474     """
475     method._api = cr_uid_id
476     upgrade = get_upgrade(method)
477
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)
482
483     return make_wrapper(method, method, new_api)
484
485
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::
489
490             @api.cr_uid_id
491             def method(self, cr, uid, id, args, context=None):
492                 ...
493
494         may be called in both record and traditional styles, like::
495
496             # rec = model.browse(cr, uid, id, context)
497             rec.method(args)
498
499             model.method(cr, uid, id, args, context=context)
500     """
501     method._api = cr_uid_id_context
502     upgrade = get_upgrade(method)
503
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)
509
510     return make_wrapper(method, method, new_api)
511
512
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
516         styles.
517     """
518     method._api = cr_uid_ids
519     upgrade = get_upgrade(method)
520
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)
525
526     return make_wrapper(method, method, new_api)
527
528
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::
532
533             @api.cr_uid_ids_context
534             def method(self, cr, uid, ids, args, context=None):
535                 ...
536
537         may be called in both record and traditional styles, like::
538
539             # recs = model.browse(cr, uid, ids, context)
540             recs.method(args)
541
542             model.method(cr, uid, ids, args, context=context)
543
544         It is generally not necessary, see :func:`guess`.
545     """
546     method._api = cr_uid_ids_context
547     upgrade = get_upgrade(method)
548
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)
554
555     return make_wrapper(method, method, new_api)
556
557
558 def v7(method_v7):
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
561         with :func:`~.v8`::
562
563             @api.v7
564             def foo(self, cr, uid, ids, context=None):
565                 ...
566
567             @api.v8
568             def foo(self):
569                 ...
570
571         Note that the wrapper method uses the docstring of the first method.
572     """
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)
577
578     wrapper = make_wrapper(method_v7, method_v7, method_v8)
579     wrapper._v7 = method_v7
580     wrapper._v8 = method_v8
581     return wrapper
582
583
584 def 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
587         with :func:`~.v7`::
588
589             @api.v8
590             def foo(self):
591                 ...
592
593             @api.v7
594             def foo(self, cr, uid, ids, context=None):
595                 ...
596
597         Note that the wrapper method uses the docstring of the first method.
598     """
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)
603
604     wrapper = make_wrapper(method_v8, method_v7, method_v8)
605     wrapper._v7 = method_v7
606     wrapper._v8 = method_v8
607     return wrapper
608
609
610 def noguess(method):
611     """ Decorate a method to prevent any effect from :func:`guess`. """
612     method._api = False
613     return method
614
615
616 def guess(method):
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.
620
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.
628
629         Method calls are considered traditional style when their first parameter
630         is a database cursor.
631     """
632     if hasattr(method, '_api'):
633         return method
634
635     # introspection on argument names to determine api style
636     args, vname, kwname, defaults = getargspec(method)
637     names = tuple(args) + (None,) * 4
638
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)
645                     else:
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)
650                     else:
651                         return cr_uid_id(method)
652                 elif 'context' in names or kwname:
653                     return cr_uid_context(method)
654                 else:
655                     return cr_uid(method)
656             elif 'context' in names:
657                 return cr_context(method)
658             else:
659                 return cr(method)
660
661     # no wrapping by default
662     return noguess(method)
663
664
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
668
669
670
671 class Environment(object):
672     """ An environment wraps data for ORM records:
673
674          - :attr:`cr`, the current database cursor;
675          - :attr:`uid`, the current user id;
676          - :attr:`context`, the current context dictionary.
677
678         It also provides access to the registry, a cache for records, and a data
679         structure to manage recomputations.
680     """
681     _local = Local()
682
683     @classmethod
684     @contextmanager
685     def manage(cls):
686         """ Context manager for a set of environments. """
687         if hasattr(cls._local, 'environments'):
688             yield
689         else:
690             try:
691                 cls._local.environments = Environments()
692                 yield
693             finally:
694                 release_local(cls._local)
695
696     @classmethod
697     def reset(cls):
698         """ Clear the set of environments.
699             This may be useful when recreating a registry inside a transaction.
700         """
701         cls._local.environments = Environments()
702
703     def __new__(cls, cr, uid, context):
704         assert context is not None
705         args = (cr, uid, context)
706
707         # if env already exists, return it
708         env, envs = None, cls._local.environments
709         for env in envs:
710             if env.args == args:
711                 return env
712
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 = set()                  # set(record)
721         self.all = envs
722         envs.add(self)
723         return self
724
725     def __getitem__(self, model_name):
726         """ return a given model """
727         return self.registry[model_name]._browse(self, ())
728
729     def __call__(self, cr=None, user=None, context=None):
730         """ Return an environment based on `self` with modified parameters.
731
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
735         """
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)
740
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)
744
745     @property
746     def user(self):
747         """ return the current user (as an instance) """
748         return self(user=SUPERUSER_ID)['res.users'].browse(self.uid)
749
750     @property
751     def lang(self):
752         """ return the current language code """
753         return self.context.get('lang')
754
755     @contextmanager
756     def _do_in_mode(self, mode):
757         if self.all.mode:
758             yield
759         else:
760             try:
761                 self.all.mode = mode
762                 yield
763             finally:
764                 self.all.mode = False
765                 self.dirty.clear()
766
767     def do_in_draft(self):
768         """ Context-switch to draft mode, where all field updates are done in
769             cache only.
770         """
771         return self._do_in_mode(True)
772
773     @property
774     def in_draft(self):
775         """ Return whether we are in draft mode. """
776         return bool(self.all.mode)
777
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.
781         """
782         return self._do_in_mode('onchange')
783
784     @property
785     def in_onchange(self):
786         """ Return whether we are in 'onchange' draft mode. """
787         return self.all.mode == 'onchange'
788
789     def invalidate(self, spec):
790         """ Invalidate some fields for some records in the cache of all
791             environments.
792
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).
796         """
797         if not spec:
798             return
799         for env in list(self.all):
800             c = env.cache
801             for field, ids in spec:
802                 if ids is None:
803                     if field in c:
804                         del c[field]
805                 else:
806                     field_cache = c[field]
807                     for id in ids:
808                         field_cache.pop(id, None)
809
810     def invalidate_all(self):
811         """ Clear the cache of all environments. """
812         for env in list(self.all):
813             env.cache.clear()
814             env.prefetch.clear()
815             env.computed.clear()
816             env.dirty.clear()
817
818     def field_todo(self, field):
819         """ Check whether `field` must be recomputed, and returns a recordset
820             with all records to recompute for `field`.
821         """
822         if field in self.all.todo:
823             return reduce(operator.or_, self.all.todo[field])
824
825     def check_todo(self, field, record):
826         """ Check whether `field` must be recomputed on `record`, and if so,
827             returns the corresponding recordset to recompute.
828         """
829         for recs in self.all.todo.get(field, []):
830             if recs & record:
831                 return recs
832
833     def add_todo(self, field, records):
834         """ Mark `field` to be recomputed on `records`. """
835         recs_list = self.all.todo.setdefault(field, [])
836         recs_list.append(records)
837
838     def remove_todo(self, field, records):
839         """ Mark `field` as recomputed on `records`. """
840         recs_list = [recs - records for recs in self.all.todo.pop(field, [])]
841         recs_list = filter(None, recs_list)
842         if recs_list:
843             self.all.todo[field] = recs_list
844
845     def has_todo(self):
846         """ Return whether some fields must be recomputed. """
847         return bool(self.all.todo)
848
849     def get_todo(self):
850         """ Return a pair `(field, records)` to recompute. """
851         for field, recs_list in self.all.todo.iteritems():
852             return field, recs_list[0]
853
854     def check_cache(self):
855         """ Check the cache consistency. """
856         # make a full copy of the cache, and invalidate it
857         cache_dump = dict(
858             (field, dict(field_cache))
859             for field, field_cache in self.cache.iteritems()
860         )
861         self.invalidate_all()
862
863         # re-fetch the records, and compare with their former cache
864         invalids = []
865         for field, field_dump in cache_dump.iteritems():
866             ids = filter(None, field_dump)
867             records = self[field.model_name].browse(ids)
868             for record in records:
869                 try:
870                     cached = field_dump[record.id]
871                     fetched = record[field.name]
872                     if fetched != cached:
873                         info = {'cached': cached, 'fetched': fetched}
874                         invalids.append((field, record, info))
875                 except (AccessError, MissingError):
876                     pass
877
878         if invalids:
879             raise Warning('Invalid cache for fields\n' + pformat(invalids))
880
881
882 class Environments(object):
883     """ A common object for all environments in a request. """
884     def __init__(self):
885         self.envs = WeakSet()           # weak set of environments
886         self.todo = {}                  # recomputations {field: [records]}
887         self.mode = False               # flag for draft/onchange
888
889     def add(self, env):
890         """ Add the environment `env`. """
891         self.envs.add(env)
892
893     def __iter__(self):
894         """ Iterate over environments. """
895         return iter(self.envs)
896
897
898 # keep those imports here in order to handle cyclic dependencies correctly
899 from openerp import SUPERUSER_ID
900 from openerp.exceptions import Warning, AccessError, MissingError
901 from openerp.modules.registry import RegistryManager