1# -*- coding: utf-8 -*- 2# Part of Odoo. See LICENSE file for full copyright and licensing details. 3 4""" High-level objects for fields. """ 5 6from collections import defaultdict 7from datetime import date, datetime, time 8from operator import attrgetter 9from xmlrpc.client import MAXINT 10import itertools 11import logging 12import base64 13import binascii 14import pytz 15import psycopg2 16 17from .tools import ( 18 float_repr, float_round, float_compare, float_is_zero, html_sanitize, human_size, 19 pg_varchar, ustr, OrderedSet, pycompat, sql, date_utils, unique, IterableGenerator, 20 image_process, merge_sequences, 21) 22from .tools import DEFAULT_SERVER_DATE_FORMAT as DATE_FORMAT 23from .tools import DEFAULT_SERVER_DATETIME_FORMAT as DATETIME_FORMAT 24from .tools.translate import html_translate, _ 25from .tools.mimetypes import guess_mimetype 26 27from odoo.exceptions import CacheMiss 28 29DATE_LENGTH = len(date.today().strftime(DATE_FORMAT)) 30DATETIME_LENGTH = len(datetime.now().strftime(DATETIME_FORMAT)) 31 32# hacky-ish way to prevent access to a field through the ORM (except for sudo mode) 33NO_ACCESS='.' 34 35IR_MODELS = ( 36 'ir.model', 'ir.model.data', 'ir.model.fields', 'ir.model.fields.selection', 37 'ir.model.relation', 'ir.model.constraint', 'ir.module.module', 38) 39 40_logger = logging.getLogger(__name__) 41_schema = logging.getLogger(__name__[:-7] + '.schema') 42 43Default = object() # default value for __init__() methods 44 45 46def first(records): 47 """ Return the first record in ``records``, with the same prefetching. """ 48 return next(iter(records)) if len(records) > 1 else records 49 50 51def resolve_mro(model, name, predicate): 52 """ Return the list of successively overridden values of attribute ``name`` 53 in mro order on ``model`` that satisfy ``predicate``. Model classes 54 (the ones that appear in the registry) are ignored. 55 """ 56 result = [] 57 for cls in model._model_classes: 58 value = cls.__dict__.get(name, Default) 59 if value is Default: 60 continue 61 if not predicate(value): 62 break 63 result.append(value) 64 return result 65 66 67class MetaField(type): 68 """ Metaclass for field classes. """ 69 by_type = {} 70 71 def __init__(cls, name, bases, attrs): 72 super(MetaField, cls).__init__(name, bases, attrs) 73 if not hasattr(cls, 'type'): 74 return 75 76 if cls.type and cls.type not in MetaField.by_type: 77 MetaField.by_type[cls.type] = cls 78 79 # compute class attributes to avoid calling dir() on fields 80 cls.related_attrs = [] 81 cls.description_attrs = [] 82 for attr in dir(cls): 83 if attr.startswith('_related_'): 84 cls.related_attrs.append((attr[9:], attr)) 85 elif attr.startswith('_description_'): 86 cls.description_attrs.append((attr[13:], attr)) 87 88 89_global_seq = iter(itertools.count()) 90 91 92class Field(MetaField('DummyField', (object,), {})): 93 """The field descriptor contains the field definition, and manages accesses 94 and assignments of the corresponding field on records. The following 95 attributes may be provided when instanciating a field: 96 97 :param str string: the label of the field seen by users; if not 98 set, the ORM takes the field name in the class (capitalized). 99 100 :param str help: the tooltip of the field seen by users 101 102 :param invisible: whether the field is invisible (boolean, by default ``False``) 103 104 :param bool readonly: whether the field is readonly (default: ``False``) 105 106 This only has an impact on the UI. Any field assignation in code will work 107 (if the field is a stored field or an inversable one). 108 109 :param bool required: whether the value of the field is required (default: ``False``) 110 111 :param bool index: whether the field is indexed in database. Note: no effect 112 on non-stored and virtual fields. (default: ``False``) 113 114 :param default: the default value for the field; this is either a static 115 value, or a function taking a recordset and returning a value; use 116 ``default=None`` to discard default values for the field 117 :type default: value or callable 118 119 :param dict states: a dictionary mapping state values to lists of UI attribute-value 120 pairs; possible attributes are: ``readonly``, ``required``, ``invisible``. 121 122 .. warning:: Any state-based condition requires the ``state`` field value to be 123 available on the client-side UI. This is typically done by including it in 124 the relevant views, possibly made invisible if not relevant for the 125 end-user. 126 127 :param str groups: comma-separated list of group xml ids (string); this 128 restricts the field access to the users of the given groups only 129 130 :param bool company_dependent: whether the field value is dependent of the current company; 131 132 The value isn't stored on the model table. It is registered as `ir.property`. 133 When the value of the company_dependent field is needed, an `ir.property` 134 is searched, linked to the current company (and current record if one property 135 exists). 136 137 If the value is changed on the record, it either modifies the existing property 138 for the current record (if one exists), or creates a new one for the current company 139 and res_id. 140 141 If the value is changed on the company side, it will impact all records on which 142 the value hasn't been changed. 143 144 :param bool copy: whether the field value should be copied when the record 145 is duplicated (default: ``True`` for normal fields, ``False`` for 146 ``one2many`` and computed fields, including property fields and 147 related fields) 148 149 :param bool store: whether the field is stored in database 150 (default:``True``, ``False`` for computed fields) 151 152 :param str group_operator: aggregate function used by :meth:`~odoo.models.Model.read_group` 153 when grouping on this field. 154 155 Supported aggregate functions are: 156 157 * ``array_agg`` : values, including nulls, concatenated into an array 158 * ``count`` : number of rows 159 * ``count_distinct`` : number of distinct rows 160 * ``bool_and`` : true if all values are true, otherwise false 161 * ``bool_or`` : true if at least one value is true, otherwise false 162 * ``max`` : maximum value of all values 163 * ``min`` : minimum value of all values 164 * ``avg`` : the average (arithmetic mean) of all values 165 * ``sum`` : sum of all values 166 167 :param str group_expand: function used to expand read_group results when grouping on 168 the current field. 169 170 .. code-block:: python 171 172 @api.model 173 def _read_group_selection_field(self, values, domain, order): 174 return ['choice1', 'choice2', ...] # available selection choices. 175 176 @api.model 177 def _read_group_many2one_field(self, records, domain, order): 178 return records + self.search([custom_domain]) 179 180 .. rubric:: Computed Fields 181 182 :param str compute: name of a method that computes the field 183 184 .. seealso:: :ref:`Advanced Fields/Compute fields <reference/fields/compute>` 185 186 :param bool compute_sudo: whether the field should be recomputed as superuser 187 to bypass access rights (by default ``True`` for stored fields, ``False`` 188 for non stored fields) 189 190 :param str inverse: name of a method that inverses the field (optional) 191 192 :param str search: name of a method that implement search on the field (optional) 193 194 :param str related: sequence of field names 195 196 .. seealso:: :ref:`Advanced fields/Related fields <reference/fields/related>` 197 """ 198 199 type = None # type of the field (string) 200 relational = False # whether the field is a relational one 201 translate = False # whether the field is translated 202 203 column_type = None # database column type (ident, spec) 204 column_format = '%s' # placeholder for value in queries 205 column_cast_from = () # column types that may be cast to this 206 207 args = None # the parameters given to __init__() 208 _module = None # the field's module name 209 _modules = None # modules that define this field 210 _setup_done = None # the field's setup state: None, 'base' or 'full' 211 _sequence = None # absolute ordering of the field 212 213 automatic = False # whether the field is automatically created ("magic" field) 214 inherited = False # whether the field is inherited (_inherits) 215 inherited_field = None # the corresponding inherited field 216 217 name = None # name of the field 218 model_name = None # name of the model of this field 219 comodel_name = None # name of the model of values (if relational) 220 221 store = True # whether the field is stored in database 222 index = False # whether the field is indexed in database 223 manual = False # whether the field is a custom field 224 copy = True # whether the field is copied over by BaseModel.copy() 225 _depends = None # collection of field dependencies 226 _depends_context = None # collection of context key dependencies 227 recursive = False # whether self depends on itself 228 compute = None # compute(recs) computes field on recs 229 compute_sudo = False # whether field should be recomputed as superuser 230 inverse = None # inverse(recs) inverses field on recs 231 search = None # search(recs, operator, value) searches on self 232 related = None # sequence of field names, for related fields 233 company_dependent = False # whether ``self`` is company-dependent (property field) 234 default = None # default(recs) returns the default value 235 236 string = None # field label 237 help = None # field tooltip 238 invisible = False # whether the field is invisible 239 readonly = False # whether the field is readonly 240 required = False # whether the field is required 241 states = None # set readonly and required depending on state 242 groups = None # csv list of group xml ids 243 change_default = False # whether the field may trigger a "user-onchange" 244 deprecated = None # whether the field is deprecated 245 246 related_field = None # corresponding related field 247 group_operator = None # operator for aggregating values 248 group_expand = None # name of method to expand groups in read_group() 249 prefetch = True # whether the field is prefetched 250 251 def __init__(self, string=Default, **kwargs): 252 kwargs['string'] = string 253 self._sequence = kwargs['_sequence'] = next(_global_seq) 254 self.args = {key: val for key, val in kwargs.items() if val is not Default} 255 256 def new(self, **kwargs): 257 """ Return a field of the same type as ``self``, with its own parameters. """ 258 return type(self)(**kwargs) 259 260 def __str__(self): 261 return "%s.%s" % (self.model_name, self.name) 262 263 def __repr__(self): 264 return "%s.%s" % (self.model_name, self.name) 265 266 ############################################################################ 267 # 268 # Base field setup: things that do not depend on other models/fields 269 # 270 271 def setup_base(self, model, name): 272 """ Base setup: things that do not depend on other models/fields. """ 273 if self._setup_done and not self.related: 274 # optimization for regular fields: keep the base setup 275 self._setup_done = 'base' 276 else: 277 # do the base setup from scratch 278 self._setup_attrs(model, name) 279 if not self.related: 280 self._setup_regular_base(model) 281 self._setup_done = 'base' 282 283 # 284 # Setup field parameter attributes 285 # 286 287 def _can_setup_from(self, field): 288 """ Return whether ``self`` can retrieve parameters from ``field``. """ 289 return isinstance(field, type(self)) 290 291 def _get_attrs(self, model, name): 292 """ Return the field parameter attributes as a dictionary. """ 293 # determine all inherited field attributes 294 modules = set() 295 attrs = {} 296 if self.args.get('automatic') and resolve_mro(model, name, self._can_setup_from): 297 # prevent an automatic field from overriding a real field 298 self.args.clear() 299 if not (self.args.get('automatic') or self.args.get('manual')): 300 # magic and custom fields do not inherit from parent classes 301 for field in reversed(resolve_mro(model, name, self._can_setup_from)): 302 attrs.update(field.args) 303 if '_module' in field.args: 304 modules.add(field.args['_module']) 305 attrs.update(self.args) # necessary in case self is not in class 306 307 attrs['args'] = self.args 308 attrs['model_name'] = model._name 309 attrs['name'] = name 310 attrs['_modules'] = modules 311 312 # initialize ``self`` with ``attrs`` 313 if name == 'state': 314 # by default, `state` fields should be reset on copy 315 attrs['copy'] = attrs.get('copy', False) 316 if attrs.get('compute'): 317 # by default, computed fields are not stored, computed in superuser 318 # mode if stored, not copied (unless stored and explicitly not 319 # readonly), and readonly (unless inversible) 320 attrs['store'] = store = attrs.get('store', False) 321 attrs['compute_sudo'] = attrs.get('compute_sudo', store) 322 if not (attrs['store'] and not attrs.get('readonly', True)): 323 attrs['copy'] = attrs.get('copy', False) 324 attrs['readonly'] = attrs.get('readonly', not attrs.get('inverse')) 325 if attrs.get('related'): 326 # by default, related fields are not stored, computed in superuser 327 # mode, not copied and readonly 328 attrs['store'] = store = attrs.get('store', False) 329 attrs['compute_sudo'] = attrs.get('compute_sudo', attrs.get('related_sudo', True)) 330 attrs['copy'] = attrs.get('copy', False) 331 attrs['readonly'] = attrs.get('readonly', True) 332 if attrs.get('company_dependent'): 333 # by default, company-dependent fields are not stored, not computed 334 # in superuser mode and not copied 335 attrs['store'] = False 336 attrs['compute_sudo'] = attrs.get('compute_sudo', False) 337 attrs['copy'] = attrs.get('copy', False) 338 attrs['default'] = attrs.get('default', self._default_company_dependent) 339 attrs['compute'] = self._compute_company_dependent 340 if not attrs.get('readonly'): 341 attrs['inverse'] = self._inverse_company_dependent 342 attrs['search'] = self._search_company_dependent 343 attrs['depends_context'] = attrs.get('depends_context', ()) + ('company',) 344 if attrs.get('translate'): 345 # by default, translatable fields are context-dependent 346 attrs['depends_context'] = attrs.get('depends_context', ()) + ('lang',) 347 348 # parameters 'depends' and 'depends_context' are stored in attributes 349 # '_depends' and '_depends_context', respectively 350 if 'depends' in attrs: 351 attrs['_depends'] = tuple(attrs.pop('depends')) 352 if 'depends_context' in attrs: 353 attrs['_depends_context'] = tuple(attrs.pop('depends_context')) 354 355 return attrs 356 357 def _setup_attrs(self, model, name): 358 """ Initialize the field parameter attributes. """ 359 attrs = self._get_attrs(model, name) 360 # validate arguments 361 for key in attrs: 362 # TODO: improve filter as there are attributes on the class which 363 # are not valid on the field, probably 364 if not (hasattr(self, key) or model._valid_field_parameter(self, key)): 365 _logger.warning( 366 "Field %s.%s: unknown parameter %r, if this is an actual" 367 " parameter you may want to override the method" 368 " _valid_field_parameter on the relevant model in order to" 369 " allow it", 370 model._name, name, key 371 ) 372 self.__dict__.update(attrs) 373 374 # prefetch only stored, column, non-manual and non-deprecated fields 375 if not (self.store and self.column_type) or self.manual or self.deprecated: 376 self.prefetch = False 377 378 if not self.string and not self.related: 379 # related fields get their string from their parent field 380 self.string = ( 381 name[:-4] if name.endswith('_ids') else 382 name[:-3] if name.endswith('_id') else name 383 ).replace('_', ' ').title() 384 385 # self.default must be a callable 386 if self.default is not None: 387 value = self.default 388 self.default = value if callable(value) else lambda model: value 389 390 ############################################################################ 391 # 392 # Full field setup: everything else, except recomputation triggers 393 # 394 395 def setup_full(self, model): 396 """ Full setup: everything else, except recomputation triggers. """ 397 if self._setup_done != 'full': 398 if not self.related: 399 self._setup_regular_full(model) 400 else: 401 self._setup_related_full(model) 402 self._setup_done = 'full' 403 404 # 405 # Setup of non-related fields 406 # 407 408 def _setup_regular_base(self, model): 409 """ Setup the attributes of a non-related field. """ 410 pass 411 412 def _setup_regular_full(self, model): 413 """ Determine the dependencies and inverse field(s) of ``self``. """ 414 if self._depends is not None: 415 # the parameter 'depends' has priority over 'depends' on compute 416 self.depends = self._depends 417 self.depends_context = self._depends_context or () 418 return 419 420 # determine the functions implementing self.compute 421 if isinstance(self.compute, str): 422 funcs = resolve_mro(model, self.compute, callable) 423 elif self.compute: 424 funcs = [self.compute] 425 else: 426 funcs = [] 427 428 # collect depends and depends_context 429 depends = [] 430 depends_context = list(self._depends_context or ()) 431 for func in funcs: 432 deps = getattr(func, '_depends', ()) 433 depends.extend(deps(model) if callable(deps) else deps) 434 depends_context.extend(getattr(func, '_depends_context', ())) 435 436 self.depends = tuple(depends) 437 self.depends_context = tuple(depends_context) 438 439 # display_name may depend on context['lang'] (`test_lp1071710`) 440 if self.automatic and self.name == 'display_name' and model._rec_name: 441 if model._fields[model._rec_name].base_field.translate: 442 if 'lang' not in self.depends_context: 443 self.depends_context += ('lang',) 444 445 # 446 # Setup of related fields 447 # 448 449 def _setup_related_full(self, model): 450 """ Setup the attributes of a related field. """ 451 # fix the type of self.related if necessary 452 if isinstance(self.related, str): 453 self.related = tuple(self.related.split('.')) 454 455 # determine the chain of fields, and make sure they are all set up 456 model_name = self.model_name 457 for name in self.related: 458 field = model.pool[model_name]._fields[name] 459 if field._setup_done != 'full': 460 field.setup_full(model.env[model_name]) 461 model_name = field.comodel_name 462 463 self.related_field = field 464 465 # check type consistency 466 if self.type != field.type: 467 raise TypeError("Type of related field %s is inconsistent with %s" % (self, field)) 468 469 # determine dependencies, compute, inverse, and search 470 if self._depends is not None: 471 self.depends = self._depends 472 else: 473 self.depends = ('.'.join(self.related),) 474 self.compute = self._compute_related 475 if self.inherited or not (self.readonly or field.readonly): 476 self.inverse = self._inverse_related 477 if field._description_searchable: 478 # allow searching on self only if the related field is searchable 479 self.search = self._search_related 480 481 # copy attributes from field to self (string, help, etc.) 482 for attr, prop in self.related_attrs: 483 if not getattr(self, attr): 484 setattr(self, attr, getattr(field, prop)) 485 486 for attr, value in field.__dict__.items(): 487 if not hasattr(self, attr) and model._valid_field_parameter(self, attr): 488 setattr(self, attr, value) 489 490 # special cases of inherited fields 491 if self.inherited: 492 if not self.states: 493 self.states = field.states 494 if field.required: 495 self.required = True 496 self._modules.update(field._modules) 497 498 if self._depends_context is not None: 499 self.depends_context = self._depends_context 500 else: 501 self.depends_context = field.depends_context 502 503 def traverse_related(self, record): 504 """ Traverse the fields of the related field `self` except for the last 505 one, and return it as a pair `(last_record, last_field)`. """ 506 for name in self.related[:-1]: 507 record = first(record[name]) 508 return record, self.related_field 509 510 def _compute_related(self, records): 511 """ Compute the related field ``self`` on ``records``. """ 512 # 513 # Traverse fields one by one for all records, in order to take advantage 514 # of prefetching for each field access. In order to clarify the impact 515 # of the algorithm, consider traversing 'foo.bar' for records a1 and a2, 516 # where 'foo' is already present in cache for a1, a2. Initially, both a1 517 # and a2 are marked for prefetching. As the commented code below shows, 518 # traversing all fields one record at a time will fetch 'bar' one record 519 # at a time. 520 # 521 # b1 = a1.foo # mark b1 for prefetching 522 # v1 = b1.bar # fetch/compute bar for b1 523 # b2 = a2.foo # mark b2 for prefetching 524 # v2 = b2.bar # fetch/compute bar for b2 525 # 526 # On the other hand, traversing all records one field at a time ensures 527 # maximal prefetching for each field access. 528 # 529 # b1 = a1.foo # mark b1 for prefetching 530 # b2 = a2.foo # mark b2 for prefetching 531 # v1 = b1.bar # fetch/compute bar for b1, b2 532 # v2 = b2.bar # value already in cache 533 # 534 # This difference has a major impact on performance, in particular in 535 # the case where 'bar' is a computed field that takes advantage of batch 536 # computation. 537 # 538 values = list(records) 539 for name in self.related[:-1]: 540 try: 541 values = [first(value[name]) for value in values] 542 except AccessError as e: 543 description = records.env['ir.model']._get(records._name).name 544 raise AccessError( 545 _("%(previous_message)s\n\nImplicitly accessed through '%(document_kind)s' (%(document_model)s).") % { 546 'previous_message': e.args[0], 547 'document_kind': description, 548 'document_model': records._name, 549 } 550 ) 551 # assign final values to records 552 for record, value in zip(records, values): 553 record[self.name] = self._process_related(value[self.related_field.name]) 554 555 def _process_related(self, value): 556 """No transformation by default, but allows override.""" 557 return value 558 559 def _inverse_related(self, records): 560 """ Inverse the related field ``self`` on ``records``. """ 561 # store record values, otherwise they may be lost by cache invalidation! 562 record_value = {record: record[self.name] for record in records} 563 for record in records: 564 target, field = self.traverse_related(record) 565 # update 'target' only if 'record' and 'target' are both real or 566 # both new (see `test_base_objects.py`, `test_basic`) 567 if target and bool(target.id) == bool(record.id): 568 target[field.name] = record_value[record] 569 570 def _search_related(self, records, operator, value): 571 """ Determine the domain to search on field ``self``. """ 572 return [('.'.join(self.related), operator, value)] 573 574 # properties used by _setup_related_full() to copy values from related field 575 _related_comodel_name = property(attrgetter('comodel_name')) 576 _related_string = property(attrgetter('string')) 577 _related_help = property(attrgetter('help')) 578 _related_groups = property(attrgetter('groups')) 579 _related_group_operator = property(attrgetter('group_operator')) 580 581 @property 582 def base_field(self): 583 """ Return the base field of an inherited field, or ``self``. """ 584 return self.inherited_field.base_field if self.inherited_field else self 585 586 # 587 # Company-dependent fields 588 # 589 590 def _default_company_dependent(self, model): 591 return model.env['ir.property']._get(self.name, self.model_name) 592 593 def _compute_company_dependent(self, records): 594 # read property as superuser, as the current user may not have access 595 Property = records.env['ir.property'].sudo() 596 values = Property._get_multi(self.name, self.model_name, records.ids) 597 for record in records: 598 record[self.name] = values.get(record.id) 599 600 def _inverse_company_dependent(self, records): 601 # update property as superuser, as the current user may not have access 602 Property = records.env['ir.property'].sudo() 603 values = { 604 record.id: self.convert_to_write(record[self.name], record) 605 for record in records 606 } 607 Property._set_multi(self.name, self.model_name, values) 608 609 def _search_company_dependent(self, records, operator, value): 610 Property = records.env['ir.property'].sudo() 611 return Property.search_multi(self.name, self.model_name, operator, value) 612 613 # 614 # Setup of field triggers 615 # 616 617 def resolve_depends(self, registry): 618 """ Return the dependencies of `self` as a collection of field tuples. """ 619 Model0 = registry[self.model_name] 620 621 for dotnames in self.depends: 622 field_seq = [] 623 model_name = self.model_name 624 625 for index, fname in enumerate(dotnames.split('.')): 626 Model = registry[model_name] 627 if Model0._transient and not Model._transient: 628 # modifying fields on regular models should not trigger 629 # recomputations of fields on transient models 630 break 631 632 try: 633 field = Model._fields[fname] 634 except KeyError: 635 msg = "Field %s cannot find dependency %r on model %r." 636 raise ValueError(msg % (self, fname, model_name)) 637 if field is self and index: 638 self.recursive = True 639 640 field_seq.append(field) 641 642 # do not make self trigger itself: for instance, a one2many 643 # field line_ids with domain [('foo', ...)] will have 644 # 'line_ids.foo' as a dependency 645 if not (field is self and not index): 646 yield tuple(field_seq) 647 648 if field.type in ('one2many', 'many2many'): 649 for inv_field in Model._field_inverses[field]: 650 yield tuple(field_seq) + (inv_field,) 651 652 model_name = field.comodel_name 653 654 ############################################################################ 655 # 656 # Field description 657 # 658 659 def get_description(self, env): 660 """ Return a dictionary that describes the field ``self``. """ 661 desc = {'type': self.type} 662 for attr, prop in self.description_attrs: 663 value = getattr(self, prop) 664 if callable(value): 665 value = value(env) 666 if value is not None: 667 desc[attr] = value 668 669 return desc 670 671 # properties used by get_description() 672 _description_store = property(attrgetter('store')) 673 _description_manual = property(attrgetter('manual')) 674 _description_depends = property(attrgetter('depends')) 675 _description_related = property(attrgetter('related')) 676 _description_company_dependent = property(attrgetter('company_dependent')) 677 _description_readonly = property(attrgetter('readonly')) 678 _description_required = property(attrgetter('required')) 679 _description_states = property(attrgetter('states')) 680 _description_groups = property(attrgetter('groups')) 681 _description_change_default = property(attrgetter('change_default')) 682 _description_deprecated = property(attrgetter('deprecated')) 683 _description_group_operator = property(attrgetter('group_operator')) 684 685 @property 686 def _description_searchable(self): 687 return bool(self.store or self.search) 688 689 @property 690 def _description_sortable(self): 691 return (self.column_type and self.store) or (self.inherited and self.related_field._description_sortable) 692 693 def _description_string(self, env): 694 if self.string and env.lang: 695 model_name = self.base_field.model_name 696 field_string = env['ir.translation'].get_field_string(model_name) 697 return field_string.get(self.name) or self.string 698 return self.string 699 700 def _description_help(self, env): 701 if self.help and env.lang: 702 model_name = self.base_field.model_name 703 field_help = env['ir.translation'].get_field_help(model_name) 704 return field_help.get(self.name) or self.help 705 return self.help 706 707 def is_editable(self): 708 """ Return whether the field can be editable in a view. """ 709 return not self.readonly or self.states and any( 710 'readonly' in item for items in self.states.values() for item in items 711 ) 712 713 ############################################################################ 714 # 715 # Conversion of values 716 # 717 718 def null(self, record): 719 """ Return the null value for this field in the record format. """ 720 return False 721 722 def convert_to_column(self, value, record, values=None, validate=True): 723 """ Convert ``value`` from the ``write`` format to the SQL format. """ 724 if value is None or value is False: 725 return None 726 return pycompat.to_text(value) 727 728 def convert_to_cache(self, value, record, validate=True): 729 """ Convert ``value`` to the cache format; ``value`` may come from an 730 assignment, or have the format of methods :meth:`BaseModel.read` or 731 :meth:`BaseModel.write`. If the value represents a recordset, it should 732 be added for prefetching on ``record``. 733 734 :param bool validate: when True, field-specific validation of ``value`` 735 will be performed 736 """ 737 return value 738 739 def convert_to_record(self, value, record): 740 """ Convert ``value`` from the cache format to the record format. 741 If the value represents a recordset, it should share the prefetching of 742 ``record``. 743 """ 744 return False if value is None else value 745 746 def convert_to_record_multi(self, values, records): 747 """ Convert a list of values from the cache format to the record format. 748 Some field classes may override this method to add optimizations for 749 batch processing. 750 """ 751 # spare the method lookup overhead 752 convert = self.convert_to_record 753 return [convert(value, records) for value in values] 754 755 def convert_to_read(self, value, record, use_name_get=True): 756 """ Convert ``value`` from the record format to the format returned by 757 method :meth:`BaseModel.read`. 758 759 :param bool use_name_get: when True, the value's display name will be 760 computed using :meth:`BaseModel.name_get`, if relevant for the field 761 """ 762 return False if value is None else value 763 764 def convert_to_write(self, value, record): 765 """ Convert ``value`` from any format to the format of method 766 :meth:`BaseModel.write`. 767 """ 768 cache_value = self.convert_to_cache(value, record, validate=False) 769 record_value = self.convert_to_record(cache_value, record) 770 return self.convert_to_read(record_value, record) 771 772 def convert_to_onchange(self, value, record, names): 773 """ Convert ``value`` from the record format to the format returned by 774 method :meth:`BaseModel.onchange`. 775 776 :param names: a tree of field names (for relational fields only) 777 """ 778 return self.convert_to_read(value, record) 779 780 def convert_to_export(self, value, record): 781 """ Convert ``value`` from the record format to the export format. """ 782 if not value: 783 return '' 784 return value 785 786 def convert_to_display_name(self, value, record): 787 """ Convert ``value`` from the record format to a suitable display name. """ 788 return ustr(value) 789 790 ############################################################################ 791 # 792 # Update database schema 793 # 794 795 def update_db(self, model, columns): 796 """ Update the database schema to implement this field. 797 798 :param model: an instance of the field's model 799 :param columns: a dict mapping column names to their configuration in database 800 :return: ``True`` if the field must be recomputed on existing rows 801 """ 802 if not self.column_type: 803 return 804 805 column = columns.get(self.name) 806 807 # create/update the column, not null constraint; the index will be 808 # managed by registry.check_indexes() 809 self.update_db_column(model, column) 810 self.update_db_notnull(model, column) 811 812 # optimization for computing simple related fields like 'foo_id.bar' 813 if ( 814 not column 815 and len(self.related or ()) == 2 816 and self.related_field.store and not self.related_field.compute 817 and not (self.related_field.type == 'binary' and self.related_field.attachment) 818 and self.related_field.type not in ('one2many', 'many2many') 819 ): 820 join_field = model._fields[self.related[0]] 821 if ( 822 join_field.type == 'many2one' 823 and join_field.store and not join_field.compute 824 ): 825 model.pool.post_init(self.update_db_related, model) 826 # discard the "classical" computation 827 return False 828 829 return not column 830 831 def update_db_column(self, model, column): 832 """ Create/update the column corresponding to ``self``. 833 834 :param model: an instance of the field's model 835 :param column: the column's configuration (dict) if it exists, or ``None`` 836 """ 837 if not column: 838 # the column does not exist, create it 839 sql.create_column(model._cr, model._table, self.name, self.column_type[1], self.string) 840 return 841 if column['udt_name'] == self.column_type[0]: 842 return 843 if column['udt_name'] in self.column_cast_from: 844 sql.convert_column(model._cr, model._table, self.name, self.column_type[1]) 845 else: 846 newname = (self.name + '_moved{}').format 847 i = 0 848 while sql.column_exists(model._cr, model._table, newname(i)): 849 i += 1 850 if column['is_nullable'] == 'NO': 851 sql.drop_not_null(model._cr, model._table, self.name) 852 sql.rename_column(model._cr, model._table, self.name, newname(i)) 853 sql.create_column(model._cr, model._table, self.name, self.column_type[1], self.string) 854 855 def update_db_notnull(self, model, column): 856 """ Add or remove the NOT NULL constraint on ``self``. 857 858 :param model: an instance of the field's model 859 :param column: the column's configuration (dict) if it exists, or ``None`` 860 """ 861 has_notnull = column and column['is_nullable'] == 'NO' 862 863 if not column or (self.required and not has_notnull): 864 # the column is new or it becomes required; initialize its values 865 if model._table_has_rows(): 866 model._init_column(self.name) 867 868 if self.required and not has_notnull: 869 # _init_column may delay computations in post-init phase 870 @model.pool.post_init 871 def add_not_null(): 872 # flush values before adding NOT NULL constraint 873 model.flush([self.name]) 874 model.pool.post_constraint(apply_required, model, self.name) 875 876 elif not self.required and has_notnull: 877 sql.drop_not_null(model._cr, model._table, self.name) 878 879 def update_db_related(self, model): 880 """ Compute a stored related field directly in SQL. """ 881 comodel = model.env[self.related_field.model_name] 882 model.env.cr.execute(""" 883 UPDATE "{model_table}" AS x 884 SET "{model_field}" = y."{comodel_field}" 885 FROM "{comodel_table}" AS y 886 WHERE x."{join_field}" = y.id 887 """.format( 888 model_table=model._table, 889 model_field=self.name, 890 comodel_table=comodel._table, 891 comodel_field=self.related[1], 892 join_field=self.related[0], 893 )) 894 895 ############################################################################ 896 # 897 # Alternatively stored fields: if fields don't have a `column_type` (not 898 # stored as regular db columns) they go through a read/create/write 899 # protocol instead 900 # 901 902 def read(self, records): 903 """ Read the value of ``self`` on ``records``, and store it in cache. """ 904 raise NotImplementedError("Method read() undefined on %s" % self) 905 906 def create(self, record_values): 907 """ Write the value of ``self`` on the given records, which have just 908 been created. 909 910 :param record_values: a list of pairs ``(record, value)``, where 911 ``value`` is in the format of method :meth:`BaseModel.write` 912 """ 913 for record, value in record_values: 914 self.write(record, value) 915 916 def write(self, records, value): 917 """ Write the value of ``self`` on ``records``. This method must update 918 the cache and prepare database updates. 919 920 :param value: a value in any format 921 :return: the subset of `records` that have been modified 922 """ 923 # discard recomputation of self on records 924 records.env.remove_to_compute(self, records) 925 926 # update the cache, and discard the records that are not modified 927 cache = records.env.cache 928 cache_value = self.convert_to_cache(value, records) 929 records = cache.get_records_different_from(records, self, cache_value) 930 if not records: 931 return records 932 cache.update(records, self, [cache_value] * len(records)) 933 934 # update towrite 935 if self.store: 936 towrite = records.env.all.towrite[self.model_name] 937 record = records[:1] 938 write_value = self.convert_to_write(cache_value, record) 939 column_value = self.convert_to_column(write_value, record) 940 for record in records.filtered('id'): 941 towrite[record.id][self.name] = column_value 942 943 return records 944 945 ############################################################################ 946 # 947 # Descriptor methods 948 # 949 950 def __get__(self, record, owner): 951 """ return the value of field ``self`` on ``record`` """ 952 if record is None: 953 return self # the field is accessed through the owner class 954 955 if not record._ids: 956 # null record -> return the null value for this field 957 value = self.convert_to_cache(False, record, validate=False) 958 return self.convert_to_record(value, record) 959 960 env = record.env 961 962 # only a single record may be accessed 963 record.ensure_one() 964 965 if self.compute and self.store: 966 # process pending computations 967 self.recompute(record) 968 969 try: 970 value = env.cache.get(record, self) 971 972 except KeyError: 973 # behavior in case of cache miss: 974 # 975 # on a real record: 976 # stored -> fetch from database (computation done above) 977 # not stored and computed -> compute 978 # not stored and not computed -> default 979 # 980 # on a new record w/ origin: 981 # stored and not (computed and readonly) -> fetch from origin 982 # stored and computed and readonly -> compute 983 # not stored and computed -> compute 984 # not stored and not computed -> default 985 # 986 # on a new record w/o origin: 987 # stored and computed -> compute 988 # stored and not computed -> new delegate or default 989 # not stored and computed -> compute 990 # not stored and not computed -> default 991 # 992 if self.store and record.id: 993 # real record: fetch from database 994 recs = record._in_cache_without(self) 995 try: 996 recs._fetch_field(self) 997 except AccessError: 998 record._fetch_field(self) 999 if not env.cache.contains(record, self) and not record.exists(): 1000 raise MissingError("\n".join([ 1001 _("Record does not exist or has been deleted."), 1002 _("(Record: %s, User: %s)") % (record, env.uid), 1003 ])) 1004 value = env.cache.get(record, self) 1005 1006 elif self.store and record._origin and not (self.compute and self.readonly): 1007 # new record with origin: fetch from origin 1008 value = self.convert_to_cache(record._origin[self.name], record) 1009 env.cache.set(record, self, value) 1010 1011 elif self.compute: 1012 # non-stored field or new record without origin: compute 1013 if env.is_protected(self, record): 1014 value = self.convert_to_cache(False, record, validate=False) 1015 env.cache.set(record, self, value) 1016 else: 1017 recs = record if self.recursive else record._in_cache_without(self) 1018 try: 1019 self.compute_value(recs) 1020 except (AccessError, MissingError): 1021 self.compute_value(record) 1022 try: 1023 value = env.cache.get(record, self) 1024 except CacheMiss: 1025 if self.readonly and not self.store: 1026 raise ValueError("Compute method failed to assign %s.%s" % (record, self.name)) 1027 # fallback to null value if compute gives nothing 1028 value = self.convert_to_cache(False, record, validate=False) 1029 env.cache.set(record, self, value) 1030 1031 elif self.type == 'many2one' and self.delegate and not record.id: 1032 # parent record of a new record: new record, with the same 1033 # values as record for the corresponding inherited fields 1034 def is_inherited_field(name): 1035 field = record._fields[name] 1036 return field.inherited and field.related[0] == self.name 1037 1038 parent = record.env[self.comodel_name].new({ 1039 name: value 1040 for name, value in record._cache.items() 1041 if is_inherited_field(name) 1042 }) 1043 value = self.convert_to_cache(parent, record) 1044 env.cache.set(record, self, value) 1045 1046 else: 1047 # non-stored field or stored field on new record: default value 1048 value = self.convert_to_cache(False, record, validate=False) 1049 env.cache.set(record, self, value) 1050 defaults = record.default_get([self.name]) 1051 if self.name in defaults: 1052 # The null value above is necessary to convert x2many field 1053 # values. For instance, converting [(4, id)] accesses the 1054 # field's current value, then adds the given id. Without an 1055 # initial value, the conversion ends up here to determine 1056 # the field's value, and generates an infinite recursion. 1057 value = self.convert_to_cache(defaults[self.name], record) 1058 env.cache.set(record, self, value) 1059 1060 return self.convert_to_record(value, record) 1061 1062 def mapped(self, records): 1063 """ Return the values of ``self`` for ``records``, either as a list 1064 (scalar fields), or as a recordset (relational fields). 1065 1066 This method is meant to be used internally and has very little benefit 1067 over a simple call to `~odoo.models.BaseModel.mapped()` on a recordset. 1068 """ 1069 if self.name == 'id': 1070 # not stored in cache 1071 return list(records._ids) 1072 1073 if self.compute and self.store: 1074 # process pending computations 1075 self.recompute(records) 1076 1077 # retrieve values in cache, and fetch missing ones 1078 vals = records.env.cache.get_until_miss(records, self) 1079 while len(vals) < len(records): 1080 # It is important to construct a 'remaining' recordset with the 1081 # _prefetch_ids of the original recordset, in order to prefetch as 1082 # many records as possible. If not done this way, scenarios such as 1083 # [rec.line_ids.mapped('name') for rec in recs] would generate one 1084 # query per record in `recs`! 1085 remaining = records._browse(records.env, records[len(vals):]._ids, records._prefetch_ids) 1086 self.__get__(first(remaining), type(remaining)) 1087 vals += records.env.cache.get_until_miss(remaining, self) 1088 1089 return self.convert_to_record_multi(vals, records) 1090 1091 def __set__(self, records, value): 1092 """ set the value of field ``self`` on ``records`` """ 1093 protected_ids = [] 1094 new_ids = [] 1095 other_ids = [] 1096 for record_id in records._ids: 1097 if record_id in records.env._protected.get(self, ()): 1098 protected_ids.append(record_id) 1099 elif not record_id: 1100 new_ids.append(record_id) 1101 else: 1102 other_ids.append(record_id) 1103 1104 if protected_ids: 1105 # records being computed: no business logic, no recomputation 1106 protected_records = records.browse(protected_ids) 1107 self.write(protected_records, value) 1108 1109 if new_ids: 1110 # new records: no business logic 1111 new_records = records.browse(new_ids) 1112 with records.env.protecting(records.pool.field_computed.get(self, [self]), records): 1113 if self.relational: 1114 new_records.modified([self.name], before=True) 1115 self.write(new_records, value) 1116 new_records.modified([self.name]) 1117 1118 if self.inherited: 1119 # special case: also assign parent records if they are new 1120 parents = records[self.related[0]] 1121 parents.filtered(lambda r: not r.id)[self.name] = value 1122 1123 if other_ids: 1124 # base case: full business logic 1125 records = records.browse(other_ids) 1126 write_value = self.convert_to_write(value, records) 1127 records.write({self.name: write_value}) 1128 1129 ############################################################################ 1130 # 1131 # Computation of field values 1132 # 1133 1134 def recompute(self, records): 1135 """ Process the pending computations of ``self`` on ``records``. This 1136 should be called only if ``self`` is computed and stored. 1137 """ 1138 to_compute_ids = records.env.all.tocompute.get(self) 1139 if not to_compute_ids: 1140 return 1141 1142 if self.recursive: 1143 for record in records: 1144 if record.id in to_compute_ids: 1145 self.compute_value(record) 1146 return 1147 1148 for record in records: 1149 if record.id in to_compute_ids: 1150 ids = expand_ids(record.id, to_compute_ids) 1151 recs = record.browse(itertools.islice(ids, PREFETCH_MAX)) 1152 try: 1153 self.compute_value(recs) 1154 except (AccessError, MissingError): 1155 self.compute_value(record) 1156 1157 def compute_value(self, records): 1158 """ Invoke the compute method on ``records``; the results are in cache. """ 1159 env = records.env 1160 if self.compute_sudo: 1161 records = records.sudo() 1162 fields = records.pool.field_computed[self] 1163 1164 # Just in case the compute method does not assign a value, we already 1165 # mark the computation as done. This is also necessary if the compute 1166 # method accesses the old value of the field: the field will be fetched 1167 # with _read(), which will flush() it. If the field is still to compute, 1168 # the latter flush() will recursively compute this field! 1169 for field in fields: 1170 if field.store: 1171 env.remove_to_compute(field, records) 1172 1173 try: 1174 with records.env.protecting(fields, records): 1175 records._compute_field_value(self) 1176 except Exception: 1177 for field in fields: 1178 if field.store: 1179 env.add_to_compute(field, records) 1180 raise 1181 1182 def determine_inverse(self, records): 1183 """ Given the value of ``self`` on ``records``, inverse the computation. """ 1184 if isinstance(self.inverse, str): 1185 getattr(records, self.inverse)() 1186 else: 1187 self.inverse(records) 1188 1189 def determine_domain(self, records, operator, value): 1190 """ Return a domain representing a condition on ``self``. """ 1191 if isinstance(self.search, str): 1192 return getattr(records, self.search)(operator, value) 1193 else: 1194 return self.search(records, operator, value) 1195 1196 ############################################################################ 1197 # 1198 # Notification when fields are modified 1199 # 1200 1201 1202class Boolean(Field): 1203 """ Encapsulates a :class:`bool`. """ 1204 type = 'boolean' 1205 column_type = ('bool', 'bool') 1206 1207 def convert_to_column(self, value, record, values=None, validate=True): 1208 return bool(value) 1209 1210 def convert_to_cache(self, value, record, validate=True): 1211 return bool(value) 1212 1213 def convert_to_export(self, value, record): 1214 return value 1215 1216 1217class Integer(Field): 1218 """ Encapsulates an :class:`int`. """ 1219 type = 'integer' 1220 column_type = ('int4', 'int4') 1221 1222 group_operator = 'sum' 1223 1224 def convert_to_column(self, value, record, values=None, validate=True): 1225 return int(value or 0) 1226 1227 def convert_to_cache(self, value, record, validate=True): 1228 if isinstance(value, dict): 1229 # special case, when an integer field is used as inverse for a one2many 1230 return value.get('id', None) 1231 return int(value or 0) 1232 1233 def convert_to_record(self, value, record): 1234 return value or 0 1235 1236 def convert_to_read(self, value, record, use_name_get=True): 1237 # Integer values greater than 2^31-1 are not supported in pure XMLRPC, 1238 # so we have to pass them as floats :-( 1239 if value and value > MAXINT: 1240 return float(value) 1241 return value 1242 1243 def _update(self, records, value): 1244 # special case, when an integer field is used as inverse for a one2many 1245 cache = records.env.cache 1246 for record in records: 1247 cache.set(record, self, value.id or 0) 1248 1249 def convert_to_export(self, value, record): 1250 if value or value == 0: 1251 return value 1252 return '' 1253 1254 1255class Float(Field): 1256 """ Encapsulates a :class:`float`. 1257 1258 The precision digits are given by the (optional) ``digits`` attribute. 1259 1260 :param digits: a pair (total, decimal) or a string referencing a 1261 :class:`~odoo.addons.base.models.decimal_precision.DecimalPrecision` record name. 1262 :type digits: tuple(int,int) or str 1263 1264 When a float is a quantity associated with an unit of measure, it is important 1265 to use the right tool to compare or round values with the correct precision. 1266 1267 The Float class provides some static methods for this purpose: 1268 1269 :func:`~odoo.fields.Float.round()` to round a float with the given precision. 1270 :func:`~odoo.fields.Float.is_zero()` to check if a float equals zero at the given precision. 1271 :func:`~odoo.fields.Float.compare()` to compare two floats at the given precision. 1272 1273 .. admonition:: Example 1274 1275 To round a quantity with the precision of the unit of mesure:: 1276 1277 fields.Float.round(self.product_uom_qty, precision_rounding=self.product_uom_id.rounding) 1278 1279 To check if the quantity is zero with the precision of the unit of mesure:: 1280 1281 fields.Float.is_zero(self.product_uom_qty, precision_rounding=self.product_uom_id.rounding) 1282 1283 To compare two quantities:: 1284 1285 field.Float.compare(self.product_uom_qty, self.qty_done, precision_rounding=self.product_uom_id.rounding) 1286 1287 The compare helper uses the __cmp__ semantics for historic purposes, therefore 1288 the proper, idiomatic way to use this helper is like so: 1289 1290 if result == 0, the first and second floats are equal 1291 if result < 0, the first float is lower than the second 1292 if result > 0, the first float is greater than the second 1293 """ 1294 1295 type = 'float' 1296 column_cast_from = ('int4', 'numeric', 'float8') 1297 1298 _digits = None # digits argument passed to class initializer 1299 group_operator = 'sum' 1300 1301 def __init__(self, string=Default, digits=Default, **kwargs): 1302 super(Float, self).__init__(string=string, _digits=digits, **kwargs) 1303 1304 @property 1305 def column_type(self): 1306 # Explicit support for "falsy" digits (0, False) to indicate a NUMERIC 1307 # field with no fixed precision. The values are saved in the database 1308 # with all significant digits. 1309 # FLOAT8 type is still the default when there is no precision because it 1310 # is faster for most operations (sums, etc.) 1311 return ('numeric', 'numeric') if self._digits is not None else \ 1312 ('float8', 'double precision') 1313 1314 def get_digits(self, env): 1315 if isinstance(self._digits, str): 1316 precision = env['decimal.precision'].precision_get(self._digits) 1317 return 16, precision 1318 else: 1319 return self._digits 1320 1321 _related__digits = property(attrgetter('_digits')) 1322 1323 def _description_digits(self, env): 1324 return self.get_digits(env) 1325 1326 def convert_to_column(self, value, record, values=None, validate=True): 1327 result = float(value or 0.0) 1328 digits = self.get_digits(record.env) 1329 if digits: 1330 precision, scale = digits 1331 result = float_repr(float_round(result, precision_digits=scale), precision_digits=scale) 1332 return result 1333 1334 def convert_to_cache(self, value, record, validate=True): 1335 # apply rounding here, otherwise value in cache may be wrong! 1336 value = float(value or 0.0) 1337 if not validate: 1338 return value 1339 digits = self.get_digits(record.env) 1340 return float_round(value, precision_digits=digits[1]) if digits else value 1341 1342 def convert_to_record(self, value, record): 1343 return value or 0.0 1344 1345 def convert_to_export(self, value, record): 1346 if value or value == 0.0: 1347 return value 1348 return '' 1349 1350 round = staticmethod(float_round) 1351 is_zero = staticmethod(float_is_zero) 1352 compare = staticmethod(float_compare) 1353 1354 1355class Monetary(Field): 1356 """ Encapsulates a :class:`float` expressed in a given 1357 :class:`res_currency<odoo.addons.base.models.res_currency.Currency>`. 1358 1359 The decimal precision and currency symbol are taken from the ``currency_field`` attribute. 1360 1361 :param str currency_field: name of the :class:`Many2one` field 1362 holding the :class:`res_currency <odoo.addons.base.models.res_currency.Currency>` 1363 this monetary field is expressed in (default: `\'currency_id\'`) 1364 """ 1365 type = 'monetary' 1366 column_type = ('numeric', 'numeric') 1367 column_cast_from = ('float8',) 1368 1369 currency_field = None 1370 group_operator = 'sum' 1371 1372 def __init__(self, string=Default, currency_field=Default, **kwargs): 1373 super(Monetary, self).__init__(string=string, currency_field=currency_field, **kwargs) 1374 1375 _description_currency_field = property(attrgetter('currency_field')) 1376 1377 def _setup_currency_field(self, model): 1378 if not self.currency_field: 1379 # pick a default, trying in order: 'currency_id', 'x_currency_id' 1380 if 'currency_id' in model._fields: 1381 self.currency_field = 'currency_id' 1382 elif 'x_currency_id' in model._fields: 1383 self.currency_field = 'x_currency_id' 1384 assert self.currency_field in model._fields, \ 1385 "Field %s with unknown currency_field %r" % (self, self.currency_field) 1386 1387 def _setup_regular_full(self, model): 1388 super(Monetary, self)._setup_regular_full(model) 1389 self._setup_currency_field(model) 1390 1391 def _setup_related_full(self, model): 1392 super(Monetary, self)._setup_related_full(model) 1393 if self.inherited: 1394 self.currency_field = self.related_field.currency_field 1395 self._setup_currency_field(model) 1396 1397 def convert_to_column(self, value, record, values=None, validate=True): 1398 # retrieve currency from values or record 1399 if values and self.currency_field in values: 1400 field = record._fields[self.currency_field] 1401 currency = field.convert_to_cache(values[self.currency_field], record, validate) 1402 currency = field.convert_to_record(currency, record) 1403 else: 1404 # Note: this is wrong if 'record' is several records with different 1405 # currencies, which is functional nonsense and should not happen 1406 # BEWARE: do not prefetch other fields, because 'value' may be in 1407 # cache, and would be overridden by the value read from database! 1408 currency = record[:1].with_context(prefetch_fields=False)[self.currency_field] 1409 1410 value = float(value or 0.0) 1411 if currency: 1412 return float_repr(currency.round(value), currency.decimal_places) 1413 return value 1414 1415 def convert_to_cache(self, value, record, validate=True): 1416 # cache format: float 1417 value = float(value or 0.0) 1418 if value and validate: 1419 # FIXME @rco-odoo: currency may not be already initialized if it is 1420 # a function or related field! 1421 # BEWARE: do not prefetch other fields, because 'value' may be in 1422 # cache, and would be overridden by the value read from database! 1423 currency = record.sudo().with_context(prefetch_fields=False)[self.currency_field] 1424 if len(currency) > 1: 1425 raise ValueError("Got multiple currencies while assigning values of monetary field %s" % str(self)) 1426 elif currency: 1427 value = currency.round(value) 1428 return value 1429 1430 def convert_to_record(self, value, record): 1431 return value or 0.0 1432 1433 def convert_to_read(self, value, record, use_name_get=True): 1434 return value 1435 1436 def convert_to_write(self, value, record): 1437 return value 1438 1439 1440class _String(Field): 1441 """ Abstract class for string fields. """ 1442 translate = False # whether the field is translated 1443 prefetch = None 1444 1445 def __init__(self, string=Default, **kwargs): 1446 # translate is either True, False, or a callable 1447 if 'translate' in kwargs and not callable(kwargs['translate']): 1448 kwargs['translate'] = bool(kwargs['translate']) 1449 super(_String, self).__init__(string=string, **kwargs) 1450 1451 def _setup_attrs(self, model, name): 1452 super()._setup_attrs(model, name) 1453 if self.prefetch is None: 1454 # do not prefetch complex translated fields by default 1455 self.prefetch = not callable(self.translate) 1456 1457 _related_translate = property(attrgetter('translate')) 1458 1459 def _description_translate(self, env): 1460 return bool(self.translate) 1461 1462 def get_trans_terms(self, value): 1463 """ Return the sequence of terms to translate found in `value`. """ 1464 if not callable(self.translate): 1465 return [value] if value else [] 1466 terms = [] 1467 self.translate(terms.append, value) 1468 return terms 1469 1470 def get_trans_func(self, records): 1471 """ Return a translation function `translate` for `self` on the given 1472 records; the function call `translate(record_id, value)` translates the 1473 field value to the language given by the environment of `records`. 1474 """ 1475 if callable(self.translate): 1476 rec_src_trans = records.env['ir.translation']._get_terms_translations(self, records) 1477 1478 def translate(record_id, value): 1479 src_trans = rec_src_trans[record_id] 1480 return self.translate(src_trans.get, value) 1481 1482 else: 1483 rec_trans = records.env['ir.translation']._get_ids( 1484 '%s,%s' % (self.model_name, self.name), 'model', records.env.lang, records.ids) 1485 1486 def translate(record_id, value): 1487 return rec_trans.get(record_id) or value 1488 1489 return translate 1490 1491 def check_trans_value(self, value): 1492 """ Check and possibly sanitize the translated term `value`. """ 1493 if callable(self.translate): 1494 # do a "no-translation" to sanitize the value 1495 callback = lambda term: None 1496 return self.translate(callback, value) 1497 else: 1498 return value 1499 1500 def write(self, records, value): 1501 # discard recomputation of self on records 1502 records.env.remove_to_compute(self, records) 1503 1504 # update the cache, and discard the records that are not modified 1505 cache = records.env.cache 1506 cache_value = self.convert_to_cache(value, records) 1507 records = cache.get_records_different_from(records, self, cache_value) 1508 if not records: 1509 return records 1510 cache.update(records, self, [cache_value] * len(records)) 1511 1512 if not self.store: 1513 return records 1514 1515 real_recs = records.filtered('id') 1516 if not real_recs._ids: 1517 return records 1518 1519 update_column = True 1520 update_trans = False 1521 single_lang = len(records.env['res.lang'].get_installed()) <= 1 1522 if self.translate: 1523 lang = records.env.lang or None # used in _update_translations below 1524 if single_lang: 1525 # a single language is installed 1526 update_trans = True 1527 elif callable(self.translate) or lang == 'en_US': 1528 # update the source and synchronize translations 1529 update_column = True 1530 update_trans = True 1531 elif lang != 'en_US' and lang is not None: 1532 # update the translations only except if emptying 1533 update_column = not cache_value 1534 update_trans = True 1535 # else: lang = None 1536 1537 # update towrite if modifying the source 1538 if update_column: 1539 towrite = records.env.all.towrite[self.model_name] 1540 for rid in real_recs._ids: 1541 # cache_value is already in database format 1542 towrite[rid][self.name] = cache_value 1543 if self.translate is True and cache_value: 1544 tname = "%s,%s" % (records._name, self.name) 1545 records.env['ir.translation']._set_source(tname, real_recs._ids, value) 1546 if self.translate: 1547 # invalidate the field in the other languages 1548 cache.invalidate([(self, records.ids)]) 1549 cache.update(records, self, [cache_value] * len(records)) 1550 1551 if update_trans: 1552 if callable(self.translate): 1553 # the source value of self has been updated, synchronize 1554 # translated terms when possible 1555 records.env['ir.translation']._sync_terms_translations(self, real_recs) 1556 1557 else: 1558 # update translations 1559 value = self.convert_to_column(value, records) 1560 source_recs = real_recs.with_context(lang=None) 1561 source_value = first(source_recs)[self.name] 1562 if not source_value: 1563 source_recs[self.name] = value 1564 source_value = value 1565 tname = "%s,%s" % (self.model_name, self.name) 1566 if not value: 1567 records.env['ir.translation'].search([ 1568 ('name', '=', tname), 1569 ('type', '=', 'model'), 1570 ('res_id', 'in', real_recs._ids) 1571 ]).unlink() 1572 elif single_lang: 1573 records.env['ir.translation']._update_translations([dict( 1574 src=source_value, 1575 value=value, 1576 name=tname, 1577 lang=lang, 1578 type='model', 1579 state='translated', 1580 res_id=res_id) for res_id in real_recs._ids]) 1581 else: 1582 records.env['ir.translation']._set_ids( 1583 tname, 'model', lang, real_recs._ids, value, source_value, 1584 ) 1585 1586 return records 1587 1588 1589class Char(_String): 1590 """ Basic string field, can be length-limited, usually displayed as a 1591 single-line string in clients. 1592 1593 :param int size: the maximum size of values stored for that field 1594 1595 :param bool trim: states whether the value is trimmed or not (by default, 1596 ``True``). Note that the trim operation is applied only by the web client. 1597 1598 :param translate: enable the translation of the field's values; use 1599 ``translate=True`` to translate field values as a whole; ``translate`` 1600 may also be a callable such that ``translate(callback, value)`` 1601 translates ``value`` by using ``callback(term)`` to retrieve the 1602 translation of terms. 1603 :type translate: bool or callable 1604 """ 1605 type = 'char' 1606 column_cast_from = ('text',) 1607 1608 size = None # maximum size of values (deprecated) 1609 trim = True # whether value is trimmed (only by web client) 1610 1611 @property 1612 def column_type(self): 1613 return ('varchar', pg_varchar(self.size)) 1614 1615 def update_db_column(self, model, column): 1616 if ( 1617 column and column['udt_name'] == 'varchar' and column['character_maximum_length'] and 1618 (self.size is None or column['character_maximum_length'] < self.size) 1619 ): 1620 # the column's varchar size does not match self.size; convert it 1621 sql.convert_column(model._cr, model._table, self.name, self.column_type[1]) 1622 super(Char, self).update_db_column(model, column) 1623 1624 _related_size = property(attrgetter('size')) 1625 _related_trim = property(attrgetter('trim')) 1626 _description_size = property(attrgetter('size')) 1627 _description_trim = property(attrgetter('trim')) 1628 1629 def _setup_regular_base(self, model): 1630 super(Char, self)._setup_regular_base(model) 1631 assert self.size is None or isinstance(self.size, int), \ 1632 "Char field %s with non-integer size %r" % (self, self.size) 1633 1634 def convert_to_column(self, value, record, values=None, validate=True): 1635 if value is None or value is False: 1636 return None 1637 # we need to convert the string to a unicode object to be able 1638 # to evaluate its length (and possibly truncate it) reliably 1639 return pycompat.to_text(value)[:self.size] 1640 1641 def convert_to_cache(self, value, record, validate=True): 1642 if value is None or value is False: 1643 return None 1644 return pycompat.to_text(value)[:self.size] 1645 1646 1647class Text(_String): 1648 """ Very similar to :class:`Char` but used for longer contents, does not 1649 have a size and usually displayed as a multiline text box. 1650 1651 :param translate: enable the translation of the field's values; use 1652 ``translate=True`` to translate field values as a whole; ``translate`` 1653 may also be a callable such that ``translate(callback, value)`` 1654 translates ``value`` by using ``callback(term)`` to retrieve the 1655 translation of terms. 1656 :type translate: bool or callable 1657 """ 1658 type = 'text' 1659 column_type = ('text', 'text') 1660 column_cast_from = ('varchar',) 1661 1662 def convert_to_cache(self, value, record, validate=True): 1663 if value is None or value is False: 1664 return None 1665 return ustr(value) 1666 1667 1668class Html(_String): 1669 """ Encapsulates an html code content. 1670 1671 :param bool sanitize: whether value must be sanitized (default: ``True``) 1672 :param bool sanitize_tags: whether to sanitize tags 1673 (only a white list of attributes is accepted, default: ``True``) 1674 :param bool sanitize_attributes: whether to sanitize attributes 1675 (only a white list of attributes is accepted, default: ``True``) 1676 :param bool sanitize_style: whether to sanitize style attributes (default: ``False``) 1677 :param bool strip_style: whether to strip style attributes 1678 (removed and therefore not sanitized, default: ``False``) 1679 :param bool strip_classes: whether to strip classes attributes (default: ``False``) 1680 """ 1681 type = 'html' 1682 column_type = ('text', 'text') 1683 1684 sanitize = True # whether value must be sanitized 1685 sanitize_tags = True # whether to sanitize tags (only a white list of attributes is accepted) 1686 sanitize_attributes = True # whether to sanitize attributes (only a white list of attributes is accepted) 1687 sanitize_style = False # whether to sanitize style attributes 1688 sanitize_form = True # whether to sanitize forms 1689 strip_style = False # whether to strip style attributes (removed and therefore not sanitized) 1690 strip_classes = False # whether to strip classes attributes 1691 1692 def _get_attrs(self, model, name): 1693 # called by _setup_attrs(), working together with _String._setup_attrs() 1694 attrs = super()._get_attrs(model, name) 1695 # Translated sanitized html fields must use html_translate or a callable. 1696 if attrs.get('translate') is True and attrs.get('sanitize', True): 1697 attrs['translate'] = html_translate 1698 return attrs 1699 1700 _related_sanitize = property(attrgetter('sanitize')) 1701 _related_sanitize_tags = property(attrgetter('sanitize_tags')) 1702 _related_sanitize_attributes = property(attrgetter('sanitize_attributes')) 1703 _related_sanitize_style = property(attrgetter('sanitize_style')) 1704 _related_strip_style = property(attrgetter('strip_style')) 1705 _related_strip_classes = property(attrgetter('strip_classes')) 1706 1707 _description_sanitize = property(attrgetter('sanitize')) 1708 _description_sanitize_tags = property(attrgetter('sanitize_tags')) 1709 _description_sanitize_attributes = property(attrgetter('sanitize_attributes')) 1710 _description_sanitize_style = property(attrgetter('sanitize_style')) 1711 _description_strip_style = property(attrgetter('strip_style')) 1712 _description_strip_classes = property(attrgetter('strip_classes')) 1713 1714 def convert_to_column(self, value, record, values=None, validate=True): 1715 if value is None or value is False: 1716 return None 1717 if self.sanitize: 1718 return html_sanitize( 1719 value, silent=True, 1720 sanitize_tags=self.sanitize_tags, 1721 sanitize_attributes=self.sanitize_attributes, 1722 sanitize_style=self.sanitize_style, 1723 sanitize_form=self.sanitize_form, 1724 strip_style=self.strip_style, 1725 strip_classes=self.strip_classes) 1726 return value 1727 1728 def convert_to_cache(self, value, record, validate=True): 1729 if value is None or value is False: 1730 return None 1731 if validate and self.sanitize: 1732 return html_sanitize( 1733 value, silent=True, 1734 sanitize_tags=self.sanitize_tags, 1735 sanitize_attributes=self.sanitize_attributes, 1736 sanitize_style=self.sanitize_style, 1737 sanitize_form=self.sanitize_form, 1738 strip_style=self.strip_style, 1739 strip_classes=self.strip_classes) 1740 return value 1741 1742 1743class Date(Field): 1744 """ Encapsulates a python :class:`date <datetime.date>` object. """ 1745 type = 'date' 1746 column_type = ('date', 'date') 1747 column_cast_from = ('timestamp',) 1748 1749 start_of = staticmethod(date_utils.start_of) 1750 end_of = staticmethod(date_utils.end_of) 1751 add = staticmethod(date_utils.add) 1752 subtract = staticmethod(date_utils.subtract) 1753 1754 @staticmethod 1755 def today(*args): 1756 """Return the current day in the format expected by the ORM. 1757 1758 .. note:: This function may be used to compute default values. 1759 """ 1760 return date.today() 1761 1762 @staticmethod 1763 def context_today(record, timestamp=None): 1764 """Return the current date as seen in the client's timezone in a format 1765 fit for date fields. 1766 1767 .. note:: This method may be used to compute default values. 1768 1769 :param record: recordset from which the timezone will be obtained. 1770 :param datetime timestamp: optional datetime value to use instead of 1771 the current date and time (must be a datetime, regular dates 1772 can't be converted between timezones). 1773 :rtype: date 1774 """ 1775 today = timestamp or datetime.now() 1776 context_today = None 1777 tz_name = record._context.get('tz') or record.env.user.tz 1778 if tz_name: 1779 try: 1780 today_utc = pytz.timezone('UTC').localize(today, is_dst=False) # UTC = no DST 1781 context_today = today_utc.astimezone(pytz.timezone(tz_name)) 1782 except Exception: 1783 _logger.debug("failed to compute context/client-specific today date, using UTC value for `today`", 1784 exc_info=True) 1785 return (context_today or today).date() 1786 1787 @staticmethod 1788 def to_date(value): 1789 """Attempt to convert ``value`` to a :class:`date` object. 1790 1791 .. warning:: 1792 1793 If a datetime object is given as value, 1794 it will be converted to a date object and all 1795 datetime-specific information will be lost (HMS, TZ, ...). 1796 1797 :param value: value to convert. 1798 :type value: str or date or datetime 1799 :return: an object representing ``value``. 1800 :rtype: date or None 1801 """ 1802 if not value: 1803 return None 1804 if isinstance(value, date): 1805 if isinstance(value, datetime): 1806 return value.date() 1807 return value 1808 value = value[:DATE_LENGTH] 1809 return datetime.strptime(value, DATE_FORMAT).date() 1810 1811 # kept for backwards compatibility, but consider `from_string` as deprecated, will probably 1812 # be removed after V12 1813 from_string = to_date 1814 1815 @staticmethod 1816 def to_string(value): 1817 """ 1818 Convert a :class:`date` or :class:`datetime` object to a string. 1819 1820 :param value: value to convert. 1821 :return: a string representing ``value`` in the server's date format, if ``value`` is of 1822 type :class:`datetime`, the hours, minute, seconds, tzinfo will be truncated. 1823 :rtype: str 1824 """ 1825 return value.strftime(DATE_FORMAT) if value else False 1826 1827 def convert_to_cache(self, value, record, validate=True): 1828 if not value: 1829 return None 1830 if isinstance(value, datetime): 1831 # TODO: better fix data files (crm demo data) 1832 value = value.date() 1833 # raise TypeError("%s (field %s) must be string or date, not datetime." % (value, self)) 1834 return self.to_date(value) 1835 1836 def convert_to_export(self, value, record): 1837 if not value: 1838 return '' 1839 return self.from_string(value) 1840 1841 1842class Datetime(Field): 1843 """ Encapsulates a python :class:`datetime <datetime.datetime>` object. """ 1844 type = 'datetime' 1845 column_type = ('timestamp', 'timestamp') 1846 column_cast_from = ('date',) 1847 1848 start_of = staticmethod(date_utils.start_of) 1849 end_of = staticmethod(date_utils.end_of) 1850 add = staticmethod(date_utils.add) 1851 subtract = staticmethod(date_utils.subtract) 1852 1853 @staticmethod 1854 def now(*args): 1855 """Return the current day and time in the format expected by the ORM. 1856 1857 .. note:: This function may be used to compute default values. 1858 """ 1859 # microseconds must be annihilated as they don't comply with the server datetime format 1860 return datetime.now().replace(microsecond=0) 1861 1862 @staticmethod 1863 def today(*args): 1864 """Return the current day, at midnight (00:00:00).""" 1865 return Datetime.now().replace(hour=0, minute=0, second=0) 1866 1867 @staticmethod 1868 def context_timestamp(record, timestamp): 1869 """Return the given timestamp converted to the client's timezone. 1870 1871 .. note:: This method is *not* meant for use as a default initializer, 1872 because datetime fields are automatically converted upon 1873 display on client side. For default values, :meth:`now` 1874 should be used instead. 1875 1876 :param record: recordset from which the timezone will be obtained. 1877 :param datetime timestamp: naive datetime value (expressed in UTC) 1878 to be converted to the client timezone. 1879 :return: timestamp converted to timezone-aware datetime in context timezone. 1880 :rtype: datetime 1881 """ 1882 assert isinstance(timestamp, datetime), 'Datetime instance expected' 1883 tz_name = record._context.get('tz') or record.env.user.tz 1884 utc_timestamp = pytz.utc.localize(timestamp, is_dst=False) # UTC = no DST 1885 if tz_name: 1886 try: 1887 context_tz = pytz.timezone(tz_name) 1888 return utc_timestamp.astimezone(context_tz) 1889 except Exception: 1890 _logger.debug("failed to compute context/client-specific timestamp, " 1891 "using the UTC value", 1892 exc_info=True) 1893 return utc_timestamp 1894 1895 @staticmethod 1896 def to_datetime(value): 1897 """Convert an ORM ``value`` into a :class:`datetime` value. 1898 1899 :param value: value to convert. 1900 :type value: str or date or datetime 1901 :return: an object representing ``value``. 1902 :rtype: datetime or None 1903 """ 1904 if not value: 1905 return None 1906 if isinstance(value, date): 1907 if isinstance(value, datetime): 1908 if value.tzinfo: 1909 raise ValueError("Datetime field expects a naive datetime: %s" % value) 1910 return value 1911 return datetime.combine(value, time.min) 1912 1913 # TODO: fix data files 1914 return datetime.strptime(value, DATETIME_FORMAT[:len(value)-2]) 1915 1916 # kept for backwards compatibility, but consider `from_string` as deprecated, will probably 1917 # be removed after V12 1918 from_string = to_datetime 1919 1920 @staticmethod 1921 def to_string(value): 1922 """Convert a :class:`datetime` or :class:`date` object to a string. 1923 1924 :param value: value to convert. 1925 :type value: datetime or date 1926 :return: a string representing ``value`` in the server's datetime format, 1927 if ``value`` is of type :class:`date`, 1928 the time portion will be midnight (00:00:00). 1929 :rtype: str 1930 """ 1931 return value.strftime(DATETIME_FORMAT) if value else False 1932 1933 def convert_to_cache(self, value, record, validate=True): 1934 return self.to_datetime(value) 1935 1936 def convert_to_export(self, value, record): 1937 if not value: 1938 return '' 1939 value = self.convert_to_display_name(value, record) 1940 return self.from_string(value) 1941 1942 def convert_to_display_name(self, value, record): 1943 assert record, 'Record expected' 1944 return Datetime.to_string(Datetime.context_timestamp(record, Datetime.from_string(value))) 1945 1946# http://initd.org/psycopg/docs/usage.html#binary-adaptation 1947# Received data is returned as buffer (in Python 2) or memoryview (in Python 3). 1948_BINARY = memoryview 1949 1950 1951class Binary(Field): 1952 """Encapsulates a binary content (e.g. a file). 1953 1954 :param bool attachment: whether the field should be stored as `ir_attachment` 1955 or in a column of the model's table (default: ``True``). 1956 """ 1957 type = 'binary' 1958 1959 prefetch = False # not prefetched by default 1960 _depends_context = ('bin_size',) # depends on context (content or size) 1961 attachment = True # whether value is stored in attachment 1962 1963 @property 1964 def column_type(self): 1965 return None if self.attachment else ('bytea', 'bytea') 1966 1967 def _get_attrs(self, model, name): 1968 attrs = super(Binary, self)._get_attrs(model, name) 1969 if not attrs.get('store', True): 1970 attrs['attachment'] = False 1971 return attrs 1972 1973 _description_attachment = property(attrgetter('attachment')) 1974 1975 def convert_to_column(self, value, record, values=None, validate=True): 1976 # Binary values may be byte strings (python 2.6 byte array), but 1977 # the legacy OpenERP convention is to transfer and store binaries 1978 # as base64-encoded strings. The base64 string may be provided as a 1979 # unicode in some circumstances, hence the str() cast here. 1980 # This str() coercion will only work for pure ASCII unicode strings, 1981 # on purpose - non base64 data must be passed as a 8bit byte strings. 1982 if not value: 1983 return None 1984 # Detect if the binary content is an SVG for restricting its upload 1985 # only to system users. 1986 magic_bytes = { 1987 b'P', # first 6 bits of '<' (0x3C) b64 encoded 1988 b'<', # plaintext XML tag opening 1989 } 1990 if isinstance(value, str): 1991 value = value.encode() 1992 if value[:1] in magic_bytes: 1993 try: 1994 decoded_value = base64.b64decode(value.translate(None, delete=b'\r\n'), validate=True) 1995 except binascii.Error: 1996 decoded_value = value 1997 # Full mimetype detection 1998 if (guess_mimetype(decoded_value).startswith('image/svg') and 1999 not record.env.is_system()): 2000 raise UserError(_("Only admins can upload SVG files.")) 2001 if isinstance(value, bytes): 2002 return psycopg2.Binary(value) 2003 try: 2004 return psycopg2.Binary(str(value).encode('ascii')) 2005 except UnicodeEncodeError: 2006 raise UserError(_("ASCII characters are required for %s in %s") % (value, self.name)) 2007 2008 def convert_to_cache(self, value, record, validate=True): 2009 if isinstance(value, _BINARY): 2010 return bytes(value) 2011 if isinstance(value, str): 2012 # the cache must contain bytes or memoryview, but sometimes a string 2013 # is given when assigning a binary field (test `TestFileSeparator`) 2014 return value.encode() 2015 if isinstance(value, int) and \ 2016 (record._context.get('bin_size') or 2017 record._context.get('bin_size_' + self.name)): 2018 # If the client requests only the size of the field, we return that 2019 # instead of the content. Presumably a separate request will be done 2020 # to read the actual content, if necessary. 2021 value = human_size(value) 2022 # human_size can return False (-> None) or a string (-> encoded) 2023 return value.encode() if value else None 2024 return None if value is False else value 2025 2026 def convert_to_record(self, value, record): 2027 if isinstance(value, _BINARY): 2028 return bytes(value) 2029 return False if value is None else value 2030 2031 def compute_value(self, records): 2032 bin_size_name = 'bin_size_' + self.name 2033 if records.env.context.get('bin_size') or records.env.context.get(bin_size_name): 2034 # always compute without bin_size 2035 records_no_bin_size = records.with_context(**{'bin_size': False, bin_size_name: False}) 2036 super().compute_value(records_no_bin_size) 2037 # manually update the bin_size cache 2038 cache = records.env.cache 2039 for record_no_bin_size, record in zip(records_no_bin_size, records): 2040 try: 2041 value = cache.get(record_no_bin_size, self) 2042 try: 2043 value = base64.b64decode(value) 2044 except (TypeError, binascii.Error): 2045 pass 2046 try: 2047 if isinstance(value, (bytes, _BINARY)): 2048 value = human_size(len(value)) 2049 except (TypeError): 2050 pass 2051 cache_value = self.convert_to_cache(value, record) 2052 cache.set(record, self, cache_value) 2053 except CacheMiss: 2054 pass 2055 else: 2056 super().compute_value(records) 2057 2058 def read(self, records): 2059 # values are stored in attachments, retrieve them 2060 assert self.attachment 2061 domain = [ 2062 ('res_model', '=', records._name), 2063 ('res_field', '=', self.name), 2064 ('res_id', 'in', records.ids), 2065 ] 2066 # Note: the 'bin_size' flag is handled by the field 'datas' itself 2067 data = { 2068 att.res_id: att.datas 2069 for att in records.env['ir.attachment'].sudo().search(domain) 2070 } 2071 cache = records.env.cache 2072 for record in records: 2073 cache.set(record, self, data.get(record.id, False)) 2074 2075 def create(self, record_values): 2076 assert self.attachment 2077 if not record_values: 2078 return 2079 # create the attachments that store the values 2080 env = record_values[0][0].env 2081 with env.norecompute(): 2082 env['ir.attachment'].sudo().with_context( 2083 binary_field_real_user=env.user, 2084 ).create([{ 2085 'name': self.name, 2086 'res_model': self.model_name, 2087 'res_field': self.name, 2088 'res_id': record.id, 2089 'type': 'binary', 2090 'datas': value, 2091 } 2092 for record, value in record_values 2093 if value 2094 ]) 2095 2096 def write(self, records, value): 2097 if not self.attachment: 2098 return super().write(records, value) 2099 2100 # discard recomputation of self on records 2101 records.env.remove_to_compute(self, records) 2102 2103 # update the cache, and discard the records that are not modified 2104 cache = records.env.cache 2105 cache_value = self.convert_to_cache(value, records) 2106 records = cache.get_records_different_from(records, self, cache_value) 2107 if not records: 2108 return records 2109 if self.store: 2110 # determine records that are known to be not null 2111 not_null = cache.get_records_different_from(records, self, None) 2112 2113 cache.update(records, self, [cache_value] * len(records)) 2114 2115 # retrieve the attachments that store the values, and adapt them 2116 if self.store and any(records._ids): 2117 real_records = records.filtered('id') 2118 atts = records.env['ir.attachment'].sudo() 2119 if not_null: 2120 atts = atts.search([ 2121 ('res_model', '=', self.model_name), 2122 ('res_field', '=', self.name), 2123 ('res_id', 'in', real_records.ids), 2124 ]) 2125 if value: 2126 # update the existing attachments 2127 atts.write({'datas': value}) 2128 atts_records = records.browse(atts.mapped('res_id')) 2129 # create the missing attachments 2130 missing = (real_records - atts_records) 2131 if missing: 2132 atts.create([{ 2133 'name': self.name, 2134 'res_model': record._name, 2135 'res_field': self.name, 2136 'res_id': record.id, 2137 'type': 'binary', 2138 'datas': value, 2139 } 2140 for record in missing 2141 ]) 2142 else: 2143 atts.unlink() 2144 2145 return records 2146 2147 2148class Image(Binary): 2149 """Encapsulates an image, extending :class:`Binary`. 2150 2151 If image size is greater than the ``max_width``/``max_height`` limit of pixels, the image will be 2152 resized to the limit by keeping aspect ratio. 2153 2154 :param int max_width: the maximum width of the image (default: ``0``, no limit) 2155 :param int max_height: the maximum height of the image (default: ``0``, no limit) 2156 :param bool verify_resolution: whether the image resolution should be verified 2157 to ensure it doesn't go over the maximum image resolution (default: ``True``). 2158 See :class:`odoo.tools.image.ImageProcess` for maximum image resolution (default: ``45e6``). 2159 2160 .. note:: 2161 2162 If no ``max_width``/``max_height`` is specified (or is set to 0) and ``verify_resolution`` is False, 2163 the field content won't be verified at all and a :class:`Binary` field should be used. 2164 """ 2165 max_width = 0 2166 max_height = 0 2167 verify_resolution = True 2168 2169 def create(self, record_values): 2170 new_record_values = [] 2171 for record, value in record_values: 2172 # strange behavior when setting related image field, when `self` 2173 # does not resize the same way as its related field 2174 new_value = self._image_process(value) 2175 new_record_values.append((record, new_value)) 2176 cache_value = self.convert_to_cache(value if self.related else new_value, record) 2177 record.env.cache.update(record, self, [cache_value] * len(record)) 2178 super(Image, self).create(new_record_values) 2179 2180 def write(self, records, value): 2181 try: 2182 new_value = self._image_process(value) 2183 except UserError: 2184 if not any(records._ids): 2185 # Some crap is assigned to a new record. This can happen in an 2186 # onchange, where the client sends the "bin size" value of the 2187 # field instead of its full value (this saves bandwidth). In 2188 # this case, we simply don't assign the field: its value will be 2189 # taken from the records' origin. 2190 return 2191 raise 2192 2193 super(Image, self).write(records, new_value) 2194 cache_value = self.convert_to_cache(value if self.related else new_value, records) 2195 records.env.cache.update(records, self, [cache_value] * len(records)) 2196 2197 def _image_process(self, value): 2198 return image_process(value, 2199 size=(self.max_width, self.max_height), 2200 verify_resolution=self.verify_resolution, 2201 ) 2202 2203 def _process_related(self, value): 2204 """Override to resize the related value before saving it on self.""" 2205 try: 2206 return self._image_process(super()._process_related(value)) 2207 except UserError: 2208 # Avoid the following `write` to fail if the related image was saved 2209 # invalid, which can happen for pre-existing databases. 2210 return False 2211 2212 2213class Selection(Field): 2214 """ Encapsulates an exclusive choice between different values. 2215 2216 :param selection: specifies the possible values for this field. 2217 It is given as either a list of pairs ``(value, label)``, or a model 2218 method, or a method name. 2219 :type selection: list(tuple(str,str)) or callable or str 2220 2221 :param selection_add: provides an extension of the selection in the case 2222 of an overridden field. It is a list of pairs ``(value, label)`` or 2223 singletons ``(value,)``, where singleton values must appear in the 2224 overridden selection. The new values are inserted in an order that is 2225 consistent with the overridden selection and this list:: 2226 2227 selection = [('a', 'A'), ('b', 'B')] 2228 selection_add = [('c', 'C'), ('b',)] 2229 > result = [('a', 'A'), ('c', 'C'), ('b', 'B')] 2230 :type selection_add: list(tuple(str,str)) 2231 2232 :param ondelete: provides a fallback mechanism for any overridden 2233 field with a selection_add. It is a dict that maps every option 2234 from the selection_add to a fallback action. 2235 2236 This fallback action will be applied to all records whose 2237 selection_add option maps to it. 2238 2239 The actions can be any of the following: 2240 - 'set null' -- the default, all records with this option 2241 will have their selection value set to False. 2242 - 'cascade' -- all records with this option will be 2243 deleted along with the option itself. 2244 - 'set default' -- all records with this option will be 2245 set to the default of the field definition 2246 - <callable> -- a callable whose first and only argument will be 2247 the set of records containing the specified Selection option, 2248 for custom processing 2249 2250 The attribute ``selection`` is mandatory except in the case of 2251 ``related`` or extended fields. 2252 """ 2253 type = 'selection' 2254 column_type = ('varchar', pg_varchar()) 2255 2256 selection = None # [(value, string), ...], function or method name 2257 validate = True # whether validating upon write 2258 ondelete = None # {value: policy} (what to do when value is deleted) 2259 2260 def __init__(self, selection=Default, string=Default, **kwargs): 2261 super(Selection, self).__init__(selection=selection, string=string, **kwargs) 2262 2263 def _setup_regular_base(self, model): 2264 super(Selection, self)._setup_regular_base(model) 2265 assert self.selection is not None, "Field %s without selection" % self 2266 if isinstance(self.selection, list): 2267 assert all(isinstance(v, str) for v, _ in self.selection), \ 2268 "Field %s with non-str value in selection" % self 2269 2270 def _setup_related_full(self, model): 2271 super(Selection, self)._setup_related_full(model) 2272 # selection must be computed on related field 2273 field = self.related_field 2274 self.selection = lambda model: field._description_selection(model.env) 2275 2276 def _get_attrs(self, model, name): 2277 attrs = super(Selection, self)._get_attrs(model, name) 2278 # arguments 'selection' and 'selection_add' are processed below 2279 attrs.pop('selection_add', None) 2280 return attrs 2281 2282 def _setup_attrs(self, model, name): 2283 super(Selection, self)._setup_attrs(model, name) 2284 2285 # determine selection (applying 'selection_add' extensions) 2286 values = None 2287 labels = {} 2288 2289 for field in reversed(resolve_mro(model, name, self._can_setup_from)): 2290 # We cannot use field.selection or field.selection_add here 2291 # because those attributes are overridden by ``_setup_attrs``. 2292 if 'selection' in field.args: 2293 if self.related: 2294 _logger.warning("%s: selection attribute will be ignored as the field is related", self) 2295 selection = field.args['selection'] 2296 if isinstance(selection, list): 2297 if values is not None and values != [kv[0] for kv in selection]: 2298 _logger.warning("%s: selection=%r overrides existing selection; use selection_add instead", self, selection) 2299 values = [kv[0] for kv in selection] 2300 labels = dict(selection) 2301 self.ondelete = {} 2302 else: 2303 values = None 2304 labels = {} 2305 self.selection = selection 2306 self.ondelete = None 2307 2308 if 'selection_add' in field.args: 2309 if self.related: 2310 _logger.warning("%s: selection_add attribute will be ignored as the field is related", self) 2311 selection_add = field.args['selection_add'] 2312 assert isinstance(selection_add, list), \ 2313 "%s: selection_add=%r must be a list" % (self, selection_add) 2314 assert values is not None, \ 2315 "%s: selection_add=%r on non-list selection %r" % (self, selection_add, self.selection) 2316 2317 ondelete = field.args.get('ondelete') or {} 2318 new_values = [kv[0] for kv in selection_add if kv[0] not in values] 2319 for key in new_values: 2320 ondelete.setdefault(key, 'set null') 2321 if self.required and new_values and 'set null' in ondelete.values(): 2322 raise ValueError( 2323 "%r: required selection fields must define an ondelete policy that " 2324 "implements the proper cleanup of the corresponding records upon " 2325 "module uninstallation. Please use one or more of the following " 2326 "policies: 'set default' (if the field has a default defined), 'cascade', " 2327 "or a single-argument callable where the argument is the recordset " 2328 "containing the specified option." % self 2329 ) 2330 2331 # check ondelete values 2332 for key, val in ondelete.items(): 2333 if callable(val) or val in ('set null', 'cascade'): 2334 continue 2335 if val == 'set default': 2336 assert self.default is not None, ( 2337 "%r: ondelete policy of type 'set default' is invalid for this field " 2338 "as it does not define a default! Either define one in the base " 2339 "field, or change the chosen ondelete policy" % self 2340 ) 2341 continue 2342 raise ValueError( 2343 "%r: ondelete policy %r for selection value %r is not a valid ondelete " 2344 "policy, please choose one of 'set null', 'set default', 'cascade' or " 2345 "a callable" % (self, val, key) 2346 ) 2347 2348 values = merge_sequences(values, [kv[0] for kv in selection_add]) 2349 labels.update(kv for kv in selection_add if len(kv) == 2) 2350 self.ondelete.update(ondelete) 2351 2352 if values is not None: 2353 self.selection = [(value, labels[value]) for value in values] 2354 2355 def _selection_modules(self, model): 2356 """ Return a mapping from selection values to modules defining each value. """ 2357 if not isinstance(self.selection, list): 2358 return {} 2359 value_modules = defaultdict(set) 2360 for field in reversed(resolve_mro(model, self.name, self._can_setup_from)): 2361 module = field.args.get('_module') 2362 if not module: 2363 continue 2364 if 'selection' in field.args: 2365 value_modules.clear() 2366 if isinstance(field.args['selection'], list): 2367 for value, label in field.args['selection']: 2368 value_modules[value].add(module) 2369 if 'selection_add' in field.args: 2370 for value_label in field.args['selection_add']: 2371 if len(value_label) > 1: 2372 value_modules[value_label[0]].add(module) 2373 return value_modules 2374 2375 def _description_selection(self, env): 2376 """ return the selection list (pairs (value, label)); labels are 2377 translated according to context language 2378 """ 2379 selection = self.selection 2380 if isinstance(selection, str): 2381 return getattr(env[self.model_name], selection)() 2382 if callable(selection): 2383 return selection(env[self.model_name]) 2384 2385 # translate selection labels 2386 if env.lang: 2387 return env['ir.translation'].get_field_selection(self.model_name, self.name) 2388 else: 2389 return selection 2390 2391 def get_values(self, env): 2392 """Return a list of the possible values.""" 2393 selection = self.selection 2394 if isinstance(selection, str): 2395 selection = getattr(env[self.model_name], selection)() 2396 elif callable(selection): 2397 selection = selection(env[self.model_name]) 2398 return [value for value, _ in selection] 2399 2400 def convert_to_column(self, value, record, values=None, validate=True): 2401 if validate and self.validate: 2402 value = self.convert_to_cache(value, record) 2403 return super(Selection, self).convert_to_column(value, record, values, validate) 2404 2405 def convert_to_cache(self, value, record, validate=True): 2406 if not validate: 2407 return value or None 2408 if value and self.column_type[0] == 'int4': 2409 value = int(value) 2410 if value in self.get_values(record.env): 2411 return value 2412 elif not value: 2413 return None 2414 raise ValueError("Wrong value for %s: %r" % (self, value)) 2415 2416 def convert_to_export(self, value, record): 2417 if not isinstance(self.selection, list): 2418 # FIXME: this reproduces an existing buggy behavior! 2419 return value if value else '' 2420 for item in self._description_selection(record.env): 2421 if item[0] == value: 2422 return item[1] 2423 return '' 2424 2425 2426class Reference(Selection): 2427 """ Pseudo-relational field (no FK in database). 2428 2429 The field value is stored as a :class:`string <str>` following the pattern 2430 ``"res_model.res_id"`` in database. 2431 """ 2432 type = 'reference' 2433 2434 @property 2435 def column_type(self): 2436 return ('varchar', pg_varchar()) 2437 2438 def convert_to_column(self, value, record, values=None, validate=True): 2439 return Field.convert_to_column(self, value, record, values, validate) 2440 2441 def convert_to_cache(self, value, record, validate=True): 2442 # cache format: str ("model,id") or None 2443 if isinstance(value, BaseModel): 2444 if not validate or (value._name in self.get_values(record.env) and len(value) <= 1): 2445 return "%s,%s" % (value._name, value.id) if value else None 2446 elif isinstance(value, str): 2447 res_model, res_id = value.split(',') 2448 if not validate or res_model in self.get_values(record.env): 2449 if record.env[res_model].browse(int(res_id)).exists(): 2450 return value 2451 else: 2452 return None 2453 elif not value: 2454 return None 2455 raise ValueError("Wrong value for %s: %r" % (self, value)) 2456 2457 def convert_to_record(self, value, record): 2458 if value: 2459 res_model, res_id = value.split(',') 2460 return record.env[res_model].browse(int(res_id)) 2461 return None 2462 2463 def convert_to_read(self, value, record, use_name_get=True): 2464 return "%s,%s" % (value._name, value.id) if value else False 2465 2466 def convert_to_export(self, value, record): 2467 return value.display_name if value else '' 2468 2469 def convert_to_display_name(self, value, record): 2470 return ustr(value and value.display_name) 2471 2472 2473class _Relational(Field): 2474 """ Abstract class for relational fields. """ 2475 relational = True 2476 domain = [] # domain for searching values 2477 context = {} # context for searching values 2478 check_company = False 2479 2480 def __get__(self, records, owner): 2481 # base case: do the regular access 2482 if records is None or len(records._ids) <= 1: 2483 return super().__get__(records, owner) 2484 # multirecord case: use mapped 2485 return self.mapped(records) 2486 2487 def _setup_regular_base(self, model): 2488 super(_Relational, self)._setup_regular_base(model) 2489 if self.comodel_name not in model.pool: 2490 _logger.warning("Field %s with unknown comodel_name %r", self, self.comodel_name) 2491 self.comodel_name = '_unknown' 2492 2493 def get_domain_list(self, model): 2494 """ Return a list domain from the domain parameter. """ 2495 domain = self.domain 2496 if callable(domain): 2497 domain = domain(model) 2498 return domain if isinstance(domain, list) else [] 2499 2500 @property 2501 def _related_domain(self): 2502 if callable(self.domain): 2503 # will be called with another model than self's 2504 return lambda recs: self.domain(recs.env[self.model_name]) 2505 else: 2506 # maybe not correct if domain is a string... 2507 return self.domain 2508 2509 _related_context = property(attrgetter('context')) 2510 2511 _description_relation = property(attrgetter('comodel_name')) 2512 _description_context = property(attrgetter('context')) 2513 2514 def _description_domain(self, env): 2515 if self.check_company and not self.domain: 2516 if self.company_dependent: 2517 if self.comodel_name == "res.users": 2518 # user needs access to current company (self.env.company) 2519 return "[('company_ids', 'in', allowed_company_ids[0])]" 2520 else: 2521 return "[('company_id', 'in', [allowed_company_ids[0], False])]" 2522 else: 2523 # when using check_company=True on a field on 'res.company', the 2524 # company_id comes from the id of the current record 2525 cid = "id" if self.model_name == "res.company" else "company_id" 2526 if self.comodel_name == "res.users": 2527 # User allowed company ids = user.company_ids 2528 return f"['|', (not {cid}, '=', True), ('company_ids', 'in', [{cid}])]" 2529 else: 2530 return f"[('company_id', 'in', [{cid}, False])]" 2531 return self.domain(env[self.model_name]) if callable(self.domain) else self.domain 2532 2533 def null(self, record): 2534 return record.env[self.comodel_name] 2535 2536 2537class Many2one(_Relational): 2538 """ The value of such a field is a recordset of size 0 (no 2539 record) or 1 (a single record). 2540 2541 :param str comodel_name: name of the target model 2542 ``Mandatory`` except for related or extended fields. 2543 2544 :param domain: an optional domain to set on candidate values on the 2545 client side (domain or string) 2546 2547 :param dict context: an optional context to use on the client side when 2548 handling that field 2549 2550 :param str ondelete: what to do when the referred record is deleted; 2551 possible values are: ``'set null'``, ``'restrict'``, ``'cascade'`` 2552 2553 :param bool auto_join: whether JOINs are generated upon search through that 2554 field (default: ``False``) 2555 2556 :param bool delegate: set it to ``True`` to make fields of the target model 2557 accessible from the current model (corresponds to ``_inherits``) 2558 2559 :param bool check_company: Mark the field to be verified in 2560 :meth:`~odoo.models.Model._check_company`. Add a default company 2561 domain depending on the field attributes. 2562 """ 2563 type = 'many2one' 2564 column_type = ('int4', 'int4') 2565 2566 ondelete = None # what to do when value is deleted 2567 auto_join = False # whether joins are generated upon search 2568 delegate = False # whether self implements delegation 2569 2570 def __init__(self, comodel_name=Default, string=Default, **kwargs): 2571 super(Many2one, self).__init__(comodel_name=comodel_name, string=string, **kwargs) 2572 2573 def _setup_attrs(self, model, name): 2574 super(Many2one, self)._setup_attrs(model, name) 2575 # determine self.delegate 2576 if not self.delegate: 2577 self.delegate = name in model._inherits.values() 2578 2579 def _setup_regular_base(self, model): 2580 super()._setup_regular_base(model) 2581 # 3 cases: 2582 # 1) The ondelete attribute is not defined, we assign it a sensible default 2583 # 2) The ondelete attribute is defined and its definition makes sense 2584 # 3) The ondelete attribute is explicitly defined as 'set null' for a required m2o, 2585 # this is considered a programming error. 2586 if not self.ondelete: 2587 comodel = model.env[self.comodel_name] 2588 if model.is_transient() and not comodel.is_transient(): 2589 # Many2one relations from TransientModel Model are annoying because 2590 # they can block deletion due to foreign keys. So unless stated 2591 # otherwise, we default them to ondelete='cascade'. 2592 self.ondelete = 'cascade' if self.required else 'set null' 2593 else: 2594 self.ondelete = 'restrict' if self.required else 'set null' 2595 if self.ondelete == 'set null' and self.required: 2596 raise ValueError( 2597 "The m2o field %s of model %s is required but declares its ondelete policy " 2598 "as being 'set null'. Only 'restrict' and 'cascade' make sense." 2599 % (self.name, model._name) 2600 ) 2601 if self.ondelete == 'restrict' and self.comodel_name in IR_MODELS: 2602 raise ValueError( 2603 f"Field {self.name} of model {model._name} is defined as ondelete='restrict' " 2604 f"while having {self.comodel_name} as comodel, the 'restrict' mode is not " 2605 f"supported for this type of field as comodel." 2606 ) 2607 2608 def update_db(self, model, columns): 2609 comodel = model.env[self.comodel_name] 2610 if not model.is_transient() and comodel.is_transient(): 2611 raise ValueError('Many2one %s from Model to TransientModel is forbidden' % self) 2612 return super(Many2one, self).update_db(model, columns) 2613 2614 def update_db_column(self, model, column): 2615 super(Many2one, self).update_db_column(model, column) 2616 model.pool.post_init(self.update_db_foreign_key, model, column) 2617 2618 def update_db_foreign_key(self, model, column): 2619 comodel = model.env[self.comodel_name] 2620 # foreign keys do not work on views, and users can define custom models on sql views. 2621 if not model._is_an_ordinary_table() or not comodel._is_an_ordinary_table(): 2622 return 2623 # ir_actions is inherited, so foreign key doesn't work on it 2624 if not comodel._auto or comodel._table == 'ir_actions': 2625 return 2626 # create/update the foreign key, and reflect it in 'ir.model.constraint' 2627 model.pool.add_foreign_key( 2628 model._table, self.name, comodel._table, 'id', self.ondelete or 'set null', 2629 model, self._module 2630 ) 2631 2632 def _update(self, records, value): 2633 """ Update the cached value of ``self`` for ``records`` with ``value``. """ 2634 cache = records.env.cache 2635 for record in records: 2636 cache.set(record, self, self.convert_to_cache(value, record, validate=False)) 2637 2638 def convert_to_column(self, value, record, values=None, validate=True): 2639 return value or None 2640 2641 def convert_to_cache(self, value, record, validate=True): 2642 # cache format: id or None 2643 if type(value) in IdType: 2644 id_ = value 2645 elif isinstance(value, BaseModel): 2646 if validate and (value._name != self.comodel_name or len(value) > 1): 2647 raise ValueError("Wrong value for %s: %r" % (self, value)) 2648 id_ = value._ids[0] if value._ids else None 2649 elif isinstance(value, tuple): 2650 # value is either a pair (id, name), or a tuple of ids 2651 id_ = value[0] if value else None 2652 elif isinstance(value, dict): 2653 # return a new record (with the given field 'id' as origin) 2654 comodel = record.env[self.comodel_name] 2655 origin = comodel.browse(value.get('id')) 2656 id_ = comodel.new(value, origin=origin).id 2657 else: 2658 id_ = None 2659 2660 if self.delegate and record and not any(record._ids): 2661 # if all records are new, then so is the parent 2662 id_ = id_ and NewId(id_) 2663 2664 return id_ 2665 2666 def convert_to_record(self, value, record): 2667 # use registry to avoid creating a recordset for the model 2668 ids = () if value is None else (value,) 2669 prefetch_ids = IterableGenerator(prefetch_many2one_ids, record, self) 2670 return record.pool[self.comodel_name]._browse(record.env, ids, prefetch_ids) 2671 2672 def convert_to_record_multi(self, values, records): 2673 # return the ids as a recordset without duplicates 2674 prefetch_ids = IterableGenerator(prefetch_many2one_ids, records, self) 2675 ids = tuple(unique(id_ for id_ in values if id_ is not None)) 2676 return records.pool[self.comodel_name]._browse(records.env, ids, prefetch_ids) 2677 2678 def convert_to_read(self, value, record, use_name_get=True): 2679 if use_name_get and value: 2680 # evaluate name_get() as superuser, because the visibility of a 2681 # many2one field value (id and name) depends on the current record's 2682 # access rights, and not the value's access rights. 2683 try: 2684 # performance: value.sudo() prefetches the same records as value 2685 return (value.id, value.sudo().display_name) 2686 except MissingError: 2687 # Should not happen, unless the foreign key is missing. 2688 return False 2689 else: 2690 return value.id 2691 2692 def convert_to_write(self, value, record): 2693 if type(value) in IdType: 2694 return value 2695 if not value: 2696 return False 2697 if isinstance(value, BaseModel) and value._name == self.comodel_name: 2698 return value.id 2699 if isinstance(value, tuple): 2700 # value is either a pair (id, name), or a tuple of ids 2701 return value[0] if value else False 2702 if isinstance(value, dict): 2703 return record.env[self.comodel_name].new(value).id 2704 raise ValueError("Wrong value for %s: %r" % (self, value)) 2705 2706 def convert_to_export(self, value, record): 2707 return value.display_name if value else '' 2708 2709 def convert_to_display_name(self, value, record): 2710 return ustr(value.display_name) 2711 2712 def convert_to_onchange(self, value, record, names): 2713 if not value.id: 2714 return False 2715 return super(Many2one, self).convert_to_onchange(value, record, names) 2716 2717 def write(self, records, value): 2718 # discard recomputation of self on records 2719 records.env.remove_to_compute(self, records) 2720 2721 # discard the records that are not modified 2722 cache = records.env.cache 2723 cache_value = self.convert_to_cache(value, records) 2724 records = cache.get_records_different_from(records, self, cache_value) 2725 if not records: 2726 return records 2727 2728 # remove records from the cache of one2many fields of old corecords 2729 self._remove_inverses(records, cache_value) 2730 2731 # update the cache of self 2732 cache.update(records, self, [cache_value] * len(records)) 2733 2734 # update towrite 2735 if self.store: 2736 towrite = records.env.all.towrite[self.model_name] 2737 for record in records.filtered('id'): 2738 # cache_value is already in database format 2739 towrite[record.id][self.name] = cache_value 2740 2741 # update the cache of one2many fields of new corecord 2742 self._update_inverses(records, cache_value) 2743 2744 return records 2745 2746 def _remove_inverses(self, records, value): 2747 """ Remove `records` from the cached values of the inverse fields of `self`. """ 2748 cache = records.env.cache 2749 record_ids = set(records._ids) 2750 2751 # align(id) returns a NewId if records are new, a real id otherwise 2752 align = (lambda id_: id_) if all(record_ids) else (lambda id_: id_ and NewId(id_)) 2753 2754 for invf in records._field_inverses[self]: 2755 corecords = records.env[self.comodel_name].browse( 2756 align(id_) for id_ in cache.get_values(records, self) 2757 ) 2758 for corecord in corecords: 2759 ids0 = cache.get(corecord, invf, None) 2760 if ids0 is not None: 2761 ids1 = tuple(id_ for id_ in ids0 if id_ not in record_ids) 2762 cache.set(corecord, invf, ids1) 2763 2764 def _update_inverses(self, records, value): 2765 """ Add `records` to the cached values of the inverse fields of `self`. """ 2766 if value is None: 2767 return 2768 cache = records.env.cache 2769 corecord = self.convert_to_record(value, records) 2770 for invf in records._field_inverses[self]: 2771 valid_records = records.filtered_domain(invf.get_domain_list(corecord)) 2772 if not valid_records: 2773 continue 2774 ids0 = cache.get(corecord, invf, None) 2775 # if the value for the corecord is not in cache, but this is a new 2776 # record, assign it anyway, as you won't be able to fetch it from 2777 # database (see `test_sale_order`) 2778 if ids0 is not None or not corecord.id: 2779 ids1 = tuple(unique((ids0 or ()) + valid_records._ids)) 2780 cache.set(corecord, invf, ids1) 2781 2782 2783class Many2oneReference(Integer): 2784 """ Pseudo-relational field (no FK in database). 2785 2786 The field value is stored as an :class:`integer <int>` id in database. 2787 2788 Contrary to :class:`Reference` fields, the model has to be specified 2789 in a :class:`Char` field, whose name has to be specified in the 2790 `model_field` attribute for the current :class:`Many2oneReference` field. 2791 2792 :param str model_field: name of the :class:`Char` where the model name is stored. 2793 """ 2794 type = 'many2one_reference' 2795 2796 model_field = None 2797 2798 _related_model_field = property(attrgetter('model_field')) 2799 2800 def convert_to_cache(self, value, record, validate=True): 2801 # cache format: id or None 2802 if isinstance(value, BaseModel): 2803 value = value._ids[0] if value._ids else None 2804 return super().convert_to_cache(value, record, validate) 2805 2806 def _remove_inverses(self, records, value): 2807 # TODO: unused 2808 # remove records from the cache of one2many fields of old corecords 2809 cache = records.env.cache 2810 record_ids = set(records._ids) 2811 model_ids = self._record_ids_per_res_model(records) 2812 2813 for invf in records._field_inverses[self]: 2814 records = records.browse(model_ids[invf.model_name]) 2815 if not records: 2816 continue 2817 corecords = records.env[invf.model_name].browse( 2818 id_ for id_ in cache.get_values(records, self) 2819 ) 2820 for corecord in corecords: 2821 ids0 = cache.get(corecord, invf, None) 2822 if ids0 is not None: 2823 ids1 = tuple(id_ for id_ in ids0 if id_ not in record_ids) 2824 cache.set(corecord, invf, ids1) 2825 2826 def _update_inverses(self, records, value): 2827 """ Add `records` to the cached values of the inverse fields of `self`. """ 2828 if not value: 2829 return 2830 cache = records.env.cache 2831 model_ids = self._record_ids_per_res_model(records) 2832 2833 for invf in records._field_inverses[self]: 2834 records = records.browse(model_ids[invf.model_name]) 2835 if not records: 2836 continue 2837 corecord = records.env[invf.model_name].browse(value) 2838 records = records.filtered_domain(invf.get_domain_list(corecord)) 2839 if not records: 2840 continue 2841 ids0 = cache.get(corecord, invf, None) 2842 # if the value for the corecord is not in cache, but this is a new 2843 # record, assign it anyway, as you won't be able to fetch it from 2844 # database (see `test_sale_order`) 2845 if ids0 is not None or not corecord.id: 2846 ids1 = tuple(unique((ids0 or ()) + records._ids)) 2847 cache.set(corecord, invf, ids1) 2848 2849 def _record_ids_per_res_model(self, records): 2850 model_ids = defaultdict(set) 2851 for record in records: 2852 model = record[self.model_field] 2853 if not model and record._fields[self.model_field].compute: 2854 # fallback when the model field is computed :-/ 2855 record._fields[self.model_field].compute_value(record) 2856 model = record[self.model_field] 2857 if not model: 2858 continue 2859 model_ids[model].add(record.id) 2860 return model_ids 2861 2862 2863class _RelationalMulti(_Relational): 2864 """ Abstract class for relational fields *2many. """ 2865 2866 # Important: the cache contains the ids of all the records in the relation, 2867 # including inactive records. Inactive records are filtered out by 2868 # convert_to_record(), depending on the context. 2869 2870 def _update(self, records, value): 2871 """ Update the cached value of ``self`` for ``records`` with ``value``, 2872 and return whether everything is in cache. 2873 """ 2874 if not isinstance(records, BaseModel): 2875 # the inverse of self is a non-relational field; `value` is a 2876 # corecord that refers to `records` by an integer field 2877 model = value.env[self.model_name] 2878 domain = self.domain(model) if callable(self.domain) else self.domain 2879 if not value.filtered_domain(domain): 2880 return 2881 records = model.browse(records) 2882 2883 result = True 2884 2885 if value: 2886 cache = records.env.cache 2887 for record in records: 2888 if cache.contains(record, self): 2889 val = self.convert_to_cache(record[self.name] | value, record, validate=False) 2890 cache.set(record, self, val) 2891 else: 2892 result = False 2893 records.modified([self.name]) 2894 2895 return result 2896 2897 def convert_to_cache(self, value, record, validate=True): 2898 # cache format: tuple(ids) 2899 if isinstance(value, BaseModel): 2900 if validate and value._name != self.comodel_name: 2901 raise ValueError("Wrong value for %s: %s" % (self, value)) 2902 ids = value._ids 2903 if record and not record.id: 2904 # x2many field value of new record is new records 2905 ids = tuple(it and NewId(it) for it in ids) 2906 return ids 2907 2908 elif isinstance(value, (list, tuple)): 2909 # value is a list/tuple of commands, dicts or record ids 2910 comodel = record.env[self.comodel_name] 2911 # if record is new, the field's value is new records 2912 if record and not record.id: 2913 browse = lambda it: comodel.browse([it and NewId(it)]) 2914 else: 2915 browse = comodel.browse 2916 # determine the value ids 2917 ids = OrderedSet(record[self.name]._ids if validate else ()) 2918 # modify ids with the commands 2919 for command in value: 2920 if isinstance(command, (tuple, list)): 2921 if command[0] == 0: 2922 ids.add(comodel.new(command[2], ref=command[1]).id) 2923 elif command[0] == 1: 2924 line = browse(command[1]) 2925 if validate: 2926 line.update(command[2]) 2927 else: 2928 line._update_cache(command[2], validate=False) 2929 ids.add(line.id) 2930 elif command[0] in (2, 3): 2931 ids.discard(browse(command[1]).id) 2932 elif command[0] == 4: 2933 ids.add(browse(command[1]).id) 2934 elif command[0] == 5: 2935 ids.clear() 2936 elif command[0] == 6: 2937 ids = OrderedSet(browse(it).id for it in command[2]) 2938 elif isinstance(command, dict): 2939 ids.add(comodel.new(command).id) 2940 else: 2941 ids.add(browse(command).id) 2942 # return result as a tuple 2943 return tuple(ids) 2944 2945 elif not value: 2946 return () 2947 2948 raise ValueError("Wrong value for %s: %s" % (self, value)) 2949 2950 def convert_to_record(self, value, record): 2951 # use registry to avoid creating a recordset for the model 2952 prefetch_ids = IterableGenerator(prefetch_x2many_ids, record, self) 2953 Comodel = record.pool[self.comodel_name] 2954 corecords = Comodel._browse(record.env, value, prefetch_ids) 2955 if ( 2956 Comodel._active_name 2957 and self.context.get('active_test', record.env.context.get('active_test', True)) 2958 ): 2959 corecords = corecords.filtered(Comodel._active_name).with_prefetch(prefetch_ids) 2960 return corecords 2961 2962 def convert_to_record_multi(self, values, records): 2963 # return the list of ids as a recordset without duplicates 2964 prefetch_ids = IterableGenerator(prefetch_x2many_ids, records, self) 2965 Comodel = records.pool[self.comodel_name] 2966 ids = tuple(unique(id_ for ids in values for id_ in ids)) 2967 corecords = Comodel._browse(records.env, ids, prefetch_ids) 2968 if ( 2969 Comodel._active_name 2970 and self.context.get('active_test', records.env.context.get('active_test', True)) 2971 ): 2972 corecords = corecords.filtered(Comodel._active_name).with_prefetch(prefetch_ids) 2973 return corecords 2974 2975 def convert_to_read(self, value, record, use_name_get=True): 2976 return value.ids 2977 2978 def convert_to_write(self, value, record): 2979 if isinstance(value, tuple): 2980 # a tuple of ids, this is the cache format 2981 value = record.env[self.comodel_name].browse(value) 2982 2983 if isinstance(value, BaseModel) and value._name == self.comodel_name: 2984 def get_origin(val): 2985 return val._origin if isinstance(val, BaseModel) else val 2986 2987 # make result with new and existing records 2988 inv_names = {field.name for field in record._field_inverses[self]} 2989 result = [(6, 0, [])] 2990 for record in value: 2991 origin = record._origin 2992 if not origin: 2993 values = record._convert_to_write({ 2994 name: record[name] 2995 for name in record._cache 2996 if name not in inv_names 2997 }) 2998 result.append((0, 0, values)) 2999 else: 3000 result[0][2].append(origin.id) 3001 if record != origin: 3002 values = record._convert_to_write({ 3003 name: record[name] 3004 for name in record._cache 3005 if name not in inv_names and get_origin(record[name]) != origin[name] 3006 }) 3007 if values: 3008 result.append((1, origin.id, values)) 3009 return result 3010 3011 if value is False or value is None: 3012 return [(5,)] 3013 3014 if isinstance(value, list): 3015 return value 3016 3017 raise ValueError("Wrong value for %s: %s" % (self, value)) 3018 3019 def convert_to_export(self, value, record): 3020 return ','.join(name for id, name in value.name_get()) if value else '' 3021 3022 def convert_to_display_name(self, value, record): 3023 raise NotImplementedError() 3024 3025 def _setup_regular_full(self, model): 3026 super(_RelationalMulti, self)._setup_regular_full(model) 3027 if not self.compute and isinstance(self.domain, list): 3028 self.depends = tuple(unique(itertools.chain(self.depends, ( 3029 self.name + '.' + arg[0] 3030 for arg in self.domain 3031 if isinstance(arg, (tuple, list)) and isinstance(arg[0], str) 3032 )))) 3033 3034 def create(self, record_values): 3035 """ Write the value of ``self`` on the given records, which have just 3036 been created. 3037 3038 :param record_values: a list of pairs ``(record, value)``, where 3039 ``value`` is in the format of method :meth:`BaseModel.write` 3040 """ 3041 self.write_batch(record_values, True) 3042 3043 def write(self, records, value): 3044 # discard recomputation of self on records 3045 records.env.remove_to_compute(self, records) 3046 return self.write_batch([(records, value)]) 3047 3048 def write_batch(self, records_commands_list, create=False): 3049 if not records_commands_list: 3050 return False 3051 3052 for idx, (recs, value) in enumerate(records_commands_list): 3053 if isinstance(value, tuple): 3054 value = [(6, 0, value)] 3055 elif isinstance(value, BaseModel) and value._name == self.comodel_name: 3056 value = [(6, 0, value._ids)] 3057 elif value is False or value is None: 3058 value = [(5,)] 3059 elif isinstance(value, list) and value and not isinstance(value[0], (tuple, list)): 3060 value = [(6, 0, tuple(value))] 3061 if not isinstance(value, list): 3062 raise ValueError("Wrong value for %s: %s" % (self, value)) 3063 records_commands_list[idx] = (recs, value) 3064 3065 record_ids = {rid for recs, cs in records_commands_list for rid in recs._ids} 3066 if all(record_ids): 3067 return self.write_real(records_commands_list, create) 3068 else: 3069 assert not any(record_ids) 3070 return self.write_new(records_commands_list) 3071 3072 3073class One2many(_RelationalMulti): 3074 """One2many field; the value of such a field is the recordset of all the 3075 records in ``comodel_name`` such that the field ``inverse_name`` is equal to 3076 the current record. 3077 3078 :param str comodel_name: name of the target model 3079 3080 :param str inverse_name: name of the inverse ``Many2one`` field in 3081 ``comodel_name`` 3082 3083 :param domain: an optional domain to set on candidate values on the 3084 client side (domain or string) 3085 3086 :param dict context: an optional context to use on the client side when 3087 handling that field 3088 3089 :param bool auto_join: whether JOINs are generated upon search through that 3090 field (default: ``False``) 3091 3092 :param int limit: optional limit to use upon read 3093 3094 The attributes ``comodel_name`` and ``inverse_name`` are mandatory except in 3095 the case of related fields or field extensions. 3096 """ 3097 type = 'one2many' 3098 3099 inverse_name = None # name of the inverse field 3100 auto_join = False # whether joins are generated upon search 3101 limit = None # optional limit to use upon read 3102 copy = False # o2m are not copied by default 3103 3104 def __init__(self, comodel_name=Default, inverse_name=Default, string=Default, **kwargs): 3105 super(One2many, self).__init__( 3106 comodel_name=comodel_name, 3107 inverse_name=inverse_name, 3108 string=string, 3109 **kwargs 3110 ) 3111 3112 def _setup_regular_full(self, model): 3113 super(One2many, self)._setup_regular_full(model) 3114 if self.inverse_name: 3115 # link self to its inverse field and vice-versa 3116 comodel = model.env[self.comodel_name] 3117 invf = comodel._fields[self.inverse_name] 3118 if isinstance(invf, (Many2one, Many2oneReference)): 3119 # setting one2many fields only invalidates many2one inverses; 3120 # integer inverses (res_model/res_id pairs) are not supported 3121 model._field_inverses.add(self, invf) 3122 comodel._field_inverses.add(invf, self) 3123 3124 _description_relation_field = property(attrgetter('inverse_name')) 3125 3126 def update_db(self, model, columns): 3127 if self.comodel_name in model.env: 3128 comodel = model.env[self.comodel_name] 3129 if self.inverse_name not in comodel._fields: 3130 raise UserError(_("No inverse field %r found for %r") % (self.inverse_name, self.comodel_name)) 3131 3132 def get_domain_list(self, records): 3133 comodel = records.env.registry[self.comodel_name] 3134 inverse_field = comodel._fields[self.inverse_name] 3135 domain = super(One2many, self).get_domain_list(records) 3136 if inverse_field.type == 'many2one_reference': 3137 domain = domain + [(inverse_field.model_field, '=', records._name)] 3138 return domain 3139 3140 def __get__(self, records, owner): 3141 if records is not None and self.inverse_name is not None: 3142 # force the computation of the inverse field to ensure that the 3143 # cache value of self is consistent 3144 inverse_field = records.pool[self.comodel_name]._fields[self.inverse_name] 3145 if inverse_field.compute: 3146 records.env[self.comodel_name].recompute([self.inverse_name]) 3147 return super().__get__(records, owner) 3148 3149 def read(self, records): 3150 # retrieve the lines in the comodel 3151 context = {'active_test': False} 3152 context.update(self.context) 3153 comodel = records.env[self.comodel_name].with_context(**context) 3154 inverse = self.inverse_name 3155 inverse_field = comodel._fields[inverse] 3156 get_id = (lambda rec: rec.id) if inverse_field.type == 'many2one' else int 3157 domain = self.get_domain_list(records) + [(inverse, 'in', records.ids)] 3158 lines = comodel.search(domain, limit=self.limit) 3159 3160 # group lines by inverse field (without prefetching other fields) 3161 group = defaultdict(list) 3162 for line in lines.with_context(prefetch_fields=False): 3163 # line[inverse] may be a record or an integer 3164 group[get_id(line[inverse])].append(line.id) 3165 3166 # store result in cache 3167 cache = records.env.cache 3168 for record in records: 3169 cache.set(record, self, tuple(group[record.id])) 3170 3171 def write_real(self, records_commands_list, create=False): 3172 """ Update real records. """ 3173 # records_commands_list = [(records, commands), ...] 3174 if not records_commands_list: 3175 return 3176 3177 model = records_commands_list[0][0].browse() 3178 comodel = model.env[self.comodel_name].with_context(**self.context) 3179 3180 ids = {rid for recs, cs in records_commands_list for rid in recs.ids} 3181 records = records_commands_list[0][0].browse(ids) 3182 3183 if self.store: 3184 inverse = self.inverse_name 3185 to_create = [] # line vals to create 3186 to_delete = [] # line ids to delete 3187 to_inverse = {} 3188 allow_full_delete = not create 3189 3190 def unlink(lines): 3191 if getattr(comodel._fields[inverse], 'ondelete', False) == 'cascade': 3192 to_delete.extend(lines._ids) 3193 else: 3194 lines[inverse] = False 3195 3196 def flush(): 3197 if to_delete: 3198 # unlink() will remove the lines from the cache 3199 comodel.browse(to_delete).unlink() 3200 to_delete.clear() 3201 if to_create: 3202 # create() will add the new lines to the cache of records 3203 comodel.create(to_create) 3204 to_create.clear() 3205 if to_inverse: 3206 for record, inverse_ids in to_inverse.items(): 3207 lines = comodel.browse(inverse_ids) 3208 lines = lines.filtered(lambda line: int(line[inverse]) != record.id) 3209 lines[inverse] = record 3210 3211 for recs, commands in records_commands_list: 3212 for command in (commands or ()): 3213 if command[0] == 0: 3214 for record in recs: 3215 to_create.append(dict(command[2], **{inverse: record.id})) 3216 allow_full_delete = False 3217 elif command[0] == 1: 3218 comodel.browse(command[1]).write(command[2]) 3219 elif command[0] == 2: 3220 to_delete.append(command[1]) 3221 elif command[0] == 3: 3222 unlink(comodel.browse(command[1])) 3223 elif command[0] == 4: 3224 to_inverse.setdefault(recs[-1], set()).add(command[1]) 3225 allow_full_delete = False 3226 elif command[0] in (5, 6) : 3227 # do not try to delete anything in creation mode if nothing has been created before 3228 line_ids = command[2] if command[0] == 6 else [] 3229 if not allow_full_delete and not line_ids: 3230 continue 3231 flush() 3232 # assign the given lines to the last record only 3233 lines = comodel.browse(line_ids) 3234 domain = self.get_domain_list(model) + \ 3235 [(inverse, 'in', recs.ids), ('id', 'not in', lines.ids)] 3236 unlink(comodel.search(domain)) 3237 lines[inverse] = recs[-1] 3238 3239 flush() 3240 3241 else: 3242 cache = records.env.cache 3243 3244 def link(record, lines): 3245 ids = record[self.name]._ids 3246 cache.set(record, self, tuple(unique(ids + lines._ids))) 3247 3248 def unlink(lines): 3249 for record in records: 3250 cache.set(record, self, (record[self.name] - lines)._ids) 3251 3252 for recs, commands in records_commands_list: 3253 for command in (commands or ()): 3254 if command[0] == 0: 3255 for record in recs: 3256 link(record, comodel.new(command[2], ref=command[1])) 3257 elif command[0] == 1: 3258 comodel.browse(command[1]).write(command[2]) 3259 elif command[0] == 2: 3260 unlink(comodel.browse(command[1])) 3261 elif command[0] == 3: 3262 unlink(comodel.browse(command[1])) 3263 elif command[0] == 4: 3264 link(recs[-1], comodel.browse(command[1])) 3265 elif command[0] in (5, 6): 3266 # assign the given lines to the last record only 3267 cache.update(recs, self, [()] * len(recs)) 3268 lines = comodel.browse(command[2] if command[0] == 6 else []) 3269 cache.set(recs[-1], self, lines._ids) 3270 3271 return records 3272 3273 def write_new(self, records_commands_list): 3274 if not records_commands_list: 3275 return 3276 3277 model = records_commands_list[0][0].browse() 3278 cache = model.env.cache 3279 comodel = model.env[self.comodel_name].with_context(**self.context) 3280 3281 ids = {record.id for records, _ in records_commands_list for record in records} 3282 records = model.browse(ids) 3283 3284 def browse(ids): 3285 return comodel.browse([id_ and NewId(id_) for id_ in ids]) 3286 3287 # make sure self is in cache 3288 records[self.name] 3289 3290 if self.store: 3291 inverse = self.inverse_name 3292 3293 # make sure self's inverse is in cache 3294 inverse_field = comodel._fields[inverse] 3295 for record in records: 3296 cache.update(record[self.name], inverse_field, itertools.repeat(record.id)) 3297 3298 for recs, commands in records_commands_list: 3299 for command in commands: 3300 if command[0] == 0: 3301 for record in recs: 3302 line = comodel.new(command[2], ref=command[1]) 3303 line[inverse] = record 3304 elif command[0] == 1: 3305 browse([command[1]]).update(command[2]) 3306 elif command[0] == 2: 3307 browse([command[1]])[inverse] = False 3308 elif command[0] == 3: 3309 browse([command[1]])[inverse] = False 3310 elif command[0] == 4: 3311 browse([command[1]])[inverse] = recs[-1] 3312 elif command[0] == 5: 3313 cache.update(recs, self, itertools.repeat(())) 3314 elif command[0] == 6: 3315 # assign the given lines to the last record only 3316 cache.update(recs, self, itertools.repeat(())) 3317 last, lines = recs[-1], browse(command[2]) 3318 cache.set(last, self, lines._ids) 3319 cache.update(lines, inverse_field, itertools.repeat(last.id)) 3320 3321 else: 3322 def link(record, lines): 3323 ids = record[self.name]._ids 3324 cache.set(record, self, tuple(unique(ids + lines._ids))) 3325 3326 def unlink(lines): 3327 for record in records: 3328 cache.set(record, self, (record[self.name] - lines)._ids) 3329 3330 for recs, commands in records_commands_list: 3331 for command in commands: 3332 if command[0] == 0: 3333 for record in recs: 3334 link(record, comodel.new(command[2], ref=command[1])) 3335 elif command[0] == 1: 3336 browse([command[1]]).update(command[2]) 3337 elif command[0] == 2: 3338 unlink(browse([command[1]])) 3339 elif command[0] == 3: 3340 unlink(browse([command[1]])) 3341 elif command[0] == 4: 3342 link(recs[-1], browse([command[1]])) 3343 elif command[0] in (5, 6): 3344 # assign the given lines to the last record only 3345 cache.update(recs, self, [()] * len(recs)) 3346 lines = comodel.browse(command[2] if command[0] == 6 else []) 3347 cache.set(recs[-1], self, lines._ids) 3348 3349 return records 3350 3351 3352class Many2many(_RelationalMulti): 3353 """ Many2many field; the value of such a field is the recordset. 3354 3355 :param comodel_name: name of the target model (string) 3356 mandatory except in the case of related or extended fields 3357 3358 :param str relation: optional name of the table that stores the relation in 3359 the database 3360 3361 :param str column1: optional name of the column referring to "these" records 3362 in the table ``relation`` 3363 3364 :param str column2: optional name of the column referring to "those" records 3365 in the table ``relation`` 3366 3367 The attributes ``relation``, ``column1`` and ``column2`` are optional. 3368 If not given, names are automatically generated from model names, 3369 provided ``model_name`` and ``comodel_name`` are different! 3370 3371 Note that having several fields with implicit relation parameters on a 3372 given model with the same comodel is not accepted by the ORM, since 3373 those field would use the same table. The ORM prevents two many2many 3374 fields to use the same relation parameters, except if 3375 3376 - both fields use the same model, comodel, and relation parameters are 3377 explicit; or 3378 3379 - at least one field belongs to a model with ``_auto = False``. 3380 3381 :param domain: an optional domain to set on candidate values on the 3382 client side (domain or string) 3383 3384 :param dict context: an optional context to use on the client side when 3385 handling that field 3386 3387 :param bool check_company: Mark the field to be verified in 3388 :meth:`~odoo.models.Model._check_company`. Add a default company 3389 domain depending on the field attributes. 3390 3391 :param int limit: optional limit to use upon read 3392 """ 3393 type = 'many2many' 3394 3395 _explicit = True # whether schema is explicitly given 3396 relation = None # name of table 3397 column1 = None # column of table referring to model 3398 column2 = None # column of table referring to comodel 3399 auto_join = False # whether joins are generated upon search 3400 limit = None # optional limit to use upon read 3401 ondelete = None # optional ondelete for the column2 fkey 3402 3403 def __init__(self, comodel_name=Default, relation=Default, column1=Default, 3404 column2=Default, string=Default, **kwargs): 3405 super(Many2many, self).__init__( 3406 comodel_name=comodel_name, 3407 relation=relation, 3408 column1=column1, 3409 column2=column2, 3410 string=string, 3411 **kwargs 3412 ) 3413 3414 def _setup_regular_base(self, model): 3415 super(Many2many, self)._setup_regular_base(model) 3416 # 3 cases: 3417 # 1) The ondelete attribute is not defined, we assign it a sensible default 3418 # 2) The ondelete attribute is defined and its definition makes sense 3419 # 3) The ondelete attribute is explicitly defined as 'set null' for a m2m, 3420 # this is considered a programming error. 3421 self.ondelete = self.ondelete or 'cascade' 3422 if self.ondelete == 'set null': 3423 raise ValueError( 3424 "The m2m field %s of model %s declares its ondelete policy " 3425 "as being 'set null'. Only 'restrict' and 'cascade' make sense." 3426 % (self.name, model._name) 3427 ) 3428 if self.store: 3429 if not (self.relation and self.column1 and self.column2): 3430 self._explicit = False 3431 # table name is based on the stable alphabetical order of tables 3432 comodel = model.env[self.comodel_name] 3433 if not self.relation: 3434 tables = sorted([model._table, comodel._table]) 3435 assert tables[0] != tables[1], \ 3436 "%s: Implicit/canonical naming of many2many relationship " \ 3437 "table is not possible when source and destination models " \ 3438 "are the same" % self 3439 self.relation = '%s_%s_rel' % tuple(tables) 3440 if not self.column1: 3441 self.column1 = '%s_id' % model._table 3442 if not self.column2: 3443 self.column2 = '%s_id' % comodel._table 3444 # check validity of table name 3445 check_pg_name(self.relation) 3446 else: 3447 self.relation = self.column1 = self.column2 = None 3448 3449 def _setup_regular_full(self, model): 3450 super(Many2many, self)._setup_regular_full(model) 3451 if self.relation: 3452 m2m = model.pool._m2m 3453 3454 # check whether other fields use the same schema 3455 fields = m2m[(self.relation, self.column1, self.column2)] 3456 for field in fields: 3457 if ( # same model: relation parameters must be explicit 3458 self.model_name == field.model_name and 3459 self.comodel_name == field.comodel_name and 3460 self._explicit and field._explicit 3461 ) or ( # different models: one model must be _auto=False 3462 self.model_name != field.model_name and 3463 not (model._auto and model.env[field.model_name]._auto) 3464 ): 3465 continue 3466 msg = "Many2many fields %s and %s use the same table and columns" 3467 raise TypeError(msg % (self, field)) 3468 fields.append(self) 3469 3470 # retrieve inverse fields, and link them in _field_inverses 3471 for field in m2m[(self.relation, self.column2, self.column1)]: 3472 model._field_inverses.add(self, field) 3473 model.env[field.model_name]._field_inverses.add(field, self) 3474 3475 def update_db(self, model, columns): 3476 cr = model._cr 3477 # Do not reflect relations for custom fields, as they do not belong to a 3478 # module. They are automatically removed when dropping the corresponding 3479 # 'ir.model.field'. 3480 if not self.manual: 3481 model.pool.post_init(model.env['ir.model.relation']._reflect_relation, 3482 model, self.relation, self._module) 3483 comodel = model.env[self.comodel_name] 3484 if not sql.table_exists(cr, self.relation): 3485 query = """ 3486 CREATE TABLE "{rel}" ("{id1}" INTEGER NOT NULL, 3487 "{id2}" INTEGER NOT NULL, 3488 PRIMARY KEY("{id1}","{id2}")); 3489 COMMENT ON TABLE "{rel}" IS %s; 3490 CREATE INDEX ON "{rel}" ("{id2}","{id1}"); 3491 """.format(rel=self.relation, id1=self.column1, id2=self.column2) 3492 cr.execute(query, ['RELATION BETWEEN %s AND %s' % (model._table, comodel._table)]) 3493 _schema.debug("Create table %r: m2m relation between %r and %r", self.relation, model._table, comodel._table) 3494 3495 model.pool.post_init(self.update_db_foreign_keys, model) 3496 3497 def update_db_foreign_keys(self, model): 3498 """ Add the foreign keys corresponding to the field's relation table. """ 3499 comodel = model.env[self.comodel_name] 3500 if model._is_an_ordinary_table(): 3501 model.pool.add_foreign_key( 3502 self.relation, self.column1, model._table, 'id', 'cascade', 3503 model, self._module, force=False, 3504 ) 3505 if comodel._is_an_ordinary_table(): 3506 model.pool.add_foreign_key( 3507 self.relation, self.column2, comodel._table, 'id', self.ondelete, 3508 model, self._module, 3509 ) 3510 3511 def read(self, records): 3512 context = {'active_test': False} 3513 context.update(self.context) 3514 comodel = records.env[self.comodel_name].with_context(**context) 3515 domain = self.get_domain_list(records) 3516 comodel._flush_search(domain) 3517 wquery = comodel._where_calc(domain) 3518 comodel._apply_ir_rules(wquery, 'read') 3519 order_by = comodel._generate_order_by(None, wquery) 3520 from_c, where_c, where_params = wquery.get_sql() 3521 query = """ SELECT {rel}.{id1}, {rel}.{id2} FROM {rel}, {from_c} 3522 WHERE {where_c} AND {rel}.{id1} IN %s AND {rel}.{id2} = {tbl}.id 3523 {order_by} {limit} OFFSET {offset} 3524 """.format(rel=self.relation, id1=self.column1, id2=self.column2, 3525 tbl=comodel._table, from_c=from_c, where_c=where_c or '1=1', 3526 limit=(' LIMIT %d' % self.limit) if self.limit else '', 3527 offset=0, order_by=order_by) 3528 where_params.append(tuple(records.ids)) 3529 3530 # retrieve lines and group them by record 3531 group = defaultdict(list) 3532 records._cr.execute(query, where_params) 3533 for row in records._cr.fetchall(): 3534 group[row[0]].append(row[1]) 3535 3536 # store result in cache 3537 cache = records.env.cache 3538 for record in records: 3539 cache.set(record, self, tuple(group[record.id])) 3540 3541 def write_real(self, records_commands_list, create=False): 3542 # records_commands_list = [(records, commands), ...] 3543 if not records_commands_list: 3544 return 3545 3546 comodel = records_commands_list[0][0].env[self.comodel_name].with_context(**self.context) 3547 cr = records_commands_list[0][0].env.cr 3548 3549 # determine old and new relation {x: ys} 3550 set = OrderedSet 3551 ids = {rid for recs, cs in records_commands_list for rid in recs.ids} 3552 records = records_commands_list[0][0].browse(ids) 3553 3554 if self.store: 3555 # Using `record[self.name]` generates 2 SQL queries when the value 3556 # is not in cache: one that actually checks access rules for 3557 # records, and the other one fetching the actual data. We use 3558 # `self.read` instead to shortcut the first query. 3559 missing_ids = list(records.env.cache.get_missing_ids(records, self)) 3560 if missing_ids: 3561 self.read(records.browse(missing_ids)) 3562 3563 old_relation = {record.id: set(record[self.name]._ids) for record in records} 3564 new_relation = {x: set(ys) for x, ys in old_relation.items()} 3565 3566 # determine new relation {x: ys} 3567 new_relation = defaultdict(set) 3568 for x, ys in old_relation.items(): 3569 new_relation[x] = set(ys) 3570 3571 # operations on new relation 3572 def relation_add(xs, y): 3573 for x in xs: 3574 new_relation[x].add(y) 3575 3576 def relation_remove(xs, y): 3577 for x in xs: 3578 new_relation[x].discard(y) 3579 3580 def relation_set(xs, ys): 3581 for x in xs: 3582 new_relation[x] = set(ys) 3583 3584 def relation_delete(ys): 3585 # the pairs (x, y) have been cascade-deleted from relation 3586 for ys1 in old_relation.values(): 3587 ys1 -= ys 3588 for ys1 in new_relation.values(): 3589 ys1 -= ys 3590 3591 for recs, commands in records_commands_list: 3592 to_create = [] # line vals to create 3593 to_delete = [] # line ids to delete 3594 for command in (commands or ()): 3595 if not isinstance(command, (list, tuple)) or not command: 3596 continue 3597 if command[0] == 0: 3598 to_create.append((recs._ids, command[2])) 3599 elif command[0] == 1: 3600 comodel.browse(command[1]).write(command[2]) 3601 elif command[0] == 2: 3602 to_delete.append(command[1]) 3603 elif command[0] == 3: 3604 relation_remove(recs._ids, command[1]) 3605 elif command[0] == 4: 3606 relation_add(recs._ids, command[1]) 3607 elif command[0] in (5, 6): 3608 # new lines must no longer be linked to records 3609 to_create = [(set(ids) - set(recs._ids), vals) for (ids, vals) in to_create] 3610 relation_set(recs._ids, command[2] if command[0] == 6 else ()) 3611 3612 if to_create: 3613 # create lines in batch, and link them 3614 lines = comodel.create([vals for ids, vals in to_create]) 3615 for line, (ids, vals) in zip(lines, to_create): 3616 relation_add(ids, line.id) 3617 3618 if to_delete: 3619 # delete lines in batch 3620 comodel.browse(to_delete).unlink() 3621 relation_delete(to_delete) 3622 3623 # update the cache of self 3624 cache = records.env.cache 3625 for record in records: 3626 cache.set(record, self, tuple(new_relation[record.id])) 3627 3628 # process pairs to add (beware of duplicates) 3629 pairs = [(x, y) for x, ys in new_relation.items() for y in ys - old_relation[x]] 3630 if pairs: 3631 if self.store: 3632 query = "INSERT INTO {} ({}, {}) VALUES {} ON CONFLICT DO NOTHING".format( 3633 self.relation, self.column1, self.column2, ", ".join(["%s"] * len(pairs)), 3634 ) 3635 cr.execute(query, pairs) 3636 3637 # update the cache of inverse fields 3638 y_to_xs = defaultdict(set) 3639 for x, y in pairs: 3640 y_to_xs[y].add(x) 3641 for invf in records._field_inverses[self]: 3642 domain = invf.get_domain_list(comodel) 3643 valid_ids = set(records.filtered_domain(domain)._ids) 3644 if not valid_ids: 3645 continue 3646 for y, xs in y_to_xs.items(): 3647 corecord = comodel.browse(y) 3648 try: 3649 ids0 = cache.get(corecord, invf) 3650 ids1 = tuple(set(ids0) | (xs & valid_ids)) 3651 cache.set(corecord, invf, ids1) 3652 except KeyError: 3653 pass 3654 3655 # process pairs to remove 3656 pairs = [(x, y) for x, ys in old_relation.items() for y in ys - new_relation[x]] 3657 if pairs: 3658 y_to_xs = defaultdict(set) 3659 for x, y in pairs: 3660 y_to_xs[y].add(x) 3661 3662 if self.store: 3663 # express pairs as the union of cartesian products: 3664 # pairs = [(1, 11), (1, 12), (1, 13), (2, 11), (2, 12), (2, 14)] 3665 # -> y_to_xs = {11: {1, 2}, 12: {1, 2}, 13: {1}, 14: {2}} 3666 # -> xs_to_ys = {{1, 2}: {11, 12}, {2}: {14}, {1}: {13}} 3667 xs_to_ys = defaultdict(set) 3668 for y, xs in y_to_xs.items(): 3669 xs_to_ys[frozenset(xs)].add(y) 3670 # delete the rows where (id1 IN xs AND id2 IN ys) OR ... 3671 COND = "{} IN %s AND {} IN %s".format(self.column1, self.column2) 3672 query = "DELETE FROM {} WHERE {}".format( 3673 self.relation, " OR ".join([COND] * len(xs_to_ys)), 3674 ) 3675 params = [arg for xs, ys in xs_to_ys.items() for arg in [tuple(xs), tuple(ys)]] 3676 cr.execute(query, params) 3677 3678 # update the cache of inverse fields 3679 for invf in records._field_inverses[self]: 3680 for y, xs in y_to_xs.items(): 3681 corecord = comodel.browse(y) 3682 try: 3683 ids0 = cache.get(corecord, invf) 3684 ids1 = tuple(id_ for id_ in ids0 if id_ not in xs) 3685 cache.set(corecord, invf, ids1) 3686 except KeyError: 3687 pass 3688 3689 return records.filtered( 3690 lambda record: new_relation[record.id] != old_relation[record.id] 3691 ) 3692 3693 def write_new(self, records_commands_list): 3694 """ Update self on new records. """ 3695 if not records_commands_list: 3696 return 3697 3698 model = records_commands_list[0][0].browse() 3699 comodel = model.env[self.comodel_name].with_context(**self.context) 3700 new = lambda id_: id_ and NewId(id_) 3701 3702 # determine old and new relation {x: ys} 3703 set = OrderedSet 3704 old_relation = {record.id: set(record[self.name]._ids) for records, _ in records_commands_list for record in records} 3705 new_relation = {x: set(ys) for x, ys in old_relation.items()} 3706 ids = set(old_relation.keys()) 3707 3708 records = model.browse(ids) 3709 3710 for recs, commands in records_commands_list: 3711 for command in commands: 3712 if not isinstance(command, (list, tuple)) or not command: 3713 continue 3714 if command[0] == 0: 3715 line_id = comodel.new(command[2], ref=command[1]).id 3716 for line_ids in new_relation.values(): 3717 line_ids.add(line_id) 3718 elif command[0] == 1: 3719 line_id = new(command[1]) 3720 comodel.browse([line_id]).update(command[2]) 3721 elif command[0] == 2: 3722 line_id = new(command[1]) 3723 for line_ids in new_relation.values(): 3724 line_ids.discard(line_id) 3725 elif command[0] == 3: 3726 line_id = new(command[1]) 3727 for line_ids in new_relation.values(): 3728 line_ids.discard(line_id) 3729 elif command[0] == 4: 3730 line_id = new(command[1]) 3731 for line_ids in new_relation.values(): 3732 line_ids.add(line_id) 3733 elif command[0] in (5, 6): 3734 # new lines must no longer be linked to records 3735 line_ids = command[2] if command[0] == 6 else () 3736 line_ids = set(new(line_id) for line_id in line_ids) 3737 for id_ in recs._ids: 3738 new_relation[id_] = set(line_ids) 3739 3740 if new_relation == old_relation: 3741 return records.browse() 3742 3743 # update the cache of self 3744 cache = records.env.cache 3745 for record in records: 3746 cache.set(record, self, tuple(new_relation[record.id])) 3747 3748 # process pairs to add (beware of duplicates) 3749 pairs = [(x, y) for x, ys in new_relation.items() for y in ys - old_relation[x]] 3750 if pairs: 3751 # update the cache of inverse fields 3752 y_to_xs = defaultdict(set) 3753 for x, y in pairs: 3754 y_to_xs[y].add(x) 3755 for invf in records._field_inverses[self]: 3756 domain = invf.get_domain_list(comodel) 3757 valid_ids = set(records.filtered_domain(domain)._ids) 3758 if not valid_ids: 3759 continue 3760 for y, xs in y_to_xs.items(): 3761 corecord = comodel.browse([y]) 3762 try: 3763 ids0 = cache.get(corecord, invf) 3764 ids1 = tuple(set(ids0) | (xs & valid_ids)) 3765 cache.set(corecord, invf, ids1) 3766 except KeyError: 3767 pass 3768 3769 # process pairs to remove 3770 pairs = [(x, y) for x, ys in old_relation.items() for y in ys - new_relation[x]] 3771 if pairs: 3772 # update the cache of inverse fields 3773 y_to_xs = defaultdict(set) 3774 for x, y in pairs: 3775 y_to_xs[y].add(x) 3776 for invf in records._field_inverses[self]: 3777 for y, xs in y_to_xs.items(): 3778 corecord = comodel.browse([y]) 3779 try: 3780 ids0 = cache.get(corecord, invf) 3781 ids1 = tuple(id_ for id_ in ids0 if id_ not in xs) 3782 cache.set(corecord, invf, ids1) 3783 except KeyError: 3784 pass 3785 3786 return records.filtered( 3787 lambda record: new_relation[record.id] != old_relation[record.id] 3788 ) 3789 3790 3791class Id(Field): 3792 """ Special case for field 'id'. """ 3793 type = 'integer' 3794 column_type = ('int4', 'int4') 3795 3796 string = 'ID' 3797 store = True 3798 readonly = True 3799 prefetch = False 3800 3801 def update_db(self, model, columns): 3802 pass # this column is created with the table 3803 3804 def __get__(self, record, owner): 3805 if record is None: 3806 return self # the field is accessed through the class owner 3807 3808 # the code below is written to make record.id as quick as possible 3809 ids = record._ids 3810 size = len(ids) 3811 if size == 0: 3812 return False 3813 elif size == 1: 3814 return ids[0] 3815 raise ValueError("Expected singleton: %s" % record) 3816 3817 def __set__(self, record, value): 3818 raise TypeError("field 'id' cannot be assigned") 3819 3820 3821def prefetch_many2one_ids(record, field): 3822 """ Return an iterator over the ids of the cached values of a many2one 3823 field for the prefetch set of a record. 3824 """ 3825 records = record.browse(record._prefetch_ids) 3826 ids = record.env.cache.get_values(records, field) 3827 return unique(id_ for id_ in ids if id_ is not None) 3828 3829 3830def prefetch_x2many_ids(record, field): 3831 """ Return an iterator over the ids of the cached values of an x2many 3832 field for the prefetch set of a record. 3833 """ 3834 records = record.browse(record._prefetch_ids) 3835 ids_list = record.env.cache.get_values(records, field) 3836 return unique(id_ for ids in ids_list for id_ in ids) 3837 3838 3839def apply_required(model, field_name): 3840 """ Set a NOT NULL constraint on the given field, if necessary. """ 3841 # At the time this function is called, the model's _fields may have been reset, although 3842 # the model's class is still the same. Retrieve the field to see whether the NOT NULL 3843 # constraint still applies 3844 field = model._fields[field_name] 3845 if field.store and field.required: 3846 sql.set_not_null(model.env.cr, model._table, field_name) 3847 3848 3849# imported here to avoid dependency cycle issues 3850from .exceptions import AccessError, MissingError, UserError 3851from .models import check_pg_name, BaseModel, NewId, IdType, expand_ids, PREFETCH_MAX 3852