· 6 years ago · Dec 11, 2019, 04:04 PM
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 OrderedDict, defaultdict
7from datetime import date, datetime
8from functools import partial
9from operator import attrgetter
10import itertools
11import logging
12
13import pytz
14
15try:
16 from xmlrpc.client import MAXINT
17except ImportError:
18 #pylint: disable=bad-python3-import
19 from xmlrpclib import MAXINT
20
21import psycopg2
22
23from .sql_db import LazyCursor
24from .tools import float_repr, float_round, frozendict, html_sanitize, human_size, pg_varchar, ustr, OrderedSet, pycompat, sql
25from .tools import DEFAULT_SERVER_DATE_FORMAT as DATE_FORMAT
26from .tools import DEFAULT_SERVER_DATETIME_FORMAT as DATETIME_FORMAT
27from .tools.translate import html_translate, _
28
29DATE_LENGTH = len(date.today().strftime(DATE_FORMAT))
30DATETIME_LENGTH = len(datetime.now().strftime(DATETIME_FORMAT))
31EMPTY_DICT = frozendict()
32
33RENAMED_ATTRS = [('select', 'index'), ('digits_compute', 'digits')]
34
35_logger = logging.getLogger(__name__)
36_schema = logging.getLogger(__name__[:-7] + '.schema')
37
38Default = object() # default value for __init__() methods
39
40def copy_cache(records, env):
41 """ Recursively copy the cache of ``records`` to the environment ``env``. """
42 src, dst = records.env.cache, env.cache
43 todo, done = set(records), set()
44 while todo:
45 record = todo.pop()
46 if record not in done:
47 done.add(record)
48 target = record.with_env(env)
49 for field in src.get_fields(record):
50 value = src.get(record, field)
51 dst.set(target, field, value)
52 if value and field.type in ('many2one', 'one2many', 'many2many', 'reference'):
53 todo.update(field.convert_to_record(value, record))
54
55def first(records):
56 """ Return the first record in ``records``, with the same prefetching. """
57 return next(iter(records)) if len(records) > 1 else records
58
59
60def resolve_mro(model, name, predicate):
61 """ Return the list of successively overridden values of attribute ``name``
62 in mro order on ``model`` that satisfy ``predicate``.
63 """
64 result = []
65 for cls in type(model).__mro__:
66 if name in cls.__dict__:
67 value = cls.__dict__[name]
68 if not predicate(value):
69 break
70 result.append(value)
71 return result
72
73
74class MetaField(type):
75 """ Metaclass for field classes. """
76 by_type = {}
77
78 def __new__(meta, name, bases, attrs):
79 """ Combine the ``_slots`` dict from parent classes, and determine
80 ``__slots__`` for them on the new class.
81 """
82 base_slots = {}
83 for base in reversed(bases):
84 base_slots.update(getattr(base, '_slots', ()))
85
86 slots = dict(base_slots)
87 slots.update(attrs.get('_slots', ()))
88
89 attrs['__slots__'] = set(slots) - set(base_slots)
90 attrs['_slots'] = slots
91 return type.__new__(meta, name, bases, attrs)
92
93 def __init__(cls, name, bases, attrs):
94 super(MetaField, cls).__init__(name, bases, attrs)
95 if not hasattr(cls, 'type'):
96 return
97
98 if cls.type and cls.type not in MetaField.by_type:
99 MetaField.by_type[cls.type] = cls
100
101 # compute class attributes to avoid calling dir() on fields
102 cls.related_attrs = []
103 cls.description_attrs = []
104 for attr in dir(cls):
105 if attr.startswith('_related_'):
106 cls.related_attrs.append((attr[9:], attr))
107 elif attr.startswith('_description_'):
108 cls.description_attrs.append((attr[13:], attr))
109
110_global_seq = iter(itertools.count())
111class Field(MetaField('DummyField', (object,), {})):
112 """ The field descriptor contains the field definition, and manages accesses
113 and assignments of the corresponding field on records. The following
114 attributes may be provided when instanciating a field:
115
116 :param string: the label of the field seen by users (string); if not
117 set, the ORM takes the field name in the class (capitalized).
118
119 :param help: the tooltip of the field seen by users (string)
120
121 :param readonly: whether the field is readonly (boolean, by default ``False``)
122
123 :param required: whether the value of the field is required (boolean, by
124 default ``False``)
125
126 :param index: whether the field is indexed in database (boolean, by
127 default ``False``)
128
129 :param default: the default value for the field; this is either a static
130 value, or a function taking a recordset and returning a value; use
131 ``default=None`` to discard default values for the field
132
133 :param states: a dictionary mapping state values to lists of UI attribute-value
134 pairs; possible attributes are: 'readonly', 'required', 'invisible'.
135 Note: Any state-based condition requires the ``state`` field value to be
136 available on the client-side UI. This is typically done by including it in
137 the relevant views, possibly made invisible if not relevant for the
138 end-user.
139
140 :param groups: comma-separated list of group xml ids (string); this
141 restricts the field access to the users of the given groups only
142
143 :param bool copy: whether the field value should be copied when the record
144 is duplicated (default: ``True`` for normal fields, ``False`` for
145 ``one2many`` and computed fields, including property fields and
146 related fields)
147
148 :param string oldname: the previous name of this field, so that ORM can rename
149 it automatically at migration
150
151 .. _field-computed:
152
153 .. rubric:: Computed fields
154
155 One can define a field whose value is computed instead of simply being
156 read from the database. The attributes that are specific to computed
157 fields are given below. To define such a field, simply provide a value
158 for the attribute ``compute``.
159
160 :param compute: name of a method that computes the field
161
162 :param inverse: name of a method that inverses the field (optional)
163
164 :param search: name of a method that implement search on the field (optional)
165
166 :param store: whether the field is stored in database (boolean, by
167 default ``False`` on computed fields)
168
169 :param compute_sudo: whether the field should be recomputed as superuser
170 to bypass access rights (boolean, by default ``False``)
171
172 The methods given for ``compute``, ``inverse`` and ``search`` are model
173 methods. Their signature is shown in the following example::
174
175 upper = fields.Char(compute='_compute_upper',
176 inverse='_inverse_upper',
177 search='_search_upper')
178
179 @api.depends('name')
180 def _compute_upper(self):
181 for rec in self:
182 rec.upper = rec.name.upper() if rec.name else False
183
184 def _inverse_upper(self):
185 for rec in self:
186 rec.name = rec.upper.lower() if rec.upper else False
187
188 def _search_upper(self, operator, value):
189 if operator == 'like':
190 operator = 'ilike'
191 return [('name', operator, value)]
192
193 The compute method has to assign the field on all records of the invoked
194 recordset. The decorator :meth:`odoo.api.depends` must be applied on
195 the compute method to specify the field dependencies; those dependencies
196 are used to determine when to recompute the field; recomputation is
197 automatic and guarantees cache/database consistency. Note that the same
198 method can be used for several fields, you simply have to assign all the
199 given fields in the method; the method will be invoked once for all
200 those fields.
201
202 By default, a computed field is not stored to the database, and is
203 computed on-the-fly. Adding the attribute ``store=True`` will store the
204 field's values in the database. The advantage of a stored field is that
205 searching on that field is done by the database itself. The disadvantage
206 is that it requires database updates when the field must be recomputed.
207
208 The inverse method, as its name says, does the inverse of the compute
209 method: the invoked records have a value for the field, and you must
210 apply the necessary changes on the field dependencies such that the
211 computation gives the expected value. Note that a computed field without
212 an inverse method is readonly by default.
213
214 The search method is invoked when processing domains before doing an
215 actual search on the model. It must return a domain equivalent to the
216 condition: ``field operator value``.
217
218 .. _field-related:
219
220 .. rubric:: Related fields
221
222 The value of a related field is given by following a sequence of
223 relational fields and reading a field on the reached model. The complete
224 sequence of fields to traverse is specified by the attribute
225
226 :param related: sequence of field names
227
228 Some field attributes are automatically copied from the source field if
229 they are not redefined: ``string``, ``help``, ``readonly``, ``required`` (only
230 if all fields in the sequence are required), ``groups``, ``digits``, ``size``,
231 ``translate``, ``sanitize``, ``selection``, ``comodel_name``, ``domain``,
232 ``context``. All semantic-free attributes are copied from the source
233 field.
234
235 By default, the values of related fields are not stored to the database.
236 Add the attribute ``store=True`` to make it stored, just like computed
237 fields. Related fields are automatically recomputed when their
238 dependencies are modified.
239
240 .. _field-company-dependent:
241
242 .. rubric:: Company-dependent fields
243
244 Formerly known as 'property' fields, the value of those fields depends
245 on the company. In other words, users that belong to different companies
246 may see different values for the field on a given record.
247
248 :param company_dependent: whether the field is company-dependent (boolean)
249
250 .. _field-incremental-definition:
251
252 .. rubric:: Incremental definition
253
254 A field is defined as class attribute on a model class. If the model
255 is extended (see :class:`~odoo.models.Model`), one can also extend
256 the field definition by redefining a field with the same name and same
257 type on the subclass. In that case, the attributes of the field are
258 taken from the parent class and overridden by the ones given in
259 subclasses.
260
261 For instance, the second class below only adds a tooltip on the field
262 ``state``::
263
264 class First(models.Model):
265 _name = 'foo'
266 state = fields.Selection([...], required=True)
267
268 class Second(models.Model):
269 _inherit = 'foo'
270 state = fields.Selection(help="Blah blah blah")
271
272 """
273
274 type = None # type of the field (string)
275 relational = False # whether the field is a relational one
276 translate = False # whether the field is translated
277
278 column_type = None # database column type (ident, spec)
279 column_format = '%s' # placeholder for value in queries
280 column_cast_from = () # column types that may be cast to this
281
282 _slots = {
283 'args': EMPTY_DICT, # the parameters given to __init__()
284 '_attrs': EMPTY_DICT, # the field's non-slot attributes
285 '_module': None, # the field's module name
286 '_setup_done': None, # the field's setup state: None, 'base' or 'full'
287 '_sequence': None, # absolute ordering of the field
288
289 'automatic': False, # whether the field is automatically created ("magic" field)
290 'inherited': False, # whether the field is inherited (_inherits)
291
292 'name': None, # name of the field
293 'model_name': None, # name of the model of this field
294 'comodel_name': None, # name of the model of values (if relational)
295
296 'store': True, # whether the field is stored in database
297 'index': False, # whether the field is indexed in database
298 'manual': False, # whether the field is a custom field
299 'copy': True, # whether the field is copied over by BaseModel.copy()
300 'depends': (), # collection of field dependencies
301 'recursive': False, # whether self depends on itself
302 'compute': None, # compute(recs) computes field on recs
303 'compute_sudo': False, # whether field should be recomputed as admin
304 'inverse': None, # inverse(recs) inverses field on recs
305 'search': None, # search(recs, operator, value) searches on self
306 'related': None, # sequence of field names, for related fields
307 'related_sudo': True, # whether related fields should be read as admin
308 'company_dependent': False, # whether ``self`` is company-dependent (property field)
309 'default': None, # default(recs) returns the default value
310
311 'string': None, # field label
312 'help': None, # field tooltip
313 'readonly': False, # whether the field is readonly
314 'required': False, # whether the field is required
315 'states': None, # set readonly and required depending on state
316 'groups': None, # csv list of group xml ids
317 'change_default': False, # whether the field may trigger a "user-onchange"
318 'deprecated': None, # whether the field is deprecated
319
320 'related_field': None, # corresponding related field
321 'group_operator': None, # operator for aggregating values
322 'group_expand': None, # name of method to expand groups in read_group()
323 'prefetch': True, # whether the field is prefetched
324 'context_dependent': False, # whether the field's value depends on context
325 }
326
327 def __init__(self, string=Default, **kwargs):
328 kwargs['string'] = string
329 self._sequence = kwargs['_sequence'] = next(_global_seq)
330 args = {key: val for key, val in kwargs.items() if val is not Default}
331 self.args = args or EMPTY_DICT
332 self._setup_done = None
333
334 def new(self, **kwargs):
335 """ Return a field of the same type as ``self``, with its own parameters. """
336 return type(self)(**kwargs)
337
338 def __getattr__(self, name):
339 """ Access non-slot field attribute. """
340 try:
341 return self._attrs[name]
342 except KeyError:
343 raise AttributeError(name)
344
345 def __setattr__(self, name, value):
346 """ Set slot or non-slot field attribute. """
347 try:
348 object.__setattr__(self, name, value)
349 except AttributeError:
350 if self._attrs:
351 self._attrs[name] = value
352 else:
353 self._attrs = {name: value} # replace EMPTY_DICT
354
355 def set_all_attrs(self, attrs):
356 """ Set all field attributes at once (with slot defaults). """
357 # optimization: we assign slots only
358 assign = object.__setattr__
359 for key, val in self._slots.items():
360 assign(self, key, attrs.pop(key, val))
361 if attrs:
362 assign(self, '_attrs', attrs)
363
364 def __delattr__(self, name):
365 """ Remove non-slot field attribute. """
366 try:
367 del self._attrs[name]
368 except KeyError:
369 raise AttributeError(name)
370
371 def __str__(self):
372 return "%s.%s" % (self.model_name, self.name)
373
374 def __repr__(self):
375 return "%s.%s" % (self.model_name, self.name)
376
377 ############################################################################
378 #
379 # Base field setup: things that do not depend on other models/fields
380 #
381
382 def setup_base(self, model, name):
383 """ Base setup: things that do not depend on other models/fields. """
384 if self._setup_done and not self.related:
385 # optimization for regular fields: keep the base setup
386 self._setup_done = 'base'
387 else:
388 # do the base setup from scratch
389 self._setup_attrs(model, name)
390 if not self.related:
391 self._setup_regular_base(model)
392 self._setup_done = 'base'
393
394 #
395 # Setup field parameter attributes
396 #
397
398 def _can_setup_from(self, field):
399 """ Return whether ``self`` can retrieve parameters from ``field``. """
400 return isinstance(field, type(self))
401
402 def _get_attrs(self, model, name):
403 """ Return the field parameter attributes as a dictionary. """
404 # determine all inherited field attributes
405 attrs = {}
406 if not (self.args.get('automatic') or self.args.get('manual')):
407 # magic and custom fields do not inherit from parent classes
408 for field in reversed(resolve_mro(model, name, self._can_setup_from)):
409 attrs.update(field.args)
410 attrs.update(self.args) # necessary in case self is not in class
411
412 attrs['args'] = self.args
413 attrs['model_name'] = model._name
414 attrs['name'] = name
415
416 # initialize ``self`` with ``attrs``
417 if attrs.get('compute'):
418 # by default, computed fields are not stored, not copied and readonly
419 attrs['store'] = attrs.get('store', False)
420 attrs['copy'] = attrs.get('copy', False)
421 attrs['readonly'] = attrs.get('readonly', not attrs.get('inverse'))
422 attrs['context_dependent'] = attrs.get('context_dependent', True)
423 if attrs.get('related'):
424 # by default, related fields are not stored and not copied
425 attrs['store'] = attrs.get('store', False)
426 attrs['copy'] = attrs.get('copy', False)
427 if attrs.get('company_dependent'):
428 # by default, company-dependent fields are not stored and not copied
429 attrs['store'] = False
430 attrs['copy'] = attrs.get('copy', False)
431 attrs['default'] = self._default_company_dependent
432 attrs['compute'] = self._compute_company_dependent
433 if not attrs.get('readonly'):
434 attrs['inverse'] = self._inverse_company_dependent
435 attrs['search'] = self._search_company_dependent
436 attrs['context_dependent'] = attrs.get('context_dependent', True)
437 if attrs.get('translate'):
438 # by default, translatable fields are context-dependent
439 attrs['context_dependent'] = attrs.get('context_dependent', True)
440
441 return attrs
442
443 def _setup_attrs(self, model, name):
444 """ Initialize the field parameter attributes. """
445 attrs = self._get_attrs(model, name)
446 self.set_all_attrs(attrs)
447
448 # check for renamed attributes (conversion errors)
449 for key1, key2 in RENAMED_ATTRS:
450 if key1 in attrs:
451 _logger.warning("Field %s: parameter %r is no longer supported; use %r instead.",
452 self, key1, key2)
453
454 # prefetch only stored, column, non-manual and non-deprecated fields
455 if not (self.store and self.column_type) or self.manual or self.deprecated:
456 self.prefetch = False
457
458 if not self.string and not self.related:
459 # related fields get their string from their parent field
460 self.string = (
461 name[:-4] if name.endswith('_ids') else
462 name[:-3] if name.endswith('_id') else name
463 ).replace('_', ' ').title()
464
465 # self.default must be a callable
466 if self.default is not None:
467 value = self.default
468 self.default = value if callable(value) else lambda model: value
469
470 ############################################################################
471 #
472 # Full field setup: everything else, except recomputation triggers
473 #
474
475 def setup_full(self, model):
476 """ Full setup: everything else, except recomputation triggers. """
477 if self._setup_done != 'full':
478 if not self.related:
479 self._setup_regular_full(model)
480 else:
481 self._setup_related_full(model)
482 self._setup_done = 'full'
483
484 #
485 # Setup of non-related fields
486 #
487
488 def _setup_regular_base(self, model):
489 """ Setup the attributes of a non-related field. """
490 def make_depends(deps):
491 return tuple(deps(model) if callable(deps) else deps)
492
493 if isinstance(self.compute, pycompat.string_types):
494 # if the compute method has been overridden, concatenate all their _depends
495 self.depends = ()
496 for method in resolve_mro(model, self.compute, callable):
497 self.depends += make_depends(getattr(method, '_depends', ()))
498 else:
499 self.depends = make_depends(getattr(self.compute, '_depends', ()))
500
501 def _setup_regular_full(self, model):
502 """ Setup the inverse field(s) of ``self``. """
503 pass
504
505 #
506 # Setup of related fields
507 #
508
509 def _setup_related_full(self, model):
510 """ Setup the attributes of a related field. """
511 # fix the type of self.related if necessary
512 if isinstance(self.related, pycompat.string_types):
513 self.related = tuple(self.related.split('.'))
514
515 # determine the chain of fields, and make sure they are all set up
516 target = model
517 for name in self.related:
518 field = target._fields[name]
519 field.setup_full(target)
520 target = target[name]
521
522 self.related_field = field
523
524 # check type consistency
525 if self.type != field.type:
526 raise TypeError("Type of related field %s is inconsistent with %s" % (self, field))
527
528 # determine dependencies, compute, inverse, and search
529 self.depends = ('.'.join(self.related),)
530 self.compute = self._compute_related
531 if not (self.readonly or field.readonly):
532 self.inverse = self._inverse_related
533 if field._description_searchable:
534 # allow searching on self only if the related field is searchable
535 self.search = self._search_related
536
537 # copy attributes from field to self (string, help, etc.)
538 for attr, prop in self.related_attrs:
539 if not getattr(self, attr):
540 setattr(self, attr, getattr(field, prop))
541
542 for attr, value in field._attrs.items():
543 if attr not in self._attrs:
544 setattr(self, attr, value)
545
546 # special case for states: copy it only for inherited fields
547 if not self.states and self.inherited:
548 self.states = field.states
549
550 # special case for inherited required fields
551 if self.inherited and field.required:
552 self.required = True
553
554 def traverse_related(self, record):
555 """ Traverse the fields of the related field `self` except for the last
556 one, and return it as a pair `(last_record, last_field)`. """
557 for name in self.related[:-1]:
558 record = record[name][:1].with_prefetch(record._prefetch)
559 return record, self.related_field
560
561 def _compute_related(self, records):
562 """ Compute the related field ``self`` on ``records``. """
563 # when related_sudo, bypass access rights checks when reading values
564 others = records.sudo() if self.related_sudo else records
565 # copy the cache of draft records into others' cache
566 if records.env != others.env:
567 copy_cache(records - records.filtered('id'), others.env)
568 #
569 # Traverse fields one by one for all records, in order to take advantage
570 # of prefetching for each field access. In order to clarify the impact
571 # of the algorithm, consider traversing 'foo.bar' for records a1 and a2,
572 # where 'foo' is already present in cache for a1, a2. Initially, both a1
573 # and a2 are marked for prefetching. As the commented code below shows,
574 # traversing all fields one record at a time will fetch 'bar' one record
575 # at a time.
576 #
577 # b1 = a1.foo # mark b1 for prefetching
578 # v1 = b1.bar # fetch/compute bar for b1
579 # b2 = a2.foo # mark b2 for prefetching
580 # v2 = b2.bar # fetch/compute bar for b2
581 #
582 # On the other hand, traversing all records one field at a time ensures
583 # maximal prefetching for each field access.
584 #
585 # b1 = a1.foo # mark b1 for prefetching
586 # b2 = a2.foo # mark b2 for prefetching
587 # v1 = b1.bar # fetch/compute bar for b1, b2
588 # v2 = b2.bar # value already in cache
589 #
590 # This difference has a major impact on performance, in particular in
591 # the case where 'bar' is a computed field that takes advantage of batch
592 # computation.
593 #
594 values = list(others)
595 for name in self.related[:-1]:
596 values = [first(value[name]) for value in values]
597 # assign final values to records
598 for record, value in pycompat.izip(records, values):
599 record[self.name] = value[self.related_field.name]
600
601 def _inverse_related(self, records):
602 """ Inverse the related field ``self`` on ``records``. """
603 # store record values, otherwise they may be lost by cache invalidation!
604 record_value = {record: record[self.name] for record in records}
605 for record in records:
606 other, field = self.traverse_related(record)
607 if other:
608 other[field.name] = record_value[record]
609
610 def _search_related(self, records, operator, value):
611 """ Determine the domain to search on field ``self``. """
612 return [('.'.join(self.related), operator, value)]
613
614 # properties used by _setup_related_full() to copy values from related field
615 _related_comodel_name = property(attrgetter('comodel_name'))
616 _related_string = property(attrgetter('string'))
617 _related_help = property(attrgetter('help'))
618 _related_readonly = property(attrgetter('readonly'))
619 _related_groups = property(attrgetter('groups'))
620 _related_group_operator = property(attrgetter('group_operator'))
621
622 @property
623 def base_field(self):
624 """ Return the base field of an inherited field, or ``self``. """
625 return self.related_field.base_field if self.inherited else self
626
627 #
628 # Company-dependent fields
629 #
630
631 def _default_company_dependent(self, model):
632 return model.env['ir.property'].get(self.name, self.model_name)
633
634 def _compute_company_dependent(self, records):
635 Property = records.env['ir.property']
636 values = Property.get_multi(self.name, self.model_name, records.ids)
637 for record in records:
638 record[self.name] = values.get(record.id)
639
640 def _inverse_company_dependent(self, records):
641 Property = records.env['ir.property']
642 values = {
643 record.id: self.convert_to_write(record[self.name], record)
644 for record in records
645 }
646 Property.set_multi(self.name, self.model_name, values)
647
648 def _search_company_dependent(self, records, operator, value):
649 Property = records.env['ir.property']
650 return Property.search_multi(self.name, self.model_name, operator, value)
651
652 #
653 # Setup of field triggers
654 #
655 # The triggers of ``self`` are a collection of pairs ``(field, path)`` of
656 # fields that depend on ``self``. When ``self`` is modified, it invalidates
657 # the cache of each ``field``, and determines the records to recompute based
658 # on ``path``. See method ``modified`` below for details.
659 #
660
661 def resolve_deps(self, model):
662 """ Return the dependencies of ``self`` as tuples ``(model, field, path)``,
663 where ``path`` is an optional list of field names.
664 """
665 model0 = model
666 result = []
667
668 # add self's own dependencies
669 for dotnames in self.depends:
670 if dotnames == self.name:
671 _logger.warning("Field %s depends on itself; please fix its decorator @api.depends().", self)
672 model, path = model0, dotnames.split('.')
673 for i, fname in enumerate(path):
674 field = model._fields[fname]
675 result.append((model, field, path[:i]))
676 model = model0.env.get(field.comodel_name)
677
678 # add self's model dependencies
679 for mname, fnames in model0._depends.items():
680 model = model0.env[mname]
681 for fname in fnames:
682 field = model._fields[fname]
683 result.append((model, field, None))
684
685 # add indirect dependencies from the dependencies found above
686 for model, field, path in list(result):
687 for inv_field in model._field_inverses[field]:
688 inv_model = model0.env[inv_field.model_name]
689 inv_path = None if path is None else path + [field.name]
690 result.append((inv_model, inv_field, inv_path))
691
692 return result
693
694 def setup_triggers(self, model):
695 """ Add the necessary triggers to invalidate/recompute ``self``. """
696 for model, field, path in self.resolve_deps(model):
697 if field is not self:
698 path_str = None if path is None else ('.'.join(path) or 'id')
699 model._field_triggers.add(field, (self, path_str))
700 elif path:
701 self.recursive = True
702 model._field_triggers.add(field, (self, '.'.join(path)))
703
704 ############################################################################
705 #
706 # Field description
707 #
708
709 def get_description(self, env):
710 """ Return a dictionary that describes the field ``self``. """
711 desc = {'type': self.type}
712 for attr, prop in self.description_attrs:
713 value = getattr(self, prop)
714 if callable(value):
715 value = value(env)
716 if value is not None:
717 desc[attr] = value
718
719 return desc
720
721 # properties used by get_description()
722 _description_store = property(attrgetter('store'))
723 _description_manual = property(attrgetter('manual'))
724 _description_depends = property(attrgetter('depends'))
725 _description_related = property(attrgetter('related'))
726 _description_company_dependent = property(attrgetter('company_dependent'))
727 _description_readonly = property(attrgetter('readonly'))
728 _description_required = property(attrgetter('required'))
729 _description_states = property(attrgetter('states'))
730 _description_groups = property(attrgetter('groups'))
731 _description_change_default = property(attrgetter('change_default'))
732 _description_deprecated = property(attrgetter('deprecated'))
733
734 @property
735 def _description_searchable(self):
736 return bool(self.store or self.search)
737
738 @property
739 def _description_sortable(self):
740 return self.store or (self.inherited and self.related_field._description_sortable)
741
742 def _description_string(self, env):
743 if self.string and env.lang:
744 model_name = self.base_field.model_name
745 field_string = env['ir.translation'].get_field_string(model_name)
746 return field_string.get(self.name) or self.string
747 return self.string
748
749 def _description_help(self, env):
750 if self.help and env.lang:
751 model_name = self.base_field.model_name
752 field_help = env['ir.translation'].get_field_help(model_name)
753 return field_help.get(self.name) or self.help
754 return self.help
755
756 ############################################################################
757 #
758 # Conversion of values
759 #
760
761 def cache_key(self, record):
762 """ Return the key to get/set the value of ``self`` on ``record`` in
763 cache, the full cache key being ``(self, record.id, key)``.
764 """
765 env = record.env
766 return env if self.context_dependent else (env.cr, env.uid)
767
768 def null(self, record):
769 """ Return the null value for this field in the record format. """
770 return False
771
772 def convert_to_column(self, value, record, values=None):
773 """ Convert ``value`` from the ``write`` format to the SQL format. """
774 if value is None or value is False:
775 return None
776 return pycompat.to_native(value)
777
778 def convert_to_cache(self, value, record, validate=True):
779 """ Convert ``value`` to the cache format; ``value`` may come from an
780 assignment, or have the format of methods :meth:`BaseModel.read` or
781 :meth:`BaseModel.write`. If the value represents a recordset, it should
782 be added for prefetching on ``record``.
783
784 :param bool validate: when True, field-specific validation of ``value``
785 will be performed
786 """
787 return value
788
789 def convert_to_record(self, value, record):
790 """ Convert ``value`` from the cache format to the record format.
791 If the value represents a recordset, it should share the prefetching of
792 ``record``.
793 """
794 return value
795
796 def convert_to_read(self, value, record, use_name_get=True):
797 """ Convert ``value`` from the record format to the format returned by
798 method :meth:`BaseModel.read`.
799
800 :param bool use_name_get: when True, the value's display name will be
801 computed using :meth:`BaseModel.name_get`, if relevant for the field
802 """
803 return False if value is None else value
804
805 def convert_to_write(self, value, record):
806 """ Convert ``value`` from the record format to the format of method
807 :meth:`BaseModel.write`.
808 """
809 return self.convert_to_read(value, record)
810
811 def convert_to_onchange(self, value, record, names):
812 """ Convert ``value`` from the record format to the format returned by
813 method :meth:`BaseModel.onchange`.
814
815 :param names: a tree of field names (for relational fields only)
816 """
817 return self.convert_to_read(value, record)
818
819 def convert_to_export(self, value, record):
820 """ Convert ``value`` from the record format to the export format. """
821 if not value:
822 return ''
823 return value if record._context.get('export_raw_data') else ustr(value)
824
825 def convert_to_display_name(self, value, record):
826 """ Convert ``value`` from the record format to a suitable display name. """
827 return ustr(value)
828
829 ############################################################################
830 #
831 # Update database schema
832 #
833
834 def update_db(self, model, columns):
835 """ Update the database schema to implement this field.
836
837 :param model: an instance of the field's model
838 :param columns: a dict mapping column names to their configuration in database
839 :return: ``True`` if the field must be recomputed on existing rows
840 """
841 if not self.column_type:
842 return
843
844 column = columns.get(self.name)
845 if not column and hasattr(self, 'oldname'):
846 # column not found; check whether it exists under its old name
847 column = columns.get(self.oldname)
848 if column:
849 sql.rename_column(model._cr, model._table, self.oldname, self.name)
850
851 # create/update the column, not null constraint, indexes
852 self.update_db_column(model, column)
853 self.update_db_notnull(model, column)
854 self.update_db_index(model, column)
855
856 return not column
857
858 def update_db_column(self, model, column):
859 """ Create/update the column corresponding to ``self``.
860
861 :param model: an instance of the field's model
862 :param column: the column's configuration (dict) if it exists, or ``None``
863 """
864 if not column:
865 # the column does not exist, create it
866 sql.create_column(model._cr, model._table, self.name, self.column_type[1], self.string)
867 return
868 if column['udt_name'] == self.column_type[0]:
869 return
870 if column['udt_name'] in self.column_cast_from:
871 sql.convert_column(model._cr, model._table, self.name, self.column_type[1])
872 else:
873 newname = (self.name + '_moved{}').format
874 i = 0
875 while sql.column_exists(model._cr, model._table, newname(i)):
876 i += 1
877 if column['is_nullable'] == 'NO':
878 sql.drop_not_null(model._cr, model._table, self.name)
879 sql.rename_column(model._cr, model._table, self.name, newname(i))
880 sql.create_column(model._cr, model._table, self.name, self.column_type[1], self.string)
881
882 def update_db_notnull(self, model, column):
883 """ Add or remove the NOT NULL constraint on ``self``.
884
885 :param model: an instance of the field's model
886 :param column: the column's configuration (dict) if it exists, or ``None``
887 """
888 has_notnull = column and column['is_nullable'] == 'NO'
889
890 if not column or (self.required and not has_notnull):
891 # the column is new or it becomes required; initialize its values
892 if model._table_has_rows():
893 model._init_column(self.name)
894
895 if self.required and not has_notnull:
896 sql.set_not_null(model._cr, model._table, self.name)
897 elif not self.required and has_notnull:
898 sql.drop_not_null(model._cr, model._table, self.name)
899
900 def update_db_index(self, model, column):
901 """ Add or remove the index corresponding to ``self``.
902
903 :param model: an instance of the field's model
904 :param column: the column's configuration (dict) if it exists, or ``None``
905 """
906 indexname = '%s_%s_index' % (model._table, self.name)
907 if self.index:
908 sql.create_index(model._cr, indexname, model._table, ['"%s"' % self.name])
909 else:
910 sql.drop_index(model._cr, indexname, model._table)
911
912 ############################################################################
913 #
914 # Read from/write to database
915 #
916
917 def read(self, records):
918 """ Read the value of ``self`` on ``records``, and store it in cache. """
919 return NotImplementedError("Method read() undefined on %s" % self)
920
921 def write(self, records, value, create=False):
922 """ Write the value of ``self`` on ``records``. The ``value`` must be in
923 the format of method :meth:`BaseModel.write`.
924
925 :param create: whether ``records`` have just been created (to enable
926 some optimizations)
927 """
928 return NotImplementedError("Method write() undefined on %s" % self)
929
930 ############################################################################
931 #
932 # Descriptor methods
933 #
934
935 def __get__(self, record, owner):
936 """ return the value of field ``self`` on ``record`` """
937 if record is None:
938 return self # the field is accessed through the owner class
939
940 if record:
941 # only a single record may be accessed
942 record.ensure_one()
943 try:
944 value = record.env.cache.get(record, self)
945 except KeyError:
946 # cache miss, determine value and retrieve it
947 if record.id:
948 self.determine_value(record)
949 else:
950 self.determine_draft_value(record)
951 value = record.env.cache.get(record, self)
952 else:
953 # null record -> return the null value for this field
954 value = self.convert_to_cache(False, record, validate=False)
955
956 return self.convert_to_record(value, record)
957
958 def __set__(self, record, value):
959 """ set the value of field ``self`` on ``record`` """
960 env = record.env
961
962 # only a single record may be updated
963 record.ensure_one()
964
965 # adapt value to the cache level
966 value = self.convert_to_cache(value, record)
967
968 if env.in_draft or not record.id:
969 # determine dependent fields
970 spec = self.modified_draft(record)
971
972 # set value in cache, inverse field, and mark record as dirty
973 record.env.cache.set(record, self, value)
974 if env.in_onchange:
975 for invf in record._field_inverses[self]:
976 invf._update(record[self.name], record)
977 env.dirty[record].add(self.name)
978
979 # determine more dependent fields, and invalidate them
980 if self.relational:
981 spec += self.modified_draft(record)
982 env.cache.invalidate(spec)
983
984 else:
985 # Write to database
986 write_value = self.convert_to_write(self.convert_to_record(value, record), record)
987 record.write({self.name: write_value})
988 # Update the cache unless value contains a new record
989 if not (self.relational and not all(value)):
990 record.env.cache.set(record, self, value)
991
992 ############################################################################
993 #
994 # Computation of field values
995 #
996
997 def _compute_value(self, records):
998 """ Invoke the compute method on ``records``. """
999 # initialize the fields to their corresponding null value in cache
1000 fields = records._field_computed[self]
1001 cache = records.env.cache
1002 for field in fields:
1003 for record in records:
1004 cache.set(record, field, field.convert_to_cache(False, record, validate=False))
1005 if isinstance(self.compute, pycompat.string_types):
1006 getattr(records, self.compute)()
1007 else:
1008 self.compute(records)
1009
1010 def compute_value(self, records):
1011 """ Invoke the compute method on ``records``; the results are in cache. """
1012 fields = records._field_computed[self]
1013 with records.env.do_in_draft(), records.env.protecting(fields, records):
1014 try:
1015 self._compute_value(records)
1016 except (AccessError, MissingError):
1017 # some record is forbidden or missing, retry record by record
1018 for record in records:
1019 try:
1020 self._compute_value(record)
1021 except Exception as exc:
1022 record.env.cache.set_failed(record, [self], exc)
1023
1024 def determine_value(self, record):
1025 """ Determine the value of ``self`` for ``record``. """
1026 env = record.env
1027
1028 if self.store and not (self.compute and env.in_onchange):
1029 # this is a stored field or an old-style function field
1030 if self.compute:
1031 # this is a stored computed field, check for recomputation
1032 recs = record._recompute_check(self)
1033 if recs:
1034 # recompute the value (only in cache)
1035 self.compute_value(recs)
1036 # HACK: if result is in the wrong cache, copy values
1037 if recs.env != env:
1038 computed = record._field_computed[self]
1039 for source, target in pycompat.izip(recs, recs.with_env(env)):
1040 try:
1041 values = {f.name: source[f.name] for f in computed}
1042 target._cache.update(target._convert_to_cache(values, validate=False))
1043 except MissingError as exc:
1044 target._cache.set_failed(target._fields, exc)
1045 # the result is saved to database by BaseModel.recompute()
1046 return
1047
1048 # read the field from database
1049 record._prefetch_field(self)
1050
1051 elif self.compute:
1052 # this is either a non-stored computed field, or a stored computed
1053 # field in onchange mode
1054 if self.recursive:
1055 self.compute_value(record)
1056 else:
1057 recs = record._in_cache_without(self)
1058 recs = recs.with_prefetch(record._prefetch)
1059 self.compute_value(recs)
1060
1061 else:
1062 # this is a non-stored non-computed field
1063 record.env.cache.set(record, self, self.convert_to_cache(False, record, validate=False))
1064
1065 def determine_draft_value(self, record):
1066 """ Determine the value of ``self`` for the given draft ``record``. """
1067 if self.compute:
1068 fields = record._field_computed[self]
1069 with record.env.protecting(fields, record):
1070 self._compute_value(record)
1071 else:
1072 null = self.convert_to_cache(False, record, validate=False)
1073 record.env.cache.set_special(record, self, lambda: null)
1074
1075 def determine_inverse(self, records):
1076 """ Given the value of ``self`` on ``records``, inverse the computation. """
1077 if isinstance(self.inverse, pycompat.string_types):
1078 getattr(records, self.inverse)()
1079 else:
1080 self.inverse(records)
1081
1082 def determine_domain(self, records, operator, value):
1083 """ Return a domain representing a condition on ``self``. """
1084 if isinstance(self.search, pycompat.string_types):
1085 return getattr(records, self.search)(operator, value)
1086 else:
1087 return self.search(records, operator, value)
1088
1089 ############################################################################
1090 #
1091 # Notification when fields are modified
1092 #
1093
1094 def modified_draft(self, records):
1095 """ Same as :meth:`modified`, but in draft mode. """
1096 env = records.env
1097
1098 # invalidate the fields on the records in cache that depend on
1099 # ``records``, except fields currently being computed
1100 spec = []
1101 for field, path in records._field_triggers[self]:
1102 if not field.compute:
1103 # Note: do not invalidate non-computed fields. Such fields may
1104 # require invalidation in general (like *2many fields with
1105 # domains) but should not be invalidated in this case, because
1106 # we would simply lose their values during an onchange!
1107 continue
1108
1109 target = env[field.model_name]
1110 protected = env.protected(field)
1111 if path == 'id' and field.model_name == records._name:
1112 target = records - protected
1113 elif path and env.in_onchange:
1114 target = (env.cache.get_records(target, field) - protected).filtered(
1115 lambda rec: rec if path == 'id' else rec._mapped_cache(path) & records
1116 )
1117 else:
1118 target = env.cache.get_records(target, field) - protected
1119
1120 if target:
1121 spec.append((field, target._ids))
1122
1123 return spec
1124
1125
1126class Boolean(Field):
1127 type = 'boolean'
1128 column_type = ('bool', 'bool')
1129
1130 def convert_to_column(self, value, record, values=None):
1131 return bool(value)
1132
1133 def convert_to_cache(self, value, record, validate=True):
1134 return bool(value)
1135
1136 def convert_to_export(self, value, record):
1137 if record._context.get('export_raw_data'):
1138 return value
1139 return ustr(value)
1140
1141
1142class Integer(Field):
1143 type = 'integer'
1144 column_type = ('int4', 'int4')
1145 _slots = {
1146 'group_operator': 'sum',
1147 }
1148
1149 _description_group_operator = property(attrgetter('group_operator'))
1150
1151 def convert_to_column(self, value, record, values=None):
1152 return int(value or 0)
1153
1154 def convert_to_cache(self, value, record, validate=True):
1155 if isinstance(value, dict):
1156 # special case, when an integer field is used as inverse for a one2many
1157 return value.get('id', False)
1158 return int(value or 0)
1159
1160 def convert_to_read(self, value, record, use_name_get=True):
1161 # Integer values greater than 2^31-1 are not supported in pure XMLRPC,
1162 # so we have to pass them as floats :-(
1163 if value and value > MAXINT:
1164 return float(value)
1165 return value
1166
1167 def _update(self, records, value):
1168 # special case, when an integer field is used as inverse for a one2many
1169 cache = records.env.cache
1170 for record in records:
1171 cache.set(record, self, value.id or 0)
1172
1173 def convert_to_export(self, value, record):
1174 if value or value == 0:
1175 return value if record._context.get('export_raw_data') else ustr(value)
1176 return ''
1177
1178
1179class Float(Field):
1180 """ The precision digits are given by the attribute
1181
1182 :param digits: a pair (total, decimal), or a function taking a database
1183 cursor and returning a pair (total, decimal)
1184 """
1185 type = 'float'
1186 column_cast_from = ('int4', 'numeric', 'float8')
1187 _slots = {
1188 '_digits': None, # digits argument passed to class initializer
1189 'group_operator': 'sum',
1190 }
1191
1192 def __init__(self, string=Default, digits=Default, **kwargs):
1193 super(Float, self).__init__(string=string, _digits=digits, **kwargs)
1194
1195 @property
1196 def column_type(self):
1197 # Explicit support for "falsy" digits (0, False) to indicate a NUMERIC
1198 # field with no fixed precision. The values are saved in the database
1199 # with all significant digits.
1200 # FLOAT8 type is still the default when there is no precision because it
1201 # is faster for most operations (sums, etc.)
1202 return ('numeric', 'numeric') if self.digits is not None else \
1203 ('float8', 'double precision')
1204
1205 @property
1206 def digits(self):
1207 if callable(self._digits):
1208 with LazyCursor() as cr:
1209 return self._digits(cr)
1210 else:
1211 return self._digits
1212
1213 _related__digits = property(attrgetter('_digits'))
1214 _description_digits = property(attrgetter('digits'))
1215 _description_group_operator = property(attrgetter('group_operator'))
1216
1217 def convert_to_column(self, value, record, values=None):
1218 result = float(value or 0.0)
1219 digits = self.digits
1220 if digits:
1221 precision, scale = digits
1222 result = float_repr(float_round(result, precision_digits=scale), precision_digits=scale)
1223 return result
1224
1225 def convert_to_cache(self, value, record, validate=True):
1226 # apply rounding here, otherwise value in cache may be wrong!
1227 value = float(value or 0.0)
1228 if not validate:
1229 return value
1230 digits = self.digits
1231 return float_round(value, precision_digits=digits[1]) if digits else value
1232
1233 def convert_to_export(self, value, record):
1234 if value or value == 0.0:
1235 return value if record._context.get('export_raw_data') else ustr(value)
1236 return ''
1237
1238
1239class Monetary(Field):
1240 """ The decimal precision and currency symbol are taken from the attribute
1241
1242 :param currency_field: name of the field holding the currency this monetary
1243 field is expressed in (default: `currency_id`)
1244 """
1245 type = 'monetary'
1246 column_type = ('numeric', 'numeric')
1247 column_cast_from = ('float8',)
1248 _slots = {
1249 'currency_field': None,
1250 'group_operator': 'sum',
1251 }
1252
1253 def __init__(self, string=Default, currency_field=Default, **kwargs):
1254 super(Monetary, self).__init__(string=string, currency_field=currency_field, **kwargs)
1255
1256 _related_currency_field = property(attrgetter('currency_field'))
1257 _description_currency_field = property(attrgetter('currency_field'))
1258 _description_group_operator = property(attrgetter('group_operator'))
1259
1260 def _setup_regular_full(self, model):
1261 super(Monetary, self)._setup_regular_full(model)
1262 if not self.currency_field:
1263 # pick a default, trying in order: 'currency_id', 'x_currency_id'
1264 if 'currency_id' in model._fields:
1265 self.currency_field = 'currency_id'
1266 elif 'x_currency_id' in model._fields:
1267 self.currency_field = 'x_currency_id'
1268 assert self.currency_field in model._fields, \
1269 "Field %s with unknown currency_field %r" % (self, self.currency_field)
1270
1271 def convert_to_column(self, value, record, values=None):
1272 # retrieve currency from values or record
1273 if values and self.currency_field in values:
1274 field = record._fields[self.currency_field]
1275 currency = field.convert_to_cache(values[self.currency_field], record)
1276 currency = field.convert_to_record(currency, record)
1277 else:
1278 # Note: this is wrong if 'record' is several records with different
1279 # currencies, which is functional nonsense and should not happen
1280 currency = record[:1][self.currency_field]
1281
1282 value = float(value or 0.0)
1283 if currency:
1284 return float_repr(currency.round(value), currency.decimal_places)
1285 return value
1286
1287 def convert_to_cache(self, value, record, validate=True):
1288 # cache format: float
1289 value = float(value or 0.0)
1290 if validate and record[self.currency_field]:
1291 # FIXME @rco-odoo: currency may not be already initialized if it is
1292 # a function or related field!
1293 value = record[self.currency_field].round(value)
1294 return value
1295
1296 def convert_to_read(self, value, record, use_name_get=True):
1297 return value
1298
1299 def convert_to_write(self, value, record):
1300 return value
1301
1302
1303class _String(Field):
1304 """ Abstract class for string fields. """
1305 _slots = {
1306 'translate': False, # whether the field is translated
1307 }
1308
1309 def __init__(self, string=Default, **kwargs):
1310 # translate is either True, False, or a callable
1311 if 'translate' in kwargs and not callable(kwargs['translate']):
1312 kwargs['translate'] = bool(kwargs['translate'])
1313 super(_String, self).__init__(string=string, **kwargs)
1314
1315 _related_translate = property(attrgetter('translate'))
1316
1317 def _description_translate(self, env):
1318 return bool(self.translate)
1319
1320 def get_trans_terms(self, value):
1321 """ Return the sequence of terms to translate found in `value`. """
1322 if not callable(self.translate):
1323 return [value] if value else []
1324 terms = []
1325 self.translate(terms.append, value)
1326 return terms
1327
1328 def get_trans_func(self, records):
1329 """ Return a translation function `translate` for `self` on the given
1330 records; the function call `translate(record_id, value)` translates the
1331 field value to the language given by the environment of `records`.
1332 """
1333 if callable(self.translate):
1334 rec_src_trans = records.env['ir.translation']._get_terms_translations(self, records)
1335
1336 def translate(record_id, value):
1337 src_trans = rec_src_trans[record_id]
1338 return self.translate(src_trans.get, value)
1339
1340 else:
1341 rec_trans = records.env['ir.translation']._get_ids(
1342 '%s,%s' % (self.model_name, self.name), 'model', records.env.lang, records.ids)
1343
1344 def translate(record_id, value):
1345 return rec_trans.get(record_id) or value
1346
1347 return translate
1348
1349 def check_trans_value(self, value):
1350 """ Check and possibly sanitize the translated term `value`. """
1351 if callable(self.translate):
1352 # do a "no-translation" to sanitize the value
1353 callback = lambda term: None
1354 return self.translate(callback, value)
1355 else:
1356 return value
1357
1358
1359class Char(_String):
1360 """ Basic string field, can be length-limited, usually displayed as a
1361 single-line string in clients.
1362
1363 :param int size: the maximum size of values stored for that field
1364
1365 :param translate: enable the translation of the field's values; use
1366 ``translate=True`` to translate field values as a whole; ``translate``
1367 may also be a callable such that ``translate(callback, value)``
1368 translates ``value`` by using ``callback(term)`` to retrieve the
1369 translation of terms.
1370 """
1371 type = 'char'
1372 column_cast_from = ('text',)
1373 _slots = {
1374 'size': None, # maximum size of values (deprecated)
1375 }
1376
1377 @property
1378 def column_type(self):
1379 return ('varchar', pg_varchar(self.size))
1380
1381 def update_db_column(self, model, column):
1382 if (
1383 column and column['udt_name'] == 'varchar' and column['character_maximum_length'] and
1384 (self.size is None or column['character_maximum_length'] < self.size)
1385 ):
1386 # the column's varchar size does not match self.size; convert it
1387 sql.convert_column(model._cr, model._table, self.name, self.column_type[1])
1388 super(Char, self).update_db_column(model, column)
1389
1390 _related_size = property(attrgetter('size'))
1391 _description_size = property(attrgetter('size'))
1392
1393 def _setup_regular_base(self, model):
1394 super(Char, self)._setup_regular_base(model)
1395 assert self.size is None or isinstance(self.size, int), \
1396 "Char field %s with non-integer size %r" % (self, self.size)
1397
1398 def convert_to_column(self, value, record, values=None):
1399 if value is None or value is False:
1400 return None
1401 # we need to convert the string to a unicode object to be able
1402 # to evaluate its length (and possibly truncate it) reliably
1403 return pycompat.to_text(value)[:self.size]
1404
1405 def convert_to_cache(self, value, record, validate=True):
1406 if value is None or value is False:
1407 return False
1408 return pycompat.to_text(value)[:self.size]
1409
1410
1411class Text(_String):
1412 """ Very similar to :class:`~.Char` but used for longer contents, does not
1413 have a size and usually displayed as a multiline text box.
1414
1415 :param translate: enable the translation of the field's values; use
1416 ``translate=True`` to translate field values as a whole; ``translate``
1417 may also be a callable such that ``translate(callback, value)``
1418 translates ``value`` by using ``callback(term)`` to retrieve the
1419 translation of terms.
1420 """
1421 type = 'text'
1422 column_type = ('text', 'text')
1423 column_cast_from = ('varchar',)
1424
1425 def convert_to_cache(self, value, record, validate=True):
1426 if value is None or value is False:
1427 return False
1428 return ustr(value)
1429
1430
1431class Html(_String):
1432 type = 'html'
1433 column_type = ('text', 'text')
1434 _slots = {
1435 'sanitize': True, # whether value must be sanitized
1436 'sanitize_tags': True, # whether to sanitize tags (only a white list of attributes is accepted)
1437 'sanitize_attributes': True, # whether to sanitize attributes (only a white list of attributes is accepted)
1438 'sanitize_style': False, # whether to sanitize style attributes
1439 'strip_style': False, # whether to strip style attributes (removed and therefore not sanitized)
1440 'strip_classes': False, # whether to strip classes attributes
1441 }
1442
1443 def _setup_attrs(self, model, name):
1444 super(Html, self)._setup_attrs(model, name)
1445 # Translated sanitized html fields must use html_translate or a callable.
1446 if self.translate is True and self.sanitize:
1447 self.translate = html_translate
1448
1449 _related_sanitize = property(attrgetter('sanitize'))
1450 _related_sanitize_tags = property(attrgetter('sanitize_tags'))
1451 _related_sanitize_attributes = property(attrgetter('sanitize_attributes'))
1452 _related_sanitize_style = property(attrgetter('sanitize_style'))
1453 _related_strip_style = property(attrgetter('strip_style'))
1454 _related_strip_classes = property(attrgetter('strip_classes'))
1455
1456 _description_sanitize = property(attrgetter('sanitize'))
1457 _description_sanitize_tags = property(attrgetter('sanitize_tags'))
1458 _description_sanitize_attributes = property(attrgetter('sanitize_attributes'))
1459 _description_sanitize_style = property(attrgetter('sanitize_style'))
1460 _description_strip_style = property(attrgetter('strip_style'))
1461 _description_strip_classes = property(attrgetter('strip_classes'))
1462
1463 def convert_to_column(self, value, record, values=None):
1464 if value is None or value is False:
1465 return None
1466 if self.sanitize:
1467 return html_sanitize(
1468 value, silent=True,
1469 sanitize_tags=self.sanitize_tags,
1470 sanitize_attributes=self.sanitize_attributes,
1471 sanitize_style=self.sanitize_style,
1472 strip_style=self.strip_style,
1473 strip_classes=self.strip_classes)
1474 return value
1475
1476 def convert_to_cache(self, value, record, validate=True):
1477 if value is None or value is False:
1478 return False
1479 if validate and self.sanitize:
1480 return html_sanitize(
1481 value, silent=True,
1482 sanitize_tags=self.sanitize_tags,
1483 sanitize_attributes=self.sanitize_attributes,
1484 sanitize_style=self.sanitize_style,
1485 strip_style=self.strip_style,
1486 strip_classes=self.strip_classes)
1487 return value
1488
1489
1490class Date(Field):
1491 type = 'date'
1492 column_type = ('date', 'date')
1493 column_cast_from = ('timestamp',)
1494
1495 @staticmethod
1496 def today(*args):
1497 """ Return the current day in the format expected by the ORM.
1498 This function may be used to compute default values.
1499 """
1500 return date.today().strftime(DATE_FORMAT)
1501
1502 @staticmethod
1503 def context_today(record, timestamp=None):
1504 """ Return the current date as seen in the client's timezone in a format
1505 fit for date fields. This method may be used to compute default
1506 values.
1507
1508 :param datetime timestamp: optional datetime value to use instead of
1509 the current date and time (must be a datetime, regular dates
1510 can't be converted between timezones.)
1511 :rtype: str
1512 """
1513 today = timestamp or datetime.now()
1514 context_today = None
1515 tz_name = record._context.get('tz') or record.env.user.tz
1516 if tz_name:
1517 try:
1518 today_utc = pytz.timezone('UTC').localize(today, is_dst=False) # UTC = no DST
1519 context_today = today_utc.astimezone(pytz.timezone(tz_name))
1520 except Exception:
1521 _logger.debug("failed to compute context/client-specific today date, using UTC value for `today`",
1522 exc_info=True)
1523 return (context_today or today).strftime(DATE_FORMAT)
1524
1525 @staticmethod
1526 def from_string(value):
1527 """ Convert an ORM ``value`` into a :class:`date` value. """
1528 if not value:
1529 return None
1530 value = value[:DATE_LENGTH]
1531 return datetime.strptime(value, DATE_FORMAT).date()
1532
1533 @staticmethod
1534 def to_string(value):
1535 """ Convert a :class:`date` value into the format expected by the ORM. """
1536 return value.strftime(DATE_FORMAT) if value else False
1537
1538 def convert_to_cache(self, value, record, validate=True):
1539 if not value:
1540 return False
1541 if isinstance(value, pycompat.string_types):
1542 if validate:
1543 # force parsing for validation
1544 self.from_string(value)
1545 return value[:DATE_LENGTH]
1546 return self.to_string(value)
1547
1548 def convert_to_export(self, value, record):
1549 if not value:
1550 return ''
1551 return self.from_string(value) if record._context.get('export_raw_data') else ustr(value)
1552
1553
1554class Datetime(Field):
1555 type = 'datetime'
1556 column_type = ('timestamp', 'timestamp')
1557 column_cast_from = ('date',)
1558
1559 @staticmethod
1560 def now(*args):
1561 """ Return the current day and time in the format expected by the ORM.
1562 This function may be used to compute default values.
1563 """
1564 return datetime.now().strftime(DATETIME_FORMAT)
1565
1566 @staticmethod
1567 def context_timestamp(record, timestamp):
1568 """Returns the given timestamp converted to the client's timezone.
1569 This method is *not* meant for use as a default initializer,
1570 because datetime fields are automatically converted upon
1571 display on client side. For default values :meth:`fields.datetime.now`
1572 should be used instead.
1573
1574 :param datetime timestamp: naive datetime value (expressed in UTC)
1575 to be converted to the client timezone
1576 :rtype: datetime
1577 :return: timestamp converted to timezone-aware datetime in context
1578 timezone
1579 """
1580 assert isinstance(timestamp, datetime), 'Datetime instance expected'
1581 tz_name = record._context.get('tz') or record.env.user.tz
1582 utc_timestamp = pytz.utc.localize(timestamp, is_dst=False) # UTC = no DST
1583 if tz_name:
1584 try:
1585 context_tz = pytz.timezone(tz_name)
1586 return utc_timestamp.astimezone(context_tz)
1587 except Exception:
1588 _logger.debug("failed to compute context/client-specific timestamp, "
1589 "using the UTC value",
1590 exc_info=True)
1591 return utc_timestamp
1592
1593 @staticmethod
1594 def from_string(value):
1595 """ Convert an ORM ``value`` into a :class:`datetime` value. """
1596 if not value:
1597 return None
1598 value = value[:DATETIME_LENGTH]
1599 if len(value) == DATE_LENGTH:
1600 value += " 00:00:00"
1601 return datetime.strptime(value, DATETIME_FORMAT)
1602
1603 @staticmethod
1604 def to_string(value):
1605 """ Convert a :class:`datetime` value into the format expected by the ORM. """
1606 return value.strftime(DATETIME_FORMAT) if value else False
1607
1608 def convert_to_cache(self, value, record, validate=True):
1609 if not value:
1610 return False
1611 if isinstance(value, pycompat.string_types):
1612 if validate:
1613 # force parsing for validation
1614 self.from_string(value)
1615 value = value[:DATETIME_LENGTH]
1616 if len(value) == DATE_LENGTH:
1617 value += " 00:00:00"
1618 return value
1619 return self.to_string(value)
1620
1621 def convert_to_export(self, value, record):
1622 if not value:
1623 return ''
1624 return self.from_string(value) if record._context.get('export_raw_data') else ustr(value)
1625
1626 def convert_to_display_name(self, value, record):
1627 assert record, 'Record expected'
1628 return Datetime.to_string(Datetime.context_timestamp(record, Datetime.from_string(value)))
1629
1630# http://initd.org/psycopg/docs/usage.html#binary-adaptation
1631# Received data is returned as buffer (in Python 2) or memoryview (in Python 3).
1632_BINARY = memoryview
1633if pycompat.PY2:
1634 _BINARY = buffer #pylint: disable=buffer-builtin
1635
1636class Binary(Field):
1637 type = 'binary'
1638 _slots = {
1639 'prefetch': False, # not prefetched by default
1640 'context_dependent': True, # depends on context (content or size)
1641 'attachment': False, # whether value is stored in attachment
1642 }
1643
1644 @property
1645 def column_type(self):
1646 return None if self.attachment else ('bytea', 'bytea')
1647
1648 _description_attachment = property(attrgetter('attachment'))
1649
1650 def convert_to_column(self, value, record, values=None):
1651 # Binary values may be byte strings (python 2.6 byte array), but
1652 # the legacy OpenERP convention is to transfer and store binaries
1653 # as base64-encoded strings. The base64 string may be provided as a
1654 # unicode in some circumstances, hence the str() cast here.
1655 # This str() coercion will only work for pure ASCII unicode strings,
1656 # on purpose - non base64 data must be passed as a 8bit byte strings.
1657 if not value:
1658 return None
1659 if isinstance(value, bytes):
1660 return psycopg2.Binary(value)
1661 return psycopg2.Binary(pycompat.text_type(value).encode('ascii'))
1662
1663 def convert_to_cache(self, value, record, validate=True):
1664 if isinstance(value, _BINARY):
1665 return bytes(value)
1666 if isinstance(value, pycompat.integer_types) and \
1667 (record._context.get('bin_size') or
1668 record._context.get('bin_size_' + self.name)):
1669 # If the client requests only the size of the field, we return that
1670 # instead of the content. Presumably a separate request will be done
1671 # to read the actual content, if necessary.
1672 return human_size(value)
1673 return value
1674
1675 def read(self, records):
1676 # values are stored in attachments, retrieve them
1677 assert self.attachment
1678 domain = [
1679 ('res_model', '=', records._name),
1680 ('res_field', '=', self.name),
1681 ('res_id', 'in', records.ids),
1682 ]
1683 # Note: the 'bin_size' flag is handled by the field 'datas' itself
1684 data = {att.res_id: att.datas
1685 for att in records.env['ir.attachment'].sudo().search(domain)}
1686 cache = records.env.cache
1687 for record in records:
1688 cache.set(record, self, data.get(record.id, False))
1689
1690 def write(self, records, value, create=False):
1691 # retrieve the attachments that stores the value, and adapt them
1692 assert self.attachment
1693 if create:
1694 atts = records.env['ir.attachment'].sudo()
1695 else:
1696 atts = records.env['ir.attachment'].sudo().search([
1697 ('res_model', '=', records._name),
1698 ('res_field', '=', self.name),
1699 ('res_id', 'in', records.ids),
1700 ])
1701 with records.env.norecompute():
1702 if value:
1703 # update the existing attachments
1704 atts.write({'datas': value})
1705 # create the missing attachments
1706 for record in (records - records.browse(atts.mapped('res_id'))):
1707 atts.create({
1708 'name': self.name,
1709 'res_model': record._name,
1710 'res_field': self.name,
1711 'res_id': record.id,
1712 'type': 'binary',
1713 'datas': value,
1714 })
1715 else:
1716 atts.unlink()
1717
1718
1719class Selection(Field):
1720 """
1721 :param selection: specifies the possible values for this field.
1722 It is given as either a list of pairs (``value``, ``string``), or a
1723 model method, or a method name.
1724 :param selection_add: provides an extension of the selection in the case
1725 of an overridden field. It is a list of pairs (``value``, ``string``).
1726
1727 The attribute ``selection`` is mandatory except in the case of
1728 :ref:`related fields <field-related>` or :ref:`field extensions
1729 <field-incremental-definition>`.
1730 """
1731 type = 'selection'
1732 _slots = {
1733 'selection': None, # [(value, string), ...], function or method name
1734 }
1735
1736 def __init__(self, selection=Default, string=Default, **kwargs):
1737 super(Selection, self).__init__(selection=selection, string=string, **kwargs)
1738
1739 @property
1740 def column_type(self):
1741 if (self.selection and
1742 isinstance(self.selection, list) and
1743 isinstance(self.selection[0][0], int)):
1744 return ('int4', 'integer')
1745 else:
1746 return ('varchar', pg_varchar())
1747
1748 def _setup_regular_base(self, model):
1749 super(Selection, self)._setup_regular_base(model)
1750 assert self.selection is not None, "Field %s without selection" % self
1751
1752 def _setup_related_full(self, model):
1753 super(Selection, self)._setup_related_full(model)
1754 # selection must be computed on related field
1755 field = self.related_field
1756 self.selection = lambda model: field._description_selection(model.env)
1757
1758 def _setup_attrs(self, model, name):
1759 super(Selection, self)._setup_attrs(model, name)
1760 # determine selection (applying 'selection_add' extensions)
1761 for field in reversed(resolve_mro(model, name, self._can_setup_from)):
1762 # We cannot use field.selection or field.selection_add here
1763 # because those attributes are overridden by ``_setup_attrs``.
1764 if 'selection' in field.args:
1765 self.selection = field.args['selection']
1766 if 'selection_add' in field.args:
1767 # use an OrderedDict to update existing values
1768 selection_add = field.args['selection_add']
1769 self.selection = list(OrderedDict(self.selection + selection_add).items())
1770
1771 def _description_selection(self, env):
1772 """ return the selection list (pairs (value, label)); labels are
1773 translated according to context language
1774 """
1775 selection = self.selection
1776 if isinstance(selection, pycompat.string_types):
1777 return getattr(env[self.model_name], selection)()
1778 if callable(selection):
1779 return selection(env[self.model_name])
1780
1781 # translate selection labels
1782 if env.lang:
1783 name = "%s,%s" % (self.model_name, self.name)
1784 translate = partial(
1785 env['ir.translation']._get_source, name, 'selection', env.lang)
1786 return [(value, translate(label) if label else label) for value, label in selection]
1787 else:
1788 return selection
1789
1790 def get_values(self, env):
1791 """ return a list of the possible values """
1792 selection = self.selection
1793 if isinstance(selection, pycompat.string_types):
1794 selection = getattr(env[self.model_name], selection)()
1795 elif callable(selection):
1796 selection = selection(env[self.model_name])
1797 return [value for value, _ in selection]
1798
1799 def convert_to_cache(self, value, record, validate=True):
1800 if not validate:
1801 return value or False
1802 if value in self.get_values(record.env):
1803 return value
1804 elif not value:
1805 return False
1806 raise ValueError("Wrong value for %s: %r" % (self, value))
1807
1808 def convert_to_export(self, value, record):
1809 if not isinstance(self.selection, list):
1810 # FIXME: this reproduces an existing buggy behavior!
1811 return value if value else ''
1812 for item in self._description_selection(record.env):
1813 if item[0] == value:
1814 return item[1]
1815 return False
1816
1817
1818class Reference(Selection):
1819 type = 'reference'
1820
1821 @property
1822 def column_type(self):
1823 return ('varchar', pg_varchar())
1824
1825 def convert_to_cache(self, value, record, validate=True):
1826 # cache format: (res_model, res_id) or False
1827 def process(res_model, res_id):
1828 record._prefetch[res_model].add(res_id)
1829 return (res_model, res_id)
1830
1831 if isinstance(value, BaseModel):
1832 if not validate or (value._name in self.get_values(record.env) and len(value) <= 1):
1833 return process(value._name, value.id) if value else False
1834 elif isinstance(value, pycompat.string_types):
1835 res_model, res_id = value.split(',')
1836 if record.env[res_model].browse(int(res_id)).exists():
1837 return process(res_model, int(res_id))
1838 else:
1839 return False
1840 elif not value:
1841 return False
1842 raise ValueError("Wrong value for %s: %r" % (self, value))
1843
1844 def convert_to_record(self, value, record):
1845 return value and record.env[value[0]].browse([value[1]], record._prefetch)
1846
1847 def convert_to_read(self, value, record, use_name_get=True):
1848 return "%s,%s" % (value._name, value.id) if value else False
1849
1850 def convert_to_export(self, value, record):
1851 return value.name_get()[0][1] if value else ''
1852
1853 def convert_to_display_name(self, value, record):
1854 return ustr(value and value.display_name)
1855
1856
1857class _Relational(Field):
1858 """ Abstract class for relational fields. """
1859 relational = True
1860 _slots = {
1861 'domain': [], # domain for searching values
1862 'context': {}, # context for searching values
1863 }
1864
1865 def _setup_regular_base(self, model):
1866 super(_Relational, self)._setup_regular_base(model)
1867 if self.comodel_name not in model.pool:
1868 _logger.warning("Field %s with unknown comodel_name %r", self, self.comodel_name)
1869 self.comodel_name = '_unknown'
1870
1871 @property
1872 def _related_domain(self):
1873 if callable(self.domain):
1874 # will be called with another model than self's
1875 return lambda recs: self.domain(recs.env[self.model_name])
1876 else:
1877 # maybe not correct if domain is a string...
1878 return self.domain
1879
1880 _related_context = property(attrgetter('context'))
1881
1882 _description_relation = property(attrgetter('comodel_name'))
1883 _description_context = property(attrgetter('context'))
1884
1885 def _description_domain(self, env):
1886 return self.domain(env[self.model_name]) if callable(self.domain) else self.domain
1887
1888 def null(self, record):
1889 return record.env[self.comodel_name]
1890
1891
1892class Many2one(_Relational):
1893 """ The value of such a field is a recordset of size 0 (no
1894 record) or 1 (a single record).
1895
1896 :param comodel_name: name of the target model (string)
1897
1898 :param domain: an optional domain to set on candidate values on the
1899 client side (domain or string)
1900
1901 :param context: an optional context to use on the client side when
1902 handling that field (dictionary)
1903
1904 :param ondelete: what to do when the referred record is deleted;
1905 possible values are: ``'set null'``, ``'restrict'``, ``'cascade'``
1906
1907 :param auto_join: whether JOINs are generated upon search through that
1908 field (boolean, by default ``False``)
1909
1910 :param delegate: set it to ``True`` to make fields of the target model
1911 accessible from the current model (corresponds to ``_inherits``)
1912
1913 The attribute ``comodel_name`` is mandatory except in the case of related
1914 fields or field extensions.
1915 """
1916 type = 'many2one'
1917 column_type = ('int4', 'int4')
1918 _slots = {
1919 'ondelete': 'set null', # what to do when value is deleted
1920 'auto_join': False, # whether joins are generated upon search
1921 'delegate': False, # whether self implements delegation
1922 }
1923
1924 def __init__(self, comodel_name=Default, string=Default, **kwargs):
1925 super(Many2one, self).__init__(comodel_name=comodel_name, string=string, **kwargs)
1926
1927 def _setup_attrs(self, model, name):
1928 super(Many2one, self)._setup_attrs(model, name)
1929 # determine self.delegate
1930 if not self.delegate:
1931 self.delegate = name in model._inherits.values()
1932
1933 def update_db(self, model, columns):
1934 comodel = model.env[self.comodel_name]
1935 if not model.is_transient() and comodel.is_transient():
1936 raise ValueError('Many2one %s from Model to TransientModel is forbidden' % self)
1937 if model.is_transient() and not comodel.is_transient():
1938 # Many2one relations from TransientModel Model are annoying because
1939 # they can block deletion due to foreign keys. So unless stated
1940 # otherwise, we default them to ondelete='cascade'.
1941 self.ondelete = self.ondelete or 'cascade'
1942 return super(Many2one, self).update_db(model, columns)
1943
1944 def update_db_column(self, model, column):
1945 super(Many2one, self).update_db_column(model, column)
1946 model.pool.post_init(self.update_db_foreign_key, model, column)
1947
1948 def update_db_foreign_key(self, model, column):
1949 comodel = model.env[self.comodel_name]
1950 # ir_actions is inherited, so foreign key doesn't work on it
1951 if not comodel._auto or comodel._table == 'ir_actions':
1952 return
1953 # create/update the foreign key, and reflect it in 'ir.model.constraint'
1954 process = sql.fix_foreign_key if column else sql.add_foreign_key
1955 new = process(model._cr, model._table, self.name, comodel._table, 'id', self.ondelete or 'set null')
1956 if new:
1957 conname = '%s_%s_fkey' % (model._table, self.name)
1958 model.env['ir.model.constraint']._reflect_constraint(model, conname, 'f', None, self._module)
1959
1960 def _update(self, records, value):
1961 """ Update the cached value of ``self`` for ``records`` with ``value``. """
1962 cache = records.env.cache
1963 for record in records:
1964 cache.set(record, self, self.convert_to_cache(value, record, validate=False))
1965
1966 def convert_to_column(self, value, record, values=None):
1967 return value or None
1968
1969 def convert_to_cache(self, value, record, validate=True):
1970 # cache format: tuple(ids)
1971 def process(ids):
1972 return record._prefetch[self.comodel_name].update(ids) or ids
1973
1974 if type(value) in IdType:
1975 return process((value,))
1976 elif isinstance(value, BaseModel):
1977 if not validate or (value._name == self.comodel_name and len(value) <= 1):
1978 return process(value._ids)
1979 raise ValueError("Wrong value for %s: %r" % (self, value))
1980 elif isinstance(value, tuple):
1981 # value is either a pair (id, name), or a tuple of ids
1982 return process(value[:1])
1983 elif isinstance(value, dict):
1984 return process(record.env[self.comodel_name].new(value)._ids)
1985 else:
1986 return ()
1987
1988 def convert_to_record(self, value, record):
1989 return record.env[self.comodel_name]._browse(value, record.env, record._prefetch)
1990
1991 def convert_to_read(self, value, record, use_name_get=True):
1992 if use_name_get and value:
1993 # evaluate name_get() as superuser, because the visibility of a
1994 # many2one field value (id and name) depends on the current record's
1995 # access rights, and not the value's access rights.
1996 try:
1997 # performance: value.sudo() prefetches the same records as value
1998 return (value.id, value.sudo().display_name)
1999 except MissingError:
2000 # Should not happen, unless the foreign key is missing.
2001 return False
2002 else:
2003 return value.id
2004
2005 def convert_to_write(self, value, record):
2006 return value.id
2007
2008 def convert_to_export(self, value, record):
2009 return value.name_get()[0][1] if value else ''
2010
2011 def convert_to_display_name(self, value, record):
2012 return ustr(value.display_name)
2013
2014 def convert_to_onchange(self, value, record, names):
2015 if not value.id:
2016 return False
2017 return super(Many2one, self).convert_to_onchange(value, record, names)
2018
2019
2020
2021class _RelationalMulti(_Relational):
2022 """ Abstract class for relational fields *2many. """
2023 _slots = {
2024 'context_dependent': True, # depends on context (active_test)
2025 }
2026
2027 def _update(self, records, value):
2028 """ Update the cached value of ``self`` for ``records`` with ``value``. """
2029 cache = records.env.cache
2030 for record in records:
2031 if cache.contains(record, self):
2032 val = self.convert_to_cache(record[self.name] | value, record, validate=False)
2033 cache.set(record, self, val)
2034 else:
2035 cache.set_special(record, self, self._update_getter(record, value))
2036
2037 def _update_getter(self, record, value):
2038 def getter():
2039 # determine the current field's value, and update it in cache only
2040 cache = record.env.cache
2041 cache.remove(record, self)
2042 val = self.convert_to_cache(record[self.name] | value, record, validate=False)
2043 cache.set(record, self, val)
2044 return val
2045 return getter
2046
2047 def convert_to_cache(self, value, record, validate=True):
2048 # cache format: tuple(ids)
2049 def process(ids):
2050 return record._prefetch[self.comodel_name].update(ids) or ids
2051
2052 if isinstance(value, BaseModel):
2053 if not validate or (value._name == self.comodel_name):
2054 return process(value._ids)
2055 elif isinstance(value, (list, tuple)):
2056 # value is a list/tuple of commands, dicts or record ids
2057 comodel = record.env[self.comodel_name]
2058 # determine the value ids; by convention empty on new records
2059 ids = OrderedSet(record[self.name].ids if record.id else ())
2060 # modify ids with the commands
2061 for command in value:
2062 if isinstance(command, (tuple, list)):
2063 if command[0] == 0:
2064 ids.add(comodel.new(command[2], command[1]).id)
2065 elif command[0] == 1:
2066 comodel.browse(command[1]).update(command[2])
2067 ids.add(command[1])
2068 elif command[0] == 2:
2069 # note: the record will be deleted by write()
2070 ids.discard(command[1])
2071 elif command[0] == 3:
2072 ids.discard(command[1])
2073 elif command[0] == 4:
2074 ids.add(command[1])
2075 elif command[0] == 5:
2076 ids.clear()
2077 elif command[0] == 6:
2078 ids = OrderedSet(command[2])
2079 elif isinstance(command, dict):
2080 ids.add(comodel.new(command).id)
2081 else:
2082 ids.add(command)
2083 # return result as a tuple
2084 return process(tuple(ids))
2085 elif not value:
2086 return ()
2087 raise ValueError("Wrong value for %s: %s" % (self, value))
2088
2089 def convert_to_record(self, value, record):
2090 return record.env[self.comodel_name]._browse(value, record.env, record._prefetch)
2091
2092 def convert_to_read(self, value, record, use_name_get=True):
2093 return value.ids
2094
2095 def convert_to_write(self, value, record):
2096 # make result with new and existing records
2097 result = [(6, 0, [])]
2098 for record in value:
2099 if not record.id:
2100 values = {name: record[name] for name in record._cache}
2101 values = record._convert_to_write(values)
2102 result.append((0, 0, values))
2103 elif record._is_dirty():
2104 values = {name: record[name] for name in record._get_dirty()}
2105 values = record._convert_to_write(values)
2106 result.append((1, record.id, values))
2107 else:
2108 result[0][2].append(record.id)
2109 return result
2110
2111 def convert_to_onchange(self, value, record, names):
2112 # return the recordset value as a list of commands; the commands may
2113 # give all fields values, the client is responsible for figuring out
2114 # which fields are actually dirty
2115 vals = {record: {} for record in value}
2116 for name, subnames in names.items():
2117 if name == 'id':
2118 continue
2119 field = value._fields[name]
2120 # read all values before converting them (better prefetching)
2121 rec_vals = [(rec, rec[name]) for rec in value]
2122 for rec, val in rec_vals:
2123 vals[rec][name] = field.convert_to_onchange(val, rec, subnames)
2124
2125 result = [(5,)]
2126 for record in value:
2127 if not record.id:
2128 result.append((0, record.id.ref or 0, vals[record]))
2129 elif vals[record]:
2130 result.append((1, record.id, vals[record]))
2131 else:
2132 result.append((4, record.id))
2133 return result
2134
2135 def convert_to_export(self, value, record):
2136 return ','.join(name for id, name in value.name_get()) if value else ''
2137
2138 def convert_to_display_name(self, value, record):
2139 raise NotImplementedError()
2140
2141 def _compute_related(self, records):
2142 """ Compute the related field ``self`` on ``records``. """
2143 super(_RelationalMulti, self)._compute_related(records)
2144 if self.related_sudo:
2145 # determine which records in the relation are actually accessible
2146 target = records.mapped(self.name)
2147 target_ids = set(target.search([('id', 'in', target.ids)]).ids)
2148 accessible = lambda target: target.id in target_ids
2149 # filter values to keep the accessible records only
2150 for record in records:
2151 record[self.name] = record[self.name].filtered(accessible)
2152
2153 def _setup_regular_base(self, model):
2154 super(_RelationalMulti, self)._setup_regular_base(model)
2155 if isinstance(self.domain, list):
2156 self.depends += tuple(
2157 self.name + '.' + arg[0]
2158 for arg in self.domain
2159 if isinstance(arg, (tuple, list)) and isinstance(arg[0], pycompat.string_types)
2160 )
2161
2162
2163class One2many(_RelationalMulti):
2164 """ One2many field; the value of such a field is the recordset of all the
2165 records in ``comodel_name`` such that the field ``inverse_name`` is equal to
2166 the current record.
2167
2168 :param comodel_name: name of the target model (string)
2169
2170 :param inverse_name: name of the inverse ``Many2one`` field in
2171 ``comodel_name`` (string)
2172
2173 :param domain: an optional domain to set on candidate values on the
2174 client side (domain or string)
2175
2176 :param context: an optional context to use on the client side when
2177 handling that field (dictionary)
2178
2179 :param auto_join: whether JOINs are generated upon search through that
2180 field (boolean, by default ``False``)
2181
2182 :param limit: optional limit to use upon read (integer)
2183
2184 The attributes ``comodel_name`` and ``inverse_name`` are mandatory except in
2185 the case of related fields or field extensions.
2186 """
2187 type = 'one2many'
2188 _slots = {
2189 'inverse_name': None, # name of the inverse field
2190 'auto_join': False, # whether joins are generated upon search
2191 'limit': None, # optional limit to use upon read
2192 'copy': False, # o2m are not copied by default
2193 }
2194
2195 def __init__(self, comodel_name=Default, inverse_name=Default, string=Default, **kwargs):
2196 super(One2many, self).__init__(
2197 comodel_name=comodel_name,
2198 inverse_name=inverse_name,
2199 string=string,
2200 **kwargs
2201 )
2202
2203 def _setup_regular_full(self, model):
2204 super(One2many, self)._setup_regular_full(model)
2205 if self.inverse_name:
2206 # link self to its inverse field and vice-versa
2207 comodel = model.env[self.comodel_name]
2208 invf = comodel._fields[self.inverse_name]
2209 # In some rare cases, a ``One2many`` field can link to ``Int`` field
2210 # (res_model/res_id pattern). Only inverse the field if this is
2211 # a ``Many2one`` field.
2212 if isinstance(invf, Many2one):
2213 model._field_inverses.add(self, invf)
2214 comodel._field_inverses.add(invf, self)
2215
2216 _description_relation_field = property(attrgetter('inverse_name'))
2217
2218 def convert_to_onchange(self, value, record, names):
2219 names = names.copy()
2220 names.pop(self.inverse_name, None)
2221 return super(One2many, self).convert_to_onchange(value, record, names)
2222
2223 def update_db(self, model, columns):
2224 if self.comodel_name in model.env:
2225 comodel = model.env[self.comodel_name]
2226 if self.inverse_name not in comodel._fields:
2227 raise UserError(_("No inverse field %r found for %r") % (self.inverse_name, self.comodel_name))
2228
2229 def read(self, records):
2230 # retrieve the lines in the comodel
2231 comodel = records.env[self.comodel_name].with_context(**self.context)
2232 inverse = self.inverse_name
2233 get_id = (lambda rec: rec.id) if comodel._fields[inverse].type == 'many2one' else int
2234 domain = self.domain(records) if callable(self.domain) else self.domain
2235 domain = domain + [(inverse, 'in', records.ids)]
2236 lines = comodel.search(domain, limit=self.limit)
2237
2238 # group lines by inverse field (without prefetching other fields)
2239 group = defaultdict(list)
2240 for line in lines.with_context(prefetch_fields=False):
2241 # line[inverse] may be a record or an integer
2242 group[get_id(line[inverse])].append(line.id)
2243
2244 # store result in cache
2245 cache = records.env.cache
2246 for record in records:
2247 cache.set(record, self, tuple(group[record.id]))
2248
2249 def write(self, records, value, create=False):
2250 comodel = records.env[self.comodel_name].with_context(**self.context)
2251 inverse = self.inverse_name
2252
2253 with records.env.norecompute():
2254 for act in (value or []):
2255 if act[0] == 0:
2256 for record in records:
2257 act[2][inverse] = record.id
2258 comodel.create(act[2])
2259 elif act[0] == 1:
2260 comodel.browse(act[1]).write(act[2])
2261 elif act[0] == 2:
2262 comodel.browse(act[1]).unlink()
2263 elif act[0] == 3:
2264 inverse_field = comodel._fields[inverse]
2265 if inverse_field.ondelete == 'cascade':
2266 comodel.browse(act[1]).unlink()
2267 else:
2268 comodel.browse(act[1]).write({inverse: False})
2269 elif act[0] == 4:
2270 record = records[-1]
2271 line = comodel.browse(act[1])
2272 line_sudo = line.sudo().with_context(prefetch_fields=False)
2273 if int(line_sudo[inverse]) != record.id:
2274 line.write({inverse: record.id})
2275 elif act[0] == 5:
2276 domain = self.domain(records) if callable(self.domain) else self.domain
2277 domain = domain + [(inverse, 'in', records.ids)]
2278 inverse_field = comodel._fields[inverse]
2279 if inverse_field.ondelete == 'cascade':
2280 comodel.search(domain).unlink()
2281 else:
2282 comodel.search(domain).write({inverse: False})
2283 elif act[0] == 6:
2284 record = records[-1]
2285 comodel.browse(act[2]).write({inverse: record.id})
2286 domain = self.domain(records) if callable(self.domain) else self.domain
2287 domain = domain + [(inverse, 'in', records.ids), ('id', 'not in', act[2] or [0])]
2288 inverse_field = comodel._fields[inverse]
2289 if inverse_field.ondelete == 'cascade':
2290 comodel.search(domain).unlink()
2291 else:
2292 comodel.search(domain).write({inverse: False})
2293
2294
2295class Many2many(_RelationalMulti):
2296 """ Many2many field; the value of such a field is the recordset.
2297
2298 :param comodel_name: name of the target model (string)
2299
2300 The attribute ``comodel_name`` is mandatory except in the case of related
2301 fields or field extensions.
2302
2303 :param relation: optional name of the table that stores the relation in
2304 the database (string)
2305
2306 :param column1: optional name of the column referring to "these" records
2307 in the table ``relation`` (string)
2308
2309 :param column2: optional name of the column referring to "those" records
2310 in the table ``relation`` (string)
2311
2312 The attributes ``relation``, ``column1`` and ``column2`` are optional. If not
2313 given, names are automatically generated from model names, provided
2314 ``model_name`` and ``comodel_name`` are different!
2315
2316 :param domain: an optional domain to set on candidate values on the
2317 client side (domain or string)
2318
2319 :param context: an optional context to use on the client side when
2320 handling that field (dictionary)
2321
2322 :param limit: optional limit to use upon read (integer)
2323
2324 """
2325 type = 'many2many'
2326 _slots = {
2327 'relation': None, # name of table
2328 'column1': None, # column of table referring to model
2329 'column2': None, # column of table referring to comodel
2330 'auto_join': False, # whether joins are generated upon search
2331 'limit': None, # optional limit to use upon read
2332 }
2333
2334 def __init__(self, comodel_name=Default, relation=Default, column1=Default,
2335 column2=Default, string=Default, **kwargs):
2336 super(Many2many, self).__init__(
2337 comodel_name=comodel_name,
2338 relation=relation,
2339 column1=column1,
2340 column2=column2,
2341 string=string,
2342 **kwargs
2343 )
2344
2345 def _setup_regular_base(self, model):
2346 super(Many2many, self)._setup_regular_base(model)
2347 if self.store:
2348 if not (self.relation and self.column1 and self.column2):
2349 # table name is based on the stable alphabetical order of tables
2350 comodel = model.env[self.comodel_name]
2351 if not self.relation:
2352 tables = sorted([model._table, comodel._table])
2353 assert tables[0] != tables[1], \
2354 "%s: Implicit/canonical naming of many2many relationship " \
2355 "table is not possible when source and destination models " \
2356 "are the same" % self
2357 self.relation = '%s_%s_rel' % tuple(tables)
2358 if not self.column1:
2359 self.column1 = '%s_id' % model._table
2360 if not self.column2:
2361 self.column2 = '%s_id' % comodel._table
2362 # check validity of table name
2363 check_pg_name(self.relation)
2364
2365 def _setup_regular_full(self, model):
2366 super(Many2many, self)._setup_regular_full(model)
2367 if self.relation:
2368 m2m = model.pool._m2m
2369 # if inverse field has already been setup, it is present in m2m
2370 invf = m2m.get((self.relation, self.column2, self.column1))
2371 if invf:
2372 comodel = model.env[self.comodel_name]
2373 model._field_inverses.add(self, invf)
2374 comodel._field_inverses.add(invf, self)
2375 else:
2376 # add self in m2m, so that its inverse field can find it
2377 m2m[(self.relation, self.column1, self.column2)] = self
2378
2379 def update_db(self, model, columns):
2380 cr = model._cr
2381 # Do not reflect relations for custom fields, as they do not belong to a
2382 # module. They are automatically removed when dropping the corresponding
2383 # 'ir.model.field'.
2384 if not self.manual:
2385 model.pool.post_init(model.env['ir.model.relation']._reflect_relation,
2386 model, self.relation, self._module)
2387 if not sql.table_exists(cr, self.relation):
2388 comodel = model.env[self.comodel_name]
2389 query = """
2390 CREATE TABLE "{rel}" ("{id1}" INTEGER NOT NULL,
2391 "{id2}" INTEGER NOT NULL,
2392 UNIQUE("{id1}","{id2}"));
2393 COMMENT ON TABLE "{rel}" IS %s;
2394 CREATE INDEX ON "{rel}" ("{id1}");
2395 CREATE INDEX ON "{rel}" ("{id2}")
2396 """.format(rel=self.relation, id1=self.column1, id2=self.column2)
2397 cr.execute(query, ['RELATION BETWEEN %s AND %s' % (model._table, comodel._table)])
2398 _schema.debug("Create table %r: m2m relation between %r and %r", self.relation, model._table, comodel._table)
2399 model.pool.post_init(self.update_db_foreign_keys, model)
2400 return True
2401
2402 def update_db_foreign_keys(self, model):
2403 """ Add the foreign keys corresponding to the field's relation table. """
2404 cr = model._cr
2405 comodel = model.env[self.comodel_name]
2406 reflect = model.env['ir.model.constraint']._reflect_constraint
2407 # create foreign key references with ondelete=cascade, unless the targets are SQL views
2408 if sql.table_kind(cr, model._table) != 'v':
2409 sql.add_foreign_key(cr, self.relation, self.column1, model._table, 'id', 'cascade')
2410 reflect(model, '%s_%s_fkey' % (self.relation, self.column1), 'f', None, self._module)
2411 if sql.table_kind(cr, comodel._table) != 'v':
2412 sql.add_foreign_key(cr, self.relation, self.column2, comodel._table, 'id', 'cascade')
2413 reflect(model, '%s_%s_fkey' % (self.relation, self.column2), 'f', None, self._module)
2414
2415 def read(self, records):
2416 comodel = records.env[self.comodel_name]
2417
2418 # String domains are supposed to be dynamic and evaluated on client-side
2419 # only (thus ignored here).
2420 domain = self.domain if isinstance(self.domain, list) else []
2421
2422 wquery = comodel._where_calc(domain)
2423 comodel._apply_ir_rules(wquery, 'read')
2424 order_by = comodel._generate_order_by(None, wquery)
2425 from_c, where_c, where_params = wquery.get_sql()
2426 query = """ SELECT {rel}.{id1}, {rel}.{id2} FROM {rel}, {from_c}
2427 WHERE {where_c} AND {rel}.{id1} IN %s AND {rel}.{id2} = {tbl}.id
2428 {order_by} {limit} OFFSET {offset}
2429 """.format(rel=self.relation, id1=self.column1, id2=self.column2,
2430 tbl=comodel._table, from_c=from_c, where_c=where_c or '1=1',
2431 limit=(' LIMIT %d' % self.limit) if self.limit else '',
2432 offset=0, order_by=order_by)
2433 where_params.append(tuple(records.ids))
2434
2435 # retrieve lines and group them by record
2436 group = defaultdict(list)
2437 records._cr.execute(query, where_params)
2438 for row in records._cr.fetchall():
2439 group[row[0]].append(row[1])
2440
2441 # store result in cache
2442 cache = records.env.cache
2443 for record in records:
2444 cache.set(record, self, tuple(group[record.id]))
2445
2446 def write(self, records, value, create=False):
2447 cr = records._cr
2448 comodel = records.env[self.comodel_name]
2449 parts = dict(rel=self.relation, id1=self.column1, id2=self.column2)
2450
2451 clear = False # whether the relation should be cleared
2452 links = {} # {id: True (link it) or False (unlink it)}
2453
2454 for act in (value or []):
2455 if not isinstance(act, (list, tuple)) or not act:
2456 continue
2457 if act[0] == 0:
2458 for record in records:
2459 links[comodel.create(act[2]).id] = True
2460 elif act[0] == 1:
2461 comodel.browse(act[1]).write(act[2])
2462 elif act[0] == 2:
2463 comodel.browse(act[1]).unlink()
2464 elif act[0] == 3:
2465 links[act[1]] = False
2466 elif act[0] == 4:
2467 links[act[1]] = True
2468 elif act[0] == 5:
2469 clear = True
2470 links.clear()
2471 elif act[0] == 6:
2472 clear = True
2473 links = dict.fromkeys(act[2], True)
2474
2475 if clear and not create:
2476 # remove all records for which user has access rights
2477 clauses, params, tables = comodel.env['ir.rule'].domain_get(comodel._name)
2478 cond = " AND ".join(clauses) if clauses else "1=1"
2479 query = """ DELETE FROM {rel} USING {tables}
2480 WHERE {rel}.{id1} IN %s AND {rel}.{id2}={table}.id AND {cond}
2481 """.format(table=comodel._table, tables=','.join(tables), cond=cond, **parts)
2482 cr.execute(query, [tuple(records.ids)] + params)
2483
2484 # link records to the ids such that links[id] = True
2485 if any(links.values()):
2486 # beware of duplicates when inserting
2487 query = """ INSERT INTO {rel} ({id1}, {id2})
2488 (SELECT a, b FROM unnest(%s) AS a, unnest(%s) AS b)
2489 EXCEPT (SELECT {id1}, {id2} FROM {rel} WHERE {id1} IN %s)
2490 """.format(**parts)
2491 ids = [id for id, flag in links.items() if flag]
2492 for sub_ids in cr.split_for_in_conditions(ids):
2493 cr.execute(query, (records.ids, list(sub_ids), tuple(records.ids)))
2494
2495 # unlink records from the ids such that links[id] = False
2496 if not all(links.values()):
2497 query = """ DELETE FROM {rel}
2498 WHERE {id1} IN %s AND {id2} IN %s
2499 """.format(**parts)
2500 ids = [id for id, flag in links.items() if not flag]
2501 for sub_ids in cr.split_for_in_conditions(ids):
2502 cr.execute(query, (tuple(records.ids), sub_ids))
2503
2504
2505class Id(Field):
2506 """ Special case for field 'id'. """
2507 type = 'integer'
2508 column_type = ('int4', 'int4')
2509 _slots = {
2510 'string': 'ID',
2511 'store': True,
2512 'readonly': True,
2513 }
2514
2515 def update_db(self, model, columns):
2516 pass # this column is created with the table
2517
2518 def __get__(self, record, owner):
2519 if record is None:
2520 return self # the field is accessed through the class owner
2521 if not record:
2522 return False
2523 return record.ensure_one()._ids[0]
2524
2525 def __set__(self, record, value):
2526 raise TypeError("field 'id' cannot be assigned")
2527
2528# imported here to avoid dependency cycle issues
2529from odoo import SUPERUSER_ID
2530from .exceptions import AccessError, MissingError, UserError
2531from .models import check_pg_name, BaseModel, IdType