diff --git a/odoo/fields.py b/odoo/fields.py index 23e2fdb5d..f33aa259a 100644 --- a/odoo/fields.py +++ b/odoo/fields.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- # Part of Odoo. See LICENSE file for full copyright and licensing details. -"""High-level objects for fields.""" +""" High-level objects for fields. """ from __future__ import annotations from collections import defaultdict @@ -30,22 +30,10 @@ from hashlib import sha256 from .models import check_property_field_value_name from .netsvc import ColoredFormatter, GREEN, RED, DEFAULT, COLOR_PATTERN from .tools import ( - float_repr, - float_round, - float_compare, - float_is_zero, - human_size, - OrderedSet, - sql, - SQL, - date_utils, - unique, - lazy_property, - image_process, - merge_sequences, - is_list_of, - html_normalize, - html_sanitize, + float_repr, float_round, float_compare, float_is_zero, human_size, + OrderedSet, sql, SQL, date_utils, unique, lazy_property, + image_process, merge_sequences, is_list_of, + html_normalize, html_sanitize, DEFAULT_SERVER_DATE_FORMAT as DATE_FORMAT, DEFAULT_SERVER_DATETIME_FORMAT as DATETIME_FORMAT, ) @@ -66,46 +54,32 @@ DATE_LENGTH = len(date.today().strftime(DATE_FORMAT)) DATETIME_LENGTH = len(datetime.now().strftime(DATETIME_FORMAT)) # hacky-ish way to prevent access to a field through the ORM (except for sudo mode) -NO_ACCESS = "." +NO_ACCESS='.' IR_MODELS = ( - "ir.model", - "ir.model.data", - "ir.model.fields", - "ir.model.fields.selection", - "ir.model.relation", - "ir.model.constraint", - "ir.module.module", + 'ir.model', 'ir.model.data', 'ir.model.fields', 'ir.model.fields.selection', + 'ir.model.relation', 'ir.model.constraint', 'ir.module.module', ) COMPANY_DEPENDENT_FIELDS = ( - "char", - "float", - "boolean", - "integer", - "text", - "many2one", - "date", - "datetime", - "selection", - "html", + 'char', 'float', 'boolean', 'integer', 'text', 'many2one', 'date', 'datetime', 'selection', 'html' ) _logger = logging.getLogger(__name__) -_schema = logging.getLogger(__name__[:-7] + ".schema") +_schema = logging.getLogger(__name__[:-7] + '.schema') NoneType = type(None) def first(records): - """Return the first record in ``records``, with the same prefetching.""" + """ Return the first record in ``records``, with the same prefetching. """ return next(iter(records)) if len(records) > 1 else records def resolve_mro(model, name, predicate): - """Return the list of successively overridden values of attribute ``name`` - in mro order on ``model`` that satisfy ``predicate``. Model registry - classes are ignored. + """ Return the list of successively overridden values of attribute ``name`` + in mro order on ``model`` that satisfy ``predicate``. Model registry + classes are ignored. """ result = [] for cls in model._model_classes: @@ -119,7 +93,7 @@ def resolve_mro(model, name, predicate): def determine(needle, records, *args): - """Simple helper for calling a method given as a string or a function. + """ Simple helper for calling a method given as a string or a function. :param needle: callable or name of method to call on ``records`` :param BaseModel records: recordset to call ``needle`` on or with @@ -132,23 +106,22 @@ def determine(needle, records, *args): raise TypeError("Determination requires a subject recordset") if isinstance(needle, str): needle = getattr(records, needle) - if needle.__name__.find("__"): + if needle.__name__.find('__'): return needle(*args) elif callable(needle): - if needle.__name__.find("__"): + if needle.__name__.find('__'): return needle(records, *args) raise TypeError("Determination requires a callable or method name") class MetaField(type): - """Metaclass for field classes.""" - + """ Metaclass for field classes. """ by_type = {} def __init__(cls, name, bases, attrs): super(MetaField, cls).__init__(name, bases, attrs) - if not hasattr(cls, "type"): + if not hasattr(cls, 'type'): return if cls.type and cls.type not in MetaField.by_type: @@ -158,16 +131,16 @@ class MetaField(type): cls.related_attrs = [] cls.description_attrs = [] for attr in dir(cls): - if attr.startswith("_related_"): + if attr.startswith('_related_'): cls.related_attrs.append((attr[9:], attr)) - elif attr.startswith("_description_"): + elif attr.startswith('_description_'): cls.description_attrs.append((attr[13:], attr)) _global_seq = iter(itertools.count()) -class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): +class Field(MetaField('DummyField', (object,), {}), typing.Generic[T]): """The field descriptor contains the field definition, and manages accesses and assignments of the corresponding field on records. The following attributes may be provided when instantiating a field: @@ -298,69 +271,67 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): .. seealso:: :ref:`Advanced fields/Related fields ` """ - type: str # type of the field (string) - relational = False # whether the field is a relational one - translate = False # whether the field is translated + type: str # type of the field (string) + relational = False # whether the field is a relational one + translate = False # whether the field is translated write_sequence = 0 # field ordering for write() # Database column type (ident, spec) for non-company-dependent fields. # Company-dependent fields are stored as jsonb (see column_type). _column_type: typing.Tuple[str, str] | None = None - args = None # the parameters given to __init__() - _module = None # the field's module name - _modules = None # modules that define this field - _setup_done = True # whether the field is completely set up - _sequence = None # absolute ordering of the field - _base_fields = () # the fields defining self, in override order - _extra_keys = () # unknown attributes set on the field - _direct = False # whether self may be used directly (shared) - _toplevel = False # whether self is on the model's registry class + args = None # the parameters given to __init__() + _module = None # the field's module name + _modules = None # modules that define this field + _setup_done = True # whether the field is completely set up + _sequence = None # absolute ordering of the field + _base_fields = () # the fields defining self, in override order + _extra_keys = () # unknown attributes set on the field + _direct = False # whether self may be used directly (shared) + _toplevel = False # whether self is on the model's registry class - automatic = False # whether the field is automatically created ("magic" field) - inherited = False # whether the field is inherited (_inherits) - inherited_field = None # the corresponding inherited field + automatic = False # whether the field is automatically created ("magic" field) + inherited = False # whether the field is inherited (_inherits) + inherited_field = None # the corresponding inherited field - name: str # name of the field - model_name: str | None = None # name of the model of this field - comodel_name: str | None = None # name of the model of values (if relational) + name: str # name of the field + model_name: str | None = None # name of the model of this field + comodel_name: str | None = None # name of the model of values (if relational) - store = True # whether the field is stored in database - index = None # how the field is indexed in database - manual = False # whether the field is a custom field - copy = True # whether the field is copied over by BaseModel.copy() - _depends = None # collection of field dependencies - _depends_context = None # collection of context key dependencies - recursive = False # whether self depends on itself - compute = None # compute(recs) computes field on recs - compute_sudo = False # whether field should be recomputed as superuser - precompute = False # whether field has to be computed before creation - inverse = None # inverse(recs) inverses field on recs - search = None # search(recs, operator, value) searches on self - related = None # sequence of field names, for related fields - company_dependent = False # whether ``self`` is company-dependent (property field) - default = None # default(recs) returns the default value + store = True # whether the field is stored in database + index = None # how the field is indexed in database + manual = False # whether the field is a custom field + copy = True # whether the field is copied over by BaseModel.copy() + _depends = None # collection of field dependencies + _depends_context = None # collection of context key dependencies + recursive = False # whether self depends on itself + compute = None # compute(recs) computes field on recs + compute_sudo = False # whether field should be recomputed as superuser + precompute = False # whether field has to be computed before creation + inverse = None # inverse(recs) inverses field on recs + search = None # search(recs, operator, value) searches on self + related = None # sequence of field names, for related fields + company_dependent = False # whether ``self`` is company-dependent (property field) + default = None # default(recs) returns the default value - string: str | None = None # field label - export_string_translation = ( - True # whether the field label translations are exported - ) - help: str | None = None # field tooltip - readonly = False # whether the field is readonly - required = False # whether the field is required - groups: str | None = None # csv list of group xml ids - change_default = False # whether the field may trigger a "user-onchange" + string: str | None = None # field label + export_string_translation = True # whether the field label translations are exported + help: str | None = None # field tooltip + readonly = False # whether the field is readonly + required = False # whether the field is required + groups: str | None = None # csv list of group xml ids + change_default = False # whether the field may trigger a "user-onchange" - related_field = None # corresponding related field - aggregator = None # operator for aggregating values - group_expand = None # name of method to expand groups in read_group() - prefetch = True # the prefetch group (False means no group) + related_field = None # corresponding related field + aggregator = None # operator for aggregating values + group_expand = None # name of method to expand groups in read_group() + prefetch = True # the prefetch group (False means no group) - default_export_compatible = False # whether the field must be exported by default in an import-compatible export + default_export_compatible = False # whether the field must be exported by default in an import-compatible export exportable = True def __init__(self, string: str | Sentinel = SENTINEL, **kwargs): - kwargs["string"] = string + kwargs['string'] = string self._sequence = next(_global_seq) self.args = {key: val for key, val in kwargs.items() if val is not SENTINEL} @@ -411,7 +382,7 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): # and '_module', which makes their __dict__'s size minimal. def __set_name__(self, owner, name): - """Perform the base setup of a field. + """ Perform the base setup of a field. :param owner: the owner class of the field (the model's definition or registry class) :param name: the name of the field @@ -424,25 +395,25 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): self._module = owner._module owner._field_definitions.append(self) - if not self.args.get("related"): + if not self.args.get('related'): self._direct = True if self._direct or self._toplevel: self._setup_attrs(owner, name) if self._toplevel: # free memory, self.args and self._base_fields are no longer useful - self.__dict__.pop("args", None) - self.__dict__.pop("_base_fields", None) + self.__dict__.pop('args', None) + self.__dict__.pop('_base_fields', None) # # Setup field parameter attributes # def _get_attrs(self, model_class, name): - """Return the field parameter attributes as a dictionary.""" + """ Return the field parameter attributes as a dictionary. """ # determine all inherited field attributes attrs = {} modules = [] - for field in self.args.get("_base_fields", ()): + for field in self.args.get('_base_fields', ()): if not isinstance(self, type(field)): # 'self' overrides 'field' and their types are not compatible; # so we ignore all the parameters collected so far @@ -456,84 +427,72 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): if self._module: modules.append(self._module) - attrs["args"] = self.args - attrs["model_name"] = model_class._name - attrs["name"] = name - attrs["_module"] = modules[-1] if modules else None - attrs["_modules"] = tuple(set(modules)) + attrs['args'] = self.args + attrs['model_name'] = model_class._name + attrs['name'] = name + attrs['_module'] = modules[-1] if modules else None + attrs['_modules'] = tuple(set(modules)) # initialize ``self`` with ``attrs`` - if name == "state": + if name == 'state': # by default, `state` fields should be reset on copy - attrs["copy"] = attrs.get("copy", False) - if attrs.get("compute"): + attrs['copy'] = attrs.get('copy', False) + if attrs.get('compute'): # by default, computed fields are not stored, computed in superuser # mode if stored, not copied (unless stored and explicitly not # readonly), and readonly (unless inversible) - attrs["store"] = store = attrs.get("store", False) - attrs["compute_sudo"] = attrs.get("compute_sudo", store) - if not (attrs["store"] and not attrs.get("readonly", True)): - attrs["copy"] = attrs.get("copy", False) - attrs["readonly"] = attrs.get("readonly", not attrs.get("inverse")) - if attrs.get("related"): + attrs['store'] = store = attrs.get('store', False) + attrs['compute_sudo'] = attrs.get('compute_sudo', store) + if not (attrs['store'] and not attrs.get('readonly', True)): + attrs['copy'] = attrs.get('copy', False) + attrs['readonly'] = attrs.get('readonly', not attrs.get('inverse')) + if attrs.get('related'): # by default, related fields are not stored, computed in superuser # mode, not copied and readonly - attrs["store"] = store = attrs.get("store", False) - attrs["compute_sudo"] = attrs.get( - "compute_sudo", attrs.get("related_sudo", True) - ) - attrs["copy"] = attrs.get("copy", False) - attrs["readonly"] = attrs.get("readonly", True) - if attrs.get("precompute"): - if not attrs.get("compute") and not attrs.get("related"): - warnings.warn( - f"precompute attribute doesn't make any sense on non computed field {self}" - ) - attrs["precompute"] = False - elif not attrs.get("store"): - warnings.warn( - f"precompute attribute has no impact on non stored field {self}" - ) - attrs["precompute"] = False - if attrs.get("company_dependent"): - if attrs.get("required"): + attrs['store'] = store = attrs.get('store', False) + attrs['compute_sudo'] = attrs.get('compute_sudo', attrs.get('related_sudo', True)) + attrs['copy'] = attrs.get('copy', False) + attrs['readonly'] = attrs.get('readonly', True) + if attrs.get('precompute'): + if not attrs.get('compute') and not attrs.get('related'): + warnings.warn(f"precompute attribute doesn't make any sense on non computed field {self}") + attrs['precompute'] = False + elif not attrs.get('store'): + warnings.warn(f"precompute attribute has no impact on non stored field {self}") + attrs['precompute'] = False + if attrs.get('company_dependent'): + if attrs.get('required'): warnings.warn(f"company_dependent field {self} cannot be required") - if attrs.get("translate"): + if attrs.get('translate'): warnings.warn(f"company_dependent field {self} cannot be translated") if self.type not in COMPANY_DEPENDENT_FIELDS: - warnings.warn( - f"company_dependent field {self} is not one of the allowed types {COMPANY_DEPENDENT_FIELDS}" - ) - attrs["copy"] = attrs.get("copy", False) + warnings.warn(f"company_dependent field {self} is not one of the allowed types {COMPANY_DEPENDENT_FIELDS}") + attrs['copy'] = attrs.get('copy', False) # speed up search and on delete - attrs["index"] = attrs.get("index", "btree_not_null") - attrs["prefetch"] = attrs.get("prefetch", "company_dependent") - attrs["_depends_context"] = ("company",) + attrs['index'] = attrs.get('index', 'btree_not_null') + attrs['prefetch'] = attrs.get('prefetch', 'company_dependent') + attrs['_depends_context'] = ('company',) # parameters 'depends' and 'depends_context' are stored in attributes # '_depends' and '_depends_context', respectively - if "depends" in attrs: - attrs["_depends"] = tuple(attrs.pop("depends")) - if "depends_context" in attrs: - attrs["_depends_context"] = tuple(attrs.pop("depends_context")) + if 'depends' in attrs: + attrs['_depends'] = tuple(attrs.pop('depends')) + if 'depends_context' in attrs: + attrs['_depends_context'] = tuple(attrs.pop('depends_context')) - if "group_operator" in attrs: - warnings.warn( - "Since Odoo 18, 'group_operator' is deprecated, use 'aggregator' instead", - DeprecationWarning, - 2, - ) - attrs["aggregator"] = attrs.pop("group_operator") + if 'group_operator' in attrs: + warnings.warn("Since Odoo 18, 'group_operator' is deprecated, use 'aggregator' instead", DeprecationWarning, 2) + attrs['aggregator'] = attrs.pop('group_operator') return attrs def _setup_attrs(self, model_class, name): - """Initialize the field parameter attributes.""" + """ Initialize the field parameter attributes. """ attrs = self._get_attrs(model_class, name) # determine parameters that must be validated extra_keys = [key for key in attrs if not hasattr(self, key)] if extra_keys: - attrs["_extra_keys"] = extra_keys + attrs['_extra_keys'] = extra_keys self.__dict__.update(attrs) @@ -544,14 +503,9 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): if not self.string and not self.related: # related fields get their string from their parent field self.string = ( - ( - name[:-4] - if name.endswith("_ids") - else name[:-3] if name.endswith("_id") else name - ) - .replace("_", " ") - .title() - ) + name[:-4] if name.endswith('_ids') else + name[:-3] if name.endswith('_id') else name + ).replace('_', ' ').title() # self.default must be either None or a callable if self.default is not None and not callable(self.default): @@ -567,7 +521,7 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): self._setup_done = False def setup(self, model): - """Perform the complete setup of a field.""" + """ Perform the complete setup of a field. """ if not self._setup_done: # validate field params for key in self._extra_keys: @@ -577,8 +531,7 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): " parameter you may want to override the method" " _valid_field_parameter on the relevant model in order to" " allow it", - self, - key, + self, key ) if self.related: self.setup_related(model) @@ -586,14 +539,10 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): self.setup_nonrelated(model) if not isinstance(self.required, bool): - warnings.warn( - f"Property {self}.required should be a boolean ({self.required})." - ) + warnings.warn(f'Property {self}.required should be a boolean ({self.required}).') if not isinstance(self.readonly, bool): - warnings.warn( - f"Property {self}.readonly should be a boolean ({self.readonly})." - ) + warnings.warn(f'Property {self}.readonly should be a boolean ({self.readonly}).') self._setup_done = True @@ -602,11 +551,11 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): # def setup_nonrelated(self, model): - """Determine the dependencies and inverse field(s) of ``self``.""" + """ Determine the dependencies and inverse field(s) of ``self``. """ pass def get_depends(self, model: BaseModel): - """Return the field's dependencies and cache dependencies.""" + """ Return the field's dependencies and cache dependencies. """ if self._depends is not None: # the parameter 'depends' has priority over 'depends' on compute return self._depends, self._depends_context or () @@ -632,15 +581,15 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): depends = [] depends_context = list(self._depends_context or ()) for func in funcs: - deps = getattr(func, "_depends", ()) + deps = getattr(func, '_depends', ()) depends.extend(deps(model) if callable(deps) else deps) - depends_context.extend(getattr(func, "_depends_context", ())) + depends_context.extend(getattr(func, '_depends_context', ())) # display_name may depend on context['lang'] (`test_lp1071710`) - if self.automatic and self.name == "display_name" and model._rec_name: + if self.automatic and self.name == 'display_name' and model._rec_name: if model._fields[model._rec_name].base_field.translate: - if "lang" not in depends_context: - depends_context.append("lang") + if 'lang' not in depends_context: + depends_context.append('lang') return depends, depends_context @@ -649,12 +598,12 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): # def setup_related(self, model): - """Setup the attributes of a related field.""" + """ Setup the attributes of a related field. """ assert isinstance(self.related, str), self.related # determine the chain of fields, and make sure they are all set up model_name = self.model_name - for name in self.related.split("."): + for name in self.related.split('.'): field = model.pool[model_name]._fields.get(name) if field is None: raise KeyError( @@ -668,9 +617,7 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): # check type consistency if self.type != field.type: - raise TypeError( - "Type of related field %s is inconsistent with %s" % (self, field) - ) + raise TypeError("Type of related field %s is inconsistent with %s" % (self, field)) # determine dependencies, compute, inverse, and search self.compute = self._compute_related @@ -689,7 +636,7 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): for attr, prop in self.related_attrs: # check whether 'attr' is explicitly set on self (from its field # definition), and ignore its class-level value (only a default) - if attr not in self.__dict__ and prop.startswith("_related_"): + if attr not in self.__dict__ and prop.startswith('_related_'): setattr(self, attr, getattr(field, prop)) for attr in field._extra_keys: @@ -704,26 +651,21 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): # add modules from delegate and target fields; the first one ensures # that inherited fields introduced via an abstract model (_inherits # being on the abstract model) are assigned an XML id - delegate_field = model._fields[self.related.split(".")[0]] - self._modules = tuple( - {*self._modules, *delegate_field._modules, *field._modules} - ) + delegate_field = model._fields[self.related.split('.')[0]] + self._modules = tuple({*self._modules, *delegate_field._modules, *field._modules}) if self.store and self.translate: - _logger.warning( - "Translated stored related field (%s) will not be computed correctly in all languages", - self, - ) + _logger.warning("Translated stored related field (%s) will not be computed correctly in all languages", self) def traverse_related(self, record): - """Traverse the fields of the related field `self` except for the last - one, and return it as a pair `(last_record, last_field)`.""" - for name in self.related.split(".")[:-1]: + """ Traverse the fields of the related field `self` except for the last + one, and return it as a pair `(last_record, last_field)`. """ + for name in self.related.split('.')[:-1]: record = first(record[name]) return record, self.related_field def _compute_related(self, records): - """Compute the related field ``self`` on ``records``.""" + """ Compute the related field ``self`` on ``records``. """ # # Traverse fields one by one for all records, in order to take advantage # of prefetching for each field access. In order to clarify the impact @@ -751,32 +693,28 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): # computation. # values = list(records) - for name in self.related.split(".")[:-1]: + for name in self.related.split('.')[:-1]: try: values = [first(value[name]) for value in values] except AccessError as e: - description = records.env["ir.model"]._get(records._name).name + description = records.env['ir.model']._get(records._name).name env = records.env - raise AccessError( - env._( - "%(previous_message)s\n\nImplicitly accessed through '%(document_kind)s' (%(document_model)s).", - previous_message=e.args[0], - document_kind=description, - document_model=records._name, - ) - ) + raise AccessError(env._( + "%(previous_message)s\n\nImplicitly accessed through '%(document_kind)s' (%(document_model)s).", + previous_message=e.args[0], + document_kind=description, + document_model=records._name, + )) # assign final values to records for record, value in zip(records, values): - record[self.name] = self._process_related( - value[self.related_field.name], record.env - ) + record[self.name] = self._process_related(value[self.related_field.name], record.env) def _process_related(self, value, env): """No transformation by default, but allows override.""" return value def _inverse_related(self, records): - """Inverse the related field ``self`` on ``records``.""" + """ Inverse the related field ``self`` on ``records``. """ # store record values, otherwise they may be lost by cache invalidation! record_value = {record: record[self.name] for record in records} for record in records: @@ -787,11 +725,11 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): target[field.name] = record_value[record] def _search_related(self, records, operator, value): - """Determine the domain to search on field ``self``.""" + """ Determine the domain to search on field ``self``. """ # This should never happen to avoid bypassing security checks # and should already be converted to (..., 'in', subquery) - assert operator not in ("any", "not any") + assert operator not in ('any', 'not any') # determine whether the related field can be null if isinstance(value, (list, tuple)): @@ -800,20 +738,21 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): value_is_null = value is False or value is None can_be_null = ( # (..., '=', False) or (..., 'not in', [truthy vals]) - operator not in expression.NEGATIVE_TERM_OPERATORS and value_is_null - ) or (operator in expression.NEGATIVE_TERM_OPERATORS and not value_is_null) + (operator not in expression.NEGATIVE_TERM_OPERATORS and value_is_null) + or (operator in expression.NEGATIVE_TERM_OPERATORS and not value_is_null) + ) def make_domain(path, model): - if "." not in path: + if '.' not in path: return [(path, operator, value)] - prefix, suffix = path.split(".", 1) + prefix, suffix = path.split('.', 1) field = model._fields[prefix] comodel = model.env[field.comodel_name] - domain = [(prefix, "in", comodel._search(make_domain(suffix, comodel)))] - if can_be_null and field.type == "many2one" and not field.required: - return expression.OR([domain, [(prefix, "=", False)]]) + domain = [(prefix, 'in', comodel._search(make_domain(suffix, comodel)))] + if can_be_null and field.type == 'many2one' and not field.required: + return expression.OR([domain, [(prefix, '=', False)]]) return domain @@ -823,24 +762,20 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): return make_domain(self.related, model) # properties used by setup_related() to copy values from related field - _related_comodel_name = property(attrgetter("comodel_name")) - _related_string = property(attrgetter("string")) - _related_help = property(attrgetter("help")) - _related_groups = property(attrgetter("groups")) - _related_aggregator = property(attrgetter("aggregator")) + _related_comodel_name = property(attrgetter('comodel_name')) + _related_string = property(attrgetter('string')) + _related_help = property(attrgetter('help')) + _related_groups = property(attrgetter('groups')) + _related_aggregator = property(attrgetter('aggregator')) @lazy_property def column_type(self) -> tuple[str, str] | None: - """Return the actual column type for this field, if stored as a column.""" - return ( - ("jsonb", "jsonb") - if self.company_dependent or self.translate - else self._column_type - ) + """ Return the actual column type for this field, if stored as a column. """ + return ('jsonb', 'jsonb') if self.company_dependent or self.translate else self._column_type @property def base_field(self): - """Return the base field of an inherited field, or ``self``.""" + """ Return the base field of an inherited field, or ``self``. """ return self.inherited_field.base_field if self.inherited_field else self # @@ -849,13 +784,10 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): def get_company_dependent_fallback(self, records): assert self.company_dependent - fallback = ( - records.env["ir.default"] - .with_user(SUPERUSER_ID) - .with_company(records.env.company) - ._get_model_defaults(records._name) - .get(self.name) - ) + fallback = records.env['ir.default'] \ + .with_user(SUPERUSER_ID) \ + .with_company(records.env.company) \ + ._get_model_defaults(records._name).get(self.name) fallback = self.convert_to_cache(fallback, records, validate=False) return self.convert_to_record(fallback, records) @@ -864,7 +796,7 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): # def resolve_depends(self, registry): - """Return the dependencies of `self` as a collection of field tuples.""" + """ Return the dependencies of `self` as a collection of field tuples. """ Model0 = registry[self.model_name] for dotnames in registry.field_depends[self]: @@ -872,7 +804,7 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): model_name = self.model_name check_precompute = self.precompute - for index, fname in enumerate(dotnames.split(".")): + for index, fname in enumerate(dotnames.split('.')): Model = registry[model_name] if Model0._transient and not Model._transient: # modifying fields on regular models should not trigger @@ -888,21 +820,12 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): ) if field is self and index and not self.recursive: self.recursive = True - warnings.warn( - f"Field {self} should be declared with recursive=True" - ) + warnings.warn(f"Field {self} should be declared with recursive=True") # precomputed fields can depend on non-precomputed ones, as long # as they are reachable through at least one many2one field - if ( - check_precompute - and field.store - and field.compute - and not field.precompute - ): - warnings.warn( - f"Field {self} cannot be precomputed as it depends on non-precomputed field {field}" - ) + if check_precompute and field.store and field.compute and not field.precompute: + warnings.warn(f"Field {self} cannot be precomputed as it depends on non-precomputed field {field}") self.precompute = False if field_seq and not field_seq[-1]._description_searchable: @@ -922,11 +845,11 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): if not (field is self and not index): yield tuple(field_seq) - if field.type == "one2many": + if field.type == 'one2many': for inv_field in Model.pool.field_inverses[field]: yield tuple(field_seq) + (inv_field,) - if check_precompute and field.type == "many2one": + if check_precompute and field.type == 'many2one': check_precompute = False model_name = field.comodel_name @@ -937,12 +860,12 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): # def get_description(self, env, attributes=None): - """Return a dictionary that describes the field ``self``.""" + """ Return a dictionary that describes the field ``self``. """ desc = {} for attr, prop in self.description_attrs: if attributes is not None and attr not in attributes: continue - if not prop.startswith("_description_"): + if not prop.startswith('_description_'): continue value = getattr(self, prop) if callable(value): @@ -953,20 +876,18 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): return desc # properties used by get_description() - _description_name = property(attrgetter("name")) - _description_type = property(attrgetter("type")) - _description_store = property(attrgetter("store")) - _description_manual = property(attrgetter("manual")) - _description_related = property(attrgetter("related")) - _description_company_dependent = property(attrgetter("company_dependent")) - _description_readonly = property(attrgetter("readonly")) - _description_required = property(attrgetter("required")) - _description_groups = property(attrgetter("groups")) - _description_change_default = property(attrgetter("change_default")) - _description_default_export_compatible = property( - attrgetter("default_export_compatible") - ) - _description_exportable = property(attrgetter("exportable")) + _description_name = property(attrgetter('name')) + _description_type = property(attrgetter('type')) + _description_store = property(attrgetter('store')) + _description_manual = property(attrgetter('manual')) + _description_related = property(attrgetter('related')) + _description_company_dependent = property(attrgetter('company_dependent')) + _description_readonly = property(attrgetter('readonly')) + _description_required = property(attrgetter('required')) + _description_groups = property(attrgetter('groups')) + _description_change_default = property(attrgetter('change_default')) + _description_default_export_compatible = property(attrgetter('default_export_compatible')) + _description_exportable = property(attrgetter('exportable')) def _description_depends(self, env): return env.registry.field_depends[self] @@ -993,9 +914,7 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): model = env[self.model_name] query = model._as_query(ordered=False) - groupby = ( - self.name if self.type not in ("date", "datetime") else f"{self.name}:month" - ) + groupby = self.name if self.type not in ('date', 'datetime') else f"{self.name}:month" try: model._read_group_groupby(groupby, query) return True @@ -1017,26 +936,26 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): def _description_string(self, env): if self.string and env.lang: model_name = self.base_field.model_name - field_string = env["ir.model.fields"].get_field_string(model_name) + field_string = env['ir.model.fields'].get_field_string(model_name) return field_string.get(self.name) or self.string return self.string def _description_help(self, env): if self.help and env.lang: model_name = self.base_field.model_name - field_help = env["ir.model.fields"].get_field_help(model_name) + field_help = env['ir.model.fields'].get_field_help(model_name) return field_help.get(self.name) or self.help return self.help def is_editable(self): - """Return whether the field can be editable in a view.""" + """ Return whether the field can be editable in a view. """ return not self.readonly def is_accessible(self, env): - """Return whether the field is accessible from the given environment.""" + """ Return whether the field is accessible from the given environment. """ if not self.groups or env.is_superuser(): return True - if self.groups == ".": + if self.groups == '.': return False return env.user.has_groups(self.groups) @@ -1046,7 +965,7 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): # def convert_to_column(self, value, record, values=None, validate=True): - """Convert ``value`` from the ``write`` format to the SQL parameter + """ Convert ``value`` from the ``write`` format to the SQL parameter format for SQL conditions. This is used to compare a field's value when the field actually stores multiple values (translated or company-dependent). """ @@ -1060,22 +979,20 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): return str(value) def convert_to_column_insert(self, value, record, values=None, validate=True): - """Convert ``value`` from the ``write`` format to the SQL parameter + """ Convert ``value`` from the ``write`` format to the SQL parameter format for INSERT queries. This method handles the case of fields that store multiple values (translated or company-dependent). """ value = self.convert_to_column(value, record, values, validate) if not self.company_dependent: return value - fallback = ( - record.env["ir.default"]._get_model_defaults(record._name).get(self.name) - ) + fallback = record.env['ir.default']._get_model_defaults(record._name).get(self.name) if value == self.convert_to_column(fallback, record): return None return PsycopgJson({record.env.company.id: value}) def convert_to_column_update(self, value, record): - """Convert ``value`` from the ``to_flush`` format to the SQL parameter + """ Convert ``value`` from the ``to_flush`` format to the SQL parameter format for UPDATE queries. The ``to_flush`` format is the same as the cache format, except for translated fields (``{'lang_code': 'value', ...}`` or ``None``) and company-dependent fields (``{company_id: value, ...}``). @@ -1088,7 +1005,7 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): ) def convert_to_cache(self, value, record, validate=True): - """Convert ``value`` to the cache format; ``value`` may come from an + """ Convert ``value`` to the cache format; ``value`` may come from an assignment, or have the format of methods :meth:`BaseModel.read` or :meth:`BaseModel.write`. If the value represents a recordset, it should be added for prefetching on ``record``. @@ -1101,14 +1018,14 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): return value def convert_to_record(self, value, record): - """Convert ``value`` from the cache format to the record format. + """ Convert ``value`` from the cache format to the record format. If the value represents a recordset, it should share the prefetching of ``record``. """ return False if value is None else value def convert_to_record_multi(self, values, records): - """Convert a list of values from the cache format to the record format. + """ Convert a list of values from the cache format to the record format. Some field classes may override this method to add optimizations for batch processing. """ @@ -1117,7 +1034,7 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): return [convert(value, record) for value, record in zip(values, records)] def convert_to_read(self, value, record, use_display_name=True): - """Convert ``value`` from the record format to the format returned by + """ Convert ``value`` from the record format to the format returned by method :meth:`BaseModel.read`. :param value: @@ -1128,7 +1045,7 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): return False if value is None else value def convert_to_write(self, value, record): - """Convert ``value`` from any format to the format of method + """ Convert ``value`` from any format to the format of method :meth:`BaseModel.write`. """ cache_value = self.convert_to_cache(value, record, validate=False) @@ -1136,13 +1053,13 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): return self.convert_to_read(record_value, record) def convert_to_export(self, value, record): - """Convert ``value`` from the record format to the export format.""" + """ Convert ``value`` from the record format to the export format. """ if not value: - return "" + return '' return value def convert_to_display_name(self, value, record): - """Convert ``value`` from the record format to a suitable display name.""" + """ Convert ``value`` from the record format to a suitable display name. """ return str(value) if value else False ############################################################################ @@ -1152,19 +1069,15 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): @property def column_order(self): - """Prescribed column order in table.""" - return ( - 0 - if self.column_type is None - else sql.SQL_ORDER_BY_TYPE[self.column_type[0]] - ) + """ Prescribed column order in table. """ + return 0 if self.column_type is None else sql.SQL_ORDER_BY_TYPE[self.column_type[0]] def update_db(self, model, columns): - """Update the database schema to implement this field. + """ Update the database schema to implement this field. - :param model: an instance of the field's model - :param columns: a dict mapping column names to their configuration in database - :return: ``True`` if the field must be recomputed on existing rows + :param model: an instance of the field's model + :param columns: a dict mapping column names to their configuration in database + :return: ``True`` if the field must be recomputed on existing rows """ if not self.column_type: return @@ -1179,20 +1092,15 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): # optimization for computing simple related fields like 'foo_id.bar' if ( not column - and self.related - and self.related.count(".") == 1 - and self.related_field.store - and not self.related_field.compute - and not ( - self.related_field.type == "binary" and self.related_field.attachment - ) - and self.related_field.type not in ("one2many", "many2many") + and self.related and self.related.count('.') == 1 + and self.related_field.store and not self.related_field.compute + and not (self.related_field.type == 'binary' and self.related_field.attachment) + and self.related_field.type not in ('one2many', 'many2many') ): - join_field = model._fields[self.related.split(".")[0]] + join_field = model._fields[self.related.split('.')[0]] if ( - join_field.type == "many2one" - and join_field.store - and not join_field.compute + join_field.type == 'many2one' + and join_field.store and not join_field.compute ): model.pool.post_init(self.update_db_related, model) # discard the "classical" computation @@ -1201,34 +1109,32 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): return not column def update_db_column(self, model, column): - """Create/update the column corresponding to ``self``. + """ Create/update the column corresponding to ``self``. - :param model: an instance of the field's model - :param column: the column's configuration (dict) if it exists, or ``None`` + :param model: an instance of the field's model + :param column: the column's configuration (dict) if it exists, or ``None`` """ if not column: # the column does not exist, create it - sql.create_column( - model._cr, model._table, self.name, self.column_type[1], self.string - ) + sql.create_column(model._cr, model._table, self.name, self.column_type[1], self.string) return - if column["udt_name"] == self.column_type[0]: + if column['udt_name'] == self.column_type[0]: return - if column["is_nullable"] == "NO": + if column['is_nullable'] == 'NO': sql.drop_not_null(model._cr, model._table, self.name) self._convert_db_column(model, column) def _convert_db_column(self, model, column): - """Convert the given database column to the type of the field.""" + """ Convert the given database column to the type of the field. """ sql.convert_column(model._cr, model._table, self.name, self.column_type[1]) def update_db_notnull(self, model, column): - """Add or remove the NOT NULL constraint on ``self``. + """ Add or remove the NOT NULL constraint on ``self``. - :param model: an instance of the field's model - :param column: the column's configuration (dict) if it exists, or ``None`` + :param model: an instance of the field's model + :param column: the column's configuration (dict) if it exists, or ``None`` """ - has_notnull = column and column["is_nullable"] == "NO" + has_notnull = column and column['is_nullable'] == 'NO' if not column or (self.required and not has_notnull): # the column is new or it becomes required; initialize its values @@ -1247,22 +1153,20 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): sql.drop_not_null(model._cr, model._table, self.name) def update_db_related(self, model): - """Compute a stored related field directly in SQL.""" + """ Compute a stored related field directly in SQL. """ comodel = model.env[self.related_field.model_name] - join_field, comodel_field = self.related.split(".") - model.env.cr.execute( - SQL( - """ UPDATE %(model_table)s AS x + join_field, comodel_field = self.related.split('.') + model.env.cr.execute(SQL( + """ UPDATE %(model_table)s AS x SET %(model_field)s = y.%(comodel_field)s FROM %(comodel_table)s AS y WHERE x.%(join_field)s = y.id """, - model_table=SQL.identifier(model._table), - model_field=SQL.identifier(self.name), - comodel_table=SQL.identifier(comodel._table), - comodel_field=SQL.identifier(comodel_field), - join_field=SQL.identifier(join_field), - ) - ) + model_table=SQL.identifier(model._table), + model_field=SQL.identifier(self.name), + comodel_table=SQL.identifier(comodel._table), + comodel_field=SQL.identifier(comodel_field), + join_field=SQL.identifier(join_field), + )) ############################################################################ # @@ -1272,12 +1176,12 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): # def read(self, records): - """Read the value of ``self`` on ``records``, and store it in cache.""" + """ Read the value of ``self`` on ``records``, and store it in cache. """ if not self.column_type: raise NotImplementedError("Method read() undefined on %s" % self) def create(self, record_values): - """Write the value of ``self`` on the given records, which have just + """ Write the value of ``self`` on the given records, which have just been created. :param record_values: a list of pairs ``(record, value)``, where @@ -1287,7 +1191,7 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): self.write(record, value) def write(self, records, value): - """Write the value of ``self`` on ``records``. This method must update + """ Write the value of ``self`` on ``records``. This method must update the cache and prepare database updates. :param records: @@ -1313,9 +1217,9 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): # def __get__(self, record: BaseModel, owner=None) -> T: - """return the value of field ``self`` on ``record``""" + """ return the value of field ``self`` on ``record`` """ if record is None: - return self # the field is accessed through the owner class + return self # the field is accessed through the owner class if not record._ids: # null record -> return the null value for this field @@ -1365,18 +1269,18 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): raise record._fetch_field(self) if not env.cache.contains(record, self): - value = self.convert_to_cache(False, record, validate=False) - else: - value = env.cache.get(record, self) + raise MissingError("\n".join([ + env._("Record does not exist or has been deleted."), + env._("(Record: %(record)s, User: %(user)s)", record=record, user=env.uid), + ])) from None + value = env.cache.get(record, self) elif self.store and record._origin and not (self.compute and self.readonly): # new record with origin: fetch from origin - value = self.convert_to_cache( - record._origin[self.name], record, validate=False - ) + value = self.convert_to_cache(record._origin[self.name], record, validate=False) value = env.cache.patch_and_set(record, self, value) - elif self.compute: # pylint: disable=using-constant-test + elif self.compute: #pylint: disable=using-constant-test # non-stored field or new record without origin: compute if env.is_protected(self, record): value = self.convert_to_cache(False, record, validate=False) @@ -1393,29 +1297,25 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): if missing_recs_ids: missing_recs = record.browse(missing_recs_ids) if self.readonly and not self.store: - raise ValueError( - f"Compute method failed to assign {missing_recs}.{self.name}" - ) + raise ValueError(f"Compute method failed to assign {missing_recs}.{self.name}") # fallback to null value if compute gives nothing, do it for every unset record false_value = self.convert_to_cache(False, record, validate=False) env.cache.update(missing_recs, self, itertools.repeat(false_value)) value = env.cache.get(record, self) - elif self.type == "many2one" and self.delegate and not record.id: + elif self.type == 'many2one' and self.delegate and not record.id: # parent record of a new record: new record, with the same # values as record for the corresponding inherited fields def is_inherited_field(name): field = record._fields[name] - return field.inherited and field.related.split(".")[0] == self.name + return field.inherited and field.related.split('.')[0] == self.name - parent = record.env[self.comodel_name].new( - { - name: value - for name, value in record._cache.items() - if is_inherited_field(name) - } - ) + parent = record.env[self.comodel_name].new({ + name: value + for name, value in record._cache.items() + if is_inherited_field(name) + }) # in case the delegate field has inverse one2many fields, this # updates the inverse fields as well record._update_cache({self.name: parent}, validate=False) @@ -1439,13 +1339,13 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): return self.convert_to_record(value, record) def mapped(self, records): - """Return the values of ``self`` for ``records``, either as a list + """ Return the values of ``self`` for ``records``, either as a list (scalar fields), or as a recordset (relational fields). This method is meant to be used internally and has very little benefit over a simple call to `~odoo.models.BaseModel.mapped()` on a recordset. """ - if self.name == "id": + if self.name == 'id': # not stored in cache return list(records._ids) @@ -1461,16 +1361,14 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): # many records as possible. If not done this way, scenarios such as # [rec.line_ids.mapped('name') for rec in recs] would generate one # query per record in `recs`! - remaining = records.__class__( - records.env, records._ids[len(vals) :], records._prefetch_ids - ) + remaining = records.__class__(records.env, records._ids[len(vals):], records._prefetch_ids) self.__get__(first(remaining)) vals += records.env.cache.get_until_miss(remaining, self) return self.convert_to_record_multi(vals, records) def __set__(self, records, value): - """set the value of field ``self`` on ``records``""" + """ set the value of field ``self`` on ``records`` """ protected_ids = [] new_ids = [] other_ids = [] @@ -1484,19 +1382,13 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): if protected_ids: # records being computed: no business logic, no recomputation - protected_records = records.__class__( - records.env, tuple(protected_ids), records._prefetch_ids - ) + protected_records = records.__class__(records.env, tuple(protected_ids), records._prefetch_ids) self.write(protected_records, value) if new_ids: # new records: no business logic - new_records = records.__class__( - records.env, tuple(new_ids), records._prefetch_ids - ) - with records.env.protecting( - records.pool.field_computed.get(self, [self]), new_records - ): + new_records = records.__class__(records.env, tuple(new_ids), records._prefetch_ids) + with records.env.protecting(records.pool.field_computed.get(self, [self]), new_records): if self.relational: new_records.modified([self.name], before=True) self.write(new_records, value) @@ -1504,14 +1396,12 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): if self.inherited: # special case: also assign parent records if they are new - parents = new_records[self.related.split(".")[0]] + parents = new_records[self.related.split('.')[0]] parents.filtered(lambda r: not r.id)[self.name] = value if other_ids: # base case: full business logic - records = records.__class__( - records.env, tuple(other_ids), records._prefetch_ids - ) + records = records.__class__(records.env, tuple(other_ids), records._prefetch_ids) write_value = self.convert_to_write(value, records) records.write({self.name: write_value}) @@ -1521,7 +1411,7 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): # def recompute(self, records): - """Process the pending computations of ``self`` on ``records``. This + """ Process the pending computations of ``self`` on ``records``. This should be called only if ``self`` is computed and stored. """ to_compute_ids = records.env.transaction.tocompute.get(self) @@ -1529,7 +1419,7 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): return def apply_except_missing(func, records): - """Apply `func` on `records`, with a fallback ignoring non-existent records.""" + """ Apply `func` on `records`, with a fallback ignoring non-existent records. """ try: func(records) except MissingError: @@ -1563,7 +1453,7 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): self.compute_value(record) def compute_value(self, records): - """Invoke the compute method on ``records``; the results are in cache.""" + """ Invoke the compute method on ``records``; the results are in cache. """ env = records.env if self.compute_sudo: records = records.sudo() @@ -1588,19 +1478,18 @@ class Field(MetaField("DummyField", (object,), {}), typing.Generic[T]): raise def determine_inverse(self, records): - """Given the value of ``self`` on ``records``, inverse the computation.""" + """ Given the value of ``self`` on ``records``, inverse the computation. """ determine(self.inverse, records) def determine_domain(self, records, operator, value): - """Return a domain representing a condition on ``self``.""" + """ Return a domain representing a condition on ``self``. """ return determine(self.search, records, operator, value) class Boolean(Field[bool]): - """Encapsulates a :class:`bool`.""" - - type = "boolean" - _column_type = ("bool", "bool") + """ Encapsulates a :class:`bool`. """ + type = 'boolean' + _column_type = ('bool', 'bool') def convert_to_column(self, value, record, values=None, validate=True): return bool(value) @@ -1618,18 +1507,17 @@ class Boolean(Field[bool]): class Integer(Field[int]): - """Encapsulates an :class:`int`.""" + """ Encapsulates an :class:`int`. """ + type = 'integer' + _column_type = ('int4', 'int4') - type = "integer" - _column_type = ("int4", "int4") - - aggregator = "sum" + aggregator = 'sum' def _get_attrs(self, model_class, name): res = super()._get_attrs(model_class, name) # The default aggregator is None for sequence fields - if "aggregator" not in res and name == "sequence": - res["aggregator"] = None + if 'aggregator' not in res and name == 'sequence': + res['aggregator'] = None return res def convert_to_column(self, value, record, values=None, validate=True): @@ -1643,7 +1531,7 @@ class Integer(Field[int]): def convert_to_cache(self, value, record, validate=True): if isinstance(value, dict): # special case, when an integer field is used as inverse for a one2many - return value.get("id", None) + return value.get('id', None) return int(value or 0) def convert_to_record(self, value, record): @@ -1664,11 +1552,11 @@ class Integer(Field[int]): def convert_to_export(self, value, record): if value or value == 0: return value - return "" + return '' class Float(Field[float]): - """Encapsulates a :class:`float`. + """ Encapsulates a :class:`float`. The precision digits are given by the (optional) ``digits`` attribute. @@ -1707,16 +1595,11 @@ class Float(Field[float]): if result > 0, the first float is greater than the second """ - type = "float" - _digits = None # digits argument passed to class initializer - aggregator = "sum" + type = 'float' + _digits = None # digits argument passed to class initializer + aggregator = 'sum' - def __init__( - self, - string: str | Sentinel = SENTINEL, - digits: str | tuple[int, int] | None | Sentinel = SENTINEL, - **kwargs, - ): + def __init__(self, string: str | Sentinel = SENTINEL, digits: str | tuple[int, int] | None | Sentinel = SENTINEL, **kwargs): super(Float, self).__init__(string=string, _digits=digits, **kwargs) @property @@ -1726,20 +1609,17 @@ class Float(Field[float]): # with all significant digits. # FLOAT8 type is still the default when there is no precision because it # is faster for most operations (sums, etc.) - return ( - ("numeric", "numeric") - if self._digits is not None - else ("float8", "double precision") - ) + return ('numeric', 'numeric') if self._digits is not None else \ + ('float8', 'double precision') def get_digits(self, env): if isinstance(self._digits, str): - precision = env["decimal.precision"].precision_get(self._digits) + precision = env['decimal.precision'].precision_get(self._digits) return 16, precision else: return self._digits - _related__digits = property(attrgetter("_digits")) + _related__digits = property(attrgetter('_digits')) def _description_digits(self, env): return self.get_digits(env) @@ -1771,7 +1651,7 @@ class Float(Field[float]): def convert_to_export(self, value, record): if value or value == 0.0: return value - return "" + return '' round = staticmethod(float_round) is_zero = staticmethod(float_is_zero) @@ -1779,7 +1659,7 @@ class Float(Field[float]): class Monetary(Field[float]): - """Encapsulates a :class:`float` expressed in a given + """ Encapsulates a :class:`float` expressed in a given :class:`res_currency`. The decimal precision and currency symbol are taken from the ``currency_field`` attribute. @@ -1788,56 +1668,38 @@ class Monetary(Field[float]): holding the :class:`res_currency ` this monetary field is expressed in (default: `\'currency_id\'`) """ - - type = "monetary" + type = 'monetary' write_sequence = 10 - _column_type = ("numeric", "numeric") + _column_type = ('numeric', 'numeric') currency_field = None - aggregator = "sum" + aggregator = 'sum' - def __init__( - self, - string: str | Sentinel = SENTINEL, - currency_field: str | Sentinel = SENTINEL, - **kwargs, - ): - super(Monetary, self).__init__( - string=string, currency_field=currency_field, **kwargs - ) + def __init__(self, string: str | Sentinel = SENTINEL, currency_field: str | Sentinel = SENTINEL, **kwargs): + super(Monetary, self).__init__(string=string, currency_field=currency_field, **kwargs) def _description_currency_field(self, env): return self.get_currency_field(env[self.model_name]) def get_currency_field(self, model): - """Return the name of the currency field.""" + """ Return the name of the currency field. """ return self.currency_field or ( - "currency_id" - if "currency_id" in model._fields - else "x_currency_id" if "x_currency_id" in model._fields else None + 'currency_id' if 'currency_id' in model._fields else + 'x_currency_id' if 'x_currency_id' in model._fields else + None ) def setup_nonrelated(self, model): super().setup_nonrelated(model) - assert ( - self.get_currency_field(model) in model._fields - ), "Field %s with unknown currency_field %r" % ( - self, - self.get_currency_field(model), - ) + assert self.get_currency_field(model) in model._fields, \ + "Field %s with unknown currency_field %r" % (self, self.get_currency_field(model)) def setup_related(self, model): super().setup_related(model) if self.inherited: - self.currency_field = self.related_field.get_currency_field( - model.env[self.related_field.model_name] - ) - assert ( - self.get_currency_field(model) in model._fields - ), "Field %s with unknown currency_field %r" % ( - self, - self.get_currency_field(model), - ) + self.currency_field = self.related_field.get_currency_field(model.env[self.related_field.model_name]) + assert self.get_currency_field(model) in model._fields, \ + "Field %s with unknown currency_field %r" % (self, self.get_currency_field(model)) def convert_to_column_insert(self, value, record, values=None, validate=True): # retrieve currency from values or record @@ -1846,12 +1708,8 @@ class Monetary(Field[float]): if values and currency_field_name in values: dummy = record.new({currency_field_name: values[currency_field_name]}) currency = dummy[currency_field_name] - elif ( - values - and currency_field.related - and currency_field.related.split(".")[0] in values - ): - related_field_name = currency_field.related.split(".")[0] + elif values and currency_field.related and currency_field.related.split('.')[0] in values: + related_field_name = currency_field.related.split('.')[0] dummy = record.new({related_field_name: values[related_field_name]}) currency = dummy[currency_field_name] else: @@ -1859,9 +1717,7 @@ class Monetary(Field[float]): # currencies, which is functional nonsense and should not happen # BEWARE: do not prefetch other fields, because 'value' may be in # cache, and would be overridden by the value read from database! - currency = record[:1].with_context(prefetch_fields=False)[ - currency_field_name - ] + currency = record[:1].with_context(prefetch_fields=False)[currency_field_name] currency = currency.with_env(record.env) value = float(value or 0.0) @@ -1880,10 +1736,7 @@ class Monetary(Field[float]): currency_field = self.get_currency_field(record) currency = record.sudo().with_context(prefetch_fields=False)[currency_field] if len(currency) > 1: - raise ValueError( - "Got multiple currencies while assigning values of monetary field %s" - % str(self) - ) + raise ValueError("Got multiple currencies while assigning values of monetary field %s" % str(self)) elif currency: value = currency.with_env(record.env).round(value) return value @@ -1900,37 +1753,34 @@ class Monetary(Field[float]): def convert_to_export(self, value, record): if value or value == 0.0: return value - return "" + return '' class _String(Field[str | typing.Literal[False]]): - """Abstract class for string fields.""" - - translate = False # whether the field is translated - size = None # maximum size of values (deprecated) + """ Abstract class for string fields. """ + translate = False # whether the field is translated + size = None # maximum size of values (deprecated) def __init__(self, string: str | Sentinel = SENTINEL, **kwargs): # translate is either True, False, or a callable - if "translate" in kwargs and not callable(kwargs["translate"]): - kwargs["translate"] = bool(kwargs["translate"]) + if 'translate' in kwargs and not callable(kwargs['translate']): + kwargs['translate'] = bool(kwargs['translate']) super(_String, self).__init__(string=string, **kwargs) - _related_translate = property(attrgetter("translate")) + _related_translate = property(attrgetter('translate')) def _description_translate(self, env): return bool(self.translate) def _convert_db_column(self, model, column): # specialized implementation for converting from/to translated fields - if self.translate or column["udt_name"] == "jsonb": - sql.convert_column_translatable( - model._cr, model._table, self.name, self.column_type[1] - ) + if self.translate or column['udt_name'] == 'jsonb': + sql.convert_column_translatable(model._cr, model._table, self.name, self.column_type[1]) else: sql.convert_column(model._cr, model._table, self.name, self.column_type[1]) def get_trans_terms(self, value): - """Return the sequence of terms to translate found in `value`.""" + """ Return the sequence of terms to translate found in `value`. """ if not callable(self.translate): return [value] if value else [] terms = [] @@ -1938,8 +1788,8 @@ class _String(Field[str | typing.Literal[False]]): return terms def get_text_content(self, term): - """Return the textual content for the given term.""" - func = getattr(self.translate, "get_text_content", lambda term: term) + """ Return the textual content for the given term. """ + func = getattr(self.translate, 'get_text_content', lambda term: term) return func(term) def convert_to_column(self, value, record, values=None, validate=True): @@ -1950,7 +1800,7 @@ class _String(Field[str | typing.Literal[False]]): value = self.convert_to_column(value, record, values, validate) if value is None: return None - return PsycopgJson({"en_US": value, record.env.lang or "en_US": value}) + return PsycopgJson({'en_US': value, record.env.lang or 'en_US': value}) return super().convert_to_column_insert(value, record, values, validate) def convert_to_column_update(self, value, record): @@ -1966,7 +1816,7 @@ class _String(Field[str | typing.Literal[False]]): s = value.decode() else: s = str(value) - value = s[: self.size] + value = s[:self.size] if callable(self.translate): # pylint: disable=not-callable value = self.translate(lambda t: None, value) @@ -1975,51 +1825,39 @@ class _String(Field[str | typing.Literal[False]]): def convert_to_record(self, value, record): if value is None: return False - if callable(self.translate) and record.env.context.get("edit_translations"): + if callable(self.translate) and record.env.context.get('edit_translations'): if not self.get_trans_terms(value): return value base_lang = record._get_base_lang() - lang = record.env.lang or "en_US" + lang = record.env.lang or 'en_US' if lang != base_lang: - base_value = record.with_context( - edit_translations=None, check_translations=True, lang=base_lang - )[self.name] + base_value = record.with_context(edit_translations=None, check_translations=True, lang=base_lang)[self.name] base_terms_iter = iter(self.get_trans_terms(base_value)) get_base = lambda term: next(base_terms_iter) else: get_base = lambda term: term - delay_translation = ( - value - != record.with_context( - edit_translations=None, check_translations=None, lang=lang - )[self.name] - ) + delay_translation = value != record.with_context(edit_translations=None, check_translations=None, lang=lang)[self.name] # use a wrapper to let the frontend js code identify each term and # its metadata in the 'edit_translations' context def translate_func(term): source_term = get_base(term) - translation_state = ( - "translated" - if lang == base_lang or source_term != term - else "to_translate" - ) + translation_state = 'translated' if lang == base_lang or source_term != term else 'to_translate' translation_source_sha = sha256(source_term.encode()).hexdigest() return ( - "" - f"{term}" - "" + '' + f'{term}' + '' ) - # pylint: disable=not-callable value = self.translate(translate_func, value) return value @@ -2028,7 +1866,7 @@ class _String(Field[str | typing.Literal[False]]): return value def get_translation_dictionary(self, from_lang_value, to_lang_values): - """Build a dictionary from terms in from_lang_value to terms in to_lang_values + """ Build a dictionary from terms in from_lang_value to terms in to_lang_values :param str from_lang_value: from xml/html :param dict to_lang_values: {lang: lang_value} @@ -2041,9 +1879,7 @@ class _String(Field[str | typing.Literal[False]]): dictionary = defaultdict(lambda: defaultdict(dict)) if not from_lang_terms: return dictionary - dictionary.update( - {from_lang_term: defaultdict(dict) for from_lang_term in from_lang_terms} - ) + dictionary.update({from_lang_term: defaultdict(dict) for from_lang_term in from_lang_terms}) for lang, to_lang_value in to_lang_values.items(): to_lang_terms = self.get_trans_terms(to_lang_value) @@ -2062,26 +1898,24 @@ class _String(Field[str | typing.Literal[False]]): # assert (self.translate and self.store and record) record.flush_recordset([self.name]) cr = record.env.cr - cr.execute( - SQL( - "SELECT %s FROM %s WHERE id = %s", - SQL.identifier(self.name), - SQL.identifier(record._table), - record.id, - ) - ) + cr.execute(SQL( + "SELECT %s FROM %s WHERE id = %s", + SQL.identifier(self.name), + SQL.identifier(record._table), + record.id, + )) res = cr.fetchone() return res[0] if res else None def get_translation_fallback_langs(self, env): - lang = (env.lang or "en_US") if self.translate is True else env._lang - if lang == "_en_US": - return "_en_US", "en_US" - if lang == "en_US": - return ("en_US",) - if lang.startswith("_"): - return lang, lang[1:], "_en_US", "en_US" - return lang, "en_US" + lang = (env.lang or 'en_US') if self.translate is True else env._lang + if lang == '_en_US': + return '_en_US', 'en_US' + if lang == 'en_US': + return ('en_US',) + if lang.startswith('_'): + return lang, lang[1:], '_en_US', 'en_US' + return lang, 'en_US' def write(self, records, value): if not self.translate or value is False or value is None: @@ -2099,20 +1933,11 @@ class _String(Field[str | typing.Literal[False]]): dirty_records.flush_recordset([self.name]) dirty = self.store and any(records._ids) - lang = ( - (records.env.lang or "en_US") - if self.translate is True - else records.env._lang - ) + lang = (records.env.lang or 'en_US') if self.translate is True else records.env._lang # not dirty fields if not dirty: - cache.update_raw( - records, - self, - [{lang: cache_value} for _id in records._ids], - dirty=False, - ) + cache.update_raw(records, self, [{lang: cache_value} for _id in records._ids], dirty=False) return # model translation @@ -2121,112 +1946,78 @@ class _String(Field[str | typing.Literal[False]]): clean_records = records - cache.get_dirty_records(records, self) clean_records.invalidate_recordset([self.name]) cache.update(records, self, itertools.repeat(cache_value), dirty=True) - if lang != "en_US" and not records.env["res.lang"]._get_data(code="en_US"): + if lang != 'en_US' and not records.env['res.lang']._get_data(code='en_US'): # if 'en_US' is not active, we always write en_US to make sure value_en is meaningful - cache.update( - records.with_context(lang="en_US"), - self, - itertools.repeat(cache_value), - dirty=True, - ) + cache.update(records.with_context(lang='en_US'), self, itertools.repeat(cache_value), dirty=True) return # model term translation new_translations_list = [] new_terms = set(self.get_trans_terms(cache_value)) - delay_translations = records.env.context.get("delay_translations") + delay_translations = records.env.context.get('delay_translations') for record in records: # shortcut when no term needs to be translated if not new_terms: - new_translations_list.append({"en_US": cache_value, lang: cache_value}) + new_translations_list.append({'en_US': cache_value, lang: cache_value}) continue # _get_stored_translations can be refactored and prefetches translations for multi records, # but it is really rare to write the same non-False/None/no-term value to multi records stored_translations = self._get_stored_translations(record) if not stored_translations: - new_translations_list.append({"en_US": cache_value, lang: cache_value}) + new_translations_list.append({'en_US': cache_value, lang: cache_value}) continue old_translations = { - k: stored_translations.get(f"_{k}", v) + k: stored_translations.get(f'_{k}', v) for k, v in stored_translations.items() - if not k.startswith("_") + if not k.startswith('_') } - from_lang_value = old_translations.pop(lang, old_translations["en_US"]) - translation_dictionary = self.get_translation_dictionary( - from_lang_value, old_translations - ) + from_lang_value = old_translations.pop(lang, old_translations['en_US']) + translation_dictionary = self.get_translation_dictionary(from_lang_value, old_translations) text2terms = defaultdict(list) for term in new_terms: text2terms[self.get_text_content(term)].append(term) - is_text = ( - self.translate.is_text - if hasattr(self.translate, "is_text") - else lambda term: True - ) - term_adapter = ( - self.translate.term_adapter - if hasattr(self.translate, "term_adapter") - else None - ) + is_text = self.translate.is_text if hasattr(self.translate, 'is_text') else lambda term: True + term_adapter = self.translate.term_adapter if hasattr(self.translate, 'term_adapter') else None for old_term in list(translation_dictionary.keys()): if old_term not in new_terms: old_term_text = self.get_text_content(old_term) matches = get_close_matches(old_term_text, text2terms, 1, 0.9) if matches: - closest_term = get_close_matches( - old_term, text2terms[matches[0]], 1, 0 - )[0] + closest_term = get_close_matches(old_term, text2terms[matches[0]], 1, 0)[0] if closest_term in translation_dictionary: continue old_is_text = is_text(old_term) closest_is_text = is_text(closest_term) if old_is_text or not closest_is_text: - if ( - not closest_is_text - and records.env.context.get("install_mode") - and lang == "en_US" - and term_adapter - ): + if not closest_is_text and records.env.context.get("install_mode") and lang == 'en_US' and term_adapter: adapter = term_adapter(closest_term) - translation_dictionary[closest_term] = { - k: adapter(v) - for k, v in translation_dictionary.pop( - old_term - ).items() - } + translation_dictionary[closest_term] = {k: adapter(v) for k, v in translation_dictionary.pop(old_term).items()} else: - translation_dictionary[closest_term] = ( - translation_dictionary.pop(old_term) - ) + translation_dictionary[closest_term] = translation_dictionary.pop(old_term) # pylint: disable=not-callable new_translations = { - l: self.translate( - lambda term: translation_dictionary.get(term, {l: None})[l], - cache_value, - ) + l: self.translate(lambda term: translation_dictionary.get(term, {l: None})[l], cache_value) for l in old_translations.keys() } if delay_translations: new_store_translations = stored_translations - new_store_translations.update( - {f"_{k}": v for k, v in new_translations.items()} - ) - new_store_translations.pop(f"_{lang}", None) + new_store_translations.update({f'_{k}': v for k, v in new_translations.items()}) + new_store_translations.pop(f'_{lang}', None) else: new_store_translations = new_translations new_store_translations[lang] = cache_value - if not records.env["res.lang"]._get_data(code="en_US"): - new_store_translations["en_US"] = cache_value - new_store_translations.pop("_en_US", None) + if not records.env['res.lang']._get_data(code='en_US'): + new_store_translations['en_US'] = cache_value + new_store_translations.pop('_en_US', None) new_translations_list.append(new_store_translations) # Maybe we can use Cache.update(records.with_context(cache_update_raw=True), self, new_translations_list, dirty=True) cache.update_raw(records, self, new_translations_list, dirty=True) class Char(_String): - """Basic string field, can be length-limited, usually displayed as a + """ Basic string field, can be length-limited, usually displayed as a single-line string in clients. :param int size: the maximum size of values stored for that field @@ -2241,40 +2032,36 @@ class Char(_String): translation of terms. :type translate: bool or callable """ - - type = "char" - trim = True # whether value is trimmed (only by web client) + type = 'char' + trim = True # whether value is trimmed (only by web client) def _setup_attrs(self, model_class, name): super()._setup_attrs(model_class, name) - assert self.size is None or isinstance( - self.size, int - ), "Char field %s with non-integer size %r" % (self, self.size) + assert self.size is None or isinstance(self.size, int), \ + "Char field %s with non-integer size %r" % (self, self.size) @property def _column_type(self): - return ("varchar", pg_varchar(self.size)) + return ('varchar', pg_varchar(self.size)) def update_db_column(self, model, column): if ( - column - and self.column_type[0] == "varchar" - and column["udt_name"] == "varchar" - and column["character_maximum_length"] - and (self.size is None or column["character_maximum_length"] < self.size) + column and self.column_type[0] == 'varchar' and + column['udt_name'] == 'varchar' and column['character_maximum_length'] and + (self.size is None or column['character_maximum_length'] < self.size) ): # the column's varchar size does not match self.size; convert it sql.convert_column(model._cr, model._table, self.name, self.column_type[1]) super().update_db_column(model, column) - _related_size = property(attrgetter("size")) - _related_trim = property(attrgetter("trim")) - _description_size = property(attrgetter("size")) - _description_trim = property(attrgetter("trim")) + _related_size = property(attrgetter('size')) + _related_trim = property(attrgetter('trim')) + _description_size = property(attrgetter('size')) + _description_trim = property(attrgetter('trim')) class Text(_String): - """Very similar to :class:`Char` but used for longer contents, does not + """ Very similar to :class:`Char` but used for longer contents, does not have a size and usually displayed as a multiline text box. :param translate: enable the translation of the field's values; use @@ -2284,13 +2071,12 @@ class Text(_String): translation of terms. :type translate: bool or callable """ - - type = "text" - _column_type = ("text", "text") + type = 'text' + _column_type = ('text', 'text') class Html(_String): - """Encapsulates an html code content. + """ Encapsulates an html code content. :param bool sanitize: whether value must be sanitized (default: ``True``) :param bool sanitize_overridable: whether the sanitation can be bypassed by @@ -2306,24 +2092,19 @@ class Html(_String): (removed and therefore not sanitized, default: ``False``) :param bool strip_classes: whether to strip classes attributes (default: ``False``) """ + type = 'html' + _column_type = ('text', 'text') - type = "html" - _column_type = ("text", "text") - - sanitize = True # whether value must be sanitized - sanitize_overridable = False # whether the sanitation can be bypassed by the users part of the `base.group_sanitize_override` group - sanitize_tags = ( - True # whether to sanitize tags (only a white list of attributes is accepted) - ) - sanitize_attributes = True # whether to sanitize attributes (only a white list of attributes is accepted) - sanitize_style = False # whether to sanitize style attributes - sanitize_form = True # whether to sanitize forms + sanitize = True # whether value must be sanitized + sanitize_overridable = False # whether the sanitation can be bypassed by the users part of the `base.group_sanitize_override` group + sanitize_tags = True # whether to sanitize tags (only a white list of attributes is accepted) + sanitize_attributes = True # whether to sanitize attributes (only a white list of attributes is accepted) + sanitize_style = False # whether to sanitize style attributes + sanitize_form = True # whether to sanitize forms sanitize_conditional_comments = True # whether to kill conditional comments. Otherwise keep them but with their content sanitized. - sanitize_output_method = "html" # whether to sanitize using html or xhtml - strip_style = ( - False # whether to strip style attributes (removed and therefore not sanitized) - ) - strip_classes = False # whether to strip classes attributes + sanitize_output_method = 'html' # whether to sanitize using html or xhtml + strip_style = False # whether to strip style attributes (removed and therefore not sanitized) + strip_classes = False # whether to strip classes attributes def _get_attrs(self, model_class, name): # called by _setup_attrs(), working together with _String._setup_attrs() @@ -2333,41 +2114,35 @@ class Html(_String): # e.g. conditional comments: no need to keep conditional comments for incoming emails, # we do not need this Microsoft Outlook client feature for emails displayed Odoo's web client. # While we need to keep them in mail templates and mass mailings, because they could be rendered in Outlook. - if attrs.get("sanitize") == "email_outgoing": - attrs["sanitize"] = True - attrs.update( - { - key: value - for key, value in { - "sanitize_tags": False, - "sanitize_attributes": False, - "sanitize_conditional_comments": False, - "sanitize_output_method": "xml", - }.items() - if key not in attrs - } - ) + if attrs.get('sanitize') == 'email_outgoing': + attrs['sanitize'] = True + attrs.update({key: value for key, value in { + 'sanitize_tags': False, + 'sanitize_attributes': False, + 'sanitize_conditional_comments': False, + 'sanitize_output_method': 'xml', + }.items() if key not in attrs}) # Translated sanitized html fields must use html_translate or a callable. # `elif` intended, because HTML fields with translate=True and sanitize=False # where not using `html_translate` before and they must remain without `html_translate`. # Otherwise, breaks `--test-tags .test_render_field`, for instance. - elif attrs.get("translate") is True and attrs.get("sanitize", True): - attrs["translate"] = html_translate + elif attrs.get('translate') is True and attrs.get('sanitize', True): + attrs['translate'] = html_translate return attrs - _related_sanitize = property(attrgetter("sanitize")) - _related_sanitize_tags = property(attrgetter("sanitize_tags")) - _related_sanitize_attributes = property(attrgetter("sanitize_attributes")) - _related_sanitize_style = property(attrgetter("sanitize_style")) - _related_strip_style = property(attrgetter("strip_style")) - _related_strip_classes = property(attrgetter("strip_classes")) + _related_sanitize = property(attrgetter('sanitize')) + _related_sanitize_tags = property(attrgetter('sanitize_tags')) + _related_sanitize_attributes = property(attrgetter('sanitize_attributes')) + _related_sanitize_style = property(attrgetter('sanitize_style')) + _related_strip_style = property(attrgetter('strip_style')) + _related_strip_classes = property(attrgetter('strip_classes')) - _description_sanitize = property(attrgetter("sanitize")) - _description_sanitize_tags = property(attrgetter("sanitize_tags")) - _description_sanitize_attributes = property(attrgetter("sanitize_attributes")) - _description_sanitize_style = property(attrgetter("sanitize_style")) - _description_strip_style = property(attrgetter("strip_style")) - _description_strip_classes = property(attrgetter("strip_classes")) + _description_sanitize = property(attrgetter('sanitize')) + _description_sanitize_tags = property(attrgetter('sanitize_tags')) + _description_sanitize_attributes = property(attrgetter('sanitize_attributes')) + _description_sanitize_style = property(attrgetter('sanitize_style')) + _description_strip_style = property(attrgetter('strip_style')) + _description_strip_classes = property(attrgetter('strip_classes')) def convert_to_column(self, value, record, values=None, validate=True): value = self._convert(value, record, validate=True) @@ -2384,27 +2159,25 @@ class Html(_String): return value sanitize_vals = { - "silent": True, - "sanitize_tags": self.sanitize_tags, - "sanitize_attributes": self.sanitize_attributes, - "sanitize_style": self.sanitize_style, - "sanitize_form": self.sanitize_form, - "sanitize_conditional_comments": self.sanitize_conditional_comments, - "output_method": self.sanitize_output_method, - "strip_style": self.strip_style, - "strip_classes": self.strip_classes, + 'silent': True, + 'sanitize_tags': self.sanitize_tags, + 'sanitize_attributes': self.sanitize_attributes, + 'sanitize_style': self.sanitize_style, + 'sanitize_form': self.sanitize_form, + 'sanitize_conditional_comments': self.sanitize_conditional_comments, + 'output_method': self.sanitize_output_method, + 'strip_style': self.strip_style, + 'strip_classes': self.strip_classes } if self.sanitize_overridable: - if record.env.user.has_group("base.group_sanitize_override"): + if record.env.user.has_group('base.group_sanitize_override'): return value original_value = record[self.name] if original_value: # Note that sanitize also normalize - original_value_sanitized = html_sanitize( - original_value, **sanitize_vals - ) + original_value_sanitized = html_sanitize(original_value, **sanitize_vals) original_value_normalized = html_normalize(original_value) if ( @@ -2421,32 +2194,23 @@ class Html(_String): original_value_normalized.splitlines(), ) - with_colors = isinstance( - logging.getLogger().handlers[0].formatter, ColoredFormatter - ) - diff_str = f"The field ({record._description}, {self.string}) will not be editable:\n" + with_colors = isinstance(logging.getLogger().handlers[0].formatter, ColoredFormatter) + diff_str = f'The field ({record._description}, {self.string}) will not be editable:\n' for line in list(diff)[2:]: if with_colors: - color = {"-": RED, "+": GREEN}.get(line[:1], DEFAULT) - diff_str += COLOR_PATTERN % ( - 30 + color, - 40 + DEFAULT, - line.rstrip() + "\n", - ) + color = {'-': RED, '+': GREEN}.get(line[:1], DEFAULT) + diff_str += COLOR_PATTERN % (30 + color, 40 + DEFAULT, line.rstrip() + "\n") else: - diff_str += line.rstrip() + "\n" + diff_str += line.rstrip() + '\n' _logger.info(diff_str) - raise UserError( - record.env._( - "The field value you're saving (%(model)s %(field)s) includes content that is " - "restricted for security reasons. It is possible that someone " - "with higher privileges previously modified it, and you are therefore " - "not able to modify it yourself while preserving the content.", - model=record._description, - field=self.string, - ) - ) + raise UserError(record.env._( + "The field value you're saving (%(model)s %(field)s) includes content that is " + "restricted for security reasons. It is possible that someone " + "with higher privileges previously modified it, and you are therefore " + "not able to modify it yourself while preserving the content.", + model=record._description, field=self.string, + )) return html_sanitize(value, **sanitize_vals) @@ -2468,10 +2232,9 @@ class Html(_String): class Date(Field[date | typing.Literal[False]]): - """Encapsulates a python :class:`date ` object.""" - - type = "date" - _column_type = ("date", "date") + """ Encapsulates a python :class:`date ` object. """ + type = 'date' + _column_type = ('date', 'date') start_of = staticmethod(date_utils.start_of) end_of = staticmethod(date_utils.end_of) @@ -2501,18 +2264,14 @@ class Date(Field[date | typing.Literal[False]]): """ today = timestamp or datetime.now() context_today = None - tz_name = record._context.get("tz") or record.env.user.tz + tz_name = record._context.get('tz') or record.env.user.tz if tz_name: try: - today_utc = pytz.timezone("UTC").localize( - today, is_dst=False - ) # UTC = no DST + today_utc = pytz.timezone('UTC').localize(today, is_dst=False) # UTC = no DST context_today = today_utc.astimezone(pytz.timezone(tz_name)) except Exception: - _logger.debug( - "failed to compute context/client-specific today date, using UTC value for `today`", - exc_info=True, - ) + _logger.debug("failed to compute context/client-specific today date, using UTC value for `today`", + exc_info=True) return (context_today or today).date() @staticmethod @@ -2571,7 +2330,7 @@ class Date(Field[date | typing.Literal[False]]): def convert_to_export(self, value, record): if not value: - return "" + return '' return self.from_string(value) def convert_to_display_name(self, value, record): @@ -2579,10 +2338,9 @@ class Date(Field[date | typing.Literal[False]]): class Datetime(Field[datetime | typing.Literal[False]]): - """Encapsulates a python :class:`datetime ` object.""" - - type = "datetime" - _column_type = ("timestamp", "timestamp") + """ Encapsulates a python :class:`datetime ` object. """ + type = 'datetime' + _column_type = ('timestamp', 'timestamp') start_of = staticmethod(date_utils.start_of) end_of = staticmethod(date_utils.end_of) @@ -2618,19 +2376,17 @@ class Datetime(Field[datetime | typing.Literal[False]]): :return: timestamp converted to timezone-aware datetime in context timezone. :rtype: datetime """ - assert isinstance(timestamp, datetime), "Datetime instance expected" - tz_name = record._context.get("tz") or record.env.user.tz + assert isinstance(timestamp, datetime), 'Datetime instance expected' + tz_name = record._context.get('tz') or record.env.user.tz utc_timestamp = pytz.utc.localize(timestamp, is_dst=False) # UTC = no DST if tz_name: try: context_tz = pytz.timezone(tz_name) return utc_timestamp.astimezone(context_tz) except Exception: - _logger.debug( - "failed to compute context/client-specific timestamp, " - "using the UTC value", - exc_info=True, - ) + _logger.debug("failed to compute context/client-specific timestamp, " + "using the UTC value", + exc_info=True) return utc_timestamp @staticmethod @@ -2647,14 +2403,12 @@ class Datetime(Field[datetime | typing.Literal[False]]): if isinstance(value, date): if isinstance(value, datetime): if value.tzinfo: - raise ValueError( - "Datetime field expects a naive datetime: %s" % value - ) + raise ValueError("Datetime field expects a naive datetime: %s" % value) return value return datetime.combine(value, time.min) # TODO: fix data files - return datetime.strptime(value, DATETIME_FORMAT[: len(value) - 2]) + return datetime.strptime(value, DATETIME_FORMAT[:len(value)-2]) # kept for backwards compatibility, but consider `from_string` as deprecated, will probably # be removed after V12 @@ -2683,7 +2437,7 @@ class Datetime(Field[datetime | typing.Literal[False]]): def convert_to_export(self, value, record): if not value: - return "" + return '' value = self.convert_to_display_name(value, record) return self.from_string(value) @@ -2692,7 +2446,6 @@ class Datetime(Field[datetime | typing.Literal[False]]): return False return Datetime.to_string(Datetime.context_timestamp(record, value)) - # http://initd.org/psycopg/docs/usage.html#binary-adaptation # Received data is returned as buffer (in Python 2) or memoryview (in Python 3). _BINARY = memoryview @@ -2704,24 +2457,23 @@ class Binary(Field): :param bool attachment: whether the field should be stored as `ir_attachment` or in a column of the model's table (default: ``True``). """ + type = 'binary' - type = "binary" - - prefetch = False # not prefetched by default - _depends_context = ("bin_size",) # depends on context (content or size) - attachment = True # whether value is stored in attachment + prefetch = False # not prefetched by default + _depends_context = ('bin_size',) # depends on context (content or size) + attachment = True # whether value is stored in attachment @lazy_property def column_type(self): - return None if self.attachment else ("bytea", "bytea") + return None if self.attachment else ('bytea', 'bytea') def _get_attrs(self, model_class, name): attrs = super()._get_attrs(model_class, name) - if not attrs.get("store", True): - attrs["attachment"] = False + if not attrs.get('store', True): + attrs['attachment'] = False return attrs - _description_attachment = property(attrgetter("attachment")) + _description_attachment = property(attrgetter('attachment')) def convert_to_column(self, value, record, values=None, validate=True): # Binary values may be byte strings (python 2.6 byte array), but @@ -2735,36 +2487,26 @@ class Binary(Field): # Detect if the binary content is an SVG for restricting its upload # only to system users. magic_bytes = { - b"P", # first 6 bits of '<' (0x3C) b64 encoded - b"<", # plaintext XML tag opening + b'P', # first 6 bits of '<' (0x3C) b64 encoded + b'<', # plaintext XML tag opening } if isinstance(value, str): value = value.encode() if value[:1] in magic_bytes: try: - decoded_value = base64.b64decode( - value.translate(None, delete=b"\r\n"), validate=True - ) + decoded_value = base64.b64decode(value.translate(None, delete=b'\r\n'), validate=True) except binascii.Error: decoded_value = value # Full mimetype detection - if ( - guess_mimetype(decoded_value).startswith("image/svg") - and not record.env.is_system() - ): + if (guess_mimetype(decoded_value).startswith('image/svg') and + not record.env.is_system()): raise UserError(record.env._("Only admins can upload SVG files.")) if isinstance(value, bytes): return psycopg2.Binary(value) try: - return psycopg2.Binary(str(value).encode("ascii")) + return psycopg2.Binary(str(value).encode('ascii')) except UnicodeEncodeError: - raise UserError( - record.env._( - "ASCII characters are required for %(value)s in %(field)s", - value=value, - field=self.name, - ) - ) + raise UserError(record.env._("ASCII characters are required for %(value)s in %(field)s", value=value, field=self.name)) def convert_to_cache(self, value, record, validate=True): if isinstance(value, _BINARY): @@ -2773,10 +2515,9 @@ class Binary(Field): # the cache must contain bytes or memoryview, but sometimes a string # is given when assigning a binary field (test `TestFileSeparator`) return value.encode() - if isinstance(value, int) and ( - record._context.get("bin_size") - or record._context.get("bin_size_" + self.name) - ): + if isinstance(value, int) and \ + (record._context.get('bin_size') or + record._context.get('bin_size_' + self.name)): # If the client requests only the size of the field, we return that # instead of the content. Presumably a separate request will be done # to read the actual content, if necessary. @@ -2791,14 +2532,10 @@ class Binary(Field): return False if value is None else value def compute_value(self, records): - bin_size_name = "bin_size_" + self.name - if records.env.context.get("bin_size") or records.env.context.get( - bin_size_name - ): + bin_size_name = 'bin_size_' + self.name + if records.env.context.get('bin_size') or records.env.context.get(bin_size_name): # always compute without bin_size - records_no_bin_size = records.with_context( - **{"bin_size": False, bin_size_name: False} - ) + records_no_bin_size = records.with_context(**{'bin_size': False, bin_size_name: False}) super().compute_value(records_no_bin_size) # manually update the bin_size cache cache = records.env.cache @@ -2812,7 +2549,7 @@ class Binary(Field): try: if isinstance(value, (bytes, _BINARY)): value = human_size(len(value)) - except TypeError: + except (TypeError): pass cache_value = self.convert_to_cache(value, record) # the dirty flag is independent from this assignment @@ -2826,14 +2563,14 @@ class Binary(Field): # values are stored in attachments, retrieve them assert self.attachment domain = [ - ("res_model", "=", records._name), - ("res_field", "=", self.name), - ("res_id", "in", records.ids), + ('res_model', '=', records._name), + ('res_field', '=', self.name), + ('res_id', 'in', records.ids), ] # Note: the 'bin_size' flag is handled by the field 'datas' itself data = { att.res_id: att.datas - for att in records.env["ir.attachment"].sudo().search(domain) + for att in records.env['ir.attachment'].sudo().search(domain) } records.env.cache.insert_missing(records, self, map(data.get, records._ids)) @@ -2843,20 +2580,18 @@ class Binary(Field): return # create the attachments that store the values env = record_values[0][0].env - env["ir.attachment"].sudo().create( - [ - { - "name": self.name, - "res_model": self.model_name, - "res_field": self.name, - "res_id": record.id, - "type": "binary", - "datas": value, - } - for record, value in record_values - if value - ] - ) + env['ir.attachment'].sudo().create([ + { + 'name': self.name, + 'res_model': self.model_name, + 'res_field': self.name, + 'res_id': record.id, + 'type': 'binary', + 'datas': value, + } + for record, value in record_values + if value + ]) def write(self, records, value): records = records.with_context(bin_size=False) @@ -2881,36 +2616,31 @@ class Binary(Field): # retrieve the attachments that store the values, and adapt them if self.store and any(records._ids): - real_records = records.filtered("id") - atts = records.env["ir.attachment"].sudo() + real_records = records.filtered('id') + atts = records.env['ir.attachment'].sudo() if not_null: - atts = atts.search( - [ - ("res_model", "=", self.model_name), - ("res_field", "=", self.name), - ("res_id", "in", real_records.ids), - ] - ) + atts = atts.search([ + ('res_model', '=', self.model_name), + ('res_field', '=', self.name), + ('res_id', 'in', real_records.ids), + ]) if value: # update the existing attachments - atts.write({"datas": value}) - atts_records = records.browse(atts.mapped("res_id")) + atts.write({'datas': value}) + atts_records = records.browse(atts.mapped('res_id')) # create the missing attachments - missing = real_records - atts_records + missing = (real_records - atts_records) if missing: - atts.create( - [ - { - "name": self.name, - "res_model": record._name, - "res_field": self.name, - "res_id": record.id, - "type": "binary", - "datas": value, - } - for record in missing - ] - ) + atts.create([{ + 'name': self.name, + 'res_model': record._name, + 'res_field': self.name, + 'res_id': record.id, + 'type': 'binary', + 'datas': value, + } + for record in missing + ]) else: atts.unlink() @@ -2932,7 +2662,6 @@ class Image(Binary): If no ``max_width``/``max_height`` is specified (or is set to 0) and ``verify_resolution`` is False, the field content won't be verified at all and a :class:`Binary` field should be used. """ - max_width = 0 max_height = 0 verify_resolution = True @@ -2940,9 +2669,7 @@ class Image(Binary): def setup(self, model): super().setup(model) if not model._abstract and not model._log_access: - warnings.warn( - f"Image field {self} requires the model to have _log_access = True" - ) + warnings.warn(f"Image field {self} requires the model to have _log_access = True") def create(self, record_values): new_record_values = [] @@ -2952,9 +2679,7 @@ class Image(Binary): # when setting related image field, keep the unprocessed image in # cache to let the inverse method use the original image; the image # will be resized once the inverse has been applied - cache_value = self.convert_to_cache( - value if self.related else new_value, record - ) + cache_value = self.convert_to_cache(value if self.related else new_value, record) record.env.cache.update(record, self, itertools.repeat(cache_value)) super(Image, self).create(new_record_values) @@ -2972,13 +2697,9 @@ class Image(Binary): raise super(Image, self).write(records, new_value) - cache_value = self.convert_to_cache( - value if self.related else new_value, records - ) + cache_value = self.convert_to_cache(value if self.related else new_value, records) dirty = self.column_type and self.store and any(records._ids) - records.env.cache.update( - records, self, itertools.repeat(cache_value), dirty=dirty - ) + records.env.cache.update(records, self, itertools.repeat(cache_value), dirty=dirty) def _inverse_related(self, records): super()._inverse_related(records) @@ -2988,42 +2709,34 @@ class Image(Binary): # cache with the resized value for record in records: value = self._process_related(record[self.name], record.env) - record.env.cache.set( - record, self, value, dirty=(self.store and self.column_type) - ) + record.env.cache.set(record, self, value, dirty=(self.store and self.column_type)) def _image_process(self, value, env): if self.readonly and not self.max_width and not self.max_height: # no need to process images for computed fields, or related fields return value try: - img = base64.b64decode(value or "") or False + img = base64.b64decode(value or '') or False except: raise UserError(env._("Image is not encoded in base64.")) - if img and guess_mimetype(img, "") == "image/webp": + if img and guess_mimetype(img, '') == 'image/webp': if not self.max_width and not self.max_height: return value # Fetch resized version. - Attachment = env["ir.attachment"] + Attachment = env['ir.attachment'] checksum = Attachment._compute_checksum(img) - origins = Attachment.search( - [ - ["id", "!=", False], # No implicit condition on res_field. - ["checksum", "=", checksum], - ] - ) + origins = Attachment.search([ + ['id', '!=', False], # No implicit condition on res_field. + ['checksum', '=', checksum], + ]) if origins: origin_ids = [attachment.id for attachment in origins] resized_domain = [ - ["id", "!=", False], # No implicit condition on res_field. - ["res_model", "=", "ir.attachment"], - ["res_id", "in", origin_ids], - [ - "description", - "=", - "resize: %s" % max(self.max_width, self.max_height), - ], + ['id', '!=', False], # No implicit condition on res_field. + ['res_model', '=', 'ir.attachment'], + ['res_id', 'in', origin_ids], + ['description', '=', 'resize: %s' % max(self.max_width, self.max_height)], ] resized = Attachment.sudo().search(resized_domain, limit=1) if resized: @@ -3031,17 +2744,10 @@ class Image(Binary): return resized.datas or value return value - return ( - base64.b64encode( - image_process( - img, - size=(self.max_width, self.max_height), - verify_resolution=self.verify_resolution, - ) - or b"" - ) - or False - ) + return base64.b64encode(image_process(img, + size=(self.max_width, self.max_height), + verify_resolution=self.verify_resolution, + ) or b'') or False def _process_related(self, value, env): """Override to resize the related value before saving it on self.""" @@ -3054,7 +2760,7 @@ class Image(Binary): class Selection(Field[str | typing.Literal[False]]): - """Encapsulates an exclusive choice between different values. + """ Encapsulates an exclusive choice between different values. :param selection: specifies the possible values for this field. It is given as either a list of pairs ``(value, label)``, or a model @@ -3095,13 +2801,12 @@ class Selection(Field[str | typing.Literal[False]]): The attribute ``selection`` is mandatory except in the case of ``related`` or extended fields. """ + type = 'selection' + _column_type = ('varchar', pg_varchar()) - type = "selection" - _column_type = ("varchar", pg_varchar()) - - selection = None # [(value, string), ...], function or method name - validate = True # whether validating upon write - ondelete = None # {value: policy} (what to do when value is deleted) + selection = None # [(value, string), ...], function or method name + validate = True # whether validating upon write + ondelete = None # {value: policy} (what to do when value is deleted) def __init__(self, selection=SENTINEL, string: str | Sentinel = SENTINEL, **kwargs): super(Selection, self).__init__(selection=selection, string=string, **kwargs) @@ -3121,10 +2826,10 @@ class Selection(Field[str | typing.Literal[False]]): def _get_attrs(self, model_class, name): attrs = super()._get_attrs(model_class, name) # arguments 'selection' and 'selection_add' are processed below - attrs.pop("selection_add", None) + attrs.pop('selection_add', None) # Selection fields have an optional default implementation of a group_expand function - if attrs.get("group_expand") is True: - attrs["group_expand"] = self._default_group_expand + if attrs.get('group_expand') is True: + attrs['group_expand'] = self._default_group_expand return attrs def _setup_attrs(self, model_class, name): @@ -3138,22 +2843,13 @@ class Selection(Field[str | typing.Literal[False]]): for field in self._base_fields: # We cannot use field.selection or field.selection_add here # because those attributes are overridden by ``_setup_attrs``. - if "selection" in field.args: + if 'selection' in field.args: if self.related: - _logger.warning( - "%s: selection attribute will be ignored as the field is related", - self, - ) - selection = field.args["selection"] + _logger.warning("%s: selection attribute will be ignored as the field is related", self) + selection = field.args['selection'] if isinstance(selection, list): - if values is not None and list(values) != [ - kv[0] for kv in selection - ]: - _logger.warning( - "%s: selection=%r overrides existing selection; use selection_add instead", - self, - selection, - ) + if values is not None and list(values) != [kv[0] for kv in selection]: + _logger.warning("%s: selection=%r overrides existing selection; use selection_add instead", self, selection) values = dict(selection) self.ondelete = {} else: @@ -3161,32 +2857,21 @@ class Selection(Field[str | typing.Literal[False]]): self.selection = selection self.ondelete = None - if "selection_add" in field.args: + if 'selection_add' in field.args: if self.related: - _logger.warning( - "%s: selection_add attribute will be ignored as the field is related", - self, - ) - selection_add = field.args["selection_add"] - assert isinstance( - selection_add, list - ), "%s: selection_add=%r must be a list" % (self, selection_add) - assert ( - values is not None - ), "%s: selection_add=%r on non-list selection %r" % ( - self, - selection_add, - self.selection, - ) + _logger.warning("%s: selection_add attribute will be ignored as the field is related", self) + selection_add = field.args['selection_add'] + assert isinstance(selection_add, list), \ + "%s: selection_add=%r must be a list" % (self, selection_add) + assert values is not None, \ + "%s: selection_add=%r on non-list selection %r" % (self, selection_add, self.selection) - values_add = { - kv[0]: (kv[1] if len(kv) > 1 else None) for kv in selection_add - } - ondelete = field.args.get("ondelete") or {} + values_add = {kv[0]: (kv[1] if len(kv) > 1 else None) for kv in selection_add} + ondelete = field.args.get('ondelete') or {} new_values = [key for key in values_add if key not in values] for key in new_values: - ondelete.setdefault(key, "set null") - if self.required and new_values and "set null" in ondelete.values(): + ondelete.setdefault(key, 'set null') + if self.required and new_values and 'set null' in ondelete.values(): raise ValueError( "%r: required selection fields must define an ondelete policy that " "implements the proper cleanup of the corresponding records upon " @@ -3198,15 +2883,15 @@ class Selection(Field[str | typing.Literal[False]]): # check ondelete values for key, val in ondelete.items(): - if callable(val) or val in ("set null", "cascade"): + if callable(val) or val in ('set null', 'cascade'): continue - if val == "set default": + if val == 'set default': assert self.default is not None, ( "%r: ondelete policy of type 'set default' is invalid for this field " "as it does not define a default! Either define one in the base " "field, or change the chosen ondelete policy" % self ) - elif val.startswith("set "): + elif val.startswith('set '): assert val[4:] in values, ( "%s: ondelete policy of type 'set %%' must be either 'set null', " "'set default', or 'set value' where value is a valid selection value." @@ -3226,37 +2911,34 @@ class Selection(Field[str | typing.Literal[False]]): if values is not None: self.selection = list(values.items()) - assert all(isinstance(key, str) for key in values), ( + assert all(isinstance(key, str) for key in values), \ "Field %s with non-str value in selection" % self - ) self._selection = values def _selection_modules(self, model): - """Return a mapping from selection values to modules defining each value.""" + """ Return a mapping from selection values to modules defining each value. """ if not isinstance(self.selection, list): return {} value_modules = defaultdict(set) - for field in reversed( - resolve_mro(model, self.name, type(self).__instancecheck__) - ): + for field in reversed(resolve_mro(model, self.name, type(self).__instancecheck__)): module = field._module if not module: continue - if "selection" in field.args: + if 'selection' in field.args: value_modules.clear() - if isinstance(field.args["selection"], list): - for value, label in field.args["selection"]: + if isinstance(field.args['selection'], list): + for value, label in field.args['selection']: value_modules[value].add(module) - if "selection_add" in field.args: - for value_label in field.args["selection_add"]: + if 'selection_add' in field.args: + for value_label in field.args['selection_add']: if len(value_label) > 1: value_modules[value_label[0]].add(module) return value_modules def _description_selection(self, env): - """return the selection list (pairs (value, label)); labels are - translated according to context language + """ return the selection list (pairs (value, label)); labels are + translated according to context language """ selection = self.selection if isinstance(selection, str) or callable(selection): @@ -3264,9 +2946,7 @@ class Selection(Field[str | typing.Literal[False]]): # translate selection labels if env.lang: - return env["ir.model.fields"].get_field_selection( - self.model_name, self.name - ) + return env['ir.model.fields'].get_field_selection(self.model_name, self.name) else: return selection @@ -3278,9 +2958,7 @@ class Selection(Field[str | typing.Literal[False]]): """Return a list of the possible values.""" selection = self.selection if isinstance(selection, str) or callable(selection): - selection = determine( - selection, env[self.model_name].with_context(lang=None) - ) + selection = determine(selection, env[self.model_name].with_context(lang=None)) return [value for value, _ in selection] def convert_to_column(self, value, record, values=None, validate=True): @@ -3300,23 +2978,22 @@ class Selection(Field[str | typing.Literal[False]]): def convert_to_export(self, value, record): if not isinstance(self.selection, list): # FIXME: this reproduces an existing buggy behavior! - return value if value else "" + return value if value else '' for item in self._description_selection(record.env): if item[0] == value: return item[1] - return "" + return '' class Reference(Selection): - """Pseudo-relational field (no FK in database). + """ Pseudo-relational field (no FK in database). The field value is stored as a :class:`string ` following the pattern ``"res_model,res_id"`` in database. """ + type = 'reference' - type = "reference" - - _column_type = ("varchar", pg_varchar()) + _column_type = ('varchar', pg_varchar()) def convert_to_column(self, value, record, values=None, validate=True): return Field.convert_to_column(self, value, record, values, validate) @@ -3324,12 +3001,10 @@ class Reference(Selection): def convert_to_cache(self, value, record, validate=True): # cache format: str ("model,id") or None if isinstance(value, BaseModel): - if not validate or ( - value._name in self.get_values(record.env) and len(value) <= 1 - ): + if not validate or (value._name in self.get_values(record.env) and len(value) <= 1): return "%s,%s" % (value._name, value.id) if value else None elif isinstance(value, str): - res_model, res_id = value.split(",") + res_model, res_id = value.split(',') if not validate or res_model in self.get_values(record.env): if record.env[res_model].browse(int(res_id)).exists(): return value @@ -3341,7 +3016,7 @@ class Reference(Selection): def convert_to_record(self, value, record): if value: - res_model, res_id = value.split(",") + res_model, res_id = value.split(',') return record.env[res_model].browse(int(res_id)) return None @@ -3349,18 +3024,17 @@ class Reference(Selection): return "%s,%s" % (value._name, value.id) if value else False def convert_to_export(self, value, record): - return value.display_name if value else "" + return value.display_name if value else '' def convert_to_display_name(self, value, record): return value.display_name if value else False class _Relational(Field[M], typing.Generic[M]): - """Abstract class for relational fields.""" - + """ Abstract class for relational fields. """ relational = True - domain: DomainType = [] # domain for searching values - context: ContextType = {} # context for searching values + domain: DomainType = [] # domain for searching values + context: ContextType = {} # context for searching values check_company = False def __get__(self, records, owner=None): @@ -3373,13 +3047,11 @@ class _Relational(Field[M], typing.Generic[M]): def setup_nonrelated(self, model): super().setup_nonrelated(model) if self.comodel_name not in model.pool: - _logger.warning( - "Field %s with unknown comodel_name %r", self, self.comodel_name - ) - self.comodel_name = "_unknown" + _logger.warning("Field %s with unknown comodel_name %r", self, self.comodel_name) + self.comodel_name = '_unknown' def get_domain_list(self, model): - """Return a list domain from the domain parameter.""" + """ Return a list domain from the domain parameter. """ domain = self.domain if callable(domain): domain = domain(model) @@ -3395,59 +3067,49 @@ class _Relational(Field[M], typing.Generic[M]): if callable(self.domain): # will be called with another model than self's - return lambda recs: validated( - self.domain(recs.env[self.model_name]) - ) # pylint: disable=not-callable + return lambda recs: validated(self.domain(recs.env[self.model_name])) # pylint: disable=not-callable else: return validated(self.domain) - _related_context = property(attrgetter("context")) + _related_context = property(attrgetter('context')) - _description_relation = property(attrgetter("comodel_name")) - _description_context = property(attrgetter("context")) + _description_relation = property(attrgetter('comodel_name')) + _description_context = property(attrgetter('context')) def _description_domain(self, env): - domain = ( - self.domain(env[self.model_name]) if callable(self.domain) else self.domain - ) # pylint: disable=not-callable + domain = self.domain(env[self.model_name]) if callable(self.domain) else self.domain # pylint: disable=not-callable if self.check_company: field_to_check = None if self.company_dependent: - cids = "[allowed_company_ids[0]]" - elif self.model_name == "res.company": + cids = '[allowed_company_ids[0]]' + elif self.model_name == 'res.company': # when using check_company=True on a field on 'res.company', the # company_id comes from the id of the current record - cids = "[id]" - elif "company_id" in env[self.model_name]: - cids = "[company_id]" - field_to_check = "company_id" - elif "company_ids" in env[self.model_name]: - cids = "company_ids" - field_to_check = "company_ids" + cids = '[id]' + elif 'company_id' in env[self.model_name]: + cids = '[company_id]' + field_to_check = 'company_id' + elif 'company_ids' in env[self.model_name]: + cids = 'company_ids' + field_to_check = 'company_ids' else: - _logger.warning( - env._( - "Couldn't generate a company-dependent domain for field %s. " - "The model doesn't have a 'company_id' or 'company_ids' field, and isn't company-dependent either.", - f"{self.model_name}.{self.name}", - ) - ) + _logger.warning(env._( + "Couldn't generate a company-dependent domain for field %s. " + "The model doesn't have a 'company_id' or 'company_ids' field, and isn't company-dependent either.", + f'{self.model_name}.{self.name}' + )) return domain - company_domain = env[self.comodel_name]._check_company_domain( - companies=unquote(cids) - ) + company_domain = env[self.comodel_name]._check_company_domain(companies=unquote(cids)) if not field_to_check: return f"{company_domain} + {domain or []}" else: - no_company_domain = env[self.comodel_name]._check_company_domain( - companies="" - ) + no_company_domain = env[self.comodel_name]._check_company_domain(companies='') return f"({field_to_check} and {company_domain} or {no_company_domain}) + ({domain or []})" return domain class Many2one(_Relational[M]): - """The value of such a field is a recordset of size 0 (no + """ The value of such a field is a recordset of size 0 (no record) or 1 (a single record). :param str comodel_name: name of the target model @@ -3477,23 +3139,15 @@ class Many2one(_Relational[M]): Constrains company_dependent fields to target records whose company_id(s) are compatible with the currently active company. """ + type = 'many2one' + _column_type = ('int4', 'int4') - type = "many2one" - _column_type = ("int4", "int4") + ondelete = None # what to do when value is deleted + auto_join = False # whether joins are generated upon search + delegate = False # whether self implements delegation - ondelete = None # what to do when value is deleted - auto_join = False # whether joins are generated upon search - delegate = False # whether self implements delegation - - def __init__( - self, - comodel_name: str | Sentinel = SENTINEL, - string: str | Sentinel = SENTINEL, - **kwargs, - ): - super(Many2one, self).__init__( - comodel_name=comodel_name, string=string, **kwargs - ) + def __init__(self, comodel_name: str | Sentinel = SENTINEL, string: str | Sentinel = SENTINEL, **kwargs): + super(Many2one, self).__init__(comodel_name=comodel_name, string=string, **kwargs) def _setup_attrs(self, model_class, name): super()._setup_attrs(model_class, name) @@ -3517,16 +3171,16 @@ class Many2one(_Relational[M]): # Many2one relations from TransientModel Model are annoying because # they can block deletion due to foreign keys. So unless stated # otherwise, we default them to ondelete='cascade'. - self.ondelete = "cascade" if self.required else "set null" + self.ondelete = 'cascade' if self.required else 'set null' else: - self.ondelete = "restrict" if self.required else "set null" - if self.ondelete == "set null" and self.required: + self.ondelete = 'restrict' if self.required else 'set null' + if self.ondelete == 'set null' and self.required: raise ValueError( "The m2o field %s of model %s is required but declares its ondelete policy " "as being 'set null'. Only 'restrict' and 'cascade' make sense." % (self.name, model._name) ) - if self.ondelete == "restrict" and self.comodel_name in IR_MODELS: + if self.ondelete == 'restrict' and self.comodel_name in IR_MODELS: raise ValueError( f"Field {self.name} of model {model._name} is defined as ondelete='restrict' " f"while having {self.comodel_name} as comodel, the 'restrict' mode is not " @@ -3536,9 +3190,7 @@ class Many2one(_Relational[M]): def update_db(self, model, columns): comodel = model.env[self.comodel_name] if not model.is_transient() and comodel.is_transient(): - raise ValueError( - "Many2one %s from Model to TransientModel is forbidden" % self - ) + raise ValueError('Many2one %s from Model to TransientModel is forbidden' % self) return super(Many2one, self).update_db(model, columns) def update_db_column(self, model, column): @@ -3553,26 +3205,19 @@ class Many2one(_Relational[M]): if not model._is_an_ordinary_table() or not comodel._is_an_ordinary_table(): return # ir_actions is inherited, so foreign key doesn't work on it - if not comodel._auto or comodel._table == "ir_actions": + if not comodel._auto or comodel._table == 'ir_actions': return # create/update the foreign key, and reflect it in 'ir.model.constraint' model.pool.add_foreign_key( - model._table, - self.name, - comodel._table, - "id", - self.ondelete or "set null", - model, - self._module, + model._table, self.name, comodel._table, 'id', self.ondelete or 'set null', + model, self._module ) def _update(self, records, value): - """Update the cached value of ``self`` for ``records`` with ``value``.""" + """ Update the cached value of ``self`` for ``records`` with ``value``. """ cache = records.env.cache for record in records: - cache.set( - record, self, self.convert_to_cache(value, record, validate=False) - ) + cache.set(record, self, self.convert_to_cache(value, record, validate=False)) def convert_to_column(self, value, record, values=None, validate=True): return value or None @@ -3591,7 +3236,7 @@ class Many2one(_Relational[M]): elif isinstance(value, dict): # return a new record (with the given field 'id' as origin) comodel = record.env[self.comodel_name] - origin = comodel.browse(value.get("id")) + origin = comodel.browse(value.get('id')) id_ = comodel.new(value, origin=origin).id else: id_ = None @@ -3643,7 +3288,7 @@ class Many2one(_Relational[M]): raise ValueError("Wrong value for %s: %r" % (self, value)) def convert_to_export(self, value, record): - return value.display_name if value else "" + return value.display_name if value else '' def convert_to_display_name(self, value, record): return value.display_name @@ -3670,14 +3315,12 @@ class Many2one(_Relational[M]): self._update_inverses(records, cache_value) def _remove_inverses(self, records, value): - """Remove `records` from the cached values of the inverse fields of `self`.""" + """ Remove `records` from the cached values of the inverse fields of `self`. """ cache = records.env.cache record_ids = set(records._ids) # align(id) returns a NewId if records are new, a real id otherwise - align = ( - (lambda id_: id_) if all(record_ids) else (lambda id_: id_ and NewId(id_)) - ) + align = (lambda id_: id_) if all(record_ids) else (lambda id_: id_ and NewId(id_)) for invf in records.pool.field_inverses[self]: corecords = records.env[self.comodel_name].browse( @@ -3690,7 +3333,7 @@ class Many2one(_Relational[M]): cache.set(corecord, invf, ids1) def _update_inverses(self, records, value): - """Add `records` to the cached values of the inverse fields of `self`.""" + """ Add `records` to the cached values of the inverse fields of `self`. """ if value is None: return cache = records.env.cache @@ -3709,7 +3352,7 @@ class Many2one(_Relational[M]): class Many2oneReference(Integer): - """Pseudo-relational field (no FK in database). + """ Pseudo-relational field (no FK in database). The field value is stored as an :class:`integer ` id in database. @@ -3719,15 +3362,14 @@ class Many2oneReference(Integer): :param str model_field: name of the :class:`Char` where the model name is stored. """ - - type = "many2one_reference" + type = 'many2one_reference' model_field = None aggregator = None - _related_model_field = property(attrgetter("model_field")) + _related_model_field = property(attrgetter('model_field')) - _description_model_field = property(attrgetter("model_field")) + _description_model_field = property(attrgetter('model_field')) def convert_to_cache(self, value, record, validate=True): # cache format: id or None @@ -3736,7 +3378,7 @@ class Many2oneReference(Integer): return super().convert_to_cache(value, record, validate) def _update_inverses(self, records, value): - """Add `records` to the cached values of the inverse fields of `self`.""" + """ Add `records` to the cached values of the inverse fields of `self`. """ if not value: return cache = records.env.cache @@ -3773,7 +3415,7 @@ class Many2oneReference(Integer): class Json(Field): - """JSON Field that contain unstructured information in jsonb PostgreSQL column. + """ JSON Field that contain unstructured information in jsonb PostgreSQL column. This field is still in beta Some features have not been implemented and won't be implemented in stable versions, including: * searching @@ -3781,11 +3423,11 @@ class Json(Field): * mutating the values. """ - type = "json" - _column_type = ("jsonb", "jsonb") + type = 'json' + _column_type = ('jsonb', 'jsonb') def convert_to_record(self, value, record): - """Return a copy of the value""" + """ Return a copy of the value """ return False if value is None else copy.deepcopy(value) def convert_to_cache(self, value, record, validate=True): @@ -3800,12 +3442,12 @@ class Json(Field): def convert_to_export(self, value, record): if not value: - return "" + return '' return json.dumps(value) class Properties(Field): - """Field that contains a list of properties (aka "sub-field") based on + """ Field that contains a list of properties (aka "sub-field") based on a definition defined on a container. Properties are pseudo-fields, acting like Odoo fields but without being independently stored in database. @@ -3824,12 +3466,11 @@ class Properties(Field): the value of the child. That way the web client has access to the full field definition (property type, ...). """ - - type = "properties" - _column_type = ("jsonb", "jsonb") + type = 'properties' + _column_type = ('jsonb', 'jsonb') copy = False prefetch = False - write_sequence = 10 # because it must be written after the definition field + write_sequence = 10 # because it must be written after the definition field # the field is computed editable by design (see the compute method below) store = True @@ -3837,31 +3478,19 @@ class Properties(Field): precompute = True definition = None - definition_record = ( - None # field on the current model that point to the definition record - ) - definition_record_field = None # field on the definition record which defined the Properties field definition + definition_record = None # field on the current model that point to the definition record + definition_record_field = None # field on the definition record which defined the Properties field definition - _description_definition_record = property(attrgetter("definition_record")) - _description_definition_record_field = property( - attrgetter("definition_record_field") - ) + _description_definition_record = property(attrgetter('definition_record')) + _description_definition_record_field = property(attrgetter('definition_record_field')) ALLOWED_TYPES = ( # standard types - "boolean", - "integer", - "float", - "char", - "date", - "datetime", + 'boolean', 'integer', 'float', 'char', 'date', 'datetime', # relational like types - "many2one", - "many2many", - "selection", - "tags", + 'many2one', 'many2many', 'selection', 'tags', # UI types - "separator", + 'separator', ) def _setup_attrs(self, model_class, name): @@ -3872,12 +3501,10 @@ class Properties(Field): if self.definition: # determine definition_record and definition_record_field assert self.definition.count(".") == 1 - self.definition_record, self.definition_record_field = ( - self.definition.rsplit(".", 1) - ) + self.definition_record, self.definition_record_field = self.definition.rsplit('.', 1) # make the field computed, and set its dependencies - self._depends = (self.definition_record,) + self._depends = (self.definition_record, ) self.compute = self._compute def setup_related(self, model): @@ -4007,15 +3634,15 @@ class Properties(Field): for record, record_values in zip(records, values_list): for property_definition in record_values: - comodel = property_definition.get("comodel") - type_ = property_definition.get("type") - property_value = property_definition.get("value") or [] - default = property_definition.get("default") or [] + comodel = property_definition.get('comodel') + type_ = property_definition.get('type') + property_value = property_definition.get('value') or [] + default = property_definition.get('default') or [] - if type_ not in ("many2one", "many2many") or comodel not in records.env: + if type_ not in ('many2one', 'many2many') or comodel not in records.env: continue - if type_ == "many2one": + if type_ == 'many2one': default = [default] if default else [] property_value = [property_value] if property_value else [] @@ -4054,31 +3681,27 @@ class Properties(Field): return super().write(records, value) definition_changed = any( - definition.get("definition_changed") or definition.get("definition_deleted") + definition.get('definition_changed') + or definition.get('definition_deleted') for definition in (value or []) ) if definition_changed: value = [ - definition - for definition in value - if not definition.get("definition_deleted") + definition for definition in value + if not definition.get('definition_deleted') ] for definition in value: - definition.pop("definition_changed", None) + definition.pop('definition_changed', None) # update the properties definition on the container container = records[self.definition_record] if container: properties_definition = copy.deepcopy(value) for property_definition in properties_definition: - property_definition.pop("value", None) + property_definition.pop('value', None) container[self.definition_record_field] = properties_definition - _logger.info( - "Properties field: User #%i changed definition of %r", - records.env.user.id, - container, - ) + _logger.info('Properties field: User #%i changed definition of %r', records.env.user.id, container) return super().write(records, value) @@ -4087,10 +3710,7 @@ class Properties(Field): for record in records: record[self.name] = self._add_default_values( record.env, - { - self.name: record[self.name], - self.definition_record: record[self.definition_record], - }, + {self.name: record[self.name], self.definition_record: record[self.definition_record]}, ) def _add_default_values(self, env, values): @@ -4121,13 +3741,10 @@ class Properties(Field): container_id = env[container_model_name].sudo().browse(container_id) properties_definition = container_id[self.definition_record_field] - if not ( - properties_definition - or ( - isinstance(properties_values, list) - and any(d.get("definition_changed") for d in properties_values) - ) - ): + if not (properties_definition or ( + isinstance(properties_values, list) + and any(d.get('definition_changed') for d in properties_values) + )): # If a parent is set without properties, we might want to change its definition # when we create the new record. But if we just set the value without changing # the definition, in that case we can just ignored the passed values @@ -4138,19 +3755,17 @@ class Properties(Field): self._remove_display_name(properties_values) properties_list_values = properties_values else: - properties_list_values = self._dict_to_list( - properties_values, properties_definition - ) + properties_list_values = self._dict_to_list(properties_values, properties_definition) for properties_value in properties_list_values: - if properties_value.get("value") is None: - property_name = properties_value.get("name") + if properties_value.get('value') is None: + property_name = properties_value.get('name') context_key = f"default_{self.name}.{property_name}" if property_name and context_key in env.context: default = env.context[context_key] else: - default = properties_value.get("default") or False - properties_value["value"] = default + default = properties_value.get('default') or False + properties_value['value'] = default return properties_list_values @@ -4161,7 +3776,7 @@ class Properties(Field): return container.sudo()[self.definition_record_field] @classmethod - def _add_display_name(cls, values_list, env, value_keys=("value", "default")): + def _add_display_name(cls, values_list, env, value_keys=('value', 'default')): """Add the "display_name" for each many2one / many2many properties. Modify in place "values_list". @@ -4170,23 +3785,17 @@ class Properties(Field): :param env: environment """ for property_definition in values_list: - property_type = property_definition.get("type") - property_model = property_definition.get("comodel") + property_type = property_definition.get('type') + property_model = property_definition.get('comodel') if not property_model: continue for value_key in value_keys: property_value = property_definition.get(value_key) - if ( - property_type == "many2one" - and property_value - and isinstance(property_value, int) - ): + if property_type == 'many2one' and property_value and isinstance(property_value, int): try: - display_name = ( - env[property_model].browse(property_value).display_name - ) + display_name = env[property_model].browse(property_value).display_name property_definition[value_key] = (property_value, display_name) except AccessError: # protect from access error message, show an empty name @@ -4194,25 +3803,19 @@ class Properties(Field): except MissingError: property_definition[value_key] = False - elif ( - property_type == "many2many" - and property_value - and is_list_of(property_value, int) - ): + elif property_type == 'many2many' and property_value and is_list_of(property_value, int): property_definition[value_key] = [] records = env[property_model].browse(property_value) for record in records: try: - property_definition[value_key].append( - (record.id, record.display_name) - ) + property_definition[value_key].append((record.id, record.display_name)) except AccessError: property_definition[value_key].append((record.id, None)) except MissingError: continue @classmethod - def _remove_display_name(cls, values_list, value_key="value"): + def _remove_display_name(cls, values_list, value_key='value'): """Remove the display name received by the web client for the relational properties. Modify in place "values_list". @@ -4224,27 +3827,24 @@ class Properties(Field): :param value_key: In which dict key we need to remove the display name """ for property_definition in values_list: - if not isinstance(property_definition, dict) or not property_definition.get( - "name" - ): + if not isinstance(property_definition, dict) or not property_definition.get('name'): continue property_value = property_definition.get(value_key) if not property_value: continue - property_type = property_definition.get("type") + property_type = property_definition.get('type') - if property_type == "many2one" and has_list_types( - property_value, [int, (str, NoneType)] - ): + if property_type == 'many2one' and has_list_types(property_value, [int, (str, NoneType)]): property_definition[value_key] = property_value[0] - elif property_type == "many2many": + elif property_type == 'many2many': if is_list_of(property_value, (list, tuple)): # [(35, 'Admin'), (36, 'Demo')] -> [35, 36] property_definition[value_key] = [ - many2many_value[0] for many2many_value in property_value + many2many_value[0] + for many2many_value in property_value ] @classmethod @@ -4256,9 +3856,9 @@ class Properties(Field): :param values_list: List of properties definition with properties value """ for definition in values_list: - if definition.get("definition_changed") and not definition.get("name"): + if definition.get('definition_changed') and not definition.get('name'): # keep only the first 64 bits - definition["name"] = str(uuid.uuid4()).replace("-", "")[:16] + definition['name'] = str(uuid.uuid4()).replace('-', '')[:16] @classmethod def _parse_json_types(cls, values_list, env, res_ids_per_model): @@ -4271,55 +3871,54 @@ class Properties(Field): :param env: environment """ for property_definition in values_list: - property_value = property_definition.get("value") - property_type = property_definition.get("type") - res_model = property_definition.get("comodel") + property_value = property_definition.get('value') + property_type = property_definition.get('type') + res_model = property_definition.get('comodel') if property_type not in cls.ALLOWED_TYPES: - raise ValueError(f"Wrong property type {property_type!r}") + raise ValueError(f'Wrong property type {property_type!r}') - if property_type == "boolean": + if property_type == 'boolean': # E.G. convert zero to False property_value = bool(property_value) - elif property_type == "char" and not isinstance(property_value, str): + elif property_type == 'char' and not isinstance(property_value, str): property_value = False - elif property_value and property_type == "selection": + elif property_value and property_type == 'selection': # check if the selection option still exists - options = property_definition.get("selection") or [] - options = { - option[0] for option in options if option or () - } # always length 2 + options = property_definition.get('selection') or [] + options = {option[0] for option in options if option or ()} # always length 2 if property_value not in options: # maybe the option has been removed on the container property_value = False - elif property_value and property_type == "tags": + elif property_value and property_type == 'tags': # remove all tags that are not defined on the container - all_tags = {tag[0] for tag in property_definition.get("tags") or ()} + all_tags = {tag[0] for tag in property_definition.get('tags') or ()} property_value = [tag for tag in property_value if tag in all_tags] - elif property_type == "many2one" and property_value and res_model in env: + elif property_type == 'many2one' and property_value and res_model in env: if not isinstance(property_value, int): - raise ValueError(f"Wrong many2one value: {property_value!r}.") + raise ValueError(f'Wrong many2one value: {property_value!r}.') if property_value not in res_ids_per_model[res_model]: property_value = False - elif property_type == "many2many" and property_value and res_model in env: + elif property_type == 'many2many' and property_value and res_model in env: if not is_list_of(property_value, int): - raise ValueError(f"Wrong many2many value: {property_value!r}.") + raise ValueError(f'Wrong many2many value: {property_value!r}.') if len(property_value) != len(set(property_value)): # remove duplicated value and preserve order property_value = list(dict.fromkeys(property_value)) property_value = [ - id_ for id_ in property_value if id_ in res_ids_per_model[res_model] + id_ for id_ in property_value + if id_ in res_ids_per_model[res_model] ] - property_definition["value"] = property_value + property_definition['value'] = property_value @classmethod def _list_to_dict(cls, values_list): @@ -4354,39 +3953,31 @@ class Properties(Field): :return: Generate a dict {name: value} from this definitions / values list """ if not is_list_of(values_list, dict): - raise ValueError(f"Wrong properties value {values_list!r}") + raise ValueError(f'Wrong properties value {values_list!r}') cls._add_missing_names(values_list) dict_value = {} for property_definition in values_list: - property_value = property_definition.get("value") - property_type = property_definition.get("type") - property_model = property_definition.get("comodel") + property_value = property_definition.get('value') + property_type = property_definition.get('type') + property_model = property_definition.get('comodel') - if property_type == "separator": + if property_type == 'separator': # "separator" is used as a visual separator in the form view UI # it does not have a value and does not need to be stored on children continue - if property_type not in ("integer", "float") or property_value != 0: + if property_type not in ('integer', 'float') or property_value != 0: property_value = property_value or False - if ( - property_type in ("many2one", "many2many") - and property_model - and property_value - ): + if property_type in ('many2one', 'many2many') and property_model and property_value: # check that value are correct before storing them in database - if ( - property_type == "many2many" - and property_value - and not is_list_of(property_value, int) - ): + if property_type == 'many2many' and property_value and not is_list_of(property_value, int): raise ValueError(f"Wrong many2many value {property_value!r}") - if property_type == "many2one" and not isinstance(property_value, int): + if property_type == 'many2one' and not isinstance(property_value, int): raise ValueError(f"Wrong many2one value {property_value!r}") - dict_value[property_definition["name"]] = property_value + dict_value[property_definition['name']] = property_value return dict_value @@ -4400,44 +3991,36 @@ class Properties(Field): Ignore every values in the child that is not defined on the container. """ if not is_list_of(properties_definition, dict): - raise ValueError(f"Wrong properties value {properties_definition!r}") + raise ValueError(f'Wrong properties value {properties_definition!r}') values_list = copy.deepcopy(properties_definition) for property_definition in values_list: - property_definition["value"] = values_dict.get(property_definition["name"]) + property_definition['value'] = values_dict.get(property_definition['name']) return values_list class PropertiesDefinition(Field): - """Field used to define the properties definition (see :class:`~odoo.fields.Properties` + """ Field used to define the properties definition (see :class:`~odoo.fields.Properties` field). This field is used on the container record to define the structure of expected properties on subrecords. It is used to check the properties - definition.""" - - type = "properties_definition" - _column_type = ("jsonb", "jsonb") - copy = True # containers may act like templates, keep definitions to ease usage + definition. """ + type = 'properties_definition' + _column_type = ('jsonb', 'jsonb') + copy = True # containers may act like templates, keep definitions to ease usage readonly = False prefetch = True - REQUIRED_KEYS = ("name", "type") + REQUIRED_KEYS = ('name', 'type') ALLOWED_KEYS = ( - "name", - "string", - "type", - "comodel", - "default", - "selection", - "tags", - "domain", - "view_in_cards", + 'name', 'string', 'type', 'comodel', 'default', + 'selection', 'tags', 'domain', 'view_in_cards', ) # those keys will be removed if the types does not match PROPERTY_PARAMETERS_MAP = { - "comodel": {"many2one", "many2many"}, - "domain": {"many2one", "many2many"}, - "selection": {"selection"}, - "tags": {"tags"}, + 'comodel': {'many2one', 'many2many'}, + 'domain': {'many2one', 'many2many'}, + 'selection': {'selection'}, + 'tags': {'tags'}, } def convert_to_column(self, value, record, values=None, validate=True): @@ -4468,9 +4051,9 @@ class PropertiesDefinition(Field): value = json.loads(value) if not isinstance(value, list): - raise ValueError(f"Wrong properties definition type {type(value)!r}") + raise ValueError(f'Wrong properties definition type {type(value)!r}') - Properties._remove_display_name(value, value_key="default") + Properties._remove_display_name(value, value_key='default') self._validate_properties_definition(value, record.env) @@ -4490,9 +4073,9 @@ class PropertiesDefinition(Field): value = json.loads(value) if not isinstance(value, list): - raise ValueError(f"Wrong properties definition type {type(value)!r}") + raise ValueError(f'Wrong properties definition type {type(value)!r}') - Properties._remove_display_name(value, value_key="default") + Properties._remove_display_name(value, value_key='default') self._validate_properties_definition(value, record.env) @@ -4517,19 +4100,19 @@ class PropertiesDefinition(Field): # check if the model still exists in the environment, the module of the # model might have been uninstalled so the model might not exist anymore - property_model = property_definition.get("comodel") + property_model = property_definition.get('comodel') if property_model and property_model not in record.env: - property_definition["comodel"] = property_model = False + property_definition['comodel'] = property_model = False - if not property_model and "domain" in property_definition: - del property_definition["domain"] + if not property_model and 'domain' in property_definition: + del property_definition['domain'] - if property_definition.get("type") in ("selection", "tags"): + if property_definition.get('type') in ('selection', 'tags'): # always set at least an empty array if there's no option - key = property_definition["type"] + key = property_definition['type'] property_definition[key] = property_definition.get(key) or [] - property_domain = property_definition.get("domain") + property_domain = property_definition.get('domain') if property_domain: # some fields in the domain might have been removed # (e.g. if the module has been uninstalled) @@ -4540,15 +4123,12 @@ class PropertiesDefinition(Field): record.env[property_model], ) except ValueError: - del property_definition["domain"] + del property_definition['domain'] result.append(property_definition) - for ( - property_parameter, - allowed_types, - ) in self.PROPERTY_PARAMETERS_MAP.items(): - if property_definition.get("type") not in allowed_types: + for property_parameter, allowed_types in self.PROPERTY_PARAMETERS_MAP.items(): + if property_definition.get('type') not in allowed_types: property_definition.pop(property_parameter, None) return result @@ -4559,7 +4139,7 @@ class PropertiesDefinition(Field): return value if use_display_name: - Properties._add_display_name(value, record.env, value_keys=("default",)) + Properties._add_display_name(value, record.env, value_keys=('default',)) return value @@ -4574,65 +4154,53 @@ class PropertiesDefinition(Field): invalid_keys = property_definition_keys - set(cls.ALLOWED_KEYS) if invalid_keys: raise ValueError( - "Some key are not allowed for a properties definition [%s]." - % ", ".join(invalid_keys), + 'Some key are not allowed for a properties definition [%s].' % + ', '.join(invalid_keys), ) - check_property_field_value_name(property_definition["name"]) + check_property_field_value_name(property_definition['name']) required_keys = set(cls.REQUIRED_KEYS) - property_definition_keys if required_keys: raise ValueError( - "Some key are missing for a properties definition [%s]." - % ", ".join(required_keys), + 'Some key are missing for a properties definition [%s].' % + ', '.join(required_keys), ) - property_name = property_definition.get("name") + property_name = property_definition.get('name') if not property_name or property_name in properties_names: - raise ValueError( - f"The property name {property_name!r} is not set or duplicated." - ) + raise ValueError(f'The property name {property_name!r} is not set or duplicated.') properties_names.add(property_name) - property_type = property_definition.get("type") + property_type = property_definition.get('type') if property_type and property_type not in Properties.ALLOWED_TYPES: - raise ValueError(f"Wrong property type {property_type!r}.") + raise ValueError(f'Wrong property type {property_type!r}.') - model = property_definition.get("comodel") - if model and ( - model not in env or env[model].is_transient() or env[model]._abstract - ): - raise ValueError(f"Invalid model name {model!r}") + model = property_definition.get('comodel') + if model and (model not in env or env[model].is_transient() or env[model]._abstract): + raise ValueError(f'Invalid model name {model!r}') - property_selection = property_definition.get("selection") + property_selection = property_definition.get('selection') if property_selection: - if not is_list_of(property_selection, (list, tuple)) or not all( - len(selection) == 2 for selection in property_selection - ): - raise ValueError(f"Wrong options {property_selection!r}.") + if (not is_list_of(property_selection, (list, tuple)) + or not all(len(selection) == 2 for selection in property_selection)): + raise ValueError(f'Wrong options {property_selection!r}.') all_options = [option[0] for option in property_selection] if len(all_options) != len(set(all_options)): - duplicated = set( - filter(lambda x: all_options.count(x) > 1, all_options) - ) - raise ValueError( - f'Some options are duplicated: {", ".join(duplicated)}.' - ) + duplicated = set(filter(lambda x: all_options.count(x) > 1, all_options)) + raise ValueError(f'Some options are duplicated: {", ".join(duplicated)}.') - property_tags = property_definition.get("tags") + property_tags = property_definition.get('tags') if property_tags: - if not is_list_of(property_tags, (list, tuple)) or not all( - len(tag) == 3 and isinstance(tag[2], int) for tag in property_tags - ): - raise ValueError(f"Wrong tags definition {property_tags!r}.") + if (not is_list_of(property_tags, (list, tuple)) + or not all(len(tag) == 3 and isinstance(tag[2], int) for tag in property_tags)): + raise ValueError(f'Wrong tags definition {property_tags!r}.') all_tags = [tag[0] for tag in property_tags] if len(all_tags) != len(set(all_tags)): duplicated = set(filter(lambda x: all_tags.count(x) > 1, all_tags)) - raise ValueError( - f'Some tags are duplicated: {", ".join(duplicated)}.' - ) + raise ValueError(f'Some tags are duplicated: {", ".join(duplicated)}.') class Command(enum.IntEnum): @@ -4754,7 +4322,6 @@ class Command(enum.IntEnum): class _RelationalMulti(_Relational[M], typing.Generic[M]): r"Abstract class for relational fields \*2many." - write_sequence = 20 # Important: the cache contains the ids of all the records in the relation, @@ -4762,7 +4329,7 @@ class _RelationalMulti(_Relational[M], typing.Generic[M]): # convert_to_record(), depending on the context. def _update(self, records, value): - """Update the cached value of ``self`` for ``records`` with ``value``.""" + """ Update the cached value of ``self`` for ``records`` with ``value``. """ records.env.cache.patch(records, self, value.id) records.modified([self.name]) @@ -4825,12 +4392,11 @@ class _RelationalMulti(_Relational[M], typing.Generic[M]): prefetch_ids = PrefetchX2many(record, self) Comodel = record.pool[self.comodel_name] corecords = Comodel(record.env, value, prefetch_ids) - if Comodel._active_name and self.context.get( - "active_test", record.env.context.get("active_test", True) + if ( + Comodel._active_name + and self.context.get('active_test', record.env.context.get('active_test', True)) ): - corecords = corecords.filtered(Comodel._active_name).with_prefetch( - prefetch_ids - ) + corecords = corecords.filtered(Comodel._active_name).with_prefetch(prefetch_ids) return corecords def convert_to_record_multi(self, values, records): @@ -4839,12 +4405,11 @@ class _RelationalMulti(_Relational[M], typing.Generic[M]): Comodel = records.pool[self.comodel_name] ids = tuple(unique(id_ for ids in values for id_ in ids)) corecords = Comodel(records.env, ids, prefetch_ids) - if Comodel._active_name and self.context.get( - "active_test", records.env.context.get("active_test", True) + if ( + Comodel._active_name + and self.context.get('active_test', records.env.context.get('active_test', True)) ): - corecords = corecords.filtered(Comodel._active_name).with_prefetch( - prefetch_ids - ) + corecords = corecords.filtered(Comodel._active_name).with_prefetch(prefetch_ids) return corecords def convert_to_read(self, value, record, use_display_name=True): @@ -4856,7 +4421,6 @@ class _RelationalMulti(_Relational[M], typing.Generic[M]): value = record.env[self.comodel_name].browse(value) if isinstance(value, BaseModel) and value._name == self.comodel_name: - def get_origin(val): return val._origin if isinstance(val, BaseModel) else val @@ -4866,25 +4430,20 @@ class _RelationalMulti(_Relational[M], typing.Generic[M]): for record in value: origin = record._origin if not origin: - values = record._convert_to_write( - { - name: record[name] - for name in record._cache - if name not in inv_names - } - ) + values = record._convert_to_write({ + name: record[name] + for name in record._cache + if name not in inv_names + }) result.append(Command.create(values)) else: result[0][2].append(origin.id) if record != origin: - values = record._convert_to_write( - { - name: record[name] - for name in record._cache - if name not in inv_names - and get_origin(record[name]) != origin[name] - } - ) + values = record._convert_to_write({ + name: record[name] + for name in record._cache + if name not in inv_names and get_origin(record[name]) != origin[name] + }) if values: result.append(Command.update(origin.id, values)) return result @@ -4898,7 +4457,7 @@ class _RelationalMulti(_Relational[M], typing.Generic[M]): raise ValueError("Wrong value for %s: %s" % (self, value)) def convert_to_export(self, value, record): - return ",".join(value.mapped("display_name")) if value else "" + return ','.join(value.mapped('display_name')) if value else '' def convert_to_display_name(self, value, record): raise NotImplementedError() @@ -4906,20 +4465,15 @@ class _RelationalMulti(_Relational[M], typing.Generic[M]): def get_depends(self, model): depends, depends_context = super().get_depends(model) if not self.compute and isinstance(self.domain, list): - depends = unique( - itertools.chain( - depends, - ( - self.name + "." + arg[0] - for arg in self.domain - if isinstance(arg, (tuple, list)) and isinstance(arg[0], str) - ), - ) - ) + depends = unique(itertools.chain(depends, ( + self.name + '.' + arg[0] + for arg in self.domain + if isinstance(arg, (tuple, list)) and isinstance(arg[0], str) + ))) return depends, depends_context def create(self, record_values): - """Write the value of ``self`` on the given records, which have just + """ Write the value of ``self`` on the given records, which have just been created. :param record_values: a list of pairs ``(record, value)``, where @@ -4943,11 +4497,7 @@ class _RelationalMulti(_Relational[M], typing.Generic[M]): value = [Command.set(value._ids)] elif value is False or value is None: value = [Command.clear()] - elif ( - isinstance(value, list) - and value - and not isinstance(value[0], (tuple, list)) - ): + elif isinstance(value, list) and value and not isinstance(value[0], (tuple, list)): value = [Command.set(tuple(value))] if not isinstance(value, list): raise ValueError("Wrong value for %s: %s" % (self, value)) @@ -4957,9 +4507,7 @@ class _RelationalMulti(_Relational[M], typing.Generic[M]): if all(record_ids): self.write_real(records_commands_list, create) else: - assert not any( - record_ids - ), f"{records_commands_list} contains a mix of real and new records. It is not supported." + assert not any(record_ids), f"{records_commands_list} contains a mix of real and new records. It is not supported." self.write_new(records_commands_list) def _check_sudo_commands(self, comodel): @@ -4993,25 +4541,19 @@ class One2many(_RelationalMulti[M]): The attributes ``comodel_name`` and ``inverse_name`` are mandatory except in the case of related fields or field extensions. """ + type = 'one2many' - type = "one2many" + inverse_name = None # name of the inverse field + auto_join = False # whether joins are generated upon search + copy = False # o2m are not copied by default - inverse_name = None # name of the inverse field - auto_join = False # whether joins are generated upon search - copy = False # o2m are not copied by default - - def __init__( - self, - comodel_name: str | Sentinel = SENTINEL, - inverse_name: str | Sentinel = SENTINEL, - string: str | Sentinel = SENTINEL, - **kwargs, - ): + def __init__(self, comodel_name: str | Sentinel = SENTINEL, inverse_name: str | Sentinel = SENTINEL, + string: str | Sentinel = SENTINEL, **kwargs): super(One2many, self).__init__( comodel_name=comodel_name, inverse_name=inverse_name, string=string, - **kwargs, + **kwargs ) def setup_nonrelated(self, model): @@ -5026,27 +4568,25 @@ class One2many(_RelationalMulti[M]): model.pool.field_inverses.add(self, invf) comodel.pool.field_inverses.add(invf, self) - _description_relation_field = property(attrgetter("inverse_name")) + _description_relation_field = property(attrgetter('inverse_name')) def update_db(self, model, columns): if self.comodel_name in model.env: comodel = model.env[self.comodel_name] if self.inverse_name not in comodel._fields: - raise UserError( - model.env._( - 'No inverse field "%(inverse_field)s" found for "%(comodel)s"', - inverse_field=self.inverse_name, - comodel=self.comodel_name, - ) - ) + raise UserError(model.env._( + 'No inverse field "%(inverse_field)s" found for "%(comodel)s"', + inverse_field=self.inverse_name, + comodel=self.comodel_name + )) def get_domain_list(self, records): domain = super().get_domain_list(records) if self.comodel_name and self.inverse_name: comodel = records.env.registry[self.comodel_name] inverse_field = comodel._fields[self.inverse_name] - if inverse_field.type == "many2one_reference": - domain = domain + [(inverse_field.model_field, "=", records._name)] + if inverse_field.type == 'many2one_reference': + domain = domain + [(inverse_field.model_field, '=', records._name)] return domain def __get__(self, records, owner=None): @@ -5060,21 +4600,21 @@ class One2many(_RelationalMulti[M]): def read(self, records): # retrieve the lines in the comodel - context = {"active_test": False} + context = {'active_test': False} context.update(self.context) comodel = records.env[self.comodel_name].with_context(**context) inverse = self.inverse_name inverse_field = comodel._fields[inverse] # optimization: fetch the inverse and active fields with search() - domain = self.get_domain_list(records) + [(inverse, "in", records.ids)] + domain = self.get_domain_list(records) + [(inverse, 'in', records.ids)] field_names = [inverse] if comodel._active_name: field_names.append(comodel._active_name) lines = comodel.search_fetch(domain, field_names) # group lines by inverse field (without prefetching other fields) - get_id = (lambda rec: rec.id) if inverse_field.type == "many2one" else int + get_id = (lambda rec: rec.id) if inverse_field.type == 'many2one' else int group = defaultdict(list) for line in lines: # line[inverse] may be a record or an integer @@ -5085,7 +4625,7 @@ class One2many(_RelationalMulti[M]): records.env.cache.insert_missing(records, self, values) def write_real(self, records_commands_list, create=False): - """Update real records.""" + """ Update real records. """ # records_commands_list = [(records, commands), ...] if not records_commands_list: return @@ -5096,13 +4636,13 @@ class One2many(_RelationalMulti[M]): if self.store: inverse = self.inverse_name - to_create = [] # line vals to create - to_delete = [] # line ids to delete - to_link = defaultdict(OrderedSet) # {record: line_ids} + to_create = [] # line vals to create + to_delete = [] # line ids to delete + to_link = defaultdict(OrderedSet) # {record: line_ids} allow_full_delete = not create def unlink(lines): - if getattr(comodel._fields[inverse], "ondelete", False) == "cascade": + if getattr(comodel._fields[inverse], 'ondelete', False) == 'cascade': to_delete.extend(lines._ids) else: lines[inverse] = False @@ -5127,16 +4667,14 @@ class One2many(_RelationalMulti[M]): to_link.clear() for recs, commands in records_commands_list: - for command in commands or (): + for command in (commands or ()): if command[0] == Command.CREATE: for record in recs: to_create.append(dict(command[2], **{inverse: record.id})) allow_full_delete = False elif command[0] == Command.UPDATE: prefetch_ids = recs[self.name]._prefetch_ids - comodel.browse(command[1]).with_prefetch(prefetch_ids).write( - command[2] - ) + comodel.browse(command[1]).with_prefetch(prefetch_ids).write(command[2]) elif command[0] == Command.DELETE: to_delete.append(command[1]) elif command[0] == Command.UNLINK: @@ -5158,19 +4696,15 @@ class One2many(_RelationalMulti[M]): flush() # assign the given lines to the last record only lines = comodel.browse(line_ids) - domain = self.get_domain_list(model) + [ - (inverse, "in", recs.ids), - ("id", "not in", lines.ids), - ] + domain = self.get_domain_list(model) + \ + [(inverse, 'in', recs.ids), ('id', 'not in', lines.ids)] unlink(comodel.search(domain)) lines[inverse] = recs[-1] flush() else: - ids = OrderedSet( - rid for recs, cs in records_commands_list for rid in recs._ids - ) + ids = OrderedSet(rid for recs, cs in records_commands_list for rid in recs._ids) records = records_commands_list[0][0].browse(ids) cache = records.env.cache @@ -5183,7 +4717,7 @@ class One2many(_RelationalMulti[M]): cache.set(record, self, (record[self.name] - lines)._ids) for recs, commands in records_commands_list: - for command in commands or (): + for command in (commands or ()): if command[0] == Command.CREATE: for record in recs: link(record, comodel.new(command[2], ref=command[1])) @@ -5198,9 +4732,7 @@ class One2many(_RelationalMulti[M]): elif command[0] in (Command.CLEAR, Command.SET): # assign the given lines to the last record only cache.update(recs, self, itertools.repeat(())) - lines = comodel.browse( - command[2] if command[0] == Command.SET else [] - ) + lines = comodel.browse(command[2] if command[0] == Command.SET else []) cache.set(recs[-1], self, lines._ids) def write_new(self, records_commands_list): @@ -5227,9 +4759,7 @@ class One2many(_RelationalMulti[M]): # make sure self's inverse is in cache inverse_field = comodel._fields[inverse] for record in records: - cache.update( - record[self.name], inverse_field, itertools.repeat(record.id) - ) + cache.update(record[self.name], inverse_field, itertools.repeat(record.id)) for recs, commands in records_commands_list: for command in commands: @@ -5255,7 +4785,6 @@ class One2many(_RelationalMulti[M]): cache.update(lines, inverse_field, itertools.repeat(last.id)) else: - def link(record, lines): ids = record[self.name]._ids cache.set(record, self, tuple(unique(ids + lines._ids))) @@ -5285,7 +4814,7 @@ class One2many(_RelationalMulti[M]): class Many2many(_RelationalMulti[M]): - """Many2many field; the value of such a field is the recordset. + """ Many2many field; the value of such a field is the recordset. :param comodel_name: name of the target model (string) mandatory except in the case of related or extended fields @@ -5325,32 +4854,25 @@ class Many2many(_RelationalMulti[M]): domain depending on the field attributes. """ + type = 'many2many' - type = "many2many" + _explicit = True # whether schema is explicitly given + relation = None # name of table + column1 = None # column of table referring to model + column2 = None # column of table referring to comodel + auto_join = False # whether joins are generated upon search + ondelete = 'cascade' # optional ondelete for the column2 fkey - _explicit = True # whether schema is explicitly given - relation = None # name of table - column1 = None # column of table referring to model - column2 = None # column of table referring to comodel - auto_join = False # whether joins are generated upon search - ondelete = "cascade" # optional ondelete for the column2 fkey - - def __init__( - self, - comodel_name: str | Sentinel = SENTINEL, - relation: str | Sentinel = SENTINEL, - column1: str | Sentinel = SENTINEL, - column2: str | Sentinel = SENTINEL, - string: str | Sentinel = SENTINEL, - **kwargs, - ): + def __init__(self, comodel_name: str | Sentinel = SENTINEL, relation: str | Sentinel = SENTINEL, + column1: str | Sentinel = SENTINEL, column2: str | Sentinel = SENTINEL, + string: str | Sentinel = SENTINEL, **kwargs): super(Many2many, self).__init__( comodel_name=comodel_name, relation=relation, column1=column1, column2=column2, string=string, - **kwargs, + **kwargs ) def setup_nonrelated(self, model): @@ -5359,7 +4881,7 @@ class Many2many(_RelationalMulti[M]): # 1) The ondelete attribute is defined and its definition makes sense # 2) The ondelete attribute is explicitly defined as 'set null' for a m2m, # this is considered a programming error. - if self.ondelete not in ("cascade", "restrict"): + if self.ondelete not in ('cascade', 'restrict'): raise ValueError( "The m2m field %s of model %s declares its ondelete policy " "as being %r. Only 'restrict' and 'cascade' make sense." @@ -5373,16 +4895,15 @@ class Many2many(_RelationalMulti[M]): comodel = model.env[self.comodel_name] if not self.relation: tables = sorted([model._table, comodel._table]) - assert tables[0] != tables[1], ( - "%s: Implicit/canonical naming of many2many relationship " - "table is not possible when source and destination models " + assert tables[0] != tables[1], \ + "%s: Implicit/canonical naming of many2many relationship " \ + "table is not possible when source and destination models " \ "are the same" % self - ) - self.relation = "%s_%s_rel" % tuple(tables) + self.relation = '%s_%s_rel' % tuple(tables) if not self.column1: - self.column1 = "%s_id" % model._table + self.column1 = '%s_id' % model._table if not self.column2: - self.column2 = "%s_id" % comodel._table + self.column2 = '%s_id' % comodel._table # check validity of table name check_pg_name(self.relation) else: @@ -5394,14 +4915,13 @@ class Many2many(_RelationalMulti[M]): # check whether other fields use the same schema fields = m2m[(self.relation, self.column1, self.column2)] for field in fields: - if ( # same model: relation parameters must be explicit - self.model_name == field.model_name - and self.comodel_name == field.comodel_name - and self._explicit - and field._explicit + if ( # same model: relation parameters must be explicit + self.model_name == field.model_name and + self.comodel_name == field.comodel_name and + self._explicit and field._explicit ) or ( # different models: one model must be _auto=False - self.model_name != field.model_name - and not (model._auto and model.env[field.model_name]._auto) + self.model_name != field.model_name and + not (model._auto and model.env[field.model_name]._auto) ): continue msg = "Many2many fields %s and %s use the same table and columns" @@ -5419,87 +4939,58 @@ class Many2many(_RelationalMulti[M]): # module. They are automatically removed when dropping the corresponding # 'ir.model.field'. if not self.manual: - model.pool.post_init( - model.env["ir.model.relation"]._reflect_relation, - model, - self.relation, - self._module, - ) + model.pool.post_init(model.env['ir.model.relation']._reflect_relation, + model, self.relation, self._module) comodel = model.env[self.comodel_name] if not sql.table_exists(cr, self.relation): - cr.execute( - SQL( - """ CREATE TABLE %(rel)s (%(id1)s INTEGER NOT NULL, + cr.execute(SQL( + """ CREATE TABLE %(rel)s (%(id1)s INTEGER NOT NULL, %(id2)s INTEGER NOT NULL, PRIMARY KEY(%(id1)s, %(id2)s)); COMMENT ON TABLE %(rel)s IS %(comment)s; CREATE INDEX ON %(rel)s (%(id2)s, %(id1)s); """, - rel=SQL.identifier(self.relation), - id1=SQL.identifier(self.column1), - id2=SQL.identifier(self.column2), - comment=f"RELATION BETWEEN {model._table} AND {comodel._table}", - ) - ) - _schema.debug( - "Create table %r: m2m relation between %r and %r", - self.relation, - model._table, - comodel._table, - ) + rel=SQL.identifier(self.relation), + id1=SQL.identifier(self.column1), + id2=SQL.identifier(self.column2), + comment=f"RELATION BETWEEN {model._table} AND {comodel._table}", + )) + _schema.debug("Create table %r: m2m relation between %r and %r", self.relation, model._table, comodel._table) model.pool.post_init(self.update_db_foreign_keys, model) return True model.pool.post_init(self.update_db_foreign_keys, model) def update_db_foreign_keys(self, model): - """Add the foreign keys corresponding to the field's relation table.""" + """ Add the foreign keys corresponding to the field's relation table. """ comodel = model.env[self.comodel_name] if model._is_an_ordinary_table(): model.pool.add_foreign_key( - self.relation, - self.column1, - model._table, - "id", - "cascade", - model, - self._module, - force=False, + self.relation, self.column1, model._table, 'id', 'cascade', + model, self._module, force=False, ) if comodel._is_an_ordinary_table(): model.pool.add_foreign_key( - self.relation, - self.column2, - comodel._table, - "id", - self.ondelete, - model, - self._module, + self.relation, self.column2, comodel._table, 'id', self.ondelete, + model, self._module, ) def read(self, records): - context = {"active_test": False} + context = {'active_test': False} context.update(self.context) comodel = records.env[self.comodel_name].with_context(**context) # make the query for the lines domain = self.get_domain_list(records) query = comodel._where_calc(domain) - comodel._apply_ir_rules(query, "read") + comodel._apply_ir_rules(query, 'read') query.order = comodel._order_to_sql(comodel._order, query) # join with many2many relation table sql_id1 = SQL.identifier(self.relation, self.column1) sql_id2 = SQL.identifier(self.relation, self.column2) - query.add_join( - "JOIN", - self.relation, - None, - SQL( - "%s = %s", - sql_id2, - SQL.identifier(comodel._table, "id"), - ), - ) + query.add_join('JOIN', self.relation, None, SQL( + "%s = %s", sql_id2, SQL.identifier(comodel._table, 'id'), + )) query.add_where(SQL("%s IN %s", sql_id1, tuple(records.ids))) # retrieve pairs (record, line) and group by record @@ -5562,16 +5053,14 @@ class Many2many(_RelationalMulti[M]): for recs, commands in records_commands_list: to_create = [] # line vals to create to_delete = [] # line ids to delete - for command in commands or (): + for command in (commands or ()): if not isinstance(command, (list, tuple)) or not command: continue if command[0] == Command.CREATE: to_create.append((recs._ids, command[2])) elif command[0] == Command.UPDATE: prefetch_ids = recs[self.name]._prefetch_ids - comodel.browse(command[1]).with_prefetch(prefetch_ids).write( - command[2] - ) + comodel.browse(command[1]).with_prefetch(prefetch_ids).write(command[2]) elif command[0] == Command.DELETE: to_delete.append(command[1]) elif command[0] == Command.UNLINK: @@ -5580,12 +5069,8 @@ class Many2many(_RelationalMulti[M]): relation_add(recs._ids, command[1]) elif command[0] in (Command.CLEAR, Command.SET): # new lines must no longer be linked to records - to_create = [ - (set(ids) - set(recs._ids), vals) for (ids, vals) in to_create - ] - relation_set( - recs._ids, command[2] if command[0] == Command.SET else () - ) + to_create = [(set(ids) - set(recs._ids), vals) for (ids, vals) in to_create] + relation_set(recs._ids, command[2] if command[0] == Command.SET else ()) if to_create: # create lines in batch, and link them @@ -5610,15 +5095,13 @@ class Many2many(_RelationalMulti[M]): pairs = [(x, y) for x, ys in new_relation.items() for y in ys - old_relation[x]] if pairs: if self.store: - cr.execute( - SQL( - "INSERT INTO %s (%s, %s) VALUES %s ON CONFLICT DO NOTHING", - SQL.identifier(self.relation), - SQL.identifier(self.column1), - SQL.identifier(self.column2), - SQL(", ").join(pairs), - ) - ) + cr.execute(SQL( + "INSERT INTO %s (%s, %s) VALUES %s ON CONFLICT DO NOTHING", + SQL.identifier(self.relation), + SQL.identifier(self.column1), + SQL.identifier(self.column2), + SQL(", ").join(pairs), + )) # update the cache of inverse fields y_to_xs = defaultdict(set) @@ -5656,22 +5139,16 @@ class Many2many(_RelationalMulti[M]): for y, xs in y_to_xs.items(): xs_to_ys[frozenset(xs)].add(y) # delete the rows where (id1 IN xs AND id2 IN ys) OR ... - cr.execute( - SQL( - "DELETE FROM %s WHERE %s", - SQL.identifier(self.relation), - SQL(" OR ").join( - SQL( - "%s IN %s AND %s IN %s", - SQL.identifier(self.column1), - tuple(xs), - SQL.identifier(self.column2), - tuple(ys), - ) - for xs, ys in xs_to_ys.items() - ), - ) - ) + cr.execute(SQL( + "DELETE FROM %s WHERE %s", + SQL.identifier(self.relation), + SQL(" OR ").join( + SQL("%s IN %s AND %s IN %s", + SQL.identifier(self.column1), tuple(xs), + SQL.identifier(self.column2), tuple(ys)) + for xs, ys in xs_to_ys.items() + ), + )) # update the cache of inverse fields for invf in records.pool.field_inverses[self]: @@ -5688,16 +5165,14 @@ class Many2many(_RelationalMulti[M]): # trigger the recomputation of fields that depend on the inverse # fields of self on the modified corecords corecords = comodel.browse(modified_corecord_ids) - corecords.modified( - [ - invf.name - for invf in model.pool.field_inverses[self] - if invf.model_name == self.comodel_name - ] - ) + corecords.modified([ + invf.name + for invf in model.pool.field_inverses[self] + if invf.model_name == self.comodel_name + ]) def write_new(self, records_commands_list): - """Update self on new records.""" + """ Update self on new records. """ if not records_commands_list: return @@ -5708,11 +5183,7 @@ class Many2many(_RelationalMulti[M]): # determine old and new relation {x: ys} set = OrderedSet - old_relation = { - record.id: set(record[self.name]._ids) - for records, _ in records_commands_list - for record in records - } + old_relation = {record.id: set(record[self.name]._ids) for records, _ in records_commands_list for record in records} new_relation = {x: set(ys) for x, ys in old_relation.items()} for recs, commands in records_commands_list: @@ -5802,32 +5273,29 @@ class Many2many(_RelationalMulti[M]): # trigger the recomputation of fields that depend on the inverse # fields of self on the modified corecords corecords = comodel.browse(modified_corecord_ids) - corecords.modified( - [ - invf.name - for invf in model.pool.field_inverses[self] - if invf.model_name == self.comodel_name - ] - ) + corecords.modified([ + invf.name + for invf in model.pool.field_inverses[self] + if invf.model_name == self.comodel_name + ]) class Id(Field[IdType | typing.Literal[False]]): - """Special case for field 'id'.""" + """ Special case for field 'id'. """ + type = 'integer' + column_type = ('int4', 'int4') - type = "integer" - column_type = ("int4", "int4") - - string = "ID" + string = 'ID' store = True readonly = True prefetch = False def update_db(self, model, columns): - pass # this column is created with the table + pass # this column is created with the table def __get__(self, record, owner=None): if record is None: - return self # the field is accessed through the class owner + return self # the field is accessed through the class owner # the code below is written to make record.id as quick as possible ids = record._ids @@ -5843,9 +5311,8 @@ class Id(Field[IdType | typing.Literal[False]]): class PrefetchMany2one: - """Iterable for the values of a many2one field on the prefetch set of a given record.""" - - __slots__ = "record", "field" + """ Iterable for the values of a many2one field on the prefetch set of a given record. """ + __slots__ = 'record', 'field' def __init__(self, record, field): self.record = record @@ -5863,9 +5330,8 @@ class PrefetchMany2one: class PrefetchX2many: - """Iterable for the values of an x2many field on the prefetch set of a given record.""" - - __slots__ = "record", "field" + """ Iterable for the values of an x2many field on the prefetch set of a given record. """ + __slots__ = 'record', 'field' def __init__(self, record, field): self.record = record @@ -5883,7 +5349,7 @@ class PrefetchX2many: def apply_required(model, field_name): - """Set a NOT NULL constraint on the given field, if necessary.""" + """ Set a NOT NULL constraint on the given field, if necessary. """ # At the time this function is called, the model's _fields may have been reset, although # the model's class is still the same. Retrieve the field to see whether the NOT NULL # constraint still applies @@ -5896,9 +5362,6 @@ def apply_required(model, field_name): # pylint: disable=wrong-import-position from .exceptions import AccessError, MissingError, UserError from .models import ( - check_pg_name, - expand_ids, - is_definition_class, - BaseModel, - PREFETCH_MAX, + check_pg_name, expand_ids, is_definition_class, + BaseModel, PREFETCH_MAX, )