From 23053ac90931fd549c323d21a9ccec90f15c3bae 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 01/11] [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 | 17 +---- spec_driven_model/models/spec_models.py | 80 +++++++++++++++------- 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, 119 insertions(+), 79 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 fdd7a814e62a..0529a48bf697 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 d685b8346842..628529dc0df5 100644 --- a/spec_driven_model/models/spec_mixin.py +++ b/spec_driven_model/models/spec_mixin.py @@ -2,7 +2,6 @@ # 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", @@ -114,6 +99,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 279620b7ac90..aeb7603f8bd1 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): """ @@ -190,7 +205,6 @@ def spec_module_classes(cls, spec_module): Cache the list of spec_module classes to save calls to slow reflection API. """ - spec_module_attr = f"_spec_cache_{spec_module.replace('.', '_')}" if not hasattr(cls, spec_module_attr): setattr( @@ -215,8 +229,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 @@ -228,24 +243,26 @@ 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 _logger.info(f"building StackedModel {cls._name} {cls}") - node = cls._odoo_name_to_class(cls._stacked, cls._spec_module) + 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 + env, node, stacking_settings ): if kind == "stacked" and klass not in cls.__bases__: cls.__bases__ = (klass,) + cls.__bases__ @@ -255,12 +272,18 @@ def _build_model(cls, pool, cr): 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. @@ -272,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] cls._map_concrete(env.cr.dbname, node._name, cls._name, quiet=True) yield "stacked", node, path, None, None @@ -296,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) @@ -311,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 @@ -320,8 +348,10 @@ 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) + child_path = "%s.%s" % (path, field_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 bce7a210bee6..c1340bb74ec1 100644 --- a/spec_driven_model/tests/test_spec_model.py +++ b/spec_driven_model/tests/test_spec_model.py @@ -51,18 +51,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( @@ -79,7 +67,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( @@ -87,7 +79,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"], ) @@ -128,7 +124,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) @@ -175,12 +175,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 0f9dd841a28fb1353305febbb4f80e188f20a076 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 02/11] [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 e8a09f1a5adc..7222c6a3032b 100644 --- a/l10n_br_nfe/models/__init__.py +++ b/l10n_br_nfe/models/__init__.py @@ -21,3 +21,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 67a1cfa80f03..fbeb3c9345e5 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() record.invalidate_cache() - 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) @@ -1343,7 +1347,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 ca9d19a9c7bc..938f9cfbd1e4 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 758ce30b0a20..8927b0a2a5c0 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 eadcf4b0f6f1..b34f59cd4dd7 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 6c7260ac35a39cfde2420aa571f83c4b1c398e95 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 03/11] [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.rst | 25 +++++++---- spec_driven_model/tests/test_spec_model.py | 2 +- 5 files changed, 34 insertions(+), 50 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 0529a48bf697..850a4a386d30 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.rst b/spec_driven_model/readme/DESCRIPTION.rst index 9810adf5c3e4..5a4e3bc57048 100644 --- a/spec_driven_model/readme/DESCRIPTION.rst +++ b/spec_driven_model/readme/DESCRIPTION.rst @@ -1,7 +1,7 @@ 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 `_ pure Python databinding framework and is now being migrated to xsdata. So a good starting point is to read `the xsdata documentation here `_ +This module is a databinding framework for Odoo and XML data: it allows to go from XML to Odoo objects back and forth. While having no hard dependency with it, it has been designed to be used with xsdata. So a good starting point is to read `the xsdata documentation here `_ But what if instead of only generating Python structures from XML files you could actually generate full blown Odoo objects or serialize Odoo objects back to XML? This is what this module is for! @@ -26,7 +26,7 @@ Now that you have generated these Odoo abstract bindings you should tell Odoo ho 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=` , `_inverse=` or simply `related=`. +**Field mapping**: You can then define two ways mapping between fields 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 what about relational fields? In your XSD schema, your electronic invoice is related to the `partner.binding.mixin` not to an Odoo `res.partner`. Don't worry, when `SpecModel` classes are instanciated for all relational fields, we look if their comodel have been injected into some existing Odoo model and if so we remap them to the proper Odoo model. @@ -36,7 +36,7 @@ Notice you should inherit from `spec_models.SpecModel` and not the usual `models StackedModel ~~~~~~~~~~~~ -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 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:: +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 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. 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:: from odoo.addons.spec_driven_model.models import spec_models @@ -45,14 +45,21 @@ Sadly real life XML is a bit more complex than that. Often XML structures are de 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. +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 `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. +Because XSD schemas can define lot's of different models, spec_driven_model comes with a handy _register_hook 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. diff --git a/spec_driven_model/tests/test_spec_model.py b/spec_driven_model/tests/test_spec_model.py index c1340bb74ec1..6509ae337701 100644 --- a/spec_driven_model/tests/test_spec_model.py +++ b/spec_driven_model/tests/test_spec_model.py @@ -124,7 +124,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 473df5a9e035bf60a7b63c3f29b7aeb3c73b3955 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 04/11] [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 fbeb3c9345e5..e0244df9477c 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() record.invalidate_cache() - 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 0748f85e08d4ee888cd9340c366d8bc7b77888c2 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 05/11] [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 | 72 ++++++++++++---------- 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, 65 insertions(+), 67 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 850a4a386d30..f0f64bcaf7bb 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? @@ -120,8 +118,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], ) @@ -195,9 +193,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] @@ -207,7 +206,7 @@ def _prepare_import_dict( elif v.related is not None and vals.get(k) is not None: if len(v.related) == 1: vals[v.related[0]] = vals.get(k) - elif len(v.related) == 2 and k.startswith(self._field_prefix): + elif len(v.related) == 2 and k.startswith(field_prefix): related_m2o = v.related[0] # don't mess with _inherits write system if not any(related_m2o == i[1] for i in model._inherits.items()): @@ -256,7 +255,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 628529dc0df5..ec0e6e9270f1 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 aeb7603f8bd1..cf506d3fa699 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,14 @@ 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): """ @@ -99,11 +101,9 @@ class as long as the generated spec mixins inherit from some else: super_parents = super_parents or [] for super_parent in super_parents: - if ( - not super_parent.startswith("spec.mixin.") - or not hasattr(pool[super_parent], "_odoo_module") - or "spec.mixin" in [c._name for c in pool[super_parent].__bases__] - ): + if not super_parent.startswith("spec.mixin.") or "spec.mixin" in [ + c._name for c in pool[super_parent].__bases__ + ]: continue pool[super_parent]._inherit = list(pool[super_parent]._inherit) + [ "spec.mixin" @@ -130,12 +130,7 @@ def _setup_fields(self): """ 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) @@ -254,11 +249,22 @@ def _build_model(cls, pool, cr): 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,)) + 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 _logger.info(f"building StackedModel {cls._name} {cls}") node = cls._odoo_name_to_class( - stacking_settings["stacking_mixin"], stacking_settings["module"] + 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( @@ -270,16 +276,18 @@ def _build_model(cls, 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 @@ -326,12 +334,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 +347,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 6509ae337701..efefea847639 100644 --- a/spec_driven_model/tests/test_spec_model.py +++ b/spec_driven_model/tests/test_spec_model.py @@ -69,7 +69,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() ) ) @@ -81,7 +81,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 985b958f52a279342062848fb6aa497120531071 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 06/11] [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 e0244df9477c..d5df2fb90444 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 938f9cfbd1e4..4885bceca961 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 = """ @@ -516,7 +514,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 b34f59cd4dd7..c84ce536e3da 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 87da59b7e96004dc323f108e014f692b766dc286 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 07/11] [REF] spec_driven_model: further multi-schemas --- spec_driven_model/README.rst | 25 +++++--- spec_driven_model/models/spec_export.py | 7 +-- spec_driven_model/models/spec_import.py | 16 ++--- spec_driven_model/models/spec_mixin.py | 58 +++++++++++++------ spec_driven_model/models/spec_models.py | 18 ++---- .../static/description/index.html | 25 +++++--- 6 files changed, 87 insertions(+), 62 deletions(-) diff --git a/spec_driven_model/README.rst b/spec_driven_model/README.rst index 03b5fae1b5be..3d5cac20b7e5 100644 --- a/spec_driven_model/README.rst +++ b/spec_driven_model/README.rst @@ -31,7 +31,7 @@ Spec Driven Model 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 `_ pure Python databinding framework and is now being migrated to xsdata. So a good starting point is to read `the xsdata documentation here `_ +This module is a databinding framework for Odoo and XML data: it allows to go from XML to Odoo objects back and forth. While having no hard dependency with it, it has been designed to be used with xsdata. So a good starting point is to read `the xsdata documentation here `_ But what if instead of only generating Python structures from XML files you could actually generate full blown Odoo objects or serialize Odoo objects back to XML? This is what this module is for! @@ -56,7 +56,7 @@ Now that you have generated these Odoo abstract bindings you should tell Odoo ho 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=` , `_inverse=` or simply `related=`. +**Field mapping**: You can then define two ways mapping between fields 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 what about relational fields? In your XSD schema, your electronic invoice is related to the `partner.binding.mixin` not to an Odoo `res.partner`. Don't worry, when `SpecModel` classes are instanciated for all relational fields, we look if their comodel have been injected into some existing Odoo model and if so we remap them to the proper Odoo model. @@ -66,7 +66,7 @@ Notice you should inherit from `spec_models.SpecModel` and not the usual `models StackedModel ~~~~~~~~~~~~ -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 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:: +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 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. 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:: from odoo.addons.spec_driven_model.models import spec_models @@ -75,17 +75,24 @@ Sadly real life XML is a bit more complex than that. Often XML structures are de 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. +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 `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. +Because XSD schemas can define lot's of different models, spec_driven_model comes with a handy _register_hook 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. **Table of contents** 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 f0f64bcaf7bb..b3f4c1a2e250 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}" @@ -193,7 +193,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 ec0e6e9270f1..cba16344c57d 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 = "_%s_loaded" % (self._spec_module,) + 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,14 @@ 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_get_keys() rec_name = next( filter( - lambda x: ( - x.startswith(self.env[spec_class._name]._field_prefix) - and "_choice" not in x - ), + lambda x: (x.startswith(field_prefix) and "_choice" not in x), fields, ) ) @@ -93,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 @@ -123,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 cf506d3fa699..123275ad513a 100644 --- a/spec_driven_model/models/spec_models.py +++ b/spec_driven_model/models/spec_models.py @@ -68,21 +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): """ @@ -241,14 +233,14 @@ class StackedModel(SpecModel): @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] + 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] - 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? diff --git a/spec_driven_model/static/description/index.html b/spec_driven_model/static/description/index.html index 6872009569dc..cdf8af40d5e1 100644 --- a/spec_driven_model/static/description/index.html +++ b/spec_driven_model/static/description/index.html @@ -372,7 +372,7 @@

Spec Driven Model

Beta License: LGPL-3 OCA/l10n-brazil Translate me on Weblate Try me on Runboat

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 pure Python databinding framework and is now being migrated to xsdata. So a good starting point is to read the xsdata documentation here

+

This module is a databinding framework for Odoo and XML data: it allows to go from XML to Odoo objects back and forth. While having no hard dependency with it, it has been designed to be used with xsdata. So a good starting point is to read the xsdata documentation here

But what if instead of only generating Python structures from XML files you could actually generate full blown Odoo objects or serialize Odoo 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.

Second you should generate Odoo abstract mixins for all these pure Python bindings. This can be achieved using xsdata-odoo. An example is OCA/l10n-brazil/l10n_br_nfe_spec for the Brazilian Electronic Invoicing.

@@ -391,13 +391,13 @@

SpecModel

]

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= , _inverse= or simply related=.

+

Field mapping: You can then define two ways mapping between fields 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 what about relational fields? In your XSD schema, your electronic invoice is related to the partner.binding.mixin not to an Odoo res.partner. Don’t worry, when SpecModel classes are instanciated for all relational fields, we look if their comodel have been injected into some existing Odoo model and if so we remap them to the proper Odoo model.

Field prefixes: to avoid field collision between the Odoo fields and the XSD fields, the XSD fields are prefixed with the name of the schema and a few digits representing the schema version (typically 2 digits). So if your schema get a minor version upgrade, the same fields and classes are used. For a major upgrade however new fields and classes may be used so data of several major versions could co-exist inside your Odoo database.

StackedModel

-

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 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:

+

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 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. 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:

 from odoo.addons.spec_driven_model.models import spec_models
 
@@ -405,15 +405,22 @@ 

StackedModel

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.

+

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 stacking_force_paths attribute. On the contrary, you can avoid some required many2one fields to be stacked using the stacking_skip_paths attribute.

-
-

Hooks

-

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.

+
+

Initialization hook

+

Because XSD schemas can define lot’s of different models, spec_driven_model comes with a handy _register_hook 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.

Table of contents

    From 6c8e015fcc48dbe85202b03ba621d1cce222d1ce 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 08/11] [REF] l10n_br_nfe_spec: multi-schemas + renamed --- l10n_br_nfe_spec/models/__init__.py | 2 +- .../models/{spec_models.py => spec_mixin.py} | 9 ++------- 2 files changed, 3 insertions(+), 8 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", From 0feb42db573572faba41881830ed040a69d6249b 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 09/11] [REF] l10n_br_nfe: further multi-schemas --- l10n_br_nfe/hooks.py | 1 - l10n_br_nfe/models/document.py | 10 +++++----- l10n_br_nfe/tests/test_nfe_import.py | 5 ----- l10n_br_nfe/tests/test_nfe_serialize.py | 1 - l10n_br_nfe/tests/test_nfe_structure.py | 1 - 5 files changed, 5 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 d5df2fb90444..c854ae327ae8 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,8 @@ 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): + super()._generate_key() for record in self.filtered(filter_processador_edoc_nfe): - date = fields.Datetime.context_timestamp(record, record.document_date) - required_fields_gen_edoc = [] if not record.company_cnpj_cpf: required_fields_gen_edoc.append("CNPJ/CPF") @@ -1089,6 +1088,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 8927b0a2a5c0..47f43f929928 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(SavepointCase): - @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 874aca8d86a8..f184df5211c1 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"]) diff --git a/l10n_br_nfe/tests/test_nfe_structure.py b/l10n_br_nfe/tests/test_nfe_structure.py index c84ce536e3da..44bb8a8d4395 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(SavepointCase): @classmethod def setUpClass(cls): super().setUpClass() - cls.env["spec.mixin.nfe"]._register_hook() @classmethod def get_stacked_tree(cls, klass): From 3cd56a961138babc80503e18183305607f6a5101 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 10/11] [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 59dddbb4e378..6333f5710dc6 100644 --- a/l10n_br_account_nfe/tests/test_nfce_contingency.py +++ b/l10n_br_account_nfe/tests/test_nfce_contingency.py @@ -7,8 +7,6 @@ class TestAccountNFCeContingency(TransactionCase): def setUp(self): super().setUp() - # this hook is required to test l10n_br_account_nfe alone: - self.env["spec.mixin.nfe"]._register_hook() self.document_id = self.env.ref("l10n_br_nfe.demo_nfce_same_state") self.prepare_account_move_nfce() From 320f4118d7856edfc82258e2e6454fbb83816b40 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 11/11] [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 94e51a9c781c..a1163bee9c15 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": "14.0.5.5.3", "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 cba16344c57d..ee246ece556a 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_get_keys() 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 123275ad513a..2ce364835628 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): """ @@ -180,7 +172,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 @@ -232,15 +223,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?