# Part of Odoo. See LICENSE file for full copyright and licensing details. from __future__ import annotations import logging import typing from collections import defaultdict from types import MappingProxyType from . import models from . import fields # must be imported after models from .utils import check_pg_name from odoo.exceptions import ValidationError from odoo.tools import ( OrderedSet, LastOrderedSet, discardattr, frozendict, sql, ) from odoo.tools.translate import FIELD_TRANSLATE if typing.TYPE_CHECKING: from odoo.api import Environment from odoo.fields import Field from odoo.models import BaseModel from odoo.modules.registry import Registry _logger = logging.getLogger('odoo.registry') # THE MODEL DEFINITIONS, MODEL CLASSES, AND MODEL INSTANCES # # The framework deals with two kinds of classes for models: the "model # definitions" and the "model classes". # # The "model definitions" are the classes defined in modules source code: they # define models and extend them. Those classes are essentially "static", for # whatever that means in Python. The only exception is custom models: their # model definition is created dynamically. # # The "model classes" are the ones you find in the registry. The recordsets of # a model actually are instances of its model class. The "model class" of a # model is created dynamically when the registry is built. It inherits (in the # Python sense) from all the model definitions of the model, and possibly other # model classes (when the model inherits from another model). It also carries # model metadata inferred from its parent classes. # # # THE MODEL CLASSES # # In the simplest case, a model class inherits from all the classes that define # the model in a flat hierarchy. Consider the definitions of model 'a' below. # The model class of 'a' inherits from the model definitions A1, A2, A3, in # reverse order, to match the expected overriding order. The model class # carries inferred metadata that is shared between all the recordsets of that # model for a given registry. # # class A(Model): # A1 Model # _name = 'a' / | \ # A3 A2 A1 <- model definitions # class A(Model): # A2 \ | / # _inherit = 'a' a <- model class: registry['a'] # | # class A(Model): # A3 records <- model instances, like env['a'] # _inherit = 'a' # # Note that when the model inherits from another model, we actually make the # model classes inherit from each other, so that extensions to an inherited # model are visible in the model class of the child model, like in the # following example. # # class A(Model): # A1 # _name = 'a' Model # / / \ \ # class B(Model): # B1 / / \ \ # _name = 'b' / A2 A1 \ # B2 \ / B1 # class B(Model): # B2 \ \ / / # _inherit = ['a', 'b'] \ a / # \ | / # class A(Model): # A2 \ | / # _inherit = 'a' b # # To be more explicit, the parent classes of model 'a' are (A2, A1), and the # ones of model 'b' are (B2, a, B1). Consequently, the MRO of model 'a' is # [a, A2, A1, Model] while the MRO of 'b' is [b, B2, a, A2, A1, B1, Model]. # # # THE FIELDS OF A MODEL # # The fields of a model are given by the model's definitions, inherited models # ('_inherit' and '_inherits') and other parties, like custom fields. Note that # a field can be partially overridden when it appears on several definitions of # its model. In that case, the field's final definition depends on the # presence or absence of each model definition, which itself depends on the # modules loaded in the registry. # # By design, the model class has access to all the fields on its model # definitions. When possible, the field is used directly from its model # definition. There are a number of cases where the field cannot be used # directly: # - the field is related (and bits may not be shared); # - the field is overridden on model definitions; # - the field is defined on another model (and accessible by mixin). # # The last case prevents sharing the field across registries, because the field # object is specific to a model, and is used as a key in several key # dictionaries, like the record cache and pending computations. # # Setting up a field on its model definition helps saving memory and time. # Indeed, when sharing is possible, the field's setup is almost entirely done # where the field was defined. It is thus done when the model definition was # created, and it may be reused across registries. # # In the example below, the field 'foo' appears once on its model definition. # Assuming that it is not related, that field can be set up directly on its # model definition. If the model appears in several registries, the # field 'foo' is effectively shared across registries. # # class A1(Model): Model # _name = 'a' / \ # foo = ... / \ # bar = ... A2 A1 # bar foo, bar # class A2(Model): \ / # _inherit = 'a' \ / # bar = ... a # bar # # On the other hand, the field 'bar' is overridden in its model definitions. In # that case, the framework recreates the field on the model class, which is # never shared across registries. The field's setup will be based on its # definitions, and will thus not be shared across registries. # # The so-called magic fields ('id', 'display_name', ...) used to be added on # model classes. But doing so prevents them from being shared. So instead, # we add them on definition classes that define a model without extending it. # This increases the number of fields that are shared across registries. def is_model_definition(cls: type) -> bool: """ Return whether ``cls`` is a model definition class. """ return isinstance(cls, models.MetaModel) and getattr(cls, 'pool', None) is None def is_model_class(cls: type) -> bool: """ Return whether ``cls`` is a model registry class. """ return getattr(cls, 'pool', None) is not None def add_to_registry(registry: Registry, model_def: type[BaseModel]) -> type[BaseModel]: """ Add a model definition to the given registry, and return its corresponding model class. This function creates or extends a model class for the given model definition. """ assert is_model_definition(model_def) if hasattr(model_def, '_constraints'): _logger.warning("Model attribute '_constraints' is no longer supported, " "please use @api.constrains on methods instead.") if hasattr(model_def, '_sql_constraints'): _logger.warning("Model attribute '_sql_constraints' is no longer supported, " "please define model.Constraint on the model.") # all models except 'base' implicitly inherit from 'base' name = model_def._name parent_names = list(model_def._inherit) if name != 'base': parent_names.append('base') # create or retrieve the model's class if name in parent_names: if name not in registry: raise TypeError(f"Model {name!r} does not exist in registry.") model_cls = registry[name] _check_model_extension(model_cls, model_def) else: model_cls = type(name, (model_def,), { 'pool': registry, # this makes it a model class '_name': name, '_register': False, '_original_module': model_def._module, '_inherit_module': {}, # map parent to introducing module '_inherit_children': OrderedSet(), # names of children models '_inherits_children': set(), # names of children models '_fields__': {}, # populated in _setup() '_table_objects': frozendict(), # populated in _setup() }) model_cls._fields = MappingProxyType(model_cls._fields__) # determine all the classes the model should inherit from bases = LastOrderedSet([model_def]) for parent_name in parent_names: if parent_name not in registry: raise TypeError(f"Model {name!r} inherits from non-existing model {parent_name!r}.") parent_cls = registry[parent_name] if parent_name == name: for base in parent_cls._base_classes__: bases.add(base) else: _check_model_parent_extension(model_cls, model_def, parent_cls) bases.add(parent_cls) model_cls._inherit_module[parent_name] = model_def._module parent_cls._inherit_children.add(name) # model_cls.__bases__ must be assigned those classes; however, this # operation is quite slow, so we do it once in method _prepare_setup() model_cls._base_classes__ = tuple(bases) # determine the attributes of the model's class _init_model_class_attributes(model_cls) check_pg_name(model_cls._table) # Transience if model_cls._transient and not model_cls._log_access: raise TypeError( "TransientModels must have log_access turned on, " "in order to implement their vacuum policy" ) # update the registry after all checks have passed registry[name] = model_cls # mark all impacted models for setup for model_name in registry.descendants([name], '_inherit', '_inherits'): registry[model_name]._setup_done__ = False return model_cls def _check_model_extension(model_cls: type[BaseModel], model_def: type[BaseModel]): """ Check whether ``model_cls`` can be extended with ``model_def``. """ if model_cls._abstract and not model_def._abstract: raise TypeError( f"{model_def} transforms the abstract model {model_cls._name!r} into a non-abstract model. " "That class should either inherit from AbstractModel, or set a different '_name'." ) if model_cls._transient != model_def._transient: if model_cls._transient: raise TypeError( f"{model_def} transforms the transient model {model_cls._name!r} into a non-transient model. " "That class should either inherit from TransientModel, or set a different '_name'." ) else: raise TypeError( f"{model_def} transforms the model {model_cls._name!r} into a transient model. " "That class should either inherit from Model, or set a different '_name'." ) def _check_model_parent_extension(model_cls: type[BaseModel], model_def: type[BaseModel], parent_cls: type[BaseModel]): """ Check whether ``model_cls`` can inherit from ``parent_cls``. """ if model_cls._abstract and not parent_cls._abstract: raise TypeError( f"In {model_def}, abstract model {model_cls._name!r} cannot inherit from non-abstract model {parent_cls._name!r}." ) def _init_model_class_attributes(model_cls: type[BaseModel]): """ Initialize model class attributes. """ assert is_model_class(model_cls) model_cls._description = model_cls._name model_cls._table = model_cls._name.replace('.', '_') model_cls._log_access = model_cls._auto inherits = {} depends = {} for base in reversed(model_cls._base_classes__): if is_model_definition(base): # the following attributes are not taken from registry classes if model_cls._name not in base._inherit and not base._description: _logger.warning("The model %s has no _description", model_cls._name) model_cls._description = base._description or model_cls._description model_cls._table = base._table or model_cls._table model_cls._log_access = getattr(base, '_log_access', model_cls._log_access) inherits.update(base._inherits) for mname, fnames in base._depends.items(): depends.setdefault(mname, []).extend(fnames) # avoid assigning an empty dict to save memory if inherits: model_cls._inherits = inherits if depends: model_cls._depends = depends # update _inherits_children of parent models registry = model_cls.pool for parent_name in model_cls._inherits: registry[parent_name]._inherits_children.add(model_cls._name) # recompute attributes of _inherit_children models for child_name in model_cls._inherit_children: _init_model_class_attributes(registry[child_name]) def setup_model_classes(env: Environment): registry = env.registry # we must setup ir.model before adding manual fields because _add_manual_models may # depend on behavior that is implemented through overrides, such as is_mail_thread which # is implemented through an override to env['ir.model']._instanciate_attrs _prepare_setup(registry['ir.model']) # add manual models if registry._init_modules: _add_manual_models(env) # prepare the setup on all models models_classes = list(registry.values()) for model_cls in models_classes: _prepare_setup(model_cls) # do the actual setup for model_cls in models_classes: _setup(model_cls, env) for model_cls in models_classes: _setup_fields(model_cls, env) for model_cls in models_classes: model_cls(env, (), ())._post_model_setup__() def _prepare_setup(model_cls: type[BaseModel]): """ Prepare the setup of the model. """ if model_cls._setup_done__: assert model_cls.__bases__ == model_cls._base_classes__ return # changing base classes is costly, do it only when necessary if model_cls.__bases__ != model_cls._base_classes__: model_cls.__bases__ = model_cls._base_classes__ # reset those attributes on the model's class for _setup_fields() below for attr in ('_rec_name', '_active_name'): discardattr(model_cls, attr) # reset properties memoized on model_cls model_cls._constraint_methods = models.BaseModel._constraint_methods model_cls._ondelete_methods = models.BaseModel._ondelete_methods model_cls._onchange_methods = models.BaseModel._onchange_methods def _setup(model_cls: type[BaseModel], env: Environment): """ Determine all the fields of the model. """ if model_cls._setup_done__: return # the classes that define this model, i.e., the ones that are not # registry classes; the purpose of this attribute is to behave as a # cache of [c for c in model_cls.mro() if not is_model_class(c))], which # is heavily used in function fields.resolve_mro() model_cls._model_classes__ = tuple(c for c in model_cls.mro() if getattr(c, 'pool', None) is None) # 1. determine the proper fields of the model: the fields defined on the # class and magic fields, not the inherited or custom ones # retrieve fields from parent classes, and duplicate them on model_cls to # avoid clashes with inheritance between different models for name in model_cls._fields: discardattr(model_cls, name) model_cls._fields__.clear() # collect the definitions of each field (base definition + overrides) definitions = defaultdict(list) for cls in reversed(model_cls._model_classes__): # this condition is an optimization of is_model_definition(cls) if isinstance(cls, models.MetaModel): for field in cls._field_definitions: definitions[field.name].append(field) for name, fields_ in definitions.items(): if f'{model_cls._name}.{name}' in model_cls.pool._database_translated_fields: # the field is currently translated in the database; ensure the # field is translated to avoid converting its column to varchar # and losing data translate = next(( field._args__['translate'] for field in reversed(fields_) if 'translate' in field._args__ ), False) if not translate: field_translate = FIELD_TRANSLATE.get( model_cls.pool._database_translated_fields[f'{model_cls._name}.{name}'], True ) # patch the field definition by adding an override _logger.debug("Patching %s.%s with translate=True", model_cls._name, name) fields_.append(type(fields_[0])(translate=field_translate)) if f'{model_cls._name}.{name}' in model_cls.pool._database_company_dependent_fields: # the field is currently company dependent in the database; ensure # the field is company dependent to avoid converting its column to # the base data type company_dependent = next(( field._args__['company_dependent'] for field in reversed(fields_) if 'company_dependent' in field._args__ ), False) if not company_dependent: # validate column type again in case the column type is changed by upgrade script rows = env.execute_query(sql.SQL( 'SELECT data_type FROM information_schema.columns' ' WHERE table_name = %s AND column_name = %s AND table_schema = current_schema', model_cls._table, name, )) if rows and rows[0][0] == 'jsonb': # patch the field definition by adding an override _logger.warning("Patching %s.%s with company_dependent=True", model_cls._name, name) fields_.append(type(fields_[0])(company_dependent=True)) if len(fields_) == 1 and fields_[0]._direct and fields_[0].model_name == model_cls._name: model_cls._fields__[name] = fields_[0] else: Field = type(fields_[-1]) add_field(model_cls, name, Field(_base_fields__=tuple(fields_))) # 2. add manual fields if model_cls.pool._init_modules: _add_manual_fields(model_cls, env) # 3. make sure that parent models determine their own fields, then add # inherited fields to model_cls _check_inherits(model_cls) for parent_name in model_cls._inherits: _setup(model_cls.pool[parent_name], env) _add_inherited_fields(model_cls) # 4. initialize more field metadata model_cls._setup_done__ = True for field in model_cls._fields.values(): field.prepare_setup() # 5. determine and validate rec_name if model_cls._rec_name: assert model_cls._rec_name in model_cls._fields, \ "Invalid _rec_name=%r for model %r" % (model_cls._rec_name, model_cls._name) elif 'name' in model_cls._fields: model_cls._rec_name = 'name' elif model_cls._custom and 'x_name' in model_cls._fields: model_cls._rec_name = 'x_name' # 6. determine and validate active_name if model_cls._active_name: assert (model_cls._active_name in model_cls._fields and model_cls._active_name in ('active', 'x_active')), \ ("Invalid _active_name=%r for model %r; only 'active' and " "'x_active' are supported and the field must be present on " "the model") % (model_cls._active_name, model_cls._name) elif 'active' in model_cls._fields: model_cls._active_name = 'active' elif 'x_active' in model_cls._fields: model_cls._active_name = 'x_active' # 7. determine table objects assert not model_cls._table_object_definitions, "model_cls is a registry model" model_cls._table_objects = frozendict({ cons.full_name(model_cls): cons for cls in reversed(model_cls._model_classes__) if isinstance(cls, models.MetaModel) for cons in cls._table_object_definitions }) def _check_inherits(model_cls: type[BaseModel]): for comodel_name, field_name in model_cls._inherits.items(): field = model_cls._fields.get(field_name) if not field or field.type != 'many2one': raise TypeError( f"Missing many2one field definition for _inherits reference {field_name!r} in model {model_cls._name!r}. " f"Add a field like: {field_name} = fields.Many2one({comodel_name!r}, required=True, ondelete='cascade')" ) if not (field.delegate and field.required and (field.ondelete or "").lower() in ("cascade", "restrict")): raise TypeError( f"Field definition for _inherits reference {field_name!r} in {model_cls._name!r} " "must be marked as 'delegate', 'required' with ondelete='cascade' or 'restrict'" ) def _add_inherited_fields(model_cls: type[BaseModel]): """ Determine inherited fields. """ if model_cls._abstract or not model_cls._inherits: return # determine which fields can be inherited to_inherit = { name: (parent_fname, field) for parent_model_name, parent_fname in model_cls._inherits.items() for name, field in model_cls.pool[parent_model_name]._fields.items() } # add inherited fields that are not redefined locally for name, (parent_fname, field) in to_inherit.items(): if name not in model_cls._fields: # inherited fields are implemented as related fields, with the # following specific properties: # - reading inherited fields should not bypass access rights # - copy inherited fields iff their original field is copied field_cls = type(field) add_field(model_cls, name, field_cls( inherited=True, inherited_field=field, related=f"{parent_fname}.{name}", related_sudo=False, copy=field.copy, readonly=field.readonly, export_string_translation=field.export_string_translation, )) def _setup_fields(model_cls: type[BaseModel], env: Environment): """ Setup the fields, except for recomputation triggers. """ bad_fields = [] many2one_company_dependents = model_cls.pool.many2one_company_dependents model = model_cls(env, (), ()) for name, field in model_cls._fields.items(): try: field.setup(model) except Exception: if field.base_field.manual: # Something goes wrong when setup a manual field. # This can happen with related fields using another manual many2one field # that hasn't been loaded because the comodel does not exist yet. # This can also be a manual function field depending on not loaded fields yet. bad_fields.append(name) continue raise if field.type == 'many2one' and field.company_dependent: many2one_company_dependents.add(field.comodel_name, field) for name in bad_fields: pop_field(model_cls, name) def _add_manual_models(env: Environment): """ Add extra models to the registry. """ # clean up registry first for name, model_cls in list(env.registry.items()): if model_cls._custom: del env.registry.models[name] # remove the model's name from its parents' _inherit_children for parent_cls in model_cls.__bases__: if hasattr(parent_cls, 'pool'): parent_cls._inherit_children.discard(name) # we cannot use self._fields to determine translated fields, as it has not been set up yet env.cr.execute("SELECT *, name->>'en_US' AS name FROM ir_model WHERE state = 'manual'") for model_data in env.cr.dictfetchall(): attrs = env['ir.model']._instanciate_attrs(model_data) # adapt _auto and _log_access if necessary table_name = model_data["model"].replace(".", "_") table_kind = sql.table_kind(env.cr, table_name) if table_kind not in (sql.TableKind.Regular, None): _logger.info( "Model %r is backed by table %r which is not a regular table (%r), disabling automatic schema management", model_data["model"], table_name, table_kind, ) attrs['_auto'] = False env.cr.execute( """ SELECT a.attname FROM pg_attribute a JOIN pg_class t ON a.attrelid = t.oid AND t.relname = %s WHERE a.attnum > 0 -- skip system columns AND t.relnamespace = current_schema::regnamespace """, [table_name] ) columns = {colinfo[0] for colinfo in env.cr.fetchall()} attrs['_log_access'] = set(models.LOG_ACCESS_COLUMNS) <= columns model_def = type('CustomDefinitionModel', (models.Model,), attrs) add_to_registry(env.registry, model_def) def _add_manual_fields(model_cls: type[BaseModel], env: Environment): """ Add extra fields on model. """ IrModelFields = env['ir.model.fields'] fields_data = IrModelFields._get_manual_field_data(model_cls._name) for name, field_data in fields_data.items(): if name not in model_cls._fields and field_data['state'] == 'manual': try: attrs = IrModelFields._instanciate_attrs(field_data) if attrs: field = fields.Field._by_type__[field_data['ttype']](**attrs) add_field(model_cls, name, field) except Exception: _logger.exception("Failed to load field %s.%s: skipped", model_cls._name, field_data['name']) def add_field(model_cls: type[BaseModel], name: str, field: Field): """ Add the given ``field`` under the given ``name`` on the model class of the given ``model``. """ # Assert the name is an existing field in the model, or any model in the _inherits # or a custom field (starting by `x_`) is_class_field = any( isinstance(getattr(model, name, None), fields.Field) for model in [model_cls] + [model_cls.pool[inherit] for inherit in model_cls._inherits] ) if not (is_class_field or model_cls.pool['ir.model.fields']._is_manual_name(None, name)): raise ValidationError( # pylint: disable=missing-gettext f"The field `{name}` is not defined in the `{model_cls._name}` Python class and does not start with 'x_'" ) # Assert the attribute to assign is a Field if not isinstance(field, fields.Field): raise ValidationError("You can only add `fields.Field` objects to a model fields") # pylint: disable=missing-gettext if not isinstance(getattr(model_cls, name, field), fields.Field): _logger.warning("In model %r, field %r overriding existing value", model_cls._name, name) setattr(model_cls, name, field) field._toplevel = True field.__set_name__(model_cls, name) # add field as an attribute and in model_cls._fields__ (for reflection) model_cls._fields__[name] = field def pop_field(model_cls: type[BaseModel], name: str) -> Field | None: """ Remove the field with the given ``name`` from the model class of ``model``. """ field = model_cls._fields__.pop(name, None) discardattr(model_cls, name) if model_cls._rec_name == name: # fixup _rec_name and display_name's dependencies model_cls._rec_name = None if model_cls.display_name in model_cls.pool.field_depends: model_cls.pool.field_depends[model_cls.display_name] = tuple( dep for dep in model_cls.pool.field_depends[model_cls.display_name] if dep != name ) return field