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