From 8edf86551e4a67f8618c20da593580a299bbb108 Mon Sep 17 00:00:00 2001 From: oca-ci Date: Fri, 10 Jan 2025 21:16:20 +0000 Subject: [PATCH 01/14] [UPD] Update l10n_br_nfe.pot --- l10n_br_nfe/i18n/l10n_br_nfe.pot | 5 ----- 1 file changed, 5 deletions(-) diff --git a/l10n_br_nfe/i18n/l10n_br_nfe.pot b/l10n_br_nfe/i18n/l10n_br_nfe.pot index e9bd024c8a5d..8d328d558a96 100644 --- a/l10n_br_nfe/i18n/l10n_br_nfe.pot +++ b/l10n_br_nfe/i18n/l10n_br_nfe.pot @@ -3092,11 +3092,6 @@ msgstr "" msgid "Mês e Ano de Referência, formato: MM/AAAA" msgstr "" -#. module: l10n_br_nfe -#: model:ir.model,name:l10n_br_nfe.model_l10n_br_fiscal_nbm -msgid "NBM" -msgstr "" - #. module: l10n_br_nfe #: model:ir.model,name:l10n_br_nfe.model_l10n_br_fiscal_ncm msgid "NCM" From d78c7268cbac5988b02fd5a1a011dd65e6f759fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Valyi?= Date: Tue, 8 Oct 2024 04:00:52 +0000 Subject: [PATCH 02/14] [REF] spec_driven_model: simplify and more robust ensure generic _stacking_points is readonly --- spec_driven_model/models/spec_mixin.py | 11 ++++++++++- spec_driven_model/models/spec_models.py | 17 ++++++++--------- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/spec_driven_model/models/spec_mixin.py b/spec_driven_model/models/spec_mixin.py index 8839bf6a4a8e..0b09aa9b13ea 100644 --- a/spec_driven_model/models/spec_mixin.py +++ b/spec_driven_model/models/spec_mixin.py @@ -2,6 +2,7 @@ # License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0.en.html). from odoo import api, models +from odoo.tools import frozendict from .spec_models import SPEC_MIXIN_MAPPINGS, SpecModel, StackedModel @@ -21,7 +22,15 @@ class SpecMixin(models.AbstractModel): _description = "root abstract model meant for xsd generated fiscal models" _name = "spec.mixin" _inherit = ["spec.mixin_export", "spec.mixin_import"] - _stacking_points = {} + + # actually _stacking_points are model and even schema specific + # but the legacy code used it extensively so a first defensive + # action we can take is to use a frozendict so it is readonly + # at least. In the future the whole stacking system should be + # scoped under a given schema and version as in + # https://github.com/OCA/l10n-brazil/pull/3424 + _stacking_points = frozendict({}) + # _spec_module = 'override.with.your.python.module' # _binding_module = 'your.pyhthon.binding.module' # _odoo_module = 'your.odoo_module' diff --git a/spec_driven_model/models/spec_models.py b/spec_driven_model/models/spec_models.py index c7779fd829f0..fa52f6633607 100644 --- a/spec_driven_model/models/spec_models.py +++ b/spec_driven_model/models/spec_models.py @@ -238,15 +238,14 @@ class StackedModel(SpecModel): @classmethod def _build_model(cls, pool, cr): # inject all stacked m2o as inherited classes - if cls._stacked: - _logger.info(f"building StackedModel {cls._name} {cls}") - node = cls._odoo_name_to_class(cls._stacked, cls._spec_module) - env = api.Environment(cr, SUPERUSER_ID, {}) - for kind, klass, _path, _field_path, _child_concrete in cls._visit_stack( - env, node - ): - if kind == "stacked" and klass not in cls.__bases__: - cls.__bases__ = (klass,) + cls.__bases__ + _logger.info(f"building StackedModel {cls._name} {cls}") + node = cls._odoo_name_to_class(cls._stacked, cls._spec_module) + env = api.Environment(cr, SUPERUSER_ID, {}) + for kind, klass, _path, _field_path, _child_concrete in cls._visit_stack( + env, node + ): + if kind == "stacked" and klass not in cls.__bases__: + cls.__bases__ = (klass,) + cls.__bases__ return super()._build_model(pool, cr) @api.model From 0e440ca0bd6d44978e8d35e56d9fe75395c51288 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Valyi?= Date: Tue, 8 Oct 2024 04:02:18 +0000 Subject: [PATCH 03/14] [FIX] l10n_br_nfe: stacking fixes --- l10n_br_nfe/models/document.py | 5 ----- l10n_br_nfe/models/document_line.py | 5 ----- l10n_br_nfe/models/document_related.py | 6 +----- l10n_br_nfe/models/document_supplement.py | 6 +----- l10n_br_nfe/tests/test_nfe_structure.py | 3 --- 5 files changed, 2 insertions(+), 23 deletions(-) diff --git a/l10n_br_nfe/models/document.py b/l10n_br_nfe/models/document.py index eb4b969bae05..c1c09c7e47d9 100644 --- a/l10n_br_nfe/models/document.py +++ b/l10n_br_nfe/models/document.py @@ -80,12 +80,7 @@ class NFe(spec_models.StackedModel): _name = "l10n_br_fiscal.document" _inherit = ["l10n_br_fiscal.document", "nfe.40.infnfe", "nfe.40.fat"] _stacked = "nfe.40.infnfe" - _field_prefix = "nfe40_" - _schema_name = "nfe" - _schema_version = "4.0.0" - _odoo_module = "l10n_br_nfe" _spec_module = "odoo.addons.l10n_br_nfe_spec.models.v4_0.leiaute_nfe_v4_00" - _spec_tab_name = "NFe" _nfe_search_keys = ["nfe40_Id"] # all m2o at this level will be stacked even if not required: diff --git a/l10n_br_nfe/models/document_line.py b/l10n_br_nfe/models/document_line.py index d11fd09cb72b..f9dba76a12f4 100644 --- a/l10n_br_nfe/models/document_line.py +++ b/l10n_br_nfe/models/document_line.py @@ -71,12 +71,7 @@ class NFeLine(spec_models.StackedModel): _name = "l10n_br_fiscal.document.line" _inherit = ["l10n_br_fiscal.document.line", "nfe.40.det"] _stacked = "nfe.40.det" - _field_prefix = "nfe40_" - _schema_name = "nfe" - _schema_version = "4.0.0" - _odoo_module = "l10n_br_nfe" _spec_module = "odoo.addons.l10n_br_nfe_spec.models.v4_0.leiaute_nfe_v4_00" - _spec_tab_name = "NFe" _stacking_points = {} # all m2o below this level will be stacked even if not required: _force_stack_paths = ("det.imposto.",) diff --git a/l10n_br_nfe/models/document_related.py b/l10n_br_nfe/models/document_related.py index ad5e8b3e1124..6dda2590a328 100644 --- a/l10n_br_nfe/models/document_related.py +++ b/l10n_br_nfe/models/document_related.py @@ -21,12 +21,8 @@ class NFeRelated(spec_models.StackedModel): _name = "l10n_br_fiscal.document.related" _inherit = ["l10n_br_fiscal.document.related", "nfe.40.nfref"] _stacked = "nfe.40.nfref" - _field_prefix = "nfe40_" - _schema_name = "nfe" - _schema_version = "4.0.0" - _odoo_module = "l10n_br_nfe" + _stacking_points = {} _spec_module = "odoo.addons.l10n_br_nfe_spec.models.v4_0.leiaute_nfe_v4_00" - _spec_tab_name = "NFe" _stack_skip = ("nfe40_NFref_ide_id",) # all m2o below this level will be stacked even if not required: _rec_name = "nfe40_refNFe" diff --git a/l10n_br_nfe/models/document_supplement.py b/l10n_br_nfe/models/document_supplement.py index 85590853eec7..a4e55aab4632 100644 --- a/l10n_br_nfe/models/document_supplement.py +++ b/l10n_br_nfe/models/document_supplement.py @@ -10,9 +10,5 @@ class NFeSupplement(spec_models.StackedModel): _description = "NFe Supplement Document" _inherit = "nfe.40.infnfesupl" _stacked = "nfe.40.infnfesupl" - _field_prefix = "nfe40_" - _schema_name = "nfe" - _schema_version = "4.0.0" - _odoo_module = "l10n_br_nfe" + _stacking_points = {} _spec_module = "odoo.addons.l10n_br_nfe_spec.models.v4_0.leiaute_nfe_v4_00" - _spec_tab_name = "NFe" diff --git a/l10n_br_nfe/tests/test_nfe_structure.py b/l10n_br_nfe/tests/test_nfe_structure.py index 7fd77b0e8d39..6f384d6ed1c6 100644 --- a/l10n_br_nfe/tests/test_nfe_structure.py +++ b/l10n_br_nfe/tests/test_nfe_structure.py @@ -112,9 +112,6 @@ def test_doc_stacking_points(self): "nfe40_ide", "nfe40_infAdic", "nfe40_pag", - "nfe40_refECF", - "nfe40_refNF", - "nfe40_refNFP", "nfe40_retTrib", "nfe40_total", "nfe40_transp", From a9cc44efd80a1a195d11c1070f944042eae5dade Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Valyi?= Date: Thu, 12 Sep 2024 03:26:35 +0000 Subject: [PATCH 04/14] [IMP] spec_driven_model: multi-schemas compat --- spec_driven_model/models/spec_export.py | 43 +++++++---- spec_driven_model/models/spec_import.py | 10 ++- spec_driven_model/models/spec_mixin.py | 21 +---- spec_driven_model/models/spec_models.py | 89 +++++++++++++++------- spec_driven_model/tests/__init__.py | 3 + spec_driven_model/tests/spec_purchase.py | 9 ++- spec_driven_model/tests/test_spec_model.py | 36 ++++----- 7 files changed, 127 insertions(+), 84 deletions(-) diff --git a/spec_driven_model/models/spec_export.py b/spec_driven_model/models/spec_export.py index 3ba7e483f294..86cddd9fc03d 100644 --- a/spec_driven_model/models/spec_export.py +++ b/spec_driven_model/models/spec_export.py @@ -70,7 +70,7 @@ def _export_fields(self, xsd_fields, class_obj, export_dict): continue if ( not self._fields.get(xsd_field) - ) and xsd_field not in self._stacking_points.keys(): + ) and xsd_field not in self._get_stacking_points().keys(): continue field_spec_name = xsd_field.replace(class_obj._field_prefix, "") field_spec = False @@ -90,7 +90,7 @@ def _export_fields(self, xsd_fields, class_obj, export_dict): field_data = self._export_field( xsd_field, class_obj, field_spec, export_dict.get(field_spec_name) ) - if xsd_field in self._stacking_points.keys(): + if xsd_field in self._get_stacking_points().keys(): if not field_data: # stacked nested tags are skipped if empty continue @@ -106,11 +106,13 @@ def _export_field(self, xsd_field, class_obj, field_spec, export_value=None): """ self.ensure_one() # TODO: Export number required fields with Zero. - field = class_obj._fields.get(xsd_field, self._stacking_points.get(xsd_field)) + field = class_obj._fields.get( + xsd_field, self._get_stacking_points().get(xsd_field) + ) xsd_required = field.xsd_required if hasattr(field, "xsd_required") else None xsd_type = field.xsd_type if hasattr(field, "xsd_type") else None if field.type == "many2one": - if (not self._stacking_points.get(xsd_field)) and ( + if (not self._get_stacking_points().get(xsd_field)) and ( not self[xsd_field] and not xsd_required ): if field.comodel_name not in self._get_spec_classes(): @@ -144,9 +146,9 @@ def _export_field(self, xsd_field, class_obj, field_spec, export_value=None): def _export_many2one(self, field_name, xsd_required, class_obj=None): self.ensure_one() - if field_name in self._stacking_points.keys(): + if field_name in self._get_stacking_points().keys(): return self._build_generateds( - class_name=self._stacking_points[field_name].comodel_name + class_name=self._get_stacking_points()[field_name].comodel_name ) else: return (self[field_name] or self)._build_generateds( @@ -158,7 +160,7 @@ def _export_one2many(self, field_name, class_obj=None): relational_data = [] for relational_field in self[field_name]: field_data = relational_field._build_generateds( - class_obj._fields[field_name].comodel_name + class_name=class_obj._fields[field_name].comodel_name ) relational_data.append(field_data) return relational_data @@ -190,10 +192,11 @@ def _export_datetime(self, field_name): ).isoformat("T") ) - def _build_generateds(self, class_name=False): + # TODO rename _build_binding + def _build_generateds(self, class_name=False, spec_schema=None, spec_version=None): """ Iterate over an Odoo record and its m2o and o2m sub-records - using a pre-order tree traversal and maps the Odoo record values + using a pre-order tree traversal and map the Odoo record values to a dict of Python binding values. These values will later be injected as **kwargs in the proper XML Python @@ -201,9 +204,16 @@ def _build_generateds(self, class_name=False): sub binding instances already properly instanciated. """ self.ensure_one() + if spec_schema and spec_version: + self = self.with_context( + self.env, spec_schema=spec_schema, spec_version=spec_version + ) + spec_prefix = self._spec_prefix(self._context) if not class_name: - if hasattr(self, "_stacked"): - class_name = self._stacked + if hasattr(self, f"_{spec_prefix}_spec_settings"): + class_name = getattr(self, f"_{spec_prefix}_spec_settings")[ + "stacking_mixin" + ] else: class_name = self._name @@ -231,12 +241,15 @@ def _build_generateds(self, class_name=False): def export_xml(self): self.ensure_one() result = [] - - if hasattr(self, "_stacked"): + if hasattr(self, f"_{self._spec_prefix(self._context)}_spec_settings"): binding_instance = self._build_generateds() result.append(binding_instance) return result - def export_ds(self): # TODO rename export_binding! + def export_ds( + self, spec_schema, spec_version + ): # TODO change name -> export_binding! self.ensure_one() - return self.export_xml() + return self.with_context( + spec_schema=spec_schema, spec_version=spec_version + ).export_xml() diff --git a/spec_driven_model/models/spec_import.py b/spec_driven_model/models/spec_import.py index 83ed68d3d682..e99bbc01e28d 100644 --- a/spec_driven_model/models/spec_import.py +++ b/spec_driven_model/models/spec_import.py @@ -27,7 +27,7 @@ class SpecMixinImport(models.AbstractModel): """ @api.model - def build_from_binding(self, node, dry_run=False): + def build_from_binding(self, spec_schema, spec_version, node, dry_run=False): """ Build an instance of an Odoo Model from a pre-populated Python binding object. Binding object such as the ones generated using @@ -42,8 +42,12 @@ def build_from_binding(self, node, dry_run=False): Defaults values and control options are meant to be passed in the context. """ - model = self._get_concrete_model(self._name) - attrs = model.with_context(dry_run=dry_run).build_attrs(node) + model = self.with_context( + spec_schema=spec_schema, spec_version=spec_version + )._get_concrete_model(self._name) + attrs = model.with_context( + dry_run=dry_run, spec_schema=spec_schema, spec_version=spec_version + ).build_attrs(node) if dry_run: return model.new(attrs) else: diff --git a/spec_driven_model/models/spec_mixin.py b/spec_driven_model/models/spec_mixin.py index 0b09aa9b13ea..ca7ac20e9ff3 100644 --- a/spec_driven_model/models/spec_mixin.py +++ b/spec_driven_model/models/spec_mixin.py @@ -1,8 +1,7 @@ -# Copyright 2019-2020 Akretion - Raphael Valyi +# Copyright 2019-TODAY Akretion - Raphael Valyi # License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0.en.html). from odoo import api, models -from odoo.tools import frozendict from .spec_models import SPEC_MIXIN_MAPPINGS, SpecModel, StackedModel @@ -23,20 +22,6 @@ class SpecMixin(models.AbstractModel): _name = "spec.mixin" _inherit = ["spec.mixin_export", "spec.mixin_import"] - # actually _stacking_points are model and even schema specific - # but the legacy code used it extensively so a first defensive - # action we can take is to use a frozendict so it is readonly - # at least. In the future the whole stacking system should be - # scoped under a given schema and version as in - # https://github.com/OCA/l10n-brazil/pull/3424 - _stacking_points = frozendict({}) - - # _spec_module = 'override.with.your.python.module' - # _binding_module = 'your.pyhthon.binding.module' - # _odoo_module = 'your.odoo_module' - # _field_prefix = 'your_field_prefix_' - # _schema_name = 'your_schema_name' - def _valid_field_parameter(self, field, name): if name in ( "xsd_type", @@ -73,6 +58,7 @@ def _register_hook(self): return res setattr(self.env.registry, load_key, True) access_data = [] + access_fields = [] self.env.cr.execute( """SELECT DISTINCT relation FROM ir_model_fields WHERE relation LIKE %s;""", @@ -85,7 +71,6 @@ def _register_hook(self): if self.env.registry.get(i[0]) and not SPEC_MIXIN_MAPPINGS[self.env.cr.dbname].get(i[0]) } - for name in remaining_models: spec_class = StackedModel._odoo_name_to_class(name, self._spec_module) if spec_class is None: @@ -113,6 +98,8 @@ def _register_hook(self): "_module": self._odoo_module, }, ) + model_type._schema_name = self._schema_name + model_type._schema_version = self._schema_version models.MetaModel.module_to_models[self._odoo_module] += [model_type] # now we init these models properly diff --git a/spec_driven_model/models/spec_models.py b/spec_driven_model/models/spec_models.py index fa52f6633607..72589b5da60c 100644 --- a/spec_driven_model/models/spec_models.py +++ b/spec_driven_model/models/spec_models.py @@ -4,6 +4,7 @@ import logging import sys from collections import OrderedDict, defaultdict +from importlib import import_module from inspect import getmembers, isclass from odoo import SUPERUSER_ID, _, api, models @@ -66,6 +67,20 @@ def _compute_display_name(self): rec.display_name = _("Abrir...") return res + def _get_stacking_points(self): + key = f"_{self._spec_prefix(self._context)}_spec_settings" + if hasattr(self, key): + return getattr(self, key)["stacking_points"] + return {} + + @classmethod + def _spec_prefix(cls, context=None, spec_schema=None, spec_version=None): + if context and context.get("spec_schema"): + spec_schema = context.get("spec_schema") + if context and context.get("spec_version"): + spec_version = context.get("spec_version") + return "%s%s" % (spec_schema, spec_version.replace(".", "")[:2]) + @classmethod def _build_model(cls, pool, cr): """ @@ -212,8 +227,9 @@ class StackedModel(SpecModel): By inheriting from StackModel instead, your models.Model can instead inherit all the mixins that would correspond to the nested xsd - nodes starting from the _stacked node. _stack_skip allows you to avoid - stacking specific nodes. + nodes starting from the stacking_mixin. stacking_skip_paths allows you to avoid + stacking specific nodes while stacking_force_paths will stack many2one + entities even if they are not required. In Brazil it allows us to have mostly the fiscal document objects and the fiscal document line object with many details @@ -225,39 +241,49 @@ class StackedModel(SpecModel): _register = False # forces you to inherit StackeModel properly - # define _stacked in your submodel to define the model of the XML tags - # where we should start to - # stack models of nested tags in the same object. - _stacked = False - _stack_path = "" - _stack_skip = () - # all m2o below these paths will be stacked even if not required: - _force_stack_paths = () - _stacking_points = {} - @classmethod def _build_model(cls, pool, cr): + mod = import_module(".".join(cls.__module__.split(".")[:-1])) + if hasattr(cls, "_schema_name"): + schema = cls._schema_name + version = cls._schema_version.replace(".", "")[:2] + else: + mod = import_module(".".join(cls.__module__.split(".")[:-1])) + schema = mod.spec_schema + version = mod.spec_version.replace(".", "")[:2] + spec_prefix = cls._spec_prefix(spec_schema=schema, spec_version=version) + stacking_settings = getattr(cls, "_%s_spec_settings" % (spec_prefix,)) # inject all stacked m2o as inherited classes + if cls._stacked: + _logger.info(f"building StackedModel {cls._name} {cls}") - node = cls._odoo_name_to_class(cls._stacked, cls._spec_module) - env = api.Environment(cr, SUPERUSER_ID, {}) - for kind, klass, _path, _field_path, _child_concrete in cls._visit_stack( - env, node - ): - if kind == "stacked" and klass not in cls.__bases__: - cls.__bases__ = (klass,) + cls.__bases__ + node = cls._odoo_name_to_class( + stacking_settings["stacking_mixin"], stacking_settings["module"] + ) + env = api.Environment(cr, SUPERUSER_ID, {}) + for kind, klass, _path, _field_path, _child_concrete in cls._visit_stack( + env, node, stacking_settings + ): + if kind == "stacked" and klass not in cls.__bases__: + cls.__bases__ = (klass,) + cls.__bases__ return super()._build_model(pool, cr) @api.model def _add_field(self, name, field): for cls in type(self).mro(): if issubclass(cls, StackedModel): - if name in type(self)._stacking_points.keys(): - return + if hasattr(self, "_schema_name"): + prefix = self._spec_prefix( + None, self._schema_name, self._schema_version + ) + key = f"_{prefix}_spec_settings" + stacking_points = getattr(self, key)["stacking_points"] + if name in stacking_points.keys(): + return return super()._add_field(name, field) @classmethod - def _visit_stack(cls, env, node, path=None): + def _visit_stack(cls, env, node, stacking_settings, path=None): """Pre-order traversal of the stacked models tree. 1. This method is used to dynamically inherit all the spec models stacked together from an XML hierarchy. @@ -269,7 +295,7 @@ def _visit_stack(cls, env, node, path=None): # https://github.com/OCA/l10n-brazil/pull/1272#issuecomment-821806603 node._description = None if path is None: - path = cls._stacked.split(".")[-1] + path = stacking_settings["stacking_mixin"].split(".")[-1] SpecModel._map_concrete(env.cr.dbname, node._name, cls._name, quiet=True) yield "stacked", node, path, None, None @@ -293,10 +319,15 @@ def _visit_stack(cls, env, node, path=None): and i[1].xsd_choice_required, } for name, f in fields.items(): - if f["type"] not in ["many2one", "one2many"] or name in cls._stack_skip: + if f["type"] not in [ + "many2one", + "one2many", + ] or name in stacking_settings.get("stacking_skip_paths", ""): # TODO change for view or export continue - child = cls._odoo_name_to_class(f["comodel_name"], cls._spec_module) + child = cls._odoo_name_to_class( + f["comodel_name"], stacking_settings["module"] + ) if child is None: # Not a spec field continue child_concrete = SPEC_MIXIN_MAPPINGS[env.cr.dbname].get(child._name) @@ -308,7 +339,7 @@ def _visit_stack(cls, env, node, path=None): force_stacked = any( stack_path in path + "." + field_path - for stack_path in cls._force_stack_paths + for stack_path in stacking_settings.get("stacking_force_paths", "") ) # many2one @@ -318,7 +349,9 @@ def _visit_stack(cls, env, node, path=None): # then we will STACK the child in the current class child._stack_path = path child_path = f"{path}.{field_path}" - cls._stacking_points[name] = env[node._name]._fields.get(name) - yield from cls._visit_stack(env, child, child_path) + stacking_settings["stacking_points"][name] = env[ + node._name + ]._fields.get(name) + yield from cls._visit_stack(env, child, stacking_settings, child_path) else: yield "many2one", node, path, field_path, child_concrete diff --git a/spec_driven_model/tests/__init__.py b/spec_driven_model/tests/__init__.py index c1e5e2a4f7a7..214f7be53ab6 100644 --- a/spec_driven_model/tests/__init__.py +++ b/spec_driven_model/tests/__init__.py @@ -1 +1,4 @@ from . import test_spec_model + +spec_schema = "poxsd" +spec_version = "10" diff --git a/spec_driven_model/tests/spec_purchase.py b/spec_driven_model/tests/spec_purchase.py index 9f01b44f760e..4ec3ad02aef6 100644 --- a/spec_driven_model/tests/spec_purchase.py +++ b/spec_driven_model/tests/spec_purchase.py @@ -41,10 +41,11 @@ class PurchaseOrder(spec_models.StackedModel): _name = "fake.purchase.order" _inherit = ["fake.purchase.order", "poxsd.10.purchaseordertype"] - _spec_module = "odoo.addons.spec_driven_model.tests.spec_poxsd" - _stacked = "poxsd.10.purchaseordertype" - _stacking_points = {} - _poxsd10_spec_module_classes = None + _poxsd10_spec_settings = { + "module": "odoo.addons.spec_driven_model.tests.spec_poxsd", + "stacking_mixin": "poxsd.10.purchaseordertype", + "stacking_points": {}, + } poxsd10_orderDate = fields.Date(compute="_compute_date") poxsd10_confirmDate = fields.Date(related="date_approve") diff --git a/spec_driven_model/tests/test_spec_model.py b/spec_driven_model/tests/test_spec_model.py index 2658e8d6a216..19c7aba012e8 100644 --- a/spec_driven_model/tests/test_spec_model.py +++ b/spec_driven_model/tests/test_spec_model.py @@ -82,18 +82,6 @@ def tearDownClass(cls): cls.loader.restore_registry() super(TestSpecModel, cls).tearDownClass() - # def test_loading_hook(self): - # - # remaining_spec_models = get_remaining_spec_models( - # self.env.cr, - # self.env.registry, - # "spec_driven_model", - # "odoo.addons.spec_driven_model.tests.spec_poxsd", - # ) - # self.assertEqual( - # remaining_spec_models, {"poxsd.10.purchaseorder", "poxsd.10.comment"} - # ) - def test_spec_models(self): self.assertTrue( set(self.env["res.partner"]._fields.keys()).issuperset( @@ -110,7 +98,11 @@ def test_spec_models(self): def test_stacked_model(self): po_fields_or_stacking = set(self.env["fake.purchase.order"]._fields.keys()) po_fields_or_stacking.update( - set(self.env["fake.purchase.order"]._stacking_points.keys()) + set( + self.env["fake.purchase.order"] + ._poxsd10_spec_settings["stacking_points"] + .keys() + ) ) self.assertTrue( po_fields_or_stacking.issuperset( @@ -118,7 +110,11 @@ def test_stacked_model(self): ) ) self.assertEqual( - list(self.env["fake.purchase.order"]._stacking_points.keys()), + list( + self.env["fake.purchase.order"] + ._poxsd10_spec_settings["stacking_points"] + .keys() + ), ["poxsd10_items"], ) @@ -159,7 +155,11 @@ def test_create_export_import(self): # 2nd we serialize it into a binding object: # (that could be further XML serialized) - po_binding = po._build_generateds() + po_binding = po._build_generateds(spec_schema="poxsd", spec_version="10") + self.assertEqual( + [s.__name__ for s in type(po_binding).mro()], + ["PurchaseOrderType", "object"], + ) self.assertEqual(po_binding.bill_to.name, "Wood Corner") self.assertEqual(po_binding.items.item[0].product_name, "Some product desc") self.assertEqual(po_binding.items.item[0].quantity, 42) @@ -206,12 +206,14 @@ def test_create_export_import(self): # 4th we import an Odoo PO from this binding object # first we will do a dry run import: imported_po_dry_run = self.env["fake.purchase.order"].build_from_binding( - po_binding, dry_run=True + "poxsd", "10", po_binding, dry_run=True ) assert isinstance(imported_po_dry_run.id, NewId) # now a real import: - imported_po = self.env["fake.purchase.order"].build_from_binding(po_binding) + imported_po = self.env["fake.purchase.order"].build_from_binding( + "poxsd", "10", po_binding + ) self.assertEqual(imported_po.partner_id.name, "Wood Corner") self.assertEqual( imported_po.partner_id.id, self.env.ref("base.res_partner_1").id From 661e63c726f24ec14c72bdbba90b82cd1efc8075 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Valyi?= Date: Mon, 7 Oct 2024 19:09:48 +0000 Subject: [PATCH 05/14] [REF] l10n_br_nfe: multi-schemas compat --- l10n_br_nfe/hooks.py | 2 +- l10n_br_nfe/models/__init__.py | 3 ++ l10n_br_nfe/models/document.py | 38 +++++++++++++---------- l10n_br_nfe/models/document_line.py | 14 +++++---- l10n_br_nfe/models/document_related.py | 10 +++--- l10n_br_nfe/models/document_supplement.py | 8 +++-- l10n_br_nfe/tests/test_nfe_import.py | 4 +-- l10n_br_nfe/tests/test_nfe_structure.py | 21 ++++++++++--- 8 files changed, 63 insertions(+), 37 deletions(-) diff --git a/l10n_br_nfe/hooks.py b/l10n_br_nfe/hooks.py index b6e8745b0167..18d2bbcdf4bd 100644 --- a/l10n_br_nfe/hooks.py +++ b/l10n_br_nfe/hooks.py @@ -37,7 +37,7 @@ def post_init_hook(cr, registry): nfe = ( env["nfe.40.infnfe"] .with_context(tracking_disable=True, edoc_type="in") - .build_from_binding(binding.NFe.infNFe) + .build_from_binding("nfe", "40", binding.NFe.infNFe) ) _logger.info(nfe.nfe40_emit.nfe40_CNPJ) except ValidationError: diff --git a/l10n_br_nfe/models/__init__.py b/l10n_br_nfe/models/__init__.py index 39dcb356be90..d57ab0b103e9 100644 --- a/l10n_br_nfe/models/__init__.py +++ b/l10n_br_nfe/models/__init__.py @@ -20,3 +20,6 @@ from . import invalidate_number from . import dfe from . import mde + +spec_schema = "nfe" +spec_version = "40" diff --git a/l10n_br_nfe/models/document.py b/l10n_br_nfe/models/document.py index c1c09c7e47d9..678c3fc67767 100644 --- a/l10n_br_nfe/models/document.py +++ b/l10n_br_nfe/models/document.py @@ -79,19 +79,21 @@ def filter_processador_edoc_nfe(record): class NFe(spec_models.StackedModel): _name = "l10n_br_fiscal.document" _inherit = ["l10n_br_fiscal.document", "nfe.40.infnfe", "nfe.40.fat"] - _stacked = "nfe.40.infnfe" - _spec_module = "odoo.addons.l10n_br_nfe_spec.models.v4_0.leiaute_nfe_v4_00" + _nfe40_spec_settings = { + "module": "odoo.addons.l10n_br_nfe_spec.models.v4_0.leiaute_nfe_v4_00", + "stacking_mixin": "nfe.40.infnfe", + "stacking_points": {}, + # all m2o at this level will be stacked even if not required: + "stacking_force_paths": ( + "infnfe.total", + "infnfe.infAdic", + "infnfe.exporta", + "infnfe.cobr", + "infnfe.cobr.fat", + ), + } _nfe_search_keys = ["nfe40_Id"] - # all m2o at this level will be stacked even if not required: - _force_stack_paths = ( - "infnfe.total", - "infnfe.infAdic", - "infnfe.exporta", - "infnfe.cobr", - "infnfe.cobr.fat", - ) - # When dynamic stacking is applied the NFe structure is: INFNFE_TREE = """ > @@ -671,7 +673,7 @@ def _export_many2one(self, field_name, xsd_required, class_obj=None): denormalized inner attribute has been set. """ self.ensure_one() - if field_name in self._stacking_points.keys(): + if field_name in self._get_stacking_points().keys(): if field_name == "nfe40_ISSQNtot" and not any( t == "issqn" for t in self.nfe40_det.mapped("product_id.tax_icms_or_issqn") @@ -679,7 +681,9 @@ def _export_many2one(self, field_name, xsd_required, class_obj=None): return False elif (not xsd_required) and field_name not in ["nfe40_enderDest"]: - comodel = self.env[self._stacking_points.get(field_name).comodel_name] + comodel = self.env[ + self._get_stacking_points().get(field_name).comodel_name + ] fields = [ f for f in comodel._fields @@ -687,7 +691,7 @@ def _export_many2one(self, field_name, xsd_required, class_obj=None): and f in self._fields.keys() and f # don't try to nfe40_fat id when reading nfe40_cobr for instance - not in self._stacking_points.keys() + not in self._get_stacking_points().keys() ] sub_tag_read = self.read(fields)[0] if not any( @@ -894,11 +898,11 @@ def _serialize(self, edocs): ): record.flush_model() self.env.invalidate_all() - inf_nfe = record.export_ds()[0] + inf_nfe = record.export_ds("nfe", "40")[0] inf_nfe_supl = None if record.nfe40_infNFeSupl: - inf_nfe_supl = record.nfe40_infNFeSupl.export_ds()[0] + inf_nfe_supl = record.nfe40_infNFeSupl.export_ds("nfe", "40")[0] nfe = Nfe(infNFe=inf_nfe, infNFeSupl=inf_nfe_supl, signature=None) edocs.append(nfe) @@ -1345,7 +1349,7 @@ def import_binding_nfe(self, binding, edoc_type="out"): document = ( self.env["nfe.40.infnfe"] .with_context(tracking_disable=True, edoc_type=edoc_type, dry_run=False) - .build_from_binding(binding.NFe.infNFe) + .build_from_binding("nfe", "40", binding.NFe.infNFe) ) if edoc_type == "in" and document.company_id.cnpj_cpf != cnpj_cpf.formata( diff --git a/l10n_br_nfe/models/document_line.py b/l10n_br_nfe/models/document_line.py index f9dba76a12f4..0a86fcd63b82 100644 --- a/l10n_br_nfe/models/document_line.py +++ b/l10n_br_nfe/models/document_line.py @@ -70,12 +70,14 @@ class NFeLine(spec_models.StackedModel): _name = "l10n_br_fiscal.document.line" _inherit = ["l10n_br_fiscal.document.line", "nfe.40.det"] - _stacked = "nfe.40.det" - _spec_module = "odoo.addons.l10n_br_nfe_spec.models.v4_0.leiaute_nfe_v4_00" - _stacking_points = {} - # all m2o below this level will be stacked even if not required: - _force_stack_paths = ("det.imposto.",) - _stack_skip = ("nfe40_det_infNFe_id",) + _nfe40_spec_settings = { + "module": "odoo.addons.l10n_br_nfe_spec.models.v4_0.leiaute_nfe_v4_00", + "stacking_mixin": "nfe.40.det", + "stacking_points": {}, + # all m2o below this level will be stacked even if not required: + "stacking_force_paths": ("det.imposto.",), + "stacking_skip_paths": ("nfe40_det_infNFe_id",), + } # When dynamic stacking is applied, the NFe line has the following structure: DET_TREE = """ diff --git a/l10n_br_nfe/models/document_related.py b/l10n_br_nfe/models/document_related.py index 6dda2590a328..a265287731d4 100644 --- a/l10n_br_nfe/models/document_related.py +++ b/l10n_br_nfe/models/document_related.py @@ -20,10 +20,12 @@ class NFeRelated(spec_models.StackedModel): _name = "l10n_br_fiscal.document.related" _inherit = ["l10n_br_fiscal.document.related", "nfe.40.nfref"] - _stacked = "nfe.40.nfref" - _stacking_points = {} - _spec_module = "odoo.addons.l10n_br_nfe_spec.models.v4_0.leiaute_nfe_v4_00" - _stack_skip = ("nfe40_NFref_ide_id",) + _nfe40_spec_settings = { + "module": "odoo.addons.l10n_br_nfe_spec.models.v4_0.leiaute_nfe_v4_00", + "stacking_mixin": "nfe.40.nfref", + "stacking_points": {}, + "stacking_skip_paths": ("nfe40_NFref_ide_id",), + } # all m2o below this level will be stacked even if not required: _rec_name = "nfe40_refNFe" diff --git a/l10n_br_nfe/models/document_supplement.py b/l10n_br_nfe/models/document_supplement.py index a4e55aab4632..6d114ed23d5f 100644 --- a/l10n_br_nfe/models/document_supplement.py +++ b/l10n_br_nfe/models/document_supplement.py @@ -9,6 +9,8 @@ class NFeSupplement(spec_models.StackedModel): _name = "l10n_br_fiscal.document.supplement" _description = "NFe Supplement Document" _inherit = "nfe.40.infnfesupl" - _stacked = "nfe.40.infnfesupl" - _stacking_points = {} - _spec_module = "odoo.addons.l10n_br_nfe_spec.models.v4_0.leiaute_nfe_v4_00" + _nfe40_spec_settings = { + "module": "odoo.addons.l10n_br_nfe_spec.models.v4_0.leiaute_nfe_v4_00", + "stacking_mixin": "nfe.40.infnfesupl", + "stacking_points": {}, + } diff --git a/l10n_br_nfe/tests/test_nfe_import.py b/l10n_br_nfe/tests/test_nfe_import.py index 8d9557b4d2cd..3a53d143cd8a 100644 --- a/l10n_br_nfe/tests/test_nfe_import.py +++ b/l10n_br_nfe/tests/test_nfe_import.py @@ -32,7 +32,7 @@ def test_import_in_nfe_dry_run(self): nfe = ( self.env["nfe.40.infnfe"] .with_context(tracking_disable=True, edoc_type="in") - .build_from_binding(binding.NFe.infNFe, dry_run=True) + .build_from_binding("nfe", "40", binding.NFe.infNFe, dry_run=True) ) assert isinstance(nfe.id, NewId) self._check_nfe(nfe) @@ -51,7 +51,7 @@ def test_import_in_nfe(self): nfe = ( self.env["nfe.40.infnfe"] .with_context(tracking_disable=True, edoc_type="in") - .build_from_binding(binding.NFe.infNFe, dry_run=False) + .build_from_binding("nfe", "40", binding.NFe.infNFe, dry_run=False) ) assert isinstance(nfe.id, int) diff --git a/l10n_br_nfe/tests/test_nfe_structure.py b/l10n_br_nfe/tests/test_nfe_structure.py index 6f384d6ed1c6..761523e0b387 100644 --- a/l10n_br_nfe/tests/test_nfe_structure.py +++ b/l10n_br_nfe/tests/test_nfe_structure.py @@ -26,11 +26,14 @@ def get_stacked_tree(cls, klass): # ≡ means o2m. Eventually followd by the mapped Odoo model """ spec_module = "odoo.addons.l10n_br_nfe_spec.models.v4_0.leiaute_nfe_v4_00" - node = SpecModel._odoo_name_to_class(klass._stacked, spec_module) + stacking_settings = klass._nfe40_spec_settings + node = SpecModel._odoo_name_to_class( + stacking_settings["stacking_mixin"], spec_module + ) tree = StringIO() visited = set() for kind, n, path, field_path, child_concrete in klass._visit_stack( - cls.env, node + cls.env, node, stacking_settings ): visited.add(n) path_items = path.split(".") @@ -118,7 +121,13 @@ def test_doc_stacking_points(self): "nfe40_cobr", "nfe40_fat", ] - keys = [k for k in self.env["l10n_br_fiscal.document"]._stacking_points.keys()] + keys = [ + k + for k in self.env["l10n_br_fiscal.document"] + .with_context(spec_schema="nfe", spec_version="40") + ._get_stacking_points() + .keys() + ] self.assertEqual(sorted(keys), sorted(doc_keys)) def test_doc_tree(self): @@ -154,7 +163,11 @@ def test_doc_line_stacking_points(self): "nfe40_prod", ] keys = [ - k for k in self.env["l10n_br_fiscal.document.line"]._stacking_points.keys() + k + for k in self.env["l10n_br_fiscal.document.line"] + .with_context(spec_schema="nfe", spec_version="40") + ._get_stacking_points() + .keys() ] self.assertEqual(sorted(keys), line_keys) From 4cf8f57a964bc3a2d7bdf9df2a1a4d9d5034a088 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Valyi?= Date: Wed, 9 Oct 2024 04:48:19 +0000 Subject: [PATCH 06/14] [REF] spec_driven_model: s/generateds/xsdata/ refs --- spec_driven_model/models/spec_export.py | 51 +++++-------------- spec_driven_model/models/spec_import.py | 4 +- spec_driven_model/models/spec_view.py | 2 +- spec_driven_model/readme/DESCRIPTION.md | 59 ++++++++++++++-------- spec_driven_model/tests/test_spec_model.py | 2 +- 5 files changed, 55 insertions(+), 63 deletions(-) diff --git a/spec_driven_model/models/spec_export.py b/spec_driven_model/models/spec_export.py index 86cddd9fc03d..04cf5eb37ae3 100644 --- a/spec_driven_model/models/spec_export.py +++ b/spec_driven_model/models/spec_export.py @@ -51,7 +51,7 @@ def _export_fields(self, xsd_fields, class_obj, export_dict): This method implements a dynamic dispatch checking if there is any method called _export_fields_CLASS_NAME to update the xsd_fields and export_dict variables, this way we allow controlling the - flow of fields to export or injecting specific values ​​in the + flow of fields to export or injecting specific values in the field export. """ self.ensure_one() @@ -118,10 +118,6 @@ def _export_field(self, xsd_field, class_obj, field_spec, export_value=None): if field.comodel_name not in self._get_spec_classes(): return False if hasattr(field, "xsd_choice_required"): - # NOTE generateds-odoo would abusively have xsd_required=True - # already in the spec file in this case. - # In xsdata-odoo we introduced xsd_choice_required. - # Here we make the legacy code compatible with xsdata-odoo: xsd_required = True return self._export_many2one(xsd_field, xsd_required, class_obj) elif self._fields[xsd_field].type == "one2many": @@ -135,7 +131,7 @@ def _export_field(self, xsd_field, class_obj, field_spec, export_value=None): and self[xsd_field] is not False ): if hasattr(field, "xsd_choice_required"): - xsd_required = True # NOTE compat, see previous NOTE + xsd_required = True return self._export_float_monetary( xsd_field, xsd_type, class_obj, xsd_required, export_value ) @@ -147,19 +143,19 @@ def _export_field(self, xsd_field, class_obj, field_spec, export_value=None): def _export_many2one(self, field_name, xsd_required, class_obj=None): self.ensure_one() if field_name in self._get_stacking_points().keys(): - return self._build_generateds( + return self._build_binding( class_name=self._get_stacking_points()[field_name].comodel_name ) else: - return (self[field_name] or self)._build_generateds( - class_obj._fields[field_name].comodel_name + return (self[field_name] or self)._build_binding( + class_name=class_obj._fields[field_name].comodel_name ) def _export_one2many(self, field_name, class_obj=None): self.ensure_one() relational_data = [] for relational_field in self[field_name]: - field_data = relational_field._build_generateds( + field_data = relational_field._build_binding( class_name=class_obj._fields[field_name].comodel_name ) relational_data.append(field_data) @@ -192,8 +188,7 @@ def _export_datetime(self, field_name): ).isoformat("T") ) - # TODO rename _build_binding - def _build_generateds(self, class_name=False, spec_schema=None, spec_version=None): + def _build_binding(self, spec_schema=None, spec_version=None, class_name=None): """ Iterate over an Odoo record and its m2o and o2m sub-records using a pre-order tree traversal and map the Odoo record values @@ -206,7 +201,7 @@ def _build_generateds(self, class_name=False, spec_schema=None, spec_version=Non self.ensure_one() if spec_schema and spec_version: self = self.with_context( - self.env, spec_schema=spec_schema, spec_version=spec_version + spec_schema=spec_schema, spec_version=spec_version ) spec_prefix = self._spec_prefix(self._context) if not class_name: @@ -229,27 +224,9 @@ def _build_generateds(self, class_name=False, spec_schema=None, spec_version=Non kwargs = {} binding_class = self._get_binding_class(class_obj) self._export_fields(xsd_fields, class_obj, export_dict=kwargs) - if kwargs: - sliced_kwargs = { - key: kwargs.get(key) - for key in binding_class.__dataclass_fields__.keys() - if kwargs.get(key) - } - binding_instance = binding_class(**sliced_kwargs) - return binding_instance - - def export_xml(self): - self.ensure_one() - result = [] - if hasattr(self, f"_{self._spec_prefix(self._context)}_spec_settings"): - binding_instance = self._build_generateds() - result.append(binding_instance) - return result - - def export_ds( - self, spec_schema, spec_version - ): # TODO change name -> export_binding! - self.ensure_one() - return self.with_context( - spec_schema=spec_schema, spec_version=spec_version - ).export_xml() + sliced_kwargs = { + key: kwargs.get(key) + for key in binding_class.__dataclass_fields__.keys() + if kwargs.get(key) + } + return binding_class(**sliced_kwargs) diff --git a/spec_driven_model/models/spec_import.py b/spec_driven_model/models/spec_import.py index e99bbc01e28d..c13a5db11515 100644 --- a/spec_driven_model/models/spec_import.py +++ b/spec_driven_model/models/spec_import.py @@ -21,7 +21,7 @@ class SpecMixinImport(models.AbstractModel): _name = "spec.mixin_import" _description = """ A recursive Odoo object builder that works along with the - GenerateDS object builder from the parsed XML. + xsdata object builder from the parsed XML. Here we take into account the concrete Odoo objects where the schema mixins where injected and possible matcher or builder overrides. """ @@ -31,7 +31,7 @@ def build_from_binding(self, spec_schema, spec_version, node, dry_run=False): """ Build an instance of an Odoo Model from a pre-populated Python binding object. Binding object such as the ones generated using - generateDS can indeed be automatically populated from an XML file. + xsdata can indeed be automatically populated from an XML file. This build method bridges the gap to build the Odoo object. It uses a pre-order tree traversal of the Python bindings and for each diff --git a/spec_driven_model/models/spec_view.py b/spec_driven_model/models/spec_view.py index 41714292c4cf..0e30b9469663 100644 --- a/spec_driven_model/models/spec_view.py +++ b/spec_driven_model/models/spec_view.py @@ -133,7 +133,7 @@ def _build_spec_fragment(self, container=None): # TODO required only if visible @api.model def build_arch(self, lib_node, view_node, fields, depth=0): - """Creates a view arch from an generateds lib model arch""" + """Creates a view arch from an xsdata lib model arch""" # _logger.info("BUILD ARCH", lib_node) choices = set() wrapper_group = None diff --git a/spec_driven_model/readme/DESCRIPTION.md b/spec_driven_model/readme/DESCRIPTION.md index 61464b1a17e7..9a42784cb900 100644 --- a/spec_driven_model/readme/DESCRIPTION.md +++ b/spec_driven_model/readme/DESCRIPTION.md @@ -1,11 +1,10 @@ ## Intro This module is a databinding framework for Odoo and XML data: it allows -to go from XML to Odoo objects back and forth. This module started with -the [GenerateDS](https://www.davekuhlman.org/generateDS.html) pure -Python databinding framework and is now being migrated to xsdata. So a -good starting point is to read [the xsdata documentation -here](https://xsdata.readthedocs.io/) +to go from XML to Odoo objects back and forth. While having no hard +dependency with it, it has been designed to be used along with xsdata. +So a good starting point is to read [the xsdata documentation +here](https://xsdata.readthedocs.io/). But what if instead of only generating Python structures from XML files you could actually generate full blown Odoo objects or serialize Odoo @@ -13,7 +12,7 @@ objects back to XML? This is what this module is for! First you should generate xsdata Python binding libraries you would generate for your specific XSD grammar, the Brazilian Electronic -Invoicing for instance, or UBL. +Invoicing for instance, or UBL or any XSD files... Second you should generate Odoo abstract mixins for all these pure Python bindings. This can be achieved using @@ -23,10 +22,12 @@ OCA/l10n-brazil/l10n_br_nfe_spec for the Brazilian Electronic Invoicing. ## SpecModel Now that you have generated these Odoo abstract bindings you should tell -Odoo how to use them. For instance you may want that your electronic -invoice abstract model matches the Odoo res.partner object. This is -fairly easy, you mostly need to define an override like: +Odoo how to use them. For instance you may want that your abstract model +for the recipient of the electronic invoice matches the Odoo +`res.partner` object. This is fairly easy, you mostly need to define an +override like: +```python from odoo.addons.spec_driven_model.models import spec_models @@ -35,12 +36,13 @@ fairly easy, you mostly need to define an override like: 'res.partner', 'partner.binding.mixin', ] +``` Notice you should inherit from spec_models.SpecModel and not the usual models.Model. **Field mapping**: You can then define two ways mapping between fields -by overriding fields from Odoo or from the binding and using \_compute= +by overriding fields from Odoo or from the binding using \_compute= , \_inverse= or simply related=. **Relational fields**: simple fields are easily mapped this way. However @@ -65,33 +67,46 @@ Sadly real life XML is a bit more complex than that. Often XML structures are deeply nested just because it makes it easier for XSD schemas to validate them! for instance an electronic invoice line can be a nested structure with lots of tax details and product details. In a -relational model like Odoo however you often want flatter data -structures. This is where StackedModel comes to the rescue! It inherits +relational model like Odoo however you often want flatter data structures +instead. This is where StackedModel comes to the rescue! It inherits from SpecModel and when you inherit from StackedModel you can inherit from all the generated mixins corresponding to the nested XML tags below some tag (here invoice.line.binding.mixin). All the fields corresponding to these XML tag attributes will be collected in your model and the XML -parsing and serialization will happen as expected: +parsing and serialization will happen as expected. +Here is an example inspired from the Brazilian Electronic Invoice where +the schema is called `nfe` and where we use the 2 digits `40` for its +short version: + +```python from odoo.addons.spec_driven_model.models import spec_models class InvoiceLine(spec_models.StackedModel): _inherit = [ 'account.move.line', - 'invoice.line.binding.mixin', + 'nfe.40.det', ] - _stacked = 'invoice.line.binding.mixin' + _nfe40_spec_settings = { + "module": "odoo.addons.l10n_br_nfe_spec.models.v4_0.leiaute_nfe_v4_00", + "stacking_mixin": "nfe.40.det", + "stacking_points": {}, + # all m2o below this level will be stacked even if not required: + "stacking_force_paths": ("det.imposto.",), + "stacking_skip_paths": ("nfe40_det_infNFe_id",), + } +``` All many2one fields that are required in the XSD (xsd_required=True) will get their model stacked automatically and recursively. You can -force non required many2one fields to be stacked using the -\_force_stack_paths attribute. On the contrary, you can avoid some -required many2one fields to be stacked using the stack_skip attribute. +also force non required many2one fields to be stacked using the +`stacking_force_paths` attribute. On the contrary, you can avoid some +required many2one fields to be stacked using the `stacking_skip_paths` attribute. -## Hooks +## Initialization hook Because XSD schemas can define lot's of different models, -spec_driven_model comes with handy hooks that will automatically make -all XSD mixins turn into concrete Odoo model (eg with a table) if you -didn't inject them into existing Odoo models. +spec_driven_model comes with a handy `_register_hook` override (in `spec.mixin`) +that will automatically make all XSD mixins turn into concrete Odoo model +(eg with a table) if you didn't inject them into existing Odoo models already. diff --git a/spec_driven_model/tests/test_spec_model.py b/spec_driven_model/tests/test_spec_model.py index 19c7aba012e8..ef4827ddfd09 100644 --- a/spec_driven_model/tests/test_spec_model.py +++ b/spec_driven_model/tests/test_spec_model.py @@ -155,7 +155,7 @@ def test_create_export_import(self): # 2nd we serialize it into a binding object: # (that could be further XML serialized) - po_binding = po._build_generateds(spec_schema="poxsd", spec_version="10") + po_binding = po._build_binding(spec_schema="poxsd", spec_version="10") self.assertEqual( [s.__name__ for s in type(po_binding).mro()], ["PurchaseOrderType", "object"], From bab580e7719d47244009a77a72349182093aab37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Valyi?= Date: Wed, 9 Oct 2024 04:23:32 +0000 Subject: [PATCH 07/14] [REF] l10n_br_nfe: export_ds -> _build_binding --- l10n_br_nfe/models/document.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/l10n_br_nfe/models/document.py b/l10n_br_nfe/models/document.py index 678c3fc67767..e2e12698e26c 100644 --- a/l10n_br_nfe/models/document.py +++ b/l10n_br_nfe/models/document.py @@ -898,11 +898,11 @@ def _serialize(self, edocs): ): record.flush_model() self.env.invalidate_all() - inf_nfe = record.export_ds("nfe", "40")[0] + inf_nfe = record._build_binding("nfe", "40") inf_nfe_supl = None if record.nfe40_infNFeSupl: - inf_nfe_supl = record.nfe40_infNFeSupl.export_ds("nfe", "40")[0] + inf_nfe_supl = record.nfe40_infNFeSupl._build_binding("nfe", "40") nfe = Nfe(infNFe=inf_nfe, infNFeSupl=inf_nfe_supl, signature=None) edocs.append(nfe) From 99bc44788c9e22c5de8246a3cd1fd6af40fea930 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Valyi?= Date: Thu, 10 Oct 2024 11:56:23 +0000 Subject: [PATCH 08/14] [REF] spec_driven_model: multi-schemas support --- spec_driven_model/models/spec_export.py | 21 ++--- spec_driven_model/models/spec_import.py | 17 ++-- spec_driven_model/models/spec_mixin.py | 1 + spec_driven_model/models/spec_models.py | 96 ++++++++++++---------- spec_driven_model/tests/fake_mixin.py | 9 +- spec_driven_model/tests/spec_purchase.py | 8 +- spec_driven_model/tests/test_spec_model.py | 4 +- 7 files changed, 78 insertions(+), 78 deletions(-) diff --git a/spec_driven_model/models/spec_export.py b/spec_driven_model/models/spec_export.py index 04cf5eb37ae3..ef032c58155e 100644 --- a/spec_driven_model/models/spec_export.py +++ b/spec_driven_model/models/spec_export.py @@ -1,5 +1,6 @@ # Copyright 2019 KMEE # License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0.en.html). + import logging import sys @@ -14,7 +15,7 @@ class SpecMixinExport(models.AbstractModel): @api.model def _get_binding_class(self, class_obj): - binding_module = sys.modules[self._binding_module] + binding_module = sys.modules[self._get_spec_property("binding_module")] for attr in class_obj._binding_type.split("."): binding_module = getattr(binding_module, attr) return binding_module @@ -72,7 +73,7 @@ def _export_fields(self, xsd_fields, class_obj, export_dict): not self._fields.get(xsd_field) ) and xsd_field not in self._get_stacking_points().keys(): continue - field_spec_name = xsd_field.replace(class_obj._field_prefix, "") + field_spec_name = xsd_field.split("_")[1] # remove schema prefix field_spec = False for fname, fspec in binding_class_spec.items(): if fspec.metadata.get("name", {}) == field_spec_name: @@ -200,24 +201,18 @@ def _build_binding(self, spec_schema=None, spec_version=None, class_name=None): """ self.ensure_one() if spec_schema and spec_version: - self = self.with_context( - spec_schema=spec_schema, spec_version=spec_version - ) - spec_prefix = self._spec_prefix(self._context) + self = self.with_context(spec_schema=spec_schema, spec_version=spec_version) if not class_name: - if hasattr(self, f"_{spec_prefix}_spec_settings"): - class_name = getattr(self, f"_{spec_prefix}_spec_settings")[ - "stacking_mixin" - ] - else: - class_name = self._name + class_name = self._get_spec_property("stacking_mixin", self._name) class_obj = self.env[class_name] xsd_fields = ( i for i in class_obj._fields - if class_obj._fields[i].name.startswith(class_obj._field_prefix) + if class_obj._fields[i].name.startswith( + f"{self._spec_prefix(self._context)}_" + ) and "_choice" not in class_obj._fields[i].name ) diff --git a/spec_driven_model/models/spec_import.py b/spec_driven_model/models/spec_import.py index c13a5db11515..9be1d592fcf9 100644 --- a/spec_driven_model/models/spec_import.py +++ b/spec_driven_model/models/spec_import.py @@ -73,10 +73,8 @@ def _build_attr(self, node, fields, vals, path, attr): value = getattr(node, attr[0]) if value is None or value == []: return False - key = "{}{}".format( - self._field_prefix, - attr[1].metadata.get("name", attr[0]), - ) + prefix = f"{self._spec_prefix(self._context)}" + key = f"{prefix}_{attr[1].metadata.get('name', attr[0])}" child_path = f"{path}.{key}" # Is attr a xsd SimpleType or a ComplexType? @@ -123,8 +121,8 @@ def _build_attr(self, node, fields, vals, path, attr): else: clean_type = binding_type.lower() comodel_name = "{}.{}.{}".format( - self._schema_name, - self._schema_version.replace(".", "")[0:2], + self._context["spec_schema"], + self._context["spec_version"].replace(".", "")[0:2], clean_type.split(".")[-1], ) @@ -198,9 +196,10 @@ def _prepare_import_dict( related_many2ones = {} fields = model._fields + field_prefix = f"{self._spec_prefix(self._context)}_" for k, v in fields.items(): # select schema choices for a friendly UI: - if k.startswith(f"{self._field_prefix}choice"): + if k.startswith(f"{field_prefix}choice"): for item in v.selection or []: if vals.get(item[0]) not in [None, []]: vals[k] = item[0] @@ -214,7 +213,7 @@ def _prepare_import_dict( related = v.related if len(related) == 1: vals[related[0]] = vals.get(k) - elif len(related) == 2 and k.startswith(self._field_prefix): + elif len(related) == 2 and k.startswith(field_prefix): related_m2o = related[0] # don't mess with _inherits write system if not any(related_m2o == i[1] for i in model._inherits.items()): @@ -263,7 +262,7 @@ def match_record(self, rec_dict, parent_dict, model=None): if model is None: model = self default_key = [model._rec_name or "name"] - search_keys = "_%s_search_keys" % (self._schema_name) + search_keys = "_%s_search_keys" % (self._context["spec_schema"]) if hasattr(model, search_keys): keys = getattr(model, search_keys) + default_key else: diff --git a/spec_driven_model/models/spec_mixin.py b/spec_driven_model/models/spec_mixin.py index ca7ac20e9ff3..8fd1cbd347e8 100644 --- a/spec_driven_model/models/spec_mixin.py +++ b/spec_driven_model/models/spec_mixin.py @@ -21,6 +21,7 @@ class SpecMixin(models.AbstractModel): _description = "root abstract model meant for xsd generated fiscal models" _name = "spec.mixin" _inherit = ["spec.mixin_export", "spec.mixin_import"] + _is_spec_driven = True def _valid_field_parameter(self, field, name): if name in ( diff --git a/spec_driven_model/models/spec_models.py b/spec_driven_model/models/spec_models.py index 72589b5da60c..9b44fc0ff5ad 100644 --- a/spec_driven_model/models/spec_models.py +++ b/spec_driven_model/models/spec_models.py @@ -67,12 +67,6 @@ def _compute_display_name(self): rec.display_name = _("Abrir...") return res - def _get_stacking_points(self): - key = f"_{self._spec_prefix(self._context)}_spec_settings" - if hasattr(self, key): - return getattr(self, key)["stacking_points"] - return {} - @classmethod def _spec_prefix(cls, context=None, spec_schema=None, spec_version=None): if context and context.get("spec_schema"): @@ -81,6 +75,15 @@ def _spec_prefix(cls, context=None, spec_schema=None, spec_version=None): spec_version = context.get("spec_version") return "%s%s" % (spec_schema, spec_version.replace(".", "")[:2]) + def _get_spec_property(self, spec_property="", fallback=None): + return getattr( + self, f"_{self._spec_prefix(self._context)}_{spec_property}", fallback + ) + + def _get_stacking_points(self): + return self._get_spec_property("stacking_points", {}) + + @classmethod def _build_model(cls, pool, cr): """ @@ -90,8 +93,8 @@ class as long as the generated spec mixins inherit from some spec.mixin. mixin. """ schema = None - if hasattr(cls, "_schema_name"): - schema = cls._schema_name + if hasattr(cls, "_spec_schema"): + schema = cls._spec_schema elif pool.get(cls._name) and hasattr(pool[cls._name], "_schema_name"): schema = pool[cls._name]._schema_name if schema and "spec.mixin" not in [ @@ -126,14 +129,9 @@ def _setup_fields(self): relational fields pointing to such mixins should be remapped to the proper concrete models where these mixins are injected. """ - cls = self.env.registry[self._name] + cls = type(self) for klass in cls.__bases__: - if ( - not hasattr(klass, "_name") - or not hasattr(klass, "_fields") - or klass._name is None - or not klass._name.startswith(self.env[cls._name]._schema_name) - ): + if not hasattr(klass, "_is_spec_driven"): continue if klass._name != cls._name: cls._map_concrete(self.env.cr.dbname, klass._name, cls._name) @@ -243,43 +241,55 @@ class StackedModel(SpecModel): @classmethod def _build_model(cls, pool, cr): - mod = import_module(".".join(cls.__module__.split(".")[:-1])) - if hasattr(cls, "_schema_name"): + if hasattr(cls, "_schema_name"): # when called via _register_hook schema = cls._schema_name version = cls._schema_version.replace(".", "")[:2] else: mod = import_module(".".join(cls.__module__.split(".")[:-1])) schema = mod.spec_schema version = mod.spec_version.replace(".", "")[:2] + cls._spec_schema = schema + cls._spec_version = version spec_prefix = cls._spec_prefix(spec_schema=schema, spec_version=version) - stacking_settings = getattr(cls, "_%s_spec_settings" % (spec_prefix,)) + setattr(cls, f"_{spec_prefix}_stacking_points", {}) + stacking_settings = { + "odoo_module": getattr(cls, f"_{spec_prefix}_odoo_module"), # TODO inherit? + "stacking_mixin": getattr(cls, f"_{spec_prefix}_stacking_mixin"), + "stacking_points": getattr(cls, f"_{spec_prefix}_stacking_points"), + "stacking_skip_paths": getattr( + cls, f"_{spec_prefix}_stacking_skip_paths", [] + ), + "stacking_force_paths": getattr( + cls, f"_{spec_prefix}_stacking_force_paths", [] + ), + } # inject all stacked m2o as inherited classes - if cls._stacked: - _logger.info(f"building StackedModel {cls._name} {cls}") - node = cls._odoo_name_to_class( - stacking_settings["stacking_mixin"], stacking_settings["module"] - ) - env = api.Environment(cr, SUPERUSER_ID, {}) - for kind, klass, _path, _field_path, _child_concrete in cls._visit_stack( - env, node, stacking_settings - ): - if kind == "stacked" and klass not in cls.__bases__: - cls.__bases__ = (klass,) + cls.__bases__ + node = cls._odoo_name_to_class( + stacking_settings["stacking_mixin"], stacking_settings["odoo_module"] + ) + env = api.Environment(cr, SUPERUSER_ID, {}) + for kind, klass, _path, _field_path, _child_concrete in cls._visit_stack( + env, node, stacking_settings + ): + if kind == "stacked" and klass not in cls.__bases__: + cls.__bases__ = (klass,) + cls.__bases__ return super()._build_model(pool, cr) @api.model def _add_field(self, name, field): - for cls in type(self).mro(): - if issubclass(cls, StackedModel): - if hasattr(self, "_schema_name"): - prefix = self._spec_prefix( - None, self._schema_name, self._schema_version - ) - key = f"_{prefix}_spec_settings" - stacking_points = getattr(self, key)["stacking_points"] - if name in stacking_points.keys(): - return + """ + Overriden to avoid adding many2one fields that are in fact "stacking points" + """ + if field.type == "many2one": + for cls in type(self).mro(): + if issubclass(cls, StackedModel): + for attr in dir(cls): + if attr != "_get_stacking_points" and attr.endswith( + "_stacking_points" + ): + if name in getattr(cls, attr).keys(): + return return super()._add_field(name, field) @classmethod @@ -296,7 +306,7 @@ def _visit_stack(cls, env, node, stacking_settings, path=None): node._description = None if path is None: path = stacking_settings["stacking_mixin"].split(".")[-1] - SpecModel._map_concrete(env.cr.dbname, node._name, cls._name, quiet=True) + cls._map_concrete(env.cr.dbname, node._name, cls._name, quiet=True) yield "stacked", node, path, None, None fields = OrderedDict() @@ -326,12 +336,12 @@ def _visit_stack(cls, env, node, stacking_settings, path=None): # TODO change for view or export continue child = cls._odoo_name_to_class( - f["comodel_name"], stacking_settings["module"] + f["comodel_name"], stacking_settings["odoo_module"] ) if child is None: # Not a spec field continue child_concrete = SPEC_MIXIN_MAPPINGS[env.cr.dbname].get(child._name) - field_path = name.replace(env[node._name]._field_prefix, "") + field_path = name.split("_")[1] # remove schema prefix if f["type"] == "one2many": yield "one2many", node, path, field_path, child_concrete @@ -339,7 +349,7 @@ def _visit_stack(cls, env, node, stacking_settings, path=None): force_stacked = any( stack_path in path + "." + field_path - for stack_path in stacking_settings.get("stacking_force_paths", "") + for stack_path in stacking_settings.get("stacking_force_paths", []) ) # many2one diff --git a/spec_driven_model/tests/fake_mixin.py b/spec_driven_model/tests/fake_mixin.py index 7f3887edf35d..4d35973f70a9 100644 --- a/spec_driven_model/tests/fake_mixin.py +++ b/spec_driven_model/tests/fake_mixin.py @@ -7,12 +7,9 @@ class PoXsdMixin(models.AbstractModel): _description = "Abstract Model for PO XSD" _name = "spec.mixin.poxsd" - _field_prefix = "poxsd10_" - _schema_name = "poxsd" - _schema_version = "1.0" - _odoo_module = "poxsd" - _spec_module = "odoo.addons.spec_driven_model.tests.spec_poxsd" - _binding_module = "odoo.addons.spec_driven_model.tests.purchase_order_lib" + + _poxsd10_odoo_module = "odoo.addons.spec_driven_model.tests.spec_poxsd" + _poxsd10_binding_module = "odoo.addons.spec_driven_model.tests.purchase_order_lib" # TODO rename brl_currency_id = fields.Many2one( diff --git a/spec_driven_model/tests/spec_purchase.py b/spec_driven_model/tests/spec_purchase.py index 4ec3ad02aef6..51c7d8aa1485 100644 --- a/spec_driven_model/tests/spec_purchase.py +++ b/spec_driven_model/tests/spec_purchase.py @@ -41,11 +41,9 @@ class PurchaseOrder(spec_models.StackedModel): _name = "fake.purchase.order" _inherit = ["fake.purchase.order", "poxsd.10.purchaseordertype"] - _poxsd10_spec_settings = { - "module": "odoo.addons.spec_driven_model.tests.spec_poxsd", - "stacking_mixin": "poxsd.10.purchaseordertype", - "stacking_points": {}, - } + + _poxsd10_odoo_module = "odoo.addons.spec_driven_model.tests.spec_poxsd" + _poxsd10_stacking_mixin = "poxsd.10.purchaseordertype" poxsd10_orderDate = fields.Date(compute="_compute_date") poxsd10_confirmDate = fields.Date(related="date_approve") diff --git a/spec_driven_model/tests/test_spec_model.py b/spec_driven_model/tests/test_spec_model.py index ef4827ddfd09..7f5b98cf0268 100644 --- a/spec_driven_model/tests/test_spec_model.py +++ b/spec_driven_model/tests/test_spec_model.py @@ -100,7 +100,7 @@ def test_stacked_model(self): po_fields_or_stacking.update( set( self.env["fake.purchase.order"] - ._poxsd10_spec_settings["stacking_points"] + ._poxsd10_stacking_points .keys() ) ) @@ -112,7 +112,7 @@ def test_stacked_model(self): self.assertEqual( list( self.env["fake.purchase.order"] - ._poxsd10_spec_settings["stacking_points"] + ._poxsd10_stacking_points .keys() ), ["poxsd10_items"], From 73b865042c138295b90014b69e0d94c7e5a9e9d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Valyi?= Date: Thu, 10 Oct 2024 15:51:31 +0000 Subject: [PATCH 09/14] [REF] l10n_br_nfe: multi-schemas support --- l10n_br_nfe/models/document.py | 24 +++++++++++------------ l10n_br_nfe/models/document_line.py | 16 +++++++-------- l10n_br_nfe/models/document_related.py | 10 ++++------ l10n_br_nfe/models/document_supplement.py | 8 +++----- l10n_br_nfe/tests/test_nfe_structure.py | 13 +++++++++++- l10n_br_nfe_spec/models/spec_models.py | 2 +- 6 files changed, 38 insertions(+), 35 deletions(-) diff --git a/l10n_br_nfe/models/document.py b/l10n_br_nfe/models/document.py index e2e12698e26c..e4d34f9b14e1 100644 --- a/l10n_br_nfe/models/document.py +++ b/l10n_br_nfe/models/document.py @@ -79,19 +79,17 @@ def filter_processador_edoc_nfe(record): class NFe(spec_models.StackedModel): _name = "l10n_br_fiscal.document" _inherit = ["l10n_br_fiscal.document", "nfe.40.infnfe", "nfe.40.fat"] - _nfe40_spec_settings = { - "module": "odoo.addons.l10n_br_nfe_spec.models.v4_0.leiaute_nfe_v4_00", - "stacking_mixin": "nfe.40.infnfe", - "stacking_points": {}, - # all m2o at this level will be stacked even if not required: - "stacking_force_paths": ( - "infnfe.total", - "infnfe.infAdic", - "infnfe.exporta", - "infnfe.cobr", - "infnfe.cobr.fat", - ), - } + + _nfe40_odoo_module = "odoo.addons.l10n_br_nfe_spec.models.v4_0.leiaute_nfe_v4_00" + _nfe40_stacking_mixin = "nfe.40.infnfe" + # all m2o at this level will be stacked even if not required: + _nfe40_stacking_force_paths = ( + "infnfe.total", + "infnfe.infAdic", + "infnfe.exporta", + "infnfe.cobr", + "infnfe.cobr.fat", + ) _nfe_search_keys = ["nfe40_Id"] # When dynamic stacking is applied the NFe structure is: diff --git a/l10n_br_nfe/models/document_line.py b/l10n_br_nfe/models/document_line.py index 0a86fcd63b82..d95ee930c9d1 100644 --- a/l10n_br_nfe/models/document_line.py +++ b/l10n_br_nfe/models/document_line.py @@ -70,14 +70,12 @@ class NFeLine(spec_models.StackedModel): _name = "l10n_br_fiscal.document.line" _inherit = ["l10n_br_fiscal.document.line", "nfe.40.det"] - _nfe40_spec_settings = { - "module": "odoo.addons.l10n_br_nfe_spec.models.v4_0.leiaute_nfe_v4_00", - "stacking_mixin": "nfe.40.det", - "stacking_points": {}, - # all m2o below this level will be stacked even if not required: - "stacking_force_paths": ("det.imposto.",), - "stacking_skip_paths": ("nfe40_det_infNFe_id",), - } + + _nfe40_odoo_module = "odoo.addons.l10n_br_nfe_spec.models.v4_0.leiaute_nfe_v4_00" + _nfe40_stacking_mixin = "nfe.40.det" + # all m2o below this level will be stacked even if not required: + _nfe40_stacking_force_paths = ("det.imposto.",) + _nfe40_stacking_skip_paths = ("nfe40_det_infNFe_id",) # When dynamic stacking is applied, the NFe line has the following structure: DET_TREE = """ @@ -527,7 +525,7 @@ def _export_fields_nfe_40_icms(self, xsd_fields, class_obj, export_dict): .replace("ICMS", "Icms") .replace("IcmsSN", "Icmssn") ) - binding_module = sys.modules[self._binding_module] + binding_module = sys.modules[self._get_spec_property("binding_module")] # Tnfe.InfNfe.Det.Imposto.Icms.Icms00 # see https://stackoverflow.com/questions/31174295/ # getattr-and-setattr-on-nested-subobjects-chained-properties diff --git a/l10n_br_nfe/models/document_related.py b/l10n_br_nfe/models/document_related.py index a265287731d4..6c3f0881e9a3 100644 --- a/l10n_br_nfe/models/document_related.py +++ b/l10n_br_nfe/models/document_related.py @@ -20,13 +20,11 @@ class NFeRelated(spec_models.StackedModel): _name = "l10n_br_fiscal.document.related" _inherit = ["l10n_br_fiscal.document.related", "nfe.40.nfref"] - _nfe40_spec_settings = { - "module": "odoo.addons.l10n_br_nfe_spec.models.v4_0.leiaute_nfe_v4_00", - "stacking_mixin": "nfe.40.nfref", - "stacking_points": {}, - "stacking_skip_paths": ("nfe40_NFref_ide_id",), - } + + _nfe40_odoo_module = "odoo.addons.l10n_br_nfe_spec.models.v4_0.leiaute_nfe_v4_00" + _nfe40_stacking_mixin = "nfe.40.nfref" # all m2o below this level will be stacked even if not required: + _nfe40_stacking_skip_paths = ("nfe40_NFref_ide_id",) _rec_name = "nfe40_refNFe" # When dynamic stacking is applied, this class has the following structure: diff --git a/l10n_br_nfe/models/document_supplement.py b/l10n_br_nfe/models/document_supplement.py index 6d114ed23d5f..1b875ea694b4 100644 --- a/l10n_br_nfe/models/document_supplement.py +++ b/l10n_br_nfe/models/document_supplement.py @@ -9,8 +9,6 @@ class NFeSupplement(spec_models.StackedModel): _name = "l10n_br_fiscal.document.supplement" _description = "NFe Supplement Document" _inherit = "nfe.40.infnfesupl" - _nfe40_spec_settings = { - "module": "odoo.addons.l10n_br_nfe_spec.models.v4_0.leiaute_nfe_v4_00", - "stacking_mixin": "nfe.40.infnfesupl", - "stacking_points": {}, - } + + _nfe40_odoo_module = "odoo.addons.l10n_br_nfe_spec.models.v4_0.leiaute_nfe_v4_00" + _nfe40_stacking_mixin = "nfe.40.infnfesupl" diff --git a/l10n_br_nfe/tests/test_nfe_structure.py b/l10n_br_nfe/tests/test_nfe_structure.py index 761523e0b387..90eec98211bd 100644 --- a/l10n_br_nfe/tests/test_nfe_structure.py +++ b/l10n_br_nfe/tests/test_nfe_structure.py @@ -26,7 +26,18 @@ def get_stacked_tree(cls, klass): # ≡ means o2m. Eventually followd by the mapped Odoo model """ spec_module = "odoo.addons.l10n_br_nfe_spec.models.v4_0.leiaute_nfe_v4_00" - stacking_settings = klass._nfe40_spec_settings + spec_prefix = "nfe40" + stacking_settings = { + "odoo_module": getattr(klass, f"_{spec_prefix}_odoo_module"), + "stacking_mixin": getattr(klass, f"_{spec_prefix}_stacking_mixin"), + "stacking_points": getattr(klass, f"_{spec_prefix}_stacking_points"), + "stacking_skip_paths": getattr( + klass, f"_{spec_prefix}_stacking_skip_paths", [] + ), + "stacking_force_paths": getattr( + klass, f"_{spec_prefix}_stacking_force_paths", [] + ), + } node = SpecModel._odoo_name_to_class( stacking_settings["stacking_mixin"], spec_module ) diff --git a/l10n_br_nfe_spec/models/spec_models.py b/l10n_br_nfe_spec/models/spec_models.py index dd9206bc853d..85be5954a6cd 100644 --- a/l10n_br_nfe_spec/models/spec_models.py +++ b/l10n_br_nfe_spec/models/spec_models.py @@ -12,7 +12,7 @@ class NfeSpecMixin(models.AbstractModel): _schema_version = "4.0.0" _odoo_module = "l10n_br_nfe" _spec_module = "odoo.addons.l10n_br_nfe_spec.models.v4_0.leiaute_nfe_v4_00" - _binding_module = "nfelib.nfe.bindings.v4_0.leiaute_nfe_v4_00" + _nfe40_binding_module = "nfelib.nfe.bindings.v4_0.leiaute_nfe_v4_00" _spec_tab_name = "NFe" brl_currency_id = fields.Many2one( From f6da0c7664d518848c52b5b25d121754325fe480 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Valyi?= Date: Mon, 14 Oct 2024 06:20:33 +0000 Subject: [PATCH 10/14] [REF] spec_driven_model: further multi-schemas --- spec_driven_model/models/spec_export.py | 7 ++- spec_driven_model/models/spec_import.py | 16 +++---- spec_driven_model/models/spec_mixin.py | 61 +++++++++++++++++-------- spec_driven_model/models/spec_models.py | 31 +++++-------- 4 files changed, 63 insertions(+), 52 deletions(-) diff --git a/spec_driven_model/models/spec_export.py b/spec_driven_model/models/spec_export.py index ef032c58155e..e29061ec8128 100644 --- a/spec_driven_model/models/spec_export.py +++ b/spec_driven_model/models/spec_export.py @@ -33,7 +33,7 @@ def _get_spec_classes(self, classes=False): for c in set(classes): if c is None: continue - if not c.startswith(f"{self._schema_name}."): + if not c.startswith(f"{self._context['spec_schema']}."): continue # the following filter to fields to show # when several XSD class are injected in the same object @@ -202,6 +202,7 @@ def _build_binding(self, spec_schema=None, spec_version=None, class_name=None): self.ensure_one() if spec_schema and spec_version: self = self.with_context(spec_schema=spec_schema, spec_version=spec_version) + self.env[f"spec.mixin.{spec_schema}"]._register_hook() if not class_name: class_name = self._get_spec_property("stacking_mixin", self._name) @@ -210,9 +211,7 @@ def _build_binding(self, spec_schema=None, spec_version=None, class_name=None): xsd_fields = ( i for i in class_obj._fields - if class_obj._fields[i].name.startswith( - f"{self._spec_prefix(self._context)}_" - ) + if class_obj._fields[i].name.startswith(f"{self._spec_prefix()}_") and "_choice" not in class_obj._fields[i].name ) diff --git a/spec_driven_model/models/spec_import.py b/spec_driven_model/models/spec_import.py index 9be1d592fcf9..bcea403c703d 100644 --- a/spec_driven_model/models/spec_import.py +++ b/spec_driven_model/models/spec_import.py @@ -42,12 +42,12 @@ def build_from_binding(self, spec_schema, spec_version, node, dry_run=False): Defaults values and control options are meant to be passed in the context. """ - model = self.with_context( - spec_schema=spec_schema, spec_version=spec_version - )._get_concrete_model(self._name) - attrs = model.with_context( - dry_run=dry_run, spec_schema=spec_schema, spec_version=spec_version - ).build_attrs(node) + self = self.with_context( + spec_schema=spec_schema, spec_version=spec_version, dry_run=dry_run + ) + self._register_hook() + model = self._get_concrete_model(self._name) + attrs = model.build_attrs(node) if dry_run: return model.new(attrs) else: @@ -73,7 +73,7 @@ def _build_attr(self, node, fields, vals, path, attr): value = getattr(node, attr[0]) if value is None or value == []: return False - prefix = f"{self._spec_prefix(self._context)}" + prefix = f"{self._spec_prefix()}" key = f"{prefix}_{attr[1].metadata.get('name', attr[0])}" child_path = f"{path}.{key}" @@ -196,7 +196,7 @@ def _prepare_import_dict( related_many2ones = {} fields = model._fields - field_prefix = f"{self._spec_prefix(self._context)}_" + field_prefix = f"{self._spec_prefix()}_" for k, v in fields.items(): # select schema choices for a friendly UI: if k.startswith(f"{field_prefix}choice"): diff --git a/spec_driven_model/models/spec_mixin.py b/spec_driven_model/models/spec_mixin.py index 8fd1cbd347e8..59cee4665241 100644 --- a/spec_driven_model/models/spec_mixin.py +++ b/spec_driven_model/models/spec_mixin.py @@ -43,27 +43,50 @@ def _get_concrete_model(self, model_name): else: return self.env.get(model_name) + def _spec_prefix(self): + """ + _spec_prefix should be available for all generated specs mixins + and it should be defined in SpecModel to avoid circular imports. + """ + return SpecModel._ensure_spec_prefix(self._context) + + def _get_spec_property(self, spec_property="", fallback=None): + """ + Used to access schema wise and version wise automatic mappings properties + """ + return getattr(self, f"_{self._spec_prefix()}_{spec_property}", fallback) + + def _get_stacking_points(self): + return self._get_spec_property("stacking_points", {}) + def _register_hook(self): """ Called once all modules are loaded. - Here we take all spec models that are not injected into existing concrete + Here we take all spec models that were not injected into existing concrete Odoo models and we make them concrete automatically with their _auto_init method that will create their SQL DDL structure. """ res = super()._register_hook() - if not hasattr(self, "_spec_module"): + if "spec_schema" not in self._context: return res - - load_key = f"_{self._spec_module}_loaded" + spec_module = self._get_spec_property("odoo_module") + if not spec_module: + return res + odoo_module = spec_module.split("_spec.")[0].split(".")[-1] + load_key = f"_{spec_module}_loaded" if hasattr(self.env.registry, load_key): # already done for registry return res setattr(self.env.registry, load_key, True) access_data = [] access_fields = [] + relation_prefix = ( + f"{self._context['spec_schema']}.{self._context['spec_version']}.%" + ) + field_prefix = f"{self._context['spec_schema']}{self._context['spec_version']}_" self.env.cr.execute( """SELECT DISTINCT relation FROM ir_model_fields WHERE relation LIKE %s;""", - (f"{self._schema_name}.{self._schema_version.replace('.', '')[:2]}.%",), + (relation_prefix,), ) # now we will filter only the spec models not injected into some existing class: remaining_models = { @@ -73,17 +96,15 @@ def _register_hook(self): and not SPEC_MIXIN_MAPPINGS[self.env.cr.dbname].get(i[0]) } for name in remaining_models: - spec_class = StackedModel._odoo_name_to_class(name, self._spec_module) + spec_class = StackedModel._odoo_name_to_class(name, spec_module) if spec_class is None: continue spec_class._module = "fiscal" # TODO use python_module ? + fields = self.env[spec_class._name]._fields rec_name = next( filter( - lambda x: ( - x.startswith(self.env[spec_class._name]._field_prefix) - and "_choice" not in x - ), - self.env[spec_class._name]._fields, + lambda x: (x.startswith(field_prefix) and "_choice" not in x), + fields, ) ) model_type = type( @@ -92,16 +113,16 @@ def _register_hook(self): { "_name": name, "_inherit": spec_class._inherit, - "_original_module": "fiscal", - "_odoo_module": self._odoo_module, - "_spec_module": self._spec_module, + "_original_module": odoo_module, "_rec_name": rec_name, - "_module": self._odoo_module, + "_module": odoo_module, }, ) - model_type._schema_name = self._schema_name - model_type._schema_version = self._schema_version - models.MetaModel.module_to_models[self._odoo_module] += [model_type] + # we set _spec_schema and _spec_version because + # _build_model will not have context access: + model_type._spec_schema = self._context["spec_schema"] + model_type._spec_version = self._context["spec_version"] + models.MetaModel.module_to_models[odoo_module] += [model_type] # now we init these models properly # a bit like odoo.modules.loading#load_module_graph would do @@ -122,11 +143,11 @@ def _register_hook(self): "perm_create", "perm_unlink", ] - model._auto_fill_access_data(self.env, self._odoo_module, access_data) + model._auto_fill_access_data(self.env, odoo_module, access_data) self.env["ir.model.access"].load(access_fields, access_data) self.env.registry.init_models( - self.env.cr, remaining_models, {"module": self._odoo_module} + self.env.cr, remaining_models, {"module": odoo_module} ) return res diff --git a/spec_driven_model/models/spec_models.py b/spec_driven_model/models/spec_models.py index 9b44fc0ff5ad..d9a299cd5e59 100644 --- a/spec_driven_model/models/spec_models.py +++ b/spec_driven_model/models/spec_models.py @@ -68,22 +68,13 @@ def _compute_display_name(self): return res @classmethod - def _spec_prefix(cls, context=None, spec_schema=None, spec_version=None): + def _ensure_spec_prefix(cls, context=None, spec_schema=None, spec_version=None): if context and context.get("spec_schema"): spec_schema = context.get("spec_schema") if context and context.get("spec_version"): spec_version = context.get("spec_version") return "%s%s" % (spec_schema, spec_version.replace(".", "")[:2]) - def _get_spec_property(self, spec_property="", fallback=None): - return getattr( - self, f"_{self._spec_prefix(self._context)}_{spec_property}", fallback - ) - - def _get_stacking_points(self): - return self._get_spec_property("stacking_points", {}) - - @classmethod def _build_model(cls, pool, cr): """ @@ -92,11 +83,12 @@ def _build_model(cls, pool, cr): class as long as the generated spec mixins inherit from some spec.mixin. mixin. """ - schema = None - if hasattr(cls, "_spec_schema"): + if hasattr(cls, "_spec_schema"): # when called via _register_hook schema = cls._spec_schema - elif pool.get(cls._name) and hasattr(pool[cls._name], "_schema_name"): - schema = pool[cls._name]._schema_name + else: + mod = import_module(".".join(cls.__module__.split(".")[:-1])) + schema = mod.spec_schema + if schema and "spec.mixin" not in [ c._name for c in pool[f"spec.mixin.{schema}"].__bases__ ]: @@ -241,16 +233,15 @@ class StackedModel(SpecModel): @classmethod def _build_model(cls, pool, cr): - if hasattr(cls, "_schema_name"): # when called via _register_hook - schema = cls._schema_name - version = cls._schema_version.replace(".", "")[:2] + mod = import_module(".".join(cls.__module__.split(".")[:-1])) + if hasattr(cls, "_spec_schema"): + schema = cls._spec_schema + version = cls._spec_version.replace(".", "")[:2] else: mod = import_module(".".join(cls.__module__.split(".")[:-1])) schema = mod.spec_schema version = mod.spec_version.replace(".", "")[:2] - cls._spec_schema = schema - cls._spec_version = version - spec_prefix = cls._spec_prefix(spec_schema=schema, spec_version=version) + spec_prefix = cls._ensure_spec_prefix(spec_schema=schema, spec_version=version) setattr(cls, f"_{spec_prefix}_stacking_points", {}) stacking_settings = { "odoo_module": getattr(cls, f"_{spec_prefix}_odoo_module"), # TODO inherit? From 2a4ca2c28b415e532dcb6f23d4fc54a1dda07a53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Valyi?= Date: Mon, 14 Oct 2024 06:23:41 +0000 Subject: [PATCH 11/14] [REF] l10n_br_nfe_spec: multi-schemas + renamed --- l10n_br_nfe_spec/models/__init__.py | 2 +- .../models/{spec_models.py => spec_mixin.py} | 9 ++------ l10n_br_nfe_spec/tests/test_nfe_import.py | 22 +++++++------------ 3 files changed, 11 insertions(+), 22 deletions(-) rename l10n_br_nfe_spec/models/{spec_models.py => spec_mixin.py} (73%) diff --git a/l10n_br_nfe_spec/models/__init__.py b/l10n_br_nfe_spec/models/__init__.py index 1d382931ae2d..3140ceedcffa 100644 --- a/l10n_br_nfe_spec/models/__init__.py +++ b/l10n_br_nfe_spec/models/__init__.py @@ -1,2 +1,2 @@ -from . import spec_models +from . import spec_mixin from . import v4_0 diff --git a/l10n_br_nfe_spec/models/spec_models.py b/l10n_br_nfe_spec/models/spec_mixin.py similarity index 73% rename from l10n_br_nfe_spec/models/spec_models.py rename to l10n_br_nfe_spec/models/spec_mixin.py index 85be5954a6cd..3eef958a594a 100644 --- a/l10n_br_nfe_spec/models/spec_models.py +++ b/l10n_br_nfe_spec/models/spec_mixin.py @@ -1,4 +1,4 @@ -# Copyright 2019-2020 Akretion - Raphael Valyi +# Copyright 2019-TODAY Akretion - Raphaël Valyi # License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0.en.html). from odoo import fields, models @@ -7,13 +7,8 @@ class NfeSpecMixin(models.AbstractModel): _description = "Abstract Model" _name = "spec.mixin.nfe" - _field_prefix = "nfe40_" - _schema_name = "nfe" - _schema_version = "4.0.0" - _odoo_module = "l10n_br_nfe" - _spec_module = "odoo.addons.l10n_br_nfe_spec.models.v4_0.leiaute_nfe_v4_00" + _nfe40_odoo_module = "odoo.addons.l10n_br_nfe_spec.models.v4_0.leiaute_nfe_v4_00" _nfe40_binding_module = "nfelib.nfe.bindings.v4_0.leiaute_nfe_v4_00" - _spec_tab_name = "NFe" brl_currency_id = fields.Many2one( comodel_name="res.currency", diff --git a/l10n_br_nfe_spec/tests/test_nfe_import.py b/l10n_br_nfe_spec/tests/test_nfe_import.py index 34cf2e9bcd9b..3d3714f51c4e 100644 --- a/l10n_br_nfe_spec/tests/test_nfe_import.py +++ b/l10n_br_nfe_spec/tests/test_nfe_import.py @@ -15,7 +15,7 @@ from odoo.tests import TransactionCase from odoo.tools import OrderedSet -from ..models import spec_models +from ..models import spec_mixin tz_datetime = re.compile(r".*[-+]0[0-9]:00$") @@ -38,10 +38,7 @@ def build_attrs_fake(self, node, create_m2o=False): value = getattr(node, fname) if value is None: continue - key = "{}{}".format( - self._field_prefix, - fspec.metadata.get("name", fname), - ) + key = f"nfe40_{fspec.metadata.get('name', fname)}" if ( fspec.type == str or not any(["." in str(i) for i in fspec.type.__args__]) ) and not str(fspec.type).startswith("typing.List"): @@ -66,12 +63,8 @@ def build_attrs_fake(self, node, create_m2o=False): key = fields[key]["related"][0] comodel_name = fields[key]["relation"] else: - clean_type = binding_type.lower() # TODO double check - comodel_name = "{}.{}.{}".format( - self._schema_name, - self._schema_version.replace(".", "")[0:2], - clean_type.split(".")[-1], - ) + clean_type = binding_type.lower() + comodel_name = f"nfe.40.{clean_type.split('.')[-1]}" comodel = self.env.get(comodel_name) if comodel is None: # example skip ICMS100 class continue @@ -114,9 +107,9 @@ def match_or_create_m2o_fake(self, comodel, new_value, create_m2o=False): return comodel.new(new_value)._ids[0] -spec_models.NfeSpecMixin.build_fake = build_fake -spec_models.NfeSpecMixin.build_attrs_fake = build_attrs_fake -spec_models.NfeSpecMixin.match_or_create_m2o_fake = match_or_create_m2o_fake +spec_mixin.NfeSpecMixin.build_fake = build_fake +spec_mixin.NfeSpecMixin.build_attrs_fake = build_attrs_fake +spec_mixin.NfeSpecMixin.match_or_create_m2o_fake = match_or_create_m2o_fake # in version 12, 13 and 14, the code above would properly allow loading NFe XMLs @@ -159,6 +152,7 @@ def fields_convert_to_cache(self, value, record, validate=True): def browse(it): return comodel.browse((it and NewId(it),)) + else: browse = comodel.browse # determine the value ids From f7a37f9189405c43e20e646d5e037e991491ce68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Valyi?= Date: Mon, 14 Oct 2024 06:22:03 +0000 Subject: [PATCH 12/14] [REF] l10n_br_nfe: further multi-schemas --- l10n_br_nfe/hooks.py | 1 - l10n_br_nfe/models/document.py | 15 ++++++++++----- l10n_br_nfe/tests/test_nfe_import.py | 5 ----- l10n_br_nfe/tests/test_nfe_serialize.py | 2 +- l10n_br_nfe/tests/test_nfe_structure.py | 1 - 5 files changed, 11 insertions(+), 13 deletions(-) diff --git a/l10n_br_nfe/hooks.py b/l10n_br_nfe/hooks.py index 18d2bbcdf4bd..e0708f83882f 100644 --- a/l10n_br_nfe/hooks.py +++ b/l10n_br_nfe/hooks.py @@ -14,7 +14,6 @@ def post_init_hook(cr, registry): env = api.Environment(cr, SUPERUSER_ID, {}) - env["nfe.40.infnfe"]._register_hook() cr.execute("select demo from ir_module_module where name='l10n_br_nfe';") is_demo = cr.fetchone()[0] if is_demo: diff --git a/l10n_br_nfe/models/document.py b/l10n_br_nfe/models/document.py index e4d34f9b14e1..a957aaec08f4 100644 --- a/l10n_br_nfe/models/document.py +++ b/l10n_br_nfe/models/document.py @@ -78,7 +78,7 @@ def filter_processador_edoc_nfe(record): class NFe(spec_models.StackedModel): _name = "l10n_br_fiscal.document" - _inherit = ["l10n_br_fiscal.document", "nfe.40.infnfe", "nfe.40.fat"] + _inherit = ["l10n_br_fiscal.document", "nfe.40.infnfe"] _nfe40_odoo_module = "odoo.addons.l10n_br_nfe_spec.models.v4_0.leiaute_nfe_v4_00" _nfe40_stacking_mixin = "nfe.40.infnfe" @@ -685,7 +685,7 @@ def _export_many2one(self, field_name, xsd_required, class_obj=None): fields = [ f for f in comodel._fields - if f.startswith(self._field_prefix) + if f.startswith(self._spec_prefix()) and f in self._fields.keys() and f # don't try to nfe40_fat id when reading nfe40_cobr for instance @@ -695,7 +695,7 @@ def _export_many2one(self, field_name, xsd_required, class_obj=None): if not any( v for k, v in sub_tag_read.items() - if k.startswith(self._field_prefix) + if k.startswith(self._spec_prefix()) ): return False @@ -1069,9 +1069,13 @@ def _exec_after_SITUACAO_EDOC_AUTORIZADA(self, old_state, new_state): return super()._exec_after_SITUACAO_EDOC_AUTORIZADA(old_state, new_state) def _generate_key(self): - for record in self.filtered(filter_processador_edoc_nfe): - date = fields.Datetime.context_timestamp(record, record.document_date) + if self.document_type_id.code not in [ + MODELO_FISCAL_NFE, + MODELO_FISCAL_NFCE, + ]: + return super()._generate_key() + for record in self.filtered(filter_processador_edoc_nfe): required_fields_gen_edoc = [] if not record.company_cnpj_cpf: required_fields_gen_edoc.append("CNPJ/CPF") @@ -1089,6 +1093,7 @@ def _generate_key(self): _("To Generate EDoc Key, you need to fill the %s field.") % field ) + date = fields.Datetime.context_timestamp(record, record.document_date) chave_edoc = ChaveEdoc( ano_mes=date.strftime("%y%m").zfill(4), cnpj_cpf_emitente=record.company_cnpj_cpf, diff --git a/l10n_br_nfe/tests/test_nfe_import.py b/l10n_br_nfe/tests/test_nfe_import.py index 3a53d143cd8a..3933d11f91bc 100644 --- a/l10n_br_nfe/tests/test_nfe_import.py +++ b/l10n_br_nfe/tests/test_nfe_import.py @@ -11,11 +11,6 @@ class NFeImportTest(TransactionCase): - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.env["spec.mixin.nfe"]._register_hook() - def test_import_in_nfe_dry_run(self): res_items = ( "nfe", diff --git a/l10n_br_nfe/tests/test_nfe_serialize.py b/l10n_br_nfe/tests/test_nfe_serialize.py index 165674b23a12..59efce0f7894 100644 --- a/l10n_br_nfe/tests/test_nfe_serialize.py +++ b/l10n_br_nfe/tests/test_nfe_serialize.py @@ -18,7 +18,6 @@ class TestNFeExport(TransactionCase): def setUp(self, nfe_list): super().setUp() - self.env["spec.mixin.nfe"]._register_hook() self.nfe_list = nfe_list for nfe_data in self.nfe_list: nfe = self.env.ref(nfe_data["record_ref"]) @@ -39,6 +38,7 @@ def prepare_test_nfe(self, nfe): line._onchange_fiscal_operation_line_id() nfe._compute_fiscal_amount() + nfe._register_hook() # required in v16 for next statement nfe.nfe40_detPag = [ (5, 0, 0), ( diff --git a/l10n_br_nfe/tests/test_nfe_structure.py b/l10n_br_nfe/tests/test_nfe_structure.py index 90eec98211bd..20ddd9d62fb4 100644 --- a/l10n_br_nfe/tests/test_nfe_structure.py +++ b/l10n_br_nfe/tests/test_nfe_structure.py @@ -16,7 +16,6 @@ class NFeStructure(TransactionCase): @classmethod def setUpClass(cls): super().setUpClass() - cls.env["spec.mixin.nfe"]._register_hook() @classmethod def get_stacked_tree(cls, klass): From 7c6b37dfcf5a481c0f5f88a5e7e887ef3d366d1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Valyi?= Date: Mon, 14 Oct 2024 06:22:42 +0000 Subject: [PATCH 13/14] [REM] l10n_br_account_nfe: drop _register_hook --- l10n_br_account_nfe/tests/test_nfce_contingency.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/l10n_br_account_nfe/tests/test_nfce_contingency.py b/l10n_br_account_nfe/tests/test_nfce_contingency.py index 7d074f4a4805..a66c7e12c076 100644 --- a/l10n_br_account_nfe/tests/test_nfce_contingency.py +++ b/l10n_br_account_nfe/tests/test_nfce_contingency.py @@ -8,8 +8,6 @@ class TestAccountNFCeContingency(TransactionCase): @classmethod def setUpClass(cls): super().setUpClass() - # this hook is required to test l10n_br_account_nfe alone: - cls.env["spec.mixin.nfe"]._register_hook() cls.document_id = cls.env.ref("l10n_br_nfe.demo_nfce_same_state") cls.prepare_account_move_nfce() From a5bc4ed1c0c680545346f2aa864c71f142fdb1c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Valyi?= Date: Tue, 15 Oct 2024 17:03:00 +0000 Subject: [PATCH 14/14] [FIX] spec_driven_model: register_hook when no ctx --- spec_driven_model/__manifest__.py | 5 +-- spec_driven_model/models/spec_mixin.py | 49 +++++++++++++++++-------- spec_driven_model/models/spec_models.py | 16 ++------ 3 files changed, 39 insertions(+), 31 deletions(-) diff --git a/spec_driven_model/__manifest__.py b/spec_driven_model/__manifest__.py index 453cf405a2e7..3ab66f43c2cf 100644 --- a/spec_driven_model/__manifest__.py +++ b/spec_driven_model/__manifest__.py @@ -3,12 +3,11 @@ { "name": "Spec Driven Model", - "summary": """ - Tools for specifications driven mixins (from xsd for instance)""", + "summary": """XML binding for Odoo: XML to Odoo models and models to XML.""", "version": "16.0.1.3.2", "maintainers": ["rvalyi"], "license": "LGPL-3", - "author": "Akretion,Odoo Community Association (OCA)", + "author": "Akretion, Odoo Community Association (OCA)", "website": "https://github.com/OCA/l10n-brazil", "depends": [], "data": [], diff --git a/spec_driven_model/models/spec_mixin.py b/spec_driven_model/models/spec_mixin.py index 59cee4665241..7516f09fa962 100644 --- a/spec_driven_model/models/spec_mixin.py +++ b/spec_driven_model/models/spec_mixin.py @@ -1,6 +1,8 @@ # Copyright 2019-TODAY Akretion - Raphael Valyi # License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0.en.html). +from importlib import import_module + from odoo import api, models from .spec_models import SPEC_MIXIN_MAPPINGS, SpecModel, StackedModel @@ -43,12 +45,31 @@ def _get_concrete_model(self, model_name): else: return self.env.get(model_name) - def _spec_prefix(self): + def _spec_prefix(self, split=False): """ - _spec_prefix should be available for all generated specs mixins - and it should be defined in SpecModel to avoid circular imports. + Get spec_schema and spec_version from context or from class module """ - return SpecModel._ensure_spec_prefix(self._context) + if self._context.get("spec_schema") and self._context.get("spec_version"): + spec_schema = self._context.get("spec_schema") + spec_version = self._context.get("spec_version") + if spec_schema and spec_version: + spec_version = spec_version.replace(".", "")[:2] + if split: + return spec_schema, spec_version + return f"{spec_schema}{spec_version}" + + for ancestor in type(self).mro(): + if not ancestor.__module__.startswith("odoo.addons."): + continue + mod = import_module(".".join(ancestor.__module__.split(".")[:-1])) + if hasattr(mod, "spec_schema"): + spec_schema = mod.spec_schema + spec_version = mod.spec_version.replace(".", "")[:2] + if split: + return spec_schema, spec_version + return f"{spec_schema}{spec_version}" + + return None, None if split else None def _get_spec_property(self, spec_property="", fallback=None): """ @@ -67,22 +88,21 @@ def _register_hook(self): their _auto_init method that will create their SQL DDL structure. """ res = super()._register_hook() - if "spec_schema" not in self._context: + spec_schema, spec_version = self._spec_prefix(split=True) + if not spec_schema: return res + spec_module = self._get_spec_property("odoo_module") - if not spec_module: - return res odoo_module = spec_module.split("_spec.")[0].split(".")[-1] load_key = f"_{spec_module}_loaded" - if hasattr(self.env.registry, load_key): # already done for registry + if hasattr(self.env.registry, load_key): # hook already done for registry return res setattr(self.env.registry, load_key, True) + access_data = [] access_fields = [] - relation_prefix = ( - f"{self._context['spec_schema']}.{self._context['spec_version']}.%" - ) - field_prefix = f"{self._context['spec_schema']}{self._context['spec_version']}_" + field_prefix = f"{spec_schema}{spec_version}" + relation_prefix = f"{spec_schema}.{spec_version}.%" self.env.cr.execute( """SELECT DISTINCT relation FROM ir_model_fields WHERE relation LIKE %s;""", @@ -99,7 +119,6 @@ def _register_hook(self): spec_class = StackedModel._odoo_name_to_class(name, spec_module) if spec_class is None: continue - spec_class._module = "fiscal" # TODO use python_module ? fields = self.env[spec_class._name]._fields rec_name = next( filter( @@ -120,8 +139,8 @@ def _register_hook(self): ) # we set _spec_schema and _spec_version because # _build_model will not have context access: - model_type._spec_schema = self._context["spec_schema"] - model_type._spec_version = self._context["spec_version"] + model_type._spec_schema = spec_schema + model_type._spec_version = spec_version models.MetaModel.module_to_models[odoo_module] += [model_type] # now we init these models properly diff --git a/spec_driven_model/models/spec_models.py b/spec_driven_model/models/spec_models.py index d9a299cd5e59..f15ff0639747 100644 --- a/spec_driven_model/models/spec_models.py +++ b/spec_driven_model/models/spec_models.py @@ -64,17 +64,9 @@ def _compute_display_name(self): res = super()._compute_display_name() for rec in self: if rec.display_name == "False" or not rec.display_name: - rec.display_name = _("Abrir...") + rec.display_name = _("Open...") return res - @classmethod - def _ensure_spec_prefix(cls, context=None, spec_schema=None, spec_version=None): - if context and context.get("spec_schema"): - spec_schema = context.get("spec_schema") - if context and context.get("spec_version"): - spec_version = context.get("spec_version") - return "%s%s" % (spec_schema, spec_version.replace(".", "")[:2]) - @classmethod def _build_model(cls, pool, cr): """ @@ -181,7 +173,6 @@ def _setup_fields(self): @classmethod def _map_concrete(cls, dbname, key, target, quiet=False): - # TODO bookkeep according to a key to allow multiple injection contexts if not quiet: _logger.debug(f"{key} ---> {target}") global SPEC_MIXIN_MAPPINGS @@ -233,15 +224,14 @@ class StackedModel(SpecModel): @classmethod def _build_model(cls, pool, cr): - mod = import_module(".".join(cls.__module__.split(".")[:-1])) - if hasattr(cls, "_spec_schema"): + if hasattr(cls, "_spec_schema"): # when called via _register_hook schema = cls._spec_schema version = cls._spec_version.replace(".", "")[:2] else: mod = import_module(".".join(cls.__module__.split(".")[:-1])) schema = mod.spec_schema version = mod.spec_version.replace(".", "")[:2] - spec_prefix = cls._ensure_spec_prefix(spec_schema=schema, spec_version=version) + spec_prefix = f"{schema}{version}" setattr(cls, f"_{spec_prefix}_stacking_points", {}) stacking_settings = { "odoo_module": getattr(cls, f"_{spec_prefix}_odoo_module"), # TODO inherit?