1# -*- coding: utf-8 -*-
2# Part of Odoo. See LICENSE file for full copyright and licensing details.
3
4"""The Odoo API module defines Odoo Environments and method decorators.
5
6.. todo:: Document this module
7"""
8
9__all__ = [
10    'Environment',
11    'Meta',
12    'model',
13    'constrains', 'depends', 'onchange', 'returns',
14    'call_kw',
15]
16
17import logging
18from collections import defaultdict
19from collections.abc import Mapping
20from contextlib import contextmanager
21from inspect import signature
22from pprint import pformat
23from weakref import WeakSet
24
25from decorator import decorate
26from werkzeug.local import Local, release_local
27
28from .exceptions import CacheMiss
29from .tools import frozendict, classproperty, lazy_property, StackMap
30from .tools.translate import _
31
32_logger = logging.getLogger(__name__)
33
34# The following attributes are used, and reflected on wrapping methods:
35#  - method._constrains: set by @constrains, specifies constraint dependencies
36#  - method._depends: set by @depends, specifies compute dependencies
37#  - method._returns: set by @returns, specifies return model
38#  - method._onchange: set by @onchange, specifies onchange fields
39#  - method.clear_cache: set by @ormcache, used to clear the cache
40#
41# On wrapping method only:
42#  - method._api: decorator function, used for re-applying decorator
43#
44
45INHERITED_ATTRS = ('_returns',)
46
47
48class Params(object):
49    def __init__(self, args, kwargs):
50        self.args = args
51        self.kwargs = kwargs
52
53    def __str__(self):
54        params = []
55        for arg in self.args:
56            params.append(repr(arg))
57        for item in sorted(self.kwargs.items()):
58            params.append("%s=%r" % item)
59        return ', '.join(params)
60
61
62class Meta(type):
63    """ Metaclass that automatically decorates traditional-style methods by
64        guessing their API. It also implements the inheritance of the
65        :func:`returns` decorators.
66    """
67
68    def __new__(meta, name, bases, attrs):
69        # dummy parent class to catch overridden methods decorated with 'returns'
70        parent = type.__new__(meta, name, bases, {})
71
72        for key, value in list(attrs.items()):
73            if not key.startswith('__') and callable(value):
74                # make the method inherit from decorators
75                value = propagate(getattr(parent, key, None), value)
76
77                if (getattr(value, '_api', None) or '').startswith('cr'):
78                    _logger.warning("Deprecated method %s.%s in module %s", name, key, attrs.get('__module__'))
79
80                attrs[key] = value
81
82        return type.__new__(meta, name, bases, attrs)
83
84
85def attrsetter(attr, value):
86    """ Return a function that sets ``attr`` on its argument and returns it. """
87    return lambda method: setattr(method, attr, value) or method
88
89def propagate(method1, method2):
90    """ Propagate decorators from ``method1`` to ``method2``, and return the
91        resulting method.
92    """
93    if method1:
94        for attr in INHERITED_ATTRS:
95            if hasattr(method1, attr) and not hasattr(method2, attr):
96                setattr(method2, attr, getattr(method1, attr))
97    return method2
98
99
100def constrains(*args):
101    """Decorate a constraint checker.
102
103    Each argument must be a field name used in the check::
104
105        @api.constrains('name', 'description')
106        def _check_description(self):
107            for record in self:
108                if record.name == record.description:
109                    raise ValidationError("Fields name and description must be different")
110
111    Invoked on the records on which one of the named fields has been modified.
112
113    Should raise :exc:`~odoo.exceptions.ValidationError` if the
114    validation failed.
115
116    .. warning::
117
118        ``@constrains`` only supports simple field names, dotted names
119        (fields of relational fields e.g. ``partner_id.customer``) are not
120        supported and will be ignored.
121
122        ``@constrains`` will be triggered only if the declared fields in the
123        decorated method are included in the ``create`` or ``write`` call.
124        It implies that fields not present in a view will not trigger a call
125        during a record creation. A override of ``create`` is necessary to make
126        sure a constraint will always be triggered (e.g. to test the absence of
127        value).
128
129    """
130    return attrsetter('_constrains', args)
131
132
133def onchange(*args):
134    """Return a decorator to decorate an onchange method for given fields.
135
136    In the form views where the field appears, the method will be called
137    when one of the given fields is modified. The method is invoked on a
138    pseudo-record that contains the values present in the form. Field
139    assignments on that record are automatically sent back to the client.
140
141    Each argument must be a field name::
142
143        @api.onchange('partner_id')
144        def _onchange_partner(self):
145            self.message = "Dear %s" % (self.partner_id.name or "")
146
147    .. code-block:: python
148
149        return {
150            'warning': {'title': "Warning", 'message': "What is this?", 'type': 'notification'},
151        }
152
153    If the type is set to notification, the warning will be displayed in a notification.
154    Otherwise it will be displayed in a dialog as default.
155
156    .. warning::
157
158        ``@onchange`` only supports simple field names, dotted names
159        (fields of relational fields e.g. ``partner_id.tz``) are not
160        supported and will be ignored
161
162    .. danger::
163
164        Since ``@onchange`` returns a recordset of pseudo-records,
165        calling any one of the CRUD methods
166        (:meth:`create`, :meth:`read`, :meth:`write`, :meth:`unlink`)
167        on the aforementioned recordset is undefined behaviour,
168        as they potentially do not exist in the database yet.
169
170        Instead, simply set the record's field like shown in the example
171        above or call the :meth:`update` method.
172
173    .. warning::
174
175        It is not possible for a ``one2many`` or ``many2many`` field to modify
176        itself via onchange. This is a webclient limitation - see `#2693 <https://github.com/odoo/odoo/issues/2693>`_.
177
178    """
179    return attrsetter('_onchange', args)
180
181
182def depends(*args):
183    """ Return a decorator that specifies the field dependencies of a "compute"
184        method (for new-style function fields). Each argument must be a string
185        that consists in a dot-separated sequence of field names::
186
187            pname = fields.Char(compute='_compute_pname')
188
189            @api.depends('partner_id.name', 'partner_id.is_company')
190            def _compute_pname(self):
191                for record in self:
192                    if record.partner_id.is_company:
193                        record.pname = (record.partner_id.name or "").upper()
194                    else:
195                        record.pname = record.partner_id.name
196
197        One may also pass a single function as argument. In that case, the
198        dependencies are given by calling the function with the field's model.
199    """
200    if args and callable(args[0]):
201        args = args[0]
202    elif any('id' in arg.split('.') for arg in args):
203        raise NotImplementedError("Compute method cannot depend on field 'id'.")
204    return attrsetter('_depends', args)
205
206
207def depends_context(*args):
208    """ Return a decorator that specifies the context dependencies of a
209    non-stored "compute" method.  Each argument is a key in the context's
210    dictionary::
211
212        price = fields.Float(compute='_compute_product_price')
213
214        @api.depends_context('pricelist')
215        def _compute_product_price(self):
216            for product in self:
217                if product.env.context.get('pricelist'):
218                    pricelist = self.env['product.pricelist'].browse(product.env.context['pricelist'])
219                else:
220                    pricelist = self.env['product.pricelist'].get_default_pricelist()
221                product.price = pricelist.get_products_price(product).get(product.id, 0.0)
222
223    All dependencies must be hashable.  The following keys have special
224    support:
225
226    * `company` (value in context or current company id),
227    * `uid` (current user id and superuser flag),
228    * `active_test` (value in env.context or value in field.context).
229    """
230    return attrsetter('_depends_context', args)
231
232
233def returns(model, downgrade=None, upgrade=None):
234    """ Return a decorator for methods that return instances of ``model``.
235
236        :param model: a model name, or ``'self'`` for the current model
237
238        :param downgrade: a function ``downgrade(self, value, *args, **kwargs)``
239            to convert the record-style ``value`` to a traditional-style output
240
241        :param upgrade: a function ``upgrade(self, value, *args, **kwargs)``
242            to convert the traditional-style ``value`` to a record-style output
243
244        The arguments ``self``, ``*args`` and ``**kwargs`` are the ones passed
245        to the method in the record-style.
246
247        The decorator adapts the method output to the api style: ``id``, ``ids`` or
248        ``False`` for the traditional style, and recordset for the record style::
249
250            @model
251            @returns('res.partner')
252            def find_partner(self, arg):
253                ...     # return some record
254
255            # output depends on call style: traditional vs record style
256            partner_id = model.find_partner(cr, uid, arg, context=context)
257
258            # recs = model.browse(cr, uid, ids, context)
259            partner_record = recs.find_partner(arg)
260
261        Note that the decorated method must satisfy that convention.
262
263        Those decorators are automatically *inherited*: a method that overrides
264        a decorated existing method will be decorated with the same
265        ``@returns(model)``.
266    """
267    return attrsetter('_returns', (model, downgrade, upgrade))
268
269
270def downgrade(method, value, self, args, kwargs):
271    """ Convert ``value`` returned by ``method`` on ``self`` to traditional style. """
272    spec = getattr(method, '_returns', None)
273    if not spec:
274        return value
275    _, convert, _ = spec
276    if convert and len(signature(convert).parameters) > 1:
277        return convert(self, value, *args, **kwargs)
278    elif convert:
279        return convert(value)
280    else:
281        return value.ids
282
283
284def split_context(method, args, kwargs):
285    """ Extract the context from a pair of positional and keyword arguments.
286        Return a triple ``context, args, kwargs``.
287    """
288    return kwargs.pop('context', None), args, kwargs
289
290
291def autovacuum(method):
292    """
293    Decorate a method so that it is called by the daily vacuum cron job (model
294    ``ir.autovacuum``).  This is typically used for garbage-collection-like
295    tasks that do not deserve a specific cron job.
296    """
297    assert method.__name__.startswith('_'), "%s: autovacuum methods must be private" % method.__name__
298    method._autovacuum = True
299    return method
300
301
302def model(method):
303    """ Decorate a record-style method where ``self`` is a recordset, but its
304        contents is not relevant, only the model is. Such a method::
305
306            @api.model
307            def method(self, args):
308                ...
309
310    """
311    if method.__name__ == 'create':
312        return model_create_single(method)
313    method._api = 'model'
314    return method
315
316
317_create_logger = logging.getLogger(__name__ + '.create')
318
319
320def _model_create_single(create, self, arg):
321    # 'create' expects a dict and returns a record
322    if isinstance(arg, Mapping):
323        return create(self, arg)
324    if len(arg) > 1:
325        _create_logger.debug("%s.create() called with %d dicts", self, len(arg))
326    return self.browse().concat(*(create(self, vals) for vals in arg))
327
328
329def model_create_single(method):
330    """ Decorate a method that takes a dictionary and creates a single record.
331        The method may be called with either a single dict or a list of dicts::
332
333            record = model.create(vals)
334            records = model.create([vals, ...])
335    """
336    wrapper = decorate(method, _model_create_single)
337    wrapper._api = 'model_create'
338    return wrapper
339
340
341def _model_create_multi(create, self, arg):
342    # 'create' expects a list of dicts and returns a recordset
343    if isinstance(arg, Mapping):
344        return create(self, [arg])
345    return create(self, arg)
346
347
348def model_create_multi(method):
349    """ Decorate a method that takes a list of dictionaries and creates multiple
350        records. The method may be called with either a single dict or a list of
351        dicts::
352
353            record = model.create(vals)
354            records = model.create([vals, ...])
355    """
356    wrapper = decorate(method, _model_create_multi)
357    wrapper._api = 'model_create'
358    return wrapper
359
360
361def _call_kw_model(method, self, args, kwargs):
362    context, args, kwargs = split_context(method, args, kwargs)
363    recs = self.with_context(context or {})
364    _logger.debug("call %s.%s(%s)", recs, method.__name__, Params(args, kwargs))
365    result = method(recs, *args, **kwargs)
366    return downgrade(method, result, recs, args, kwargs)
367
368
369def _call_kw_model_create(method, self, args, kwargs):
370    # special case for method 'create'
371    context, args, kwargs = split_context(method, args, kwargs)
372    recs = self.with_context(context or {})
373    _logger.debug("call %s.%s(%s)", recs, method.__name__, Params(args, kwargs))
374    result = method(recs, *args, **kwargs)
375    return result.id if isinstance(args[0], Mapping) else result.ids
376
377
378def _call_kw_multi(method, self, args, kwargs):
379    ids, args = args[0], args[1:]
380    context, args, kwargs = split_context(method, args, kwargs)
381    recs = self.with_context(context or {}).browse(ids)
382    _logger.debug("call %s.%s(%s)", recs, method.__name__, Params(args, kwargs))
383    result = method(recs, *args, **kwargs)
384    return downgrade(method, result, recs, args, kwargs)
385
386
387def call_kw(model, name, args, kwargs):
388    """ Invoke the given method ``name`` on the recordset ``model``. """
389    method = getattr(type(model), name)
390    api = getattr(method, '_api', None)
391    if api == 'model':
392        result = _call_kw_model(method, model, args, kwargs)
393    elif api == 'model_create':
394        result = _call_kw_model_create(method, model, args, kwargs)
395    else:
396        result = _call_kw_multi(method, model, args, kwargs)
397    model.flush()
398    return result
399
400
401class Environment(Mapping):
402    """ An environment wraps data for ORM records:
403
404        - :attr:`cr`, the current database cursor;
405        - :attr:`uid`, the current user id;
406        - :attr:`context`, the current context dictionary;
407        - :attr:`su`, whether in superuser mode.
408
409        It provides access to the registry by implementing a mapping from model
410        names to new api models. It also holds a cache for records, and a data
411        structure to manage recomputations.
412    """
413    _local = Local()
414
415    @classproperty
416    def envs(cls):
417        return getattr(cls._local, 'environments', ())
418
419    @classmethod
420    @contextmanager
421    def manage(cls):
422        """ Context manager for a set of environments. """
423        if hasattr(cls._local, 'environments'):
424            yield
425        else:
426            try:
427                cls._local.environments = Environments()
428                yield
429            finally:
430                release_local(cls._local)
431
432    @classmethod
433    def reset(cls):
434        """ Clear the set of environments.
435            This may be useful when recreating a registry inside a transaction.
436        """
437        cls._local.environments = Environments()
438
439    def __new__(cls, cr, uid, context, su=False):
440        if uid == SUPERUSER_ID:
441            su = True
442        assert context is not None
443        args = (cr, uid, context, su)
444
445        # if env already exists, return it
446        env, envs = None, cls.envs
447        for env in envs:
448            if env.args == args:
449                return env
450
451        # otherwise create environment, and add it in the set
452        self = object.__new__(cls)
453        args = (cr, uid, frozendict(context), su)
454        self.cr, self.uid, self.context, self.su = self.args = args
455        self.registry = Registry(cr.dbname)
456        self.cache = envs.cache
457        self._cache_key = {}                    # memo {field: cache_key}
458        self._protected = envs.protected        # proxy to shared data structure
459        self.all = envs
460        envs.add(self)
461        return self
462
463    #
464    # Mapping methods
465    #
466
467    def __contains__(self, model_name):
468        """ Test whether the given model exists. """
469        return model_name in self.registry
470
471    def __getitem__(self, model_name):
472        """ Return an empty recordset from the given model. """
473        return self.registry[model_name]._browse(self, (), ())
474
475    def __iter__(self):
476        """ Return an iterator on model names. """
477        return iter(self.registry)
478
479    def __len__(self):
480        """ Return the size of the model registry. """
481        return len(self.registry)
482
483    def __eq__(self, other):
484        return self is other
485
486    def __ne__(self, other):
487        return self is not other
488
489    def __hash__(self):
490        return object.__hash__(self)
491
492    def __call__(self, cr=None, user=None, context=None, su=None):
493        """ Return an environment based on ``self`` with modified parameters.
494
495            :param cr: optional database cursor to change the current cursor
496            :param user: optional user/user id to change the current user
497            :param context: optional context dictionary to change the current context
498            :param su: optional boolean to change the superuser mode
499            :type context: dict
500            :type user: int or :class:`~odoo.addons.base.models.res_users`
501            :type su: bool
502        """
503        cr = self.cr if cr is None else cr
504        uid = self.uid if user is None else int(user)
505        context = self.context if context is None else context
506        su = (user is None and self.su) if su is None else su
507        return Environment(cr, uid, context, su)
508
509    def ref(self, xml_id, raise_if_not_found=True):
510        """Return the record corresponding to the given ``xml_id``."""
511        return self['ir.model.data'].xmlid_to_object(xml_id, raise_if_not_found=raise_if_not_found)
512
513    def is_superuser(self):
514        """ Return whether the environment is in superuser mode. """
515        return self.su
516
517    def is_admin(self):
518        """ Return whether the current user has group "Access Rights", or is in
519            superuser mode. """
520        return self.su or self.user._is_admin()
521
522    def is_system(self):
523        """ Return whether the current user has group "Settings", or is in
524            superuser mode. """
525        return self.su or self.user._is_system()
526
527    @lazy_property
528    def user(self):
529        """Return the current user (as an instance).
530
531        :rtype: :class:`~odoo.addons.base.models.res_users`"""
532        return self(su=True)['res.users'].browse(self.uid)
533
534    @lazy_property
535    def company(self):
536        """Return the current company (as an instance).
537
538        If not specified in the context (`allowed_company_ids`),
539        fallback on current user main company.
540
541        :raise AccessError: invalid or unauthorized `allowed_company_ids` context key content.
542        :return: current company (default=`self.user.company_id`)
543        :rtype: res.company
544
545        .. warning::
546
547            No sanity checks applied in sudo mode !
548            When in sudo mode, a user can access any company,
549            even if not in his allowed companies.
550
551            This allows to trigger inter-company modifications,
552            even if the current user doesn't have access to
553            the targeted company.
554        """
555        company_ids = self.context.get('allowed_company_ids', [])
556        if company_ids:
557            if not self.su:
558                user_company_ids = self.user.company_ids.ids
559                if any(cid not in user_company_ids for cid in company_ids):
560                    raise AccessError(_("Access to unauthorized or invalid companies."))
561            return self['res.company'].browse(company_ids[0])
562        return self.user.company_id
563
564    @lazy_property
565    def companies(self):
566        """Return a recordset of the enabled companies by the user.
567
568        If not specified in the context(`allowed_company_ids`),
569        fallback on current user companies.
570
571        :raise AccessError: invalid or unauthorized `allowed_company_ids` context key content.
572        :return: current companies (default=`self.user.company_ids`)
573        :rtype: res.company
574
575        .. warning::
576
577            No sanity checks applied in sudo mode !
578            When in sudo mode, a user can access any company,
579            even if not in his allowed companies.
580
581            This allows to trigger inter-company modifications,
582            even if the current user doesn't have access to
583            the targeted company.
584        """
585        company_ids = self.context.get('allowed_company_ids', [])
586        if company_ids:
587            if not self.su:
588                user_company_ids = self.user.company_ids.ids
589                if any(cid not in user_company_ids for cid in company_ids):
590                    raise AccessError(_("Access to unauthorized or invalid companies."))
591            return self['res.company'].browse(company_ids)
592        # By setting the default companies to all user companies instead of the main one
593        # we save a lot of potential trouble in all "out of context" calls, such as
594        # /mail/redirect or /web/image, etc. And it is not unsafe because the user does
595        # have access to these other companies. The risk of exposing foreign records
596        # (wrt to the context) is low because all normal RPCs will have a proper
597        # allowed_company_ids.
598        # Examples:
599        #   - when printing a report for several records from several companies
600        #   - when accessing to a record from the notification email template
601        #   - when loading an binary image on a template
602        return self.user.company_ids
603
604    @property
605    def lang(self):
606        """Return the current language code.
607
608        :rtype: str
609        """
610        return self.context.get('lang')
611
612    def clear(self):
613        """ Clear all record caches, and discard all fields to recompute.
614            This may be useful when recovering from a failed ORM operation.
615        """
616        self.cache.invalidate()
617        self.all.tocompute.clear()
618        self.all.towrite.clear()
619
620    @contextmanager
621    def clear_upon_failure(self):
622        """ Context manager that clears the environments (caches and fields to
623            recompute) upon exception.
624        """
625        tocompute = {
626            field: set(ids)
627            for field, ids in self.all.tocompute.items()
628        }
629        towrite = {
630            model: {
631                record_id: dict(values)
632                for record_id, values in id_values.items()
633            }
634            for model, id_values in self.all.towrite.items()
635        }
636        try:
637            yield
638        except Exception:
639            self.clear()
640            self.all.tocompute.update(tocompute)
641            for model, id_values in towrite.items():
642                for record_id, values in id_values.items():
643                    self.all.towrite[model][record_id].update(values)
644            raise
645
646    def is_protected(self, field, record):
647        """ Return whether `record` is protected against invalidation or
648            recomputation for `field`.
649        """
650        return record.id in self._protected.get(field, ())
651
652    def protected(self, field):
653        """ Return the recordset for which ``field`` should not be invalidated or recomputed. """
654        return self[field.model_name].browse(self._protected.get(field, ()))
655
656    @contextmanager
657    def protecting(self, what, records=None):
658        """ Prevent the invalidation or recomputation of fields on records.
659            The parameters are either:
660             - ``what`` a collection of fields and ``records`` a recordset, or
661             - ``what`` a collection of pairs ``(fields, records)``.
662        """
663        protected = self._protected
664        try:
665            protected.pushmap()
666            what = what if records is None else [(what, records)]
667            for fields, records in what:
668                for field in fields:
669                    ids = protected.get(field, frozenset())
670                    protected[field] = ids.union(records._ids)
671            yield
672        finally:
673            protected.popmap()
674
675    def fields_to_compute(self):
676        """ Return a view on the field to compute. """
677        return self.all.tocompute.keys()
678
679    def records_to_compute(self, field):
680        """ Return the records to compute for ``field``. """
681        ids = self.all.tocompute.get(field, ())
682        return self[field.model_name].browse(ids)
683
684    def is_to_compute(self, field, record):
685        """ Return whether ``field`` must be computed on ``record``. """
686        return record.id in self.all.tocompute.get(field, ())
687
688    def not_to_compute(self, field, records):
689        """ Return the subset of ``records`` for which ``field`` must not be computed. """
690        ids = self.all.tocompute.get(field, ())
691        return records.browse(id_ for id_ in records._ids if id_ not in ids)
692
693    def add_to_compute(self, field, records):
694        """ Mark ``field`` to be computed on ``records``. """
695        if not records:
696            return records
697        self.all.tocompute[field].update(records._ids)
698
699    def remove_to_compute(self, field, records):
700        """ Mark ``field`` as computed on ``records``. """
701        if not records:
702            return
703        ids = self.all.tocompute.get(field, None)
704        if ids is None:
705            return
706        ids.difference_update(records._ids)
707        if not ids:
708            del self.all.tocompute[field]
709
710    @contextmanager
711    def norecompute(self):
712        """ Delay recomputations (deprecated: this is not the default behavior). """
713        yield
714
715    def cache_key(self, field):
716        """ Return the cache key corresponding to ``field.depends_context``. """
717        try:
718            return self._cache_key[field]
719
720        except KeyError:
721            def get(key, get_context=self.context.get):
722                if key == 'company':
723                    return self.company.id
724                elif key == 'uid':
725                    return (self.uid, self.su)
726                elif key == 'active_test':
727                    return get_context('active_test', field.context.get('active_test', True))
728                else:
729                    val = get_context(key)
730                    if type(val) is list:
731                        val = tuple(val)
732                    try:
733                        hash(val)
734                    except TypeError:
735                        raise TypeError(
736                            "Can only create cache keys from hashable values, "
737                            "got non-hashable value {!r} at context key {!r} "
738                            "(dependency of field {})".format(val, key, field)
739                        ) from None  # we don't need to chain the exception created 2 lines above
740                    else:
741                        return val
742
743            result = tuple(get(key) for key in field.depends_context)
744            self._cache_key[field] = result
745            return result
746
747
748class Environments(object):
749    """ A common object for all environments in a request. """
750    def __init__(self):
751        self.envs = WeakSet()                   # weak set of environments
752        self.cache = Cache()                    # cache for all records
753        self.protected = StackMap()             # fields to protect {field: ids, ...}
754        self.tocompute = defaultdict(set)       # recomputations {field: ids}
755        # updates {model: {id: {field: value}}}
756        self.towrite = defaultdict(lambda: defaultdict(dict))
757
758    def add(self, env):
759        """ Add the environment ``env``. """
760        self.envs.add(env)
761
762    def __iter__(self):
763        """ Iterate over environments. """
764        return iter(self.envs)
765
766
767# sentinel value for optional parameters
768NOTHING = object()
769
770
771class Cache(object):
772    """ Implementation of the cache of records. """
773    def __init__(self):
774        # {field: {record_id: value}, field: {context_key: {record_id: value}}}
775        self._data = defaultdict(dict)
776
777    def contains(self, record, field):
778        """ Return whether ``record`` has a value for ``field``. """
779        field_cache = self._data.get(field, ())
780        if field_cache and field.depends_context:
781            field_cache = field_cache.get(record.env.cache_key(field), ())
782        return record.id in field_cache
783
784    def get(self, record, field, default=NOTHING):
785        """ Return the value of ``field`` for ``record``. """
786        try:
787            field_cache = self._data[field]
788            if field.depends_context:
789                field_cache = field_cache[record.env.cache_key(field)]
790            return field_cache[record._ids[0]]
791        except KeyError:
792            if default is NOTHING:
793                raise CacheMiss(record, field)
794            return default
795
796    def set(self, record, field, value):
797        """ Set the value of ``field`` for ``record``. """
798        field_cache = self._data[field]
799        if field.depends_context:
800            field_cache = field_cache.setdefault(record.env.cache_key(field), {})
801        field_cache[record._ids[0]] = value
802
803    def update(self, records, field, values):
804        """ Set the values of ``field`` for several ``records``. """
805        field_cache = self._data[field]
806        if field.depends_context:
807            field_cache = field_cache.setdefault(records.env.cache_key(field), {})
808        field_cache.update(zip(records._ids, values))
809
810    def remove(self, record, field):
811        """ Remove the value of ``field`` for ``record``. """
812        try:
813            field_cache = self._data[field]
814            if field.depends_context:
815                field_cache = field_cache[record.env.cache_key(field)]
816            del field_cache[record._ids[0]]
817        except KeyError:
818            pass
819
820    def get_values(self, records, field):
821        """ Return the cached values of ``field`` for ``records``. """
822        field_cache = self._data[field]
823        if field.depends_context:
824            field_cache = field_cache.get(records.env.cache_key(field), {})
825        for record_id in records._ids:
826            try:
827                yield field_cache[record_id]
828            except KeyError:
829                pass
830
831    def get_until_miss(self, records, field):
832        """ Return the cached values of ``field`` for ``records`` until a value is not found. """
833        field_cache = self._data[field]
834        if field.depends_context:
835            field_cache = field_cache.get(records.env.cache_key(field), {})
836        vals = []
837        for record_id in records._ids:
838            try:
839                vals.append(field_cache[record_id])
840            except KeyError:
841                break
842        return vals
843
844    def get_records_different_from(self, records, field, value):
845        """ Return the subset of ``records`` that has not ``value`` for ``field``. """
846        field_cache = self._data[field]
847        if field.depends_context:
848            field_cache = field_cache.get(records.env.cache_key(field), {})
849        ids = []
850        for record_id in records._ids:
851            try:
852                val = field_cache[record_id]
853            except KeyError:
854                ids.append(record_id)
855            else:
856                if val != value:
857                    ids.append(record_id)
858        return records.browse(ids)
859
860    def get_fields(self, record):
861        """ Return the fields with a value for ``record``. """
862        for name, field in record._fields.items():
863            if name == 'id':
864                continue
865            field_cache = self._data.get(field, {})
866            if field.depends_context:
867                field_cache = field_cache.get(record.env.cache_key(field), ())
868            if record.id in field_cache:
869                yield field
870
871    def get_records(self, model, field):
872        """ Return the records of ``model`` that have a value for ``field``. """
873        field_cache = self._data[field]
874        if field.depends_context:
875            field_cache = field_cache.get(model.env.cache_key(field), ())
876        return model.browse(field_cache)
877
878    def get_missing_ids(self, records, field):
879        """ Return the ids of ``records`` that have no value for ``field``. """
880        field_cache = self._data[field]
881        if field.depends_context:
882            field_cache = field_cache.get(records.env.cache_key(field), ())
883        for record_id in records._ids:
884            if record_id not in field_cache:
885                yield record_id
886
887    def invalidate(self, spec=None):
888        """ Invalidate the cache, partially or totally depending on ``spec``. """
889        if spec is None:
890            self._data.clear()
891        elif spec:
892            for field, ids in spec:
893                if ids is None:
894                    self._data.pop(field, None)
895                else:
896                    field_cache = self._data.get(field, {})
897                    field_caches = field_cache.values() if field.depends_context else [field_cache]
898                    for field_cache in field_caches:
899                        for id in ids:
900                            field_cache.pop(id, None)
901
902    def check(self, env):
903        """ Check the consistency of the cache for the given environment. """
904        # flush fields to be recomputed before evaluating the cache
905        env['res.partner'].recompute()
906
907        # make a copy of the cache, and invalidate it
908        dump = dict(self._data)
909        self.invalidate()
910
911        # re-fetch the records, and compare with their former cache
912        invalids = []
913
914        def check(model, field, field_dump):
915            records = env[field.model_name].browse(field_dump)
916            for record in records:
917                if not record.id:
918                    continue
919                try:
920                    cached = field_dump[record.id]
921                    value = field.convert_to_record(cached, record)
922                    fetched = record[field.name]
923                    if fetched != value:
924                        info = {'cached': value, 'fetched': fetched}
925                        invalids.append((record, field, info))
926                except (AccessError, MissingError):
927                    pass
928
929        for field, field_dump in dump.items():
930            model = env[field.model_name]
931            if field.depends_context:
932                for context_keys, field_cache in field_dump.items():
933                    context = dict(zip(field.depends_context, context_keys))
934                    check(model.with_context(context), field, field_cache)
935            else:
936                check(model, field, field_dump)
937
938        if invalids:
939            raise UserError('Invalid cache for fields\n' + pformat(invalids))
940
941
942# keep those imports here in order to handle cyclic dependencies correctly
943from odoo import SUPERUSER_ID
944from odoo.exceptions import UserError, AccessError, MissingError
945from odoo.modules.registry import Registry
946