From 3617af557c06bd9396aa4e8283b4ac84df4fe26a 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/12] [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 | 24 ++++--- spec_driven_model/models/spec_models.py | 77 +++++++++++++++------- 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, 131 insertions(+), 71 deletions(-) diff --git a/spec_driven_model/models/spec_export.py b/spec_driven_model/models/spec_export.py index aa553814352c..410518e25178 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 40d595574fb7..751432569195 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 a31f6cb4b9af..d685b8346842 100644 --- a/spec_driven_model/models/spec_mixin.py +++ b/spec_driven_model/models/spec_mixin.py @@ -1,7 +1,8 @@ -# Copyright 2019-2020 Akretion - Raphael Valyi +# Copyright 2019-TODAY Akretion - Raphael Valyi # License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl-3.0.en.html). from odoo import api, models +from odoo.tools import frozendict from .spec_models import SPEC_MIXIN_MAPPINGS, SpecModel, StackedModel @@ -21,7 +22,15 @@ class SpecMixin(models.AbstractModel): _description = "root abstract model meant for xsd generated fiscal models" _name = "spec.mixin" _inherit = ["spec.mixin_export", "spec.mixin_import"] - _stacking_points = {} + + # actually _stacking_points are model and even schema specific + # but the legacy code used it extensively so a first defensive + # action we can take is to use a frozendict so it is readonly + # at least. In the future the whole stacking system should be + # scoped under a given schema and version as in + # https://github.com/OCA/l10n-brazil/pull/3424 + _stacking_points = frozendict({}) + # _spec_module = 'override.with.your.python.module' # _binding_module = 'your.pyhthon.binding.module' # _odoo_module = 'your.odoo_module' @@ -64,6 +73,7 @@ def _register_hook(self): return res setattr(self.env.registry, load_key, True) access_data = [] + access_fields = [] self.env.cr.execute( """SELECT DISTINCT relation FROM ir_model_fields WHERE relation LIKE %s;""", @@ -76,7 +86,6 @@ def _register_hook(self): if self.env.registry.get(i[0]) and not SPEC_MIXIN_MAPPINGS[self.env.cr.dbname].get(i[0]) } - for name in remaining_models: spec_class = StackedModel._odoo_name_to_class(name, self._spec_module) if spec_class is None: @@ -141,10 +150,7 @@ def _auto_fill_access_data(cls, env, module_name: str, access_data: list): """ underline_name = cls._name.replace(".", "_") - model_id = "%s_spec.model_%s" % ( - module_name, - underline_name, - ) + model_id = f"{module_name}_spec.model_{underline_name}" user_access_name = f"access_{underline_name}_user" if not env["ir.model.access"].search( [ @@ -157,7 +163,7 @@ def _auto_fill_access_data(cls, env, module_name: str, access_data: list): user_access_name, user_access_name, model_id, - "%s.group_user" % (module_name,), + f"{module_name}.group_user", "1", "0", "0", @@ -176,7 +182,7 @@ def _auto_fill_access_data(cls, env, module_name: str, access_data: list): manager_access_name, manager_access_name, model_id, - "%s.group_manager" % (module_name,), + f"{module_name}.group_manager", "1", "1", "1", diff --git a/spec_driven_model/models/spec_models.py b/spec_driven_model/models/spec_models.py index 6faae882cb6a..dca2bb2a6288 100644 --- a/spec_driven_model/models/spec_models.py +++ b/spec_driven_model/models/spec_models.py @@ -4,6 +4,7 @@ import logging import sys from collections import OrderedDict, defaultdict +from importlib import import_module from inspect import getmembers, isclass from odoo import SUPERUSER_ID, _, api, models @@ -66,6 +67,20 @@ def _compute_display_name(self): rec.display_name = _("Abrir...") return res + def _get_stacking_points(self): + key = f"_{self._spec_prefix(self._context)}_spec_settings" + if hasattr(self, key): + return getattr(self, key)["stacking_points"] + return {} + + @classmethod + def _spec_prefix(cls, context=None, spec_schema=None, spec_version=None): + if context and context.get("spec_schema"): + spec_schema = context.get("spec_schema") + if context and context.get("spec_version"): + spec_version = context.get("spec_version") + return "%s%s" % (spec_schema, spec_version.replace(".", "")[:2]) + @classmethod def _build_model(cls, pool, cr): """ @@ -212,8 +227,9 @@ class StackedModel(SpecModel): By inheriting from StackModel instead, your models.Model can instead inherit all the mixins that would correspond to the nested xsd - nodes starting from the _stacked node. _stack_skip allows you to avoid - stacking specific nodes. + nodes starting from the stacking_mixin. stacking_skip_paths allows you to avoid + stacking specific nodes while stacking_force_paths will stack many2one + entities even if they are not required. In Brazil it allows us to have mostly the fiscal document objects and the fiscal document line object with many details @@ -225,25 +241,27 @@ class StackedModel(SpecModel): _register = False # forces you to inherit StackeModel properly - # define _stacked in your submodel to define the model of the XML tags - # where we should start to - # stack models of nested tags in the same object. - _stacked = False - _stack_path = "" - _stack_skip = () - # all m2o below these paths will be stacked even if not required: - _force_stack_paths = () - _stacking_points = {} - @classmethod def _build_model(cls, pool, cr): + mod = import_module(".".join(cls.__module__.split(".")[:-1])) + if hasattr(cls, "_schema_name"): + schema = cls._schema_name + version = cls._schema_version.replace(".", "")[:2] + else: + mod = import_module(".".join(cls.__module__.split(".")[:-1])) + schema = mod.spec_schema + version = mod.spec_version.replace(".", "")[:2] + spec_prefix = cls._spec_prefix(spec_schema=schema, spec_version=version) + stacking_settings = getattr(cls, "_%s_spec_settings" % (spec_prefix,)) # inject all stacked m2o as inherited classes if cls._stacked: _logger.info("building StackedModel %s %s" % (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__ @@ -253,12 +271,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. @@ -270,7 +294,7 @@ def _visit_stack(cls, env, node, path=None): # https://github.com/OCA/l10n-brazil/pull/1272#issuecomment-821806603 node._description = None if path is None: - path = cls._stacked.split(".")[-1] + path = stacking_settings["stacking_mixin"].split(".")[-1] SpecModel._map_concrete(env.cr.dbname, node._name, cls._name, quiet=True) yield "stacked", node, path, None, None @@ -294,10 +318,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) @@ -309,7 +338,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 @@ -319,7 +348,9 @@ def _visit_stack(cls, env, node, path=None): # then we will STACK the child in the current class child._stack_path = path child_path = "%s.%s" % (path, field_path) - cls._stacking_points[name] = env[node._name]._fields.get(name) - yield from cls._visit_stack(env, child, child_path) + stacking_settings["stacking_points"][name] = env[ + node._name + ]._fields.get(name) + yield from cls._visit_stack(env, child, stacking_settings, child_path) else: yield "many2one", node, path, field_path, child_concrete diff --git a/spec_driven_model/tests/__init__.py b/spec_driven_model/tests/__init__.py index c1e5e2a4f7a7..214f7be53ab6 100644 --- a/spec_driven_model/tests/__init__.py +++ b/spec_driven_model/tests/__init__.py @@ -1 +1,4 @@ from . import test_spec_model + +spec_schema = "poxsd" +spec_version = "10" diff --git a/spec_driven_model/tests/spec_purchase.py b/spec_driven_model/tests/spec_purchase.py index 9f01b44f760e..4ec3ad02aef6 100644 --- a/spec_driven_model/tests/spec_purchase.py +++ b/spec_driven_model/tests/spec_purchase.py @@ -41,10 +41,11 @@ class PurchaseOrder(spec_models.StackedModel): _name = "fake.purchase.order" _inherit = ["fake.purchase.order", "poxsd.10.purchaseordertype"] - _spec_module = "odoo.addons.spec_driven_model.tests.spec_poxsd" - _stacked = "poxsd.10.purchaseordertype" - _stacking_points = {} - _poxsd10_spec_module_classes = None + _poxsd10_spec_settings = { + "module": "odoo.addons.spec_driven_model.tests.spec_poxsd", + "stacking_mixin": "poxsd.10.purchaseordertype", + "stacking_points": {}, + } poxsd10_orderDate = fields.Date(compute="_compute_date") poxsd10_confirmDate = fields.Date(related="date_approve") diff --git a/spec_driven_model/tests/test_spec_model.py b/spec_driven_model/tests/test_spec_model.py index 2658e8d6a216..19c7aba012e8 100644 --- a/spec_driven_model/tests/test_spec_model.py +++ b/spec_driven_model/tests/test_spec_model.py @@ -82,18 +82,6 @@ def tearDownClass(cls): cls.loader.restore_registry() super(TestSpecModel, cls).tearDownClass() - # def test_loading_hook(self): - # - # remaining_spec_models = get_remaining_spec_models( - # self.env.cr, - # self.env.registry, - # "spec_driven_model", - # "odoo.addons.spec_driven_model.tests.spec_poxsd", - # ) - # self.assertEqual( - # remaining_spec_models, {"poxsd.10.purchaseorder", "poxsd.10.comment"} - # ) - def test_spec_models(self): self.assertTrue( set(self.env["res.partner"]._fields.keys()).issuperset( @@ -110,7 +98,11 @@ def test_spec_models(self): def test_stacked_model(self): po_fields_or_stacking = set(self.env["fake.purchase.order"]._fields.keys()) po_fields_or_stacking.update( - set(self.env["fake.purchase.order"]._stacking_points.keys()) + set( + self.env["fake.purchase.order"] + ._poxsd10_spec_settings["stacking_points"] + .keys() + ) ) self.assertTrue( po_fields_or_stacking.issuperset( @@ -118,7 +110,11 @@ def test_stacked_model(self): ) ) self.assertEqual( - list(self.env["fake.purchase.order"]._stacking_points.keys()), + list( + self.env["fake.purchase.order"] + ._poxsd10_spec_settings["stacking_points"] + .keys() + ), ["poxsd10_items"], ) @@ -159,7 +155,11 @@ def test_create_export_import(self): # 2nd we serialize it into a binding object: # (that could be further XML serialized) - po_binding = po._build_generateds() + po_binding = po._build_generateds(spec_schema="poxsd", spec_version="10") + self.assertEqual( + [s.__name__ for s in type(po_binding).mro()], + ["PurchaseOrderType", "object"], + ) self.assertEqual(po_binding.bill_to.name, "Wood Corner") self.assertEqual(po_binding.items.item[0].product_name, "Some product desc") self.assertEqual(po_binding.items.item[0].quantity, 42) @@ -206,12 +206,14 @@ def test_create_export_import(self): # 4th we import an Odoo PO from this binding object # first we will do a dry run import: imported_po_dry_run = self.env["fake.purchase.order"].build_from_binding( - po_binding, dry_run=True + "poxsd", "10", po_binding, dry_run=True ) assert isinstance(imported_po_dry_run.id, NewId) # now a real import: - imported_po = self.env["fake.purchase.order"].build_from_binding(po_binding) + imported_po = self.env["fake.purchase.order"].build_from_binding( + "poxsd", "10", po_binding + ) self.assertEqual(imported_po.partner_id.name, "Wood Corner") self.assertEqual( imported_po.partner_id.id, self.env.ref("base.res_partner_1").id From f787b5d8897841054fda82c8cc95eccf6cb34b18 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/12] [REF] l10n_br_nfe: multi-schemas compat [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 | 55 +++++++++--------- l10n_br_nfe/models/document_line.py | 68 +++++++++-------------- l10n_br_nfe/models/document_related.py | 14 ++--- l10n_br_nfe/models/document_supplement.py | 12 ++-- l10n_br_nfe/tests/test_nfe_import.py | 4 +- l10n_br_nfe/tests/test_nfe_structure.py | 21 +++++-- 8 files changed, 87 insertions(+), 92 deletions(-) diff --git a/l10n_br_nfe/hooks.py b/l10n_br_nfe/hooks.py index 549504601e50..c42c231452a8 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 7f62f604fa9a..59874468e4b3 100644 --- a/l10n_br_nfe/models/document.py +++ b/l10n_br_nfe/models/document.py @@ -78,27 +78,24 @@ 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" - _field_prefix = "nfe40_" - _schema_name = "nfe" - _schema_version = "4.0.0" - _odoo_module = "l10n_br_nfe" - _spec_module = "odoo.addons.l10n_br_nfe_spec.models.v4_0.leiaute_nfe_v4_00" - _spec_tab_name = "NFe" + _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 = """ -> + > > l10n_br_fiscal.document.related - res.company @@ -675,7 +672,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") @@ -683,7 +680,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 @@ -691,7 +690,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( @@ -733,7 +732,7 @@ def _prepare_import_dict( } def _build_attr(self, node, fields, vals, path, attr): - key = "nfe40_%s" % (attr[0],) # TODO schema wise + key = f"nfe40_{attr[0]}" # TODO schema wise value = getattr(node, attr[0]) if key == "nfe40_mod": @@ -866,7 +865,8 @@ def _check_document_date_key(self): ): raise ValidationError( _( - "The document date does not match the date in the document key." + "The document date does not match the date in the document " + "key." ) ) @@ -897,11 +897,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) @@ -1066,7 +1066,7 @@ def _exec_after_SITUACAO_EDOC_AUTORIZADA(self, old_state, new_state): # autorizado, podendo perder dados. # Se der problema que apareça quando # o usuário clicar no gerar PDF novamente. - _logger.error("DANFE Error \n {}".format(e)) + _logger.error(f"DANFE Error \n {e}") return super()._exec_after_SITUACAO_EDOC_AUTORIZADA(old_state, new_state) def _generate_key(self): @@ -1346,7 +1346,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( @@ -1362,7 +1362,8 @@ def _document_cancel(self, justificative): if not justificative or len(justificative) < 15: raise ValidationError( _( - "Please enter a justification that is at least 15 characters long." + "Please enter a justification that is at least 15 characters " + "long." ) ) result = super()._document_cancel(justificative) diff --git a/l10n_br_nfe/models/document_line.py b/l10n_br_nfe/models/document_line.py index b3d9bf801841..938f9cfbd1e4 100644 --- a/l10n_br_nfe/models/document_line.py +++ b/l10n_br_nfe/models/document_line.py @@ -38,7 +38,7 @@ "ICMSSN900", ] -ICMS_SELECTION = list(map(lambda tag: ("nfe40_%s" % (tag,), tag), ICMS_SUB_TAGS)) +ICMS_SELECTION = list(map(lambda tag: (f"nfe40_{tag}", tag), ICMS_SUB_TAGS)) PIS_SUB_TAGS = [ "PISAliq", @@ -47,7 +47,7 @@ "PISOutr", ] -PIS_SELECTION = list(map(lambda tag: ("nfe40_%s" % (tag,), tag), PIS_SUB_TAGS)) +PIS_SELECTION = list(map(lambda tag: (f"nfe40_{tag}", tag), PIS_SUB_TAGS)) COFINS_SUB_TAGS = [ "COFINSAliq", @@ -56,7 +56,7 @@ "COFINSOutr", ] -COFINS_SELECTION = list(map(lambda tag: ("nfe40_%s" % (tag,), tag), COFINS_SUB_TAGS)) +COFINS_SELECTION = list(map(lambda tag: (f"nfe40_{tag}", tag), COFINS_SUB_TAGS)) class NFeLine(spec_models.StackedModel): @@ -70,17 +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" - _field_prefix = "nfe40_" - _schema_name = "nfe" - _schema_version = "4.0.0" - _odoo_module = "l10n_br_nfe" - _spec_module = "odoo.addons.l10n_br_nfe_spec.models.v4_0.leiaute_nfe_v4_00" - _spec_tab_name = "NFe" - _stacking_points = {} - # all m2o below this level will be stacked even if not required: - _force_stack_paths = ("det.imposto.",) - _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 = """ @@ -125,13 +122,6 @@ class NFeLine(spec_models.StackedModel): # NF-e spec related fields ########################## - ###################################### - # NF-e tag: det - # Grupo I. Produtos e Serviços da NF-e - ###################################### - - nfe40_det_infNFe_id = fields.Many2one(related="document_id") - ###################################### # NF-e tag: prod # Grupo I. Produtos e Serviços da NF-e @@ -1196,27 +1186,21 @@ def map_binding_attr(attr, odoo_attr=None): # common attributes CST, VBC, p*, v*: cst = map_binding_attr("CST") if cst: - cst_id = self.env.ref( - "l10n_br_fiscal.cst_%s_%s" - % ( - kind, - cst, - ) - ).id - odoo_attrs["%s_cst_id" % (kind,)] = cst_id + cst_id = self.env.ref(f"l10n_br_fiscal.cst_{kind}_{cst}").id + odoo_attrs[f"{kind}_cst_id"] = cst_id else: cst_id = None - map_binding_attr("vBC", "%s_base" % (kind,)) + map_binding_attr("vBC", f"{kind}_base") percent = map_binding_attr( - "p%s" % (kind.upper().replace("ST", ""),), "%s_percent" % (kind,) + f"p{kind.upper().replace('ST', '')}", f"{kind}_percent" ) if kind in ("icms", "icmsufdest"): map_binding_attr("modBC", "icms_base_type") icms_percent_red = map_binding_attr("pRedBC", "icms_reduction") else: - map_binding_attr("modBC", "%s_base_type" % (kind,)) + map_binding_attr("modBC", f"{kind}_base_type") icms_percent_red = None if "ICMSSN" in key: @@ -1227,15 +1211,13 @@ def map_binding_attr(attr, odoo_attr=None): tax_group_kind = "icms" else: tax_group_kind = kind - tax_group_id = self.env.ref( - "l10n_br_fiscal.tax_group_%s" % (tax_group_kind,) - ).id + tax_group_id = self.env.ref(f"l10n_br_fiscal.tax_group_{tax_group_kind}").id tax_domain = [("tax_group_id", "=", tax_group_id)] if percent: tax_domain.append(("percent_amount", "=", percent)) tax_domain_with_cst = None if cst_id: - cst_kind = "cst_%s_id" % (self.env.context.get("edoc_type", "in"),) + cst_kind = "cst_{}_id".format(self.env.context.get("edoc_type", "in")) tax_domain_with_cst = tax_domain + [(cst_kind, "=", cst_id)] fiscal_tax_id = None @@ -1263,24 +1245,24 @@ def map_binding_attr(attr, odoo_attr=None): ) if fiscal_tax_id: - odoo_attrs["%s_tax_id" % (kind,)] = fiscal_tax_id.id + odoo_attrs[f"{kind}_tax_id"] = fiscal_tax_id.id if not odoo_attrs.get("fiscal_tax_ids"): odoo_attrs["fiscal_tax_ids"] = [] odoo_attrs["fiscal_tax_ids"].append(fiscal_tax_id.id) - elif not odoo_attrs.get("%s_tax_id" % (kind,)): - nt_tax_ref = "l10n_br_fiscal.tax_%s_nt" % (kind,) + elif not odoo_attrs.get(f"{kind}_tax_id"): + nt_tax_ref = f"l10n_br_fiscal.tax_{kind}_nt" nt_tax = self.env.ref(nt_tax_ref, raise_if_not_found=False) if nt_tax: # NOTE, can it be isento or something else? - odoo_attrs["%s_tax_id" % (kind,)] = nt_tax.id + odoo_attrs[f"{kind}_tax_id"] = nt_tax.id - map_binding_attr("v%s" % (kind.upper(),), "%s_value" % (kind,)) + map_binding_attr(f"v{kind.upper()}", f"{kind}_value") if kind in ("icms", "icmsufdest"): map_binding_attr("orig", "icms_origin") mot_des_icms = map_binding_attr("motDesICMS") if mot_des_icms: odoo_attrs["icms_relief_id"] = self.env.ref( - "l10n_br_fiscal.icms_relief_%s" % (mot_des_icms,) + f"l10n_br_fiscal.icms_relief_{mot_des_icms}" ).id map_binding_attr("vICMSDeson", "icms_relief_value") map_binding_attr("vICMSSubstituto", "icms_substitute") @@ -1321,7 +1303,7 @@ def map_binding_attr(attr, odoo_attr=None): csosn = map_binding_attr("CSOSN") if csosn: odoo_attrs["icms_cst_id"] = self.env.ref( - "l10n_br_fiscal.cst_icmssn_%s" % (csosn,) + f"l10n_br_fiscal.cst_icmssn_{csosn}" ).id map_binding_attr("pCredSN", "icmssn_percent") map_binding_attr("vCredICMSSN", "icmssn_credit_value") diff --git a/l10n_br_nfe/models/document_related.py b/l10n_br_nfe/models/document_related.py index ad5e8b3e1124..a265287731d4 100644 --- a/l10n_br_nfe/models/document_related.py +++ b/l10n_br_nfe/models/document_related.py @@ -20,14 +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" - _field_prefix = "nfe40_" - _schema_name = "nfe" - _schema_version = "4.0.0" - _odoo_module = "l10n_br_nfe" - _spec_module = "odoo.addons.l10n_br_nfe_spec.models.v4_0.leiaute_nfe_v4_00" - _spec_tab_name = "NFe" - _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 85590853eec7..6d114ed23d5f 100644 --- a/l10n_br_nfe/models/document_supplement.py +++ b/l10n_br_nfe/models/document_supplement.py @@ -9,10 +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" - _field_prefix = "nfe40_" - _schema_name = "nfe" - _schema_version = "4.0.0" - _odoo_module = "l10n_br_nfe" - _spec_module = "odoo.addons.l10n_br_nfe_spec.models.v4_0.leiaute_nfe_v4_00" - _spec_tab_name = "NFe" + _nfe40_spec_settings = { + "module": "odoo.addons.l10n_br_nfe_spec.models.v4_0.leiaute_nfe_v4_00", + "stacking_mixin": "nfe.40.infnfesupl", + "stacking_points": {}, + } diff --git a/l10n_br_nfe/tests/test_nfe_import.py b/l10n_br_nfe/tests/test_nfe_import.py index 8d9557b4d2cd..3a53d143cd8a 100644 --- a/l10n_br_nfe/tests/test_nfe_import.py +++ b/l10n_br_nfe/tests/test_nfe_import.py @@ -32,7 +32,7 @@ def test_import_in_nfe_dry_run(self): nfe = ( self.env["nfe.40.infnfe"] .with_context(tracking_disable=True, edoc_type="in") - .build_from_binding(binding.NFe.infNFe, dry_run=True) + .build_from_binding("nfe", "40", binding.NFe.infNFe, dry_run=True) ) assert isinstance(nfe.id, NewId) self._check_nfe(nfe) @@ -51,7 +51,7 @@ def test_import_in_nfe(self): nfe = ( self.env["nfe.40.infnfe"] .with_context(tracking_disable=True, edoc_type="in") - .build_from_binding(binding.NFe.infNFe, dry_run=False) + .build_from_binding("nfe", "40", binding.NFe.infNFe, dry_run=False) ) assert isinstance(nfe.id, int) diff --git a/l10n_br_nfe/tests/test_nfe_structure.py b/l10n_br_nfe/tests/test_nfe_structure.py index 20441ea93694..eafc9ba4d755 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(".") @@ -119,7 +122,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): @@ -155,7 +164,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 090aa7d0f733cf02d172cb9d190d255676d8c921 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/12] [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 410518e25178..44467dc10d25 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 751432569195..023de8b97dc5 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 a3cad41e326d..b8085eda44c1 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 19c7aba012e8..ef4827ddfd09 100644 --- a/spec_driven_model/tests/test_spec_model.py +++ b/spec_driven_model/tests/test_spec_model.py @@ -155,7 +155,7 @@ def test_create_export_import(self): # 2nd we serialize it into a binding object: # (that could be further XML serialized) - po_binding = po._build_generateds(spec_schema="poxsd", spec_version="10") + po_binding = po._build_binding(spec_schema="poxsd", spec_version="10") self.assertEqual( [s.__name__ for s in type(po_binding).mro()], ["PurchaseOrderType", "object"], From 935c8d97a3348fc6edfc772a367082ad376839ff 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/12] [REF] l10n_br_nfe: export_ds -> _build_binding --- l10n_br_nfe/models/document.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/l10n_br_nfe/models/document.py b/l10n_br_nfe/models/document.py index 59874468e4b3..79c8dda31403 100644 --- a/l10n_br_nfe/models/document.py +++ b/l10n_br_nfe/models/document.py @@ -95,7 +95,7 @@ class NFe(spec_models.StackedModel): # When dynamic stacking is applied the NFe structure is: INFNFE_TREE = """ - > +> > l10n_br_fiscal.document.related - res.company @@ -897,11 +897,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 399a33a9bee31dd0e04233b19476e32a70c76127 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/12] [REF] spec_driven_model: multi-schemas support --- spec_driven_model/models/spec_export.py | 20 ++--- spec_driven_model/models/spec_import.py | 25 +++--- spec_driven_model/models/spec_mixin.py | 1 + spec_driven_model/models/spec_models.py | 97 ++++++++++++---------- 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, 83 insertions(+), 81 deletions(-) diff --git a/spec_driven_model/models/spec_export.py b/spec_driven_model/models/spec_export.py index 44467dc10d25..153697a2b8a6 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: @@ -201,23 +202,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) + 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 023de8b97dc5..9be1d592fcf9 100644 --- a/spec_driven_model/models/spec_import.py +++ b/spec_driven_model/models/spec_import.py @@ -73,11 +73,9 @@ def _build_attr(self, node, fields, vals, path, attr): value = getattr(node, attr[0]) if value is None or value == []: return False - key = "%s%s" % ( - self._field_prefix, - attr[1].metadata.get("name", attr[0]), - ) - child_path = "%s.%s" % (path, key) + 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? # with xsdata a ComplexType can have a type like: @@ -122,9 +120,9 @@ def _build_attr(self, node, fields, vals, path, attr): comodel_name = fields[key].comodel_name else: clean_type = binding_type.lower() - comodel_name = "%s.%s.%s" % ( - self._schema_name, - self._schema_version.replace(".", "")[0:2], + comodel_name = "{}.{}.{}".format( + self._context["spec_schema"], + self._context["spec_version"].replace(".", "")[0:2], clean_type.split(".")[-1], ) @@ -198,9 +196,10 @@ def _prepare_import_dict( related_many2ones = {} fields = model._fields + field_prefix = f"{self._spec_prefix(self._context)}_" for k, v in fields.items(): # select schema choices for a friendly UI: - if k.startswith("%schoice" % (self._field_prefix,)): + if k.startswith(f"{field_prefix}choice"): for item in v.selection or []: if vals.get(item[0]) not in [None, []]: vals[k] = item[0] @@ -214,7 +213,7 @@ def _prepare_import_dict( related = v.related if len(related) == 1: vals[related[0]] = vals.get(k) - elif len(related) == 2 and k.startswith(self._field_prefix): + elif len(related) == 2 and k.startswith(field_prefix): related_m2o = related[0] # don't mess with _inherits write system if not any(related_m2o == i[1] for i in model._inherits.items()): @@ -263,7 +262,7 @@ def match_record(self, rec_dict, parent_dict, model=None): if model is None: model = self default_key = [model._rec_name or "name"] - search_keys = "_%s_search_keys" % (self._schema_name) + search_keys = "_%s_search_keys" % (self._context["spec_schema"]) if hasattr(model, search_keys): keys = getattr(model, search_keys) + default_key else: @@ -281,8 +280,8 @@ def match_record(self, rec_dict, parent_dict, model=None): if match_ids: if len(match_ids) > 1: _logger.warning( - "!! WARNING more than 1 record found!! model: %s, domain: %s" - % (model, domain) + f"!! WARNING more than 1 record found!! model: {model}," + f" domain:{domain}" ) return match_ids[0].id return False diff --git a/spec_driven_model/models/spec_mixin.py b/spec_driven_model/models/spec_mixin.py index d685b8346842..8157ac8fff2e 100644 --- a/spec_driven_model/models/spec_mixin.py +++ b/spec_driven_model/models/spec_mixin.py @@ -22,6 +22,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 # actually _stacking_points are model and even schema specific # but the legacy code used it extensively so a first defensive diff --git a/spec_driven_model/models/spec_models.py b/spec_driven_model/models/spec_models.py index dca2bb2a6288..309ba0e282a2 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): """ @@ -90,8 +92,8 @@ class as long as the generated spec mixins inherit from some spec.mixin. mixin. """ schema = None - if hasattr(cls, "_schema_name"): - schema = cls._schema_name + if hasattr(cls, "_spec_schema"): + schema = cls._spec_schema elif pool.get(cls._name) and hasattr(pool[cls._name], "_schema_name"): schema = pool[cls._name]._schema_name if schema and "spec.mixin" not in [ @@ -126,14 +128,9 @@ def _setup_fields(self): relational fields pointing to such mixins should be remapped to the proper concrete models where these mixins are injected. """ - cls = self.env.registry[self._name] + cls = type(self) for klass in cls.__bases__: - if ( - not hasattr(klass, "_name") - or not hasattr(klass, "_fields") - or klass._name is None - or not klass._name.startswith(self.env[cls._name]._schema_name) - ): + if not hasattr(klass, "_is_spec_driven"): continue if klass._name != cls._name: cls._map_concrete(self.env.cr.dbname, klass._name, cls._name) @@ -193,7 +190,7 @@ def _setup_fields(self): 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("%s ---> %s" % (key, target)) + _logger.debug(f"{key} ---> {target}") global SPEC_MIXIN_MAPPINGS SPEC_MIXIN_MAPPINGS[dbname][key] = target @@ -203,7 +200,7 @@ def spec_module_classes(cls, spec_module): Cache the list of spec_module classes to save calls to slow reflection API. """ - spec_module_attr = "_spec_cache_%s" % (spec_module.replace(".", "_"),) + spec_module_attr = f"_spec_cache_{spec_module.replace('.', '_')}" if not hasattr(cls, spec_module_attr): setattr( cls, spec_module_attr, getmembers(sys.modules[spec_module], isclass) @@ -251,34 +248,48 @@ def _build_model(cls, pool, cr): mod = import_module(".".join(cls.__module__.split(".")[:-1])) schema = mod.spec_schema version = mod.spec_version.replace(".", "")[:2] + cls._spec_schema = schema + cls._spec_version = version spec_prefix = cls._spec_prefix(spec_schema=schema, spec_version=version) - stacking_settings = getattr(cls, "_%s_spec_settings" % (spec_prefix,)) + setattr(cls, f"_{spec_prefix}_stacking_points", {}) + stacking_settings = { + "odoo_module": getattr(cls, f"_{spec_prefix}_odoo_module"), # TODO inherit? + "stacking_mixin": getattr(cls, f"_{spec_prefix}_stacking_mixin"), + "stacking_points": getattr(cls, f"_{spec_prefix}_stacking_points"), + "stacking_skip_paths": getattr( + cls, f"_{spec_prefix}_stacking_skip_paths", [] + ), + "stacking_force_paths": getattr( + cls, f"_{spec_prefix}_stacking_force_paths", [] + ), + } # inject all stacked m2o as inherited classes - if cls._stacked: - _logger.info("building StackedModel %s %s" % (cls._name, cls)) - node = cls._odoo_name_to_class( - stacking_settings["stacking_mixin"], stacking_settings["module"] - ) - env = api.Environment(cr, SUPERUSER_ID, {}) - for kind, klass, _path, _field_path, _child_concrete in cls._visit_stack( - env, node, stacking_settings - ): - if kind == "stacked" and klass not in cls.__bases__: - cls.__bases__ = (klass,) + cls.__bases__ + _logger.info(f"building StackedModel {cls._name} {cls}") + node = cls._odoo_name_to_class( + stacking_settings["stacking_mixin"], stacking_settings["odoo_module"] + ) + env = api.Environment(cr, SUPERUSER_ID, {}) + for kind, klass, _path, _field_path, _child_concrete in cls._visit_stack( + env, node, stacking_settings + ): + if kind == "stacked" and klass not in cls.__bases__: + cls.__bases__ = (klass,) + cls.__bases__ return super()._build_model(pool, cr) @api.model def _add_field(self, name, field): - for cls in type(self).mro(): - if issubclass(cls, StackedModel): - if hasattr(self, "_schema_name"): - prefix = self._spec_prefix( - None, self._schema_name, self._schema_version - ) - key = f"_{prefix}_spec_settings" - stacking_points = getattr(self, key)["stacking_points"] - if name in stacking_points.keys(): - return + """ + Overriden to avoid adding many2one fields that are in fact "stacking points" + """ + if field.type == "many2one": + for cls in type(self).mro(): + if issubclass(cls, StackedModel): + for attr in dir(cls): + if attr != "_get_stacking_points" and attr.endswith( + "_stacking_points" + ): + if name in getattr(cls, attr).keys(): + return return super()._add_field(name, field) @classmethod @@ -295,7 +306,7 @@ def _visit_stack(cls, env, node, stacking_settings, path=None): node._description = None if path is None: path = stacking_settings["stacking_mixin"].split(".")[-1] - SpecModel._map_concrete(env.cr.dbname, node._name, cls._name, quiet=True) + cls._map_concrete(env.cr.dbname, node._name, cls._name, quiet=True) yield "stacked", node, path, None, None fields = OrderedDict() @@ -325,12 +336,12 @@ def _visit_stack(cls, env, node, stacking_settings, path=None): # TODO change for view or export continue child = cls._odoo_name_to_class( - f["comodel_name"], stacking_settings["module"] + f["comodel_name"], stacking_settings["odoo_module"] ) if child is None: # Not a spec field continue child_concrete = SPEC_MIXIN_MAPPINGS[env.cr.dbname].get(child._name) - field_path = name.replace(env[node._name]._field_prefix, "") + field_path = name.split("_")[1] # remove schema prefix if f["type"] == "one2many": yield "one2many", node, path, field_path, child_concrete @@ -338,7 +349,7 @@ def _visit_stack(cls, env, node, stacking_settings, path=None): force_stacked = any( stack_path in path + "." + field_path - for stack_path in stacking_settings.get("stacking_force_paths", "") + for stack_path in stacking_settings.get("stacking_force_paths", []) ) # many2one diff --git a/spec_driven_model/tests/fake_mixin.py b/spec_driven_model/tests/fake_mixin.py index 7f3887edf35d..4d35973f70a9 100644 --- a/spec_driven_model/tests/fake_mixin.py +++ b/spec_driven_model/tests/fake_mixin.py @@ -7,12 +7,9 @@ class PoXsdMixin(models.AbstractModel): _description = "Abstract Model for PO XSD" _name = "spec.mixin.poxsd" - _field_prefix = "poxsd10_" - _schema_name = "poxsd" - _schema_version = "1.0" - _odoo_module = "poxsd" - _spec_module = "odoo.addons.spec_driven_model.tests.spec_poxsd" - _binding_module = "odoo.addons.spec_driven_model.tests.purchase_order_lib" + + _poxsd10_odoo_module = "odoo.addons.spec_driven_model.tests.spec_poxsd" + _poxsd10_binding_module = "odoo.addons.spec_driven_model.tests.purchase_order_lib" # TODO rename brl_currency_id = fields.Many2one( diff --git a/spec_driven_model/tests/spec_purchase.py b/spec_driven_model/tests/spec_purchase.py index 4ec3ad02aef6..51c7d8aa1485 100644 --- a/spec_driven_model/tests/spec_purchase.py +++ b/spec_driven_model/tests/spec_purchase.py @@ -41,11 +41,9 @@ class PurchaseOrder(spec_models.StackedModel): _name = "fake.purchase.order" _inherit = ["fake.purchase.order", "poxsd.10.purchaseordertype"] - _poxsd10_spec_settings = { - "module": "odoo.addons.spec_driven_model.tests.spec_poxsd", - "stacking_mixin": "poxsd.10.purchaseordertype", - "stacking_points": {}, - } + + _poxsd10_odoo_module = "odoo.addons.spec_driven_model.tests.spec_poxsd" + _poxsd10_stacking_mixin = "poxsd.10.purchaseordertype" poxsd10_orderDate = fields.Date(compute="_compute_date") poxsd10_confirmDate = fields.Date(related="date_approve") diff --git a/spec_driven_model/tests/test_spec_model.py b/spec_driven_model/tests/test_spec_model.py index ef4827ddfd09..7f5b98cf0268 100644 --- a/spec_driven_model/tests/test_spec_model.py +++ b/spec_driven_model/tests/test_spec_model.py @@ -100,7 +100,7 @@ def test_stacked_model(self): po_fields_or_stacking.update( set( self.env["fake.purchase.order"] - ._poxsd10_spec_settings["stacking_points"] + ._poxsd10_stacking_points .keys() ) ) @@ -112,7 +112,7 @@ def test_stacked_model(self): self.assertEqual( list( self.env["fake.purchase.order"] - ._poxsd10_spec_settings["stacking_points"] + ._poxsd10_stacking_points .keys() ), ["poxsd10_items"], From 62f480a9af5ddf1140bf4c8a993cac8e2f51de99 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/12] [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 79c8dda31403..12272cd24db8 100644 --- a/l10n_br_nfe/models/document.py +++ b/l10n_br_nfe/models/document.py @@ -78,19 +78,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 eafc9ba4d755..4be6a8f9401a 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 020f0156fe541b3a3a742444fa20cb88df03ec38 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/12] [REF] spec_driven_model: further multi-schemas --- spec_driven_model/README.rst | 25 ++++++--- spec_driven_model/models/spec_export.py | 14 ++--- spec_driven_model/models/spec_import.py | 16 +++--- spec_driven_model/models/spec_mixin.py | 56 +++++++++++++------ spec_driven_model/models/spec_models.py | 18 ++---- .../static/description/index.html | 25 ++++++--- 6 files changed, 90 insertions(+), 64 deletions(-) diff --git a/spec_driven_model/README.rst b/spec_driven_model/README.rst index 0a3d44265827..a07e4b95f388 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 153697a2b8a6..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("%s." % (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 @@ -136,7 +136,7 @@ def _export_field(self, xsd_field, class_obj, field_spec, export_value=None): return self._export_float_monetary( xsd_field, xsd_type, class_obj, xsd_required, export_value ) - elif type(self[xsd_field]) is str: + elif isinstance(self[xsd_field], str): return self[xsd_field].strip() else: return self[xsd_field] @@ -174,7 +174,7 @@ def _export_float_monetary( tdec = "".join(filter(lambda x: x.isdigit(), xsd_type))[-2:] else: tdec = "" - my_format = "%.{}f".format(tdec) + my_format = f"%.{tdec}f" return str(my_format % field_data) def _export_date(self, field_name): @@ -201,8 +201,8 @@ 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 = 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) @@ -211,9 +211,7 @@ def _build_binding(self, spec_schema=None, spec_version=None, class_name=None): xsd_fields = ( i for i in class_obj._fields - if class_obj._fields[i].name.startswith( - f"{self._spec_prefix(self._context)}_" - ) + if class_obj._fields[i].name.startswith(f"{self._spec_prefix()}_") and "_choice" not in class_obj._fields[i].name ) diff --git a/spec_driven_model/models/spec_import.py b/spec_driven_model/models/spec_import.py index 9be1d592fcf9..bcea403c703d 100644 --- a/spec_driven_model/models/spec_import.py +++ b/spec_driven_model/models/spec_import.py @@ -42,12 +42,12 @@ def build_from_binding(self, spec_schema, spec_version, node, dry_run=False): Defaults values and control options are meant to be passed in the context. """ - model = self.with_context( - spec_schema=spec_schema, spec_version=spec_version - )._get_concrete_model(self._name) - attrs = model.with_context( - dry_run=dry_run, spec_schema=spec_schema, spec_version=spec_version - ).build_attrs(node) + self = self.with_context( + spec_schema=spec_schema, spec_version=spec_version, dry_run=dry_run + ) + self._register_hook() + model = self._get_concrete_model(self._name) + attrs = model.build_attrs(node) if dry_run: return model.new(attrs) else: @@ -73,7 +73,7 @@ def _build_attr(self, node, fields, vals, path, attr): value = getattr(node, attr[0]) if value is None or value == []: return False - prefix = f"{self._spec_prefix(self._context)}" + prefix = f"{self._spec_prefix()}" key = f"{prefix}_{attr[1].metadata.get('name', attr[0])}" child_path = f"{path}.{key}" @@ -196,7 +196,7 @@ def _prepare_import_dict( related_many2ones = {} fields = model._fields - field_prefix = f"{self._spec_prefix(self._context)}_" + field_prefix = f"{self._spec_prefix()}_" for k, v in fields.items(): # select schema choices for a friendly UI: if k.startswith(f"{field_prefix}choice"): diff --git a/spec_driven_model/models/spec_mixin.py b/spec_driven_model/models/spec_mixin.py index 8157ac8fff2e..10f99da855b6 100644 --- a/spec_driven_model/models/spec_mixin.py +++ b/spec_driven_model/models/spec_mixin.py @@ -58,27 +58,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 = { @@ -88,17 +111,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, ) ) @@ -108,14 +128,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, }, ) - 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 @@ -136,11 +158,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 309ba0e282a2..2c2278813b47 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,16 +233,16 @@ 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] cls._spec_schema = schema cls._spec_version = version - spec_prefix = cls._spec_prefix(spec_schema=schema, spec_version=version) + spec_prefix = cls._ensure_spec_prefix(spec_schema=schema, spec_version=version) setattr(cls, f"_{spec_prefix}_stacking_points", {}) stacking_settings = { "odoo_module": getattr(cls, f"_{spec_prefix}_odoo_module"), # TODO inherit? diff --git a/spec_driven_model/static/description/index.html b/spec_driven_model/static/description/index.html index 378a1b7ee56c..a662a7722942 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 686876f7b04612dd65d220032ed37d6432e7ec19 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/12] [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 6df41161257bbe3fe89e9ac59d58a13a0ff2e490 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/12] [REF] l10n_br_nfe: further multi-schemas --- l10n_br_nfe/hooks.py | 1 - l10n_br_nfe/models/document.py | 15 ++++++++++----- l10n_br_nfe/tests/test_nfe_import.py | 5 ----- l10n_br_nfe/tests/test_nfe_serialize.py | 1 - l10n_br_nfe/tests/test_nfe_structure.py | 1 - 5 files changed, 10 insertions(+), 13 deletions(-) diff --git a/l10n_br_nfe/hooks.py b/l10n_br_nfe/hooks.py index c42c231452a8..b89c621ef816 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 12272cd24db8..d40287928547 100644 --- a/l10n_br_nfe/models/document.py +++ b/l10n_br_nfe/models/document.py @@ -77,7 +77,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" @@ -684,7 +684,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 @@ -694,7 +694,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 @@ -1068,9 +1068,13 @@ def _exec_after_SITUACAO_EDOC_AUTORIZADA(self, old_state, new_state): return super()._exec_after_SITUACAO_EDOC_AUTORIZADA(old_state, new_state) def _generate_key(self): - for record in self.filtered(filter_processador_edoc_nfe): - date = fields.Datetime.context_timestamp(record, record.document_date) + if self.document_type_id.code not in [ + MODELO_FISCAL_NFE, + MODELO_FISCAL_NFCE, + ]: + return super()._generate_key() + for record in self.filtered(filter_processador_edoc_nfe): required_fields_gen_edoc = [] if not record.company_cnpj_cpf: required_fields_gen_edoc.append("CNPJ/CPF") @@ -1088,6 +1092,7 @@ def _generate_key(self): _("To Generate EDoc Key, you need to fill the %s field.") % field ) + date = fields.Datetime.context_timestamp(record, record.document_date) chave_edoc = ChaveEdoc( ano_mes=date.strftime("%y%m").zfill(4), cnpj_cpf_emitente=record.company_cnpj_cpf, diff --git a/l10n_br_nfe/tests/test_nfe_import.py b/l10n_br_nfe/tests/test_nfe_import.py index 3a53d143cd8a..3933d11f91bc 100644 --- a/l10n_br_nfe/tests/test_nfe_import.py +++ b/l10n_br_nfe/tests/test_nfe_import.py @@ -11,11 +11,6 @@ class NFeImportTest(TransactionCase): - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.env["spec.mixin.nfe"]._register_hook() - def test_import_in_nfe_dry_run(self): res_items = ( "nfe", diff --git a/l10n_br_nfe/tests/test_nfe_serialize.py b/l10n_br_nfe/tests/test_nfe_serialize.py index c0011bf661b8..20ee5ba03426 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 4be6a8f9401a..441b63c091a9 100644 --- a/l10n_br_nfe/tests/test_nfe_structure.py +++ b/l10n_br_nfe/tests/test_nfe_structure.py @@ -16,7 +16,6 @@ class NFeStructure(TransactionCase): @classmethod def setUpClass(cls): super().setUpClass() - cls.env["spec.mixin.nfe"]._register_hook() @classmethod def get_stacked_tree(cls, klass): From 6a6cc39383cab8ca6dfca6d8d464aa2bb7433664 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/12] [REM] l10n_br_account_nfe: drop _register_hook --- .../tests/test_nfce_contingency.py | 46 +++++++++---------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/l10n_br_account_nfe/tests/test_nfce_contingency.py b/l10n_br_account_nfe/tests/test_nfce_contingency.py index 954eb7db3683..6333f5710dc6 100644 --- a/l10n_br_account_nfe/tests/test_nfce_contingency.py +++ b/l10n_br_account_nfe/tests/test_nfce_contingency.py @@ -5,65 +5,61 @@ class TestAccountNFCeContingency(TransactionCase): - @classmethod - def setUpClass(cls): - super().setUpClass() - # this hook is required to test l10n_br_account_nfe alone: - cls.env["spec.mixin.nfe"]._register_hook() - cls.document_id = cls.env.ref("l10n_br_nfe.demo_nfce_same_state") - cls.prepare_account_move_nfce() + def setUp(self): + super().setUp() + self.document_id = self.env.ref("l10n_br_nfe.demo_nfce_same_state") + self.prepare_account_move_nfce() - @classmethod - def prepare_account_move_nfce(cls): - receivable_account_id = cls.env["account.account"].create( + def prepare_account_move_nfce(self): + receivable_account_id = self.env["account.account"].create( { "name": "TEST ACCOUNT", - "code": "01.1.1.2.2", + "code": "1.1.1.2.2", "reconcile": 1, - "company_id": cls.env.ref("base.main_company").id, - "user_type_id": cls.env.ref("account.data_account_type_receivable").id, + "company_id": self.env.ref("base.main_company").id, + "user_type_id": self.env.ref("account.data_account_type_receivable").id, } ) - payable_account_id = cls.env["account.account"].create( + payable_account_id = self.env["account.account"].create( { "name": "TEST ACCOUNT 2", - "code": "01.1.1.2.3", + "code": "1.1.1.2.3", "reconcile": 1, - "company_id": cls.env.ref("base.main_company").id, - "user_type_id": cls.env.ref("account.data_account_type_payable").id, + "company_id": self.env.ref("base.main_company").id, + "user_type_id": self.env.ref("account.data_account_type_payable").id, } ) - payment_method = cls.env.ref("account.account_payment_method_manual_in").id - journal_id = cls.env["account.journal"].create( + payment_method = self.env.ref("account.account_payment_method_manual_in").id + journal_id = self.env["account.journal"].create( { "name": "JOURNAL TEST", "code": "TEST", "type": "bank", - "company_id": cls.env.ref("base.main_company").id, + "company_id": self.env.ref("base.main_company").id, } ) - payment_mode = cls.env["account.payment.mode"].create( + payment_mode = self.env["account.payment.mode"].create( { "name": "PAYMENT MODE TEST", - "company_id": cls.env.ref("base.main_company").id, + "company_id": self.env.ref("base.main_company").id, "payment_method_id": payment_method, "fiscal_payment_mode": "15", "bank_account_link": "fixed", "fixed_journal_id": journal_id.id, } ) - cls.document_move_id = cls.env["account.move"].create( + self.document_move_id = self.env["account.move"].create( { "name": "MOVE TEST", "payment_mode_id": payment_mode.id, - "company_id": cls.env.ref("base.main_company").id, + "company_id": self.env.ref("base.main_company").id, "line_ids": [ (0, 0, {"account_id": receivable_account_id.id, "credit": 10}), (0, 0, {"account_id": payable_account_id.id, "debit": 10}), ], } ) - cls.document_move_id.fiscal_document_id = cls.document_id.id + self.document_move_id.fiscal_document_id = self.document_id.id def test_nfce_contingencia(self): self.document_id._update_nfce_for_offline_contingency() From 81b7bb4166a122bff8c38a043d4a673d8be4f7dd 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/12] [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 885597b722a3..4dded68f3697 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": "15.0.1.3.1", "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 10f99da855b6..dbed5e2a2b65 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 odoo.tools import frozendict @@ -58,12 +60,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): """ @@ -82,22 +103,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;""", @@ -114,7 +134,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( @@ -135,8 +154,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 2c2278813b47..e402b1f12d72 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,8 +223,7 @@ 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: @@ -242,7 +232,7 @@ def _build_model(cls, pool, cr): version = mod.spec_version.replace(".", "")[:2] cls._spec_schema = schema cls._spec_version = version - 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? From e124651b998eac3d2a871b8c68a08ff83adedbf3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Valyi?= Date: Fri, 29 Nov 2024 14:43:30 +0000 Subject: [PATCH 12/12] oca-port: blacklist PR(s) 3335, 3345 for spec_driven_model --- .oca/oca-port/blacklist/spec_driven_model.json | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .oca/oca-port/blacklist/spec_driven_model.json diff --git a/.oca/oca-port/blacklist/spec_driven_model.json b/.oca/oca-port/blacklist/spec_driven_model.json new file mode 100644 index 000000000000..ebb5ebf7873a --- /dev/null +++ b/.oca/oca-port/blacklist/spec_driven_model.json @@ -0,0 +1,6 @@ +{ + "pull_requests": { + "OCA/l10n-brazil#3335": "done in #3442", + "OCA/l10n-brazil#3345": "dotfiles not ported to 15.0 yet" + } +}