diff --git a/l10n_br_account_nfe/tests/test_nfce_contingency.py b/l10n_br_account_nfe/tests/test_nfce_contingency.py index 7d074f4a4805..a66c7e12c076 100644 --- a/l10n_br_account_nfe/tests/test_nfce_contingency.py +++ b/l10n_br_account_nfe/tests/test_nfce_contingency.py @@ -8,8 +8,6 @@ class TestAccountNFCeContingency(TransactionCase): @classmethod def setUpClass(cls): super().setUpClass() - # this hook is required to test l10n_br_account_nfe alone: - cls.env["spec.mixin.nfe"]._register_hook() cls.document_id = cls.env.ref("l10n_br_nfe.demo_nfce_same_state") cls.prepare_account_move_nfce() diff --git a/l10n_br_nfe/hooks.py b/l10n_br_nfe/hooks.py index b6e8745b0167..e0708f83882f 100644 --- a/l10n_br_nfe/hooks.py +++ b/l10n_br_nfe/hooks.py @@ -14,7 +14,6 @@ def post_init_hook(cr, registry): env = api.Environment(cr, SUPERUSER_ID, {}) - env["nfe.40.infnfe"]._register_hook() cr.execute("select demo from ir_module_module where name='l10n_br_nfe';") is_demo = cr.fetchone()[0] if is_demo: @@ -37,7 +36,7 @@ def post_init_hook(cr, registry): nfe = ( env["nfe.40.infnfe"] .with_context(tracking_disable=True, edoc_type="in") - .build_from_binding(binding.NFe.infNFe) + .build_from_binding("nfe", "40", binding.NFe.infNFe) ) _logger.info(nfe.nfe40_emit.nfe40_CNPJ) except ValidationError: diff --git a/l10n_br_nfe/models/__init__.py b/l10n_br_nfe/models/__init__.py index 39dcb356be90..d57ab0b103e9 100644 --- a/l10n_br_nfe/models/__init__.py +++ b/l10n_br_nfe/models/__init__.py @@ -20,3 +20,6 @@ from . import invalidate_number from . import dfe from . import mde + +spec_schema = "nfe" +spec_version = "40" diff --git a/l10n_br_nfe/models/document.py b/l10n_br_nfe/models/document.py index eb4b969bae05..a957aaec08f4 100644 --- a/l10n_br_nfe/models/document.py +++ b/l10n_br_nfe/models/document.py @@ -78,24 +78,19 @@ 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" - _nfe_search_keys = ["nfe40_Id"] + _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" # all m2o at this level will be stacked even if not required: - _force_stack_paths = ( + _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: INFNFE_TREE = """ @@ -676,7 +671,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") @@ -684,21 +679,23 @@ 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 - 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 - not in self._stacking_points.keys() + not in self._get_stacking_points().keys() ] sub_tag_read = self.read(fields)[0] 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 @@ -899,11 +896,11 @@ def _serialize(self, edocs): ): record.flush_model() self.env.invalidate_all() - inf_nfe = record.export_ds()[0] + inf_nfe = record._build_binding("nfe", "40") inf_nfe_supl = None if record.nfe40_infNFeSupl: - inf_nfe_supl = record.nfe40_infNFeSupl.export_ds()[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) @@ -1072,9 +1069,13 @@ def _exec_after_SITUACAO_EDOC_AUTORIZADA(self, old_state, new_state): return super()._exec_after_SITUACAO_EDOC_AUTORIZADA(old_state, new_state) def _generate_key(self): - for record in self.filtered(filter_processador_edoc_nfe): - date = fields.Datetime.context_timestamp(record, record.document_date) + if self.document_type_id.code not in [ + MODELO_FISCAL_NFE, + MODELO_FISCAL_NFCE, + ]: + return super()._generate_key() + for record in self.filtered(filter_processador_edoc_nfe): required_fields_gen_edoc = [] if not record.company_cnpj_cpf: required_fields_gen_edoc.append("CNPJ/CPF") @@ -1092,6 +1093,7 @@ def _generate_key(self): _("To Generate EDoc Key, you need to fill the %s field.") % field ) + date = fields.Datetime.context_timestamp(record, record.document_date) chave_edoc = ChaveEdoc( ano_mes=date.strftime("%y%m").zfill(4), cnpj_cpf_emitente=record.company_cnpj_cpf, @@ -1350,7 +1352,7 @@ def import_binding_nfe(self, binding, edoc_type="out"): document = ( self.env["nfe.40.infnfe"] .with_context(tracking_disable=True, edoc_type=edoc_type, dry_run=False) - .build_from_binding(binding.NFe.infNFe) + .build_from_binding("nfe", "40", binding.NFe.infNFe) ) if edoc_type == "in" and document.company_id.cnpj_cpf != cnpj_cpf.formata( diff --git a/l10n_br_nfe/models/document_line.py b/l10n_br_nfe/models/document_line.py index d11fd09cb72b..d95ee930c9d1 100644 --- a/l10n_br_nfe/models/document_line.py +++ b/l10n_br_nfe/models/document_line.py @@ -70,17 +70,12 @@ 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 = {} + + _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: - _force_stack_paths = ("det.imposto.",) - _stack_skip = ("nfe40_det_infNFe_id",) + _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 = """ @@ -530,7 +525,7 @@ def _export_fields_nfe_40_icms(self, xsd_fields, class_obj, export_dict): .replace("ICMS", "Icms") .replace("IcmsSN", "Icmssn") ) - binding_module = sys.modules[self._binding_module] + binding_module = sys.modules[self._get_spec_property("binding_module")] # Tnfe.InfNfe.Det.Imposto.Icms.Icms00 # see https://stackoverflow.com/questions/31174295/ # getattr-and-setattr-on-nested-subobjects-chained-properties diff --git a/l10n_br_nfe/models/document_related.py b/l10n_br_nfe/models/document_related.py index ad5e8b3e1124..6c3f0881e9a3 100644 --- a/l10n_br_nfe/models/document_related.py +++ b/l10n_br_nfe/models/document_related.py @@ -20,15 +20,11 @@ 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_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 85590853eec7..1b875ea694b4 100644 --- a/l10n_br_nfe/models/document_supplement.py +++ b/l10n_br_nfe/models/document_supplement.py @@ -9,10 +9,6 @@ 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_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_import.py b/l10n_br_nfe/tests/test_nfe_import.py index 8d9557b4d2cd..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", @@ -32,7 +27,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 +46,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_serialize.py b/l10n_br_nfe/tests/test_nfe_serialize.py index 165674b23a12..59efce0f7894 100644 --- a/l10n_br_nfe/tests/test_nfe_serialize.py +++ b/l10n_br_nfe/tests/test_nfe_serialize.py @@ -18,7 +18,6 @@ class TestNFeExport(TransactionCase): def setUp(self, nfe_list): super().setUp() - self.env["spec.mixin.nfe"]._register_hook() self.nfe_list = nfe_list for nfe_data in self.nfe_list: nfe = self.env.ref(nfe_data["record_ref"]) @@ -39,6 +38,7 @@ def prepare_test_nfe(self, nfe): line._onchange_fiscal_operation_line_id() nfe._compute_fiscal_amount() + nfe._register_hook() # required in v16 for next statement nfe.nfe40_detPag = [ (5, 0, 0), ( diff --git a/l10n_br_nfe/tests/test_nfe_structure.py b/l10n_br_nfe/tests/test_nfe_structure.py index 7fd77b0e8d39..20ddd9d62fb4 100644 --- a/l10n_br_nfe/tests/test_nfe_structure.py +++ b/l10n_br_nfe/tests/test_nfe_structure.py @@ -16,7 +16,6 @@ class NFeStructure(TransactionCase): @classmethod def setUpClass(cls): super().setUpClass() - cls.env["spec.mixin.nfe"]._register_hook() @classmethod def get_stacked_tree(cls, klass): @@ -26,11 +25,25 @@ 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) + 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 + ) 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(".") @@ -112,16 +125,19 @@ def test_doc_stacking_points(self): "nfe40_ide", "nfe40_infAdic", "nfe40_pag", - "nfe40_refECF", - "nfe40_refNF", - "nfe40_refNFP", "nfe40_retTrib", "nfe40_total", "nfe40_transp", "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): @@ -157,7 +173,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) 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 67% rename from l10n_br_nfe_spec/models/spec_models.py rename to l10n_br_nfe_spec/models/spec_mixin.py index dd9206bc853d..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" - _binding_module = "nfelib.nfe.bindings.v4_0.leiaute_nfe_v4_00" - _spec_tab_name = "NFe" + _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" brl_currency_id = fields.Many2one( comodel_name="res.currency", diff --git a/l10n_br_nfe_spec/tests/test_nfe_import.py b/l10n_br_nfe_spec/tests/test_nfe_import.py index 34cf2e9bcd9b..3d3714f51c4e 100644 --- a/l10n_br_nfe_spec/tests/test_nfe_import.py +++ b/l10n_br_nfe_spec/tests/test_nfe_import.py @@ -15,7 +15,7 @@ from odoo.tests import TransactionCase from odoo.tools import OrderedSet -from ..models import spec_models +from ..models import spec_mixin tz_datetime = re.compile(r".*[-+]0[0-9]:00$") @@ -38,10 +38,7 @@ def build_attrs_fake(self, node, create_m2o=False): value = getattr(node, fname) if value is None: continue - key = "{}{}".format( - self._field_prefix, - fspec.metadata.get("name", fname), - ) + key = f"nfe40_{fspec.metadata.get('name', fname)}" if ( fspec.type == str or not any(["." in str(i) for i in fspec.type.__args__]) ) and not str(fspec.type).startswith("typing.List"): @@ -66,12 +63,8 @@ def build_attrs_fake(self, node, create_m2o=False): key = fields[key]["related"][0] comodel_name = fields[key]["relation"] else: - clean_type = binding_type.lower() # TODO double check - comodel_name = "{}.{}.{}".format( - self._schema_name, - self._schema_version.replace(".", "")[0:2], - clean_type.split(".")[-1], - ) + clean_type = binding_type.lower() + comodel_name = f"nfe.40.{clean_type.split('.')[-1]}" comodel = self.env.get(comodel_name) if comodel is None: # example skip ICMS100 class continue @@ -114,9 +107,9 @@ def match_or_create_m2o_fake(self, comodel, new_value, create_m2o=False): return comodel.new(new_value)._ids[0] -spec_models.NfeSpecMixin.build_fake = build_fake -spec_models.NfeSpecMixin.build_attrs_fake = build_attrs_fake -spec_models.NfeSpecMixin.match_or_create_m2o_fake = match_or_create_m2o_fake +spec_mixin.NfeSpecMixin.build_fake = build_fake +spec_mixin.NfeSpecMixin.build_attrs_fake = build_attrs_fake +spec_mixin.NfeSpecMixin.match_or_create_m2o_fake = match_or_create_m2o_fake # in version 12, 13 and 14, the code above would properly allow loading NFe XMLs @@ -159,6 +152,7 @@ def fields_convert_to_cache(self, value, record, validate=True): def browse(it): return comodel.browse((it and NewId(it),)) + else: browse = comodel.browse # determine the value ids diff --git a/spec_driven_model/__manifest__.py b/spec_driven_model/__manifest__.py index 453cf405a2e7..3ab66f43c2cf 100644 --- a/spec_driven_model/__manifest__.py +++ b/spec_driven_model/__manifest__.py @@ -3,12 +3,11 @@ { "name": "Spec Driven Model", - "summary": """ - Tools for specifications driven mixins (from xsd for instance)""", + "summary": """XML binding for Odoo: XML to Odoo models and models to XML.""", "version": "16.0.1.3.2", "maintainers": ["rvalyi"], "license": "LGPL-3", - "author": "Akretion,Odoo Community Association (OCA)", + "author": "Akretion, Odoo Community Association (OCA)", "website": "https://github.com/OCA/l10n-brazil", "depends": [], "data": [], diff --git a/spec_driven_model/models/spec_export.py b/spec_driven_model/models/spec_export.py index 3ba7e483f294..e29061ec8128 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 @@ -32,7 +33,7 @@ def _get_spec_classes(self, classes=False): for c in set(classes): if c is None: continue - if not c.startswith(f"{self._schema_name}."): + if not c.startswith(f"{self._context['spec_schema']}."): continue # the following filter to fields to show # when several XSD class are injected in the same object @@ -51,7 +52,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() @@ -70,9 +71,9 @@ 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_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: @@ -90,7 +91,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,20 +107,18 @@ 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(): 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": @@ -133,7 +132,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 ) @@ -144,21 +143,21 @@ 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(): - return self._build_generateds( - class_name=self._stacking_points[field_name].comodel_name + if field_name in self._get_stacking_points().keys(): + 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( - class_obj._fields[field_name].comodel_name + field_data = relational_field._build_binding( + class_name=class_obj._fields[field_name].comodel_name ) relational_data.append(field_data) return relational_data @@ -190,10 +189,10 @@ def _export_datetime(self, field_name): ).isoformat("T") ) - def _build_generateds(self, class_name=False): + 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 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,42 +200,27 @@ 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(spec_schema=spec_schema, spec_version=spec_version) + self.env[f"spec.mixin.{spec_schema}"]._register_hook() if not class_name: - if hasattr(self, "_stacked"): - class_name = self._stacked - 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()}_") and "_choice" not in class_obj._fields[i].name ) 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, "_stacked"): - binding_instance = self._build_generateds() - result.append(binding_instance) - return result - - def export_ds(self): # TODO rename export_binding! - self.ensure_one() - return self.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 83ed68d3d682..bcea403c703d 100644 --- a/spec_driven_model/models/spec_import.py +++ b/spec_driven_model/models/spec_import.py @@ -21,17 +21,17 @@ 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. """ @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 - 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 @@ -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. """ + 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.with_context(dry_run=dry_run).build_attrs(node) + attrs = model.build_attrs(node) if dry_run: return model.new(attrs) else: @@ -69,10 +73,8 @@ def _build_attr(self, node, fields, vals, path, attr): value = getattr(node, attr[0]) if value is None or value == []: return False - key = "{}{}".format( - self._field_prefix, - attr[1].metadata.get("name", attr[0]), - ) + prefix = f"{self._spec_prefix()}" + key = f"{prefix}_{attr[1].metadata.get('name', attr[0])}" child_path = f"{path}.{key}" # Is attr a xsd SimpleType or a ComplexType? @@ -119,8 +121,8 @@ def _build_attr(self, node, fields, vals, path, attr): else: clean_type = binding_type.lower() comodel_name = "{}.{}.{}".format( - self._schema_name, - self._schema_version.replace(".", "")[0:2], + self._context["spec_schema"], + self._context["spec_version"].replace(".", "")[0:2], clean_type.split(".")[-1], ) @@ -194,9 +196,10 @@ def _prepare_import_dict( related_many2ones = {} fields = model._fields + field_prefix = f"{self._spec_prefix()}_" for k, v in fields.items(): # select schema choices for a friendly UI: - if k.startswith(f"{self._field_prefix}choice"): + if k.startswith(f"{field_prefix}choice"): for item in v.selection or []: if vals.get(item[0]) not in [None, []]: vals[k] = item[0] @@ -210,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()): @@ -259,7 +262,7 @@ def match_record(self, rec_dict, parent_dict, model=None): if model is None: model = self default_key = [model._rec_name or "name"] - search_keys = "_%s_search_keys" % (self._schema_name) + search_keys = "_%s_search_keys" % (self._context["spec_schema"]) if hasattr(model, search_keys): keys = getattr(model, search_keys) + default_key else: diff --git a/spec_driven_model/models/spec_mixin.py b/spec_driven_model/models/spec_mixin.py index 8839bf6a4a8e..7516f09fa962 100644 --- a/spec_driven_model/models/spec_mixin.py +++ b/spec_driven_model/models/spec_mixin.py @@ -1,6 +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 importlib import import_module + from odoo import api, models from .spec_models import SPEC_MIXIN_MAPPINGS, SpecModel, StackedModel @@ -21,12 +23,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"] - _stacking_points = {} - # _spec_module = 'override.with.your.python.module' - # _binding_module = 'your.pyhthon.binding.module' - # _odoo_module = 'your.odoo_module' - # _field_prefix = 'your_field_prefix_' - # _schema_name = 'your_schema_name' + _is_spec_driven = True def _valid_field_parameter(self, field, name): if name in ( @@ -48,26 +45,68 @@ def _get_concrete_model(self, model_name): else: return self.env.get(model_name) + def _spec_prefix(self, split=False): + """ + Get spec_schema and spec_version from context or from class module + """ + 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): + """ + 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"): + spec_schema, spec_version = self._spec_prefix(split=True) + if not spec_schema: return res - load_key = f"_{self._spec_module}_loaded" - if hasattr(self.env.registry, load_key): # already done for registry + spec_module = self._get_spec_property("odoo_module") + odoo_module = spec_module.split("_spec.")[0].split(".")[-1] + load_key = f"_{spec_module}_loaded" + 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 = [] + 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;""", - (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 = { @@ -76,19 +115,15 @@ 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) + spec_class = StackedModel._odoo_name_to_class(name, spec_module) if spec_class is None: continue - spec_class._module = "fiscal" # TODO use python_module ? + fields = self.env[spec_class._name]._fields rec_name = next( filter( - lambda x: ( - x.startswith(self.env[spec_class._name]._field_prefix) - and "_choice" not in x - ), - self.env[spec_class._name]._fields, + lambda x: (x.startswith(field_prefix) and "_choice" not in x), + fields, ) ) model_type = type( @@ -97,14 +132,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 = spec_schema + model_type._spec_version = 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 @@ -125,11 +162,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 c7779fd829f0..f15ff0639747 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 @@ -63,7 +64,7 @@ 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 @@ -74,11 +75,12 @@ def _build_model(cls, pool, cr): class as long as the generated spec mixins inherit from some spec.mixin. mixin. """ - schema = None - if hasattr(cls, "_schema_name"): - schema = cls._schema_name - elif pool.get(cls._name) and hasattr(pool[cls._name], "_schema_name"): - schema = pool[cls._name]._schema_name + if hasattr(cls, "_spec_schema"): # when called via _register_hook + schema = cls._spec_schema + else: + mod = import_module(".".join(cls.__module__.split(".")[:-1])) + schema = mod.spec_schema + if schema and "spec.mixin" not in [ c._name for c in pool[f"spec.mixin.{schema}"].__bases__ ]: @@ -111,14 +113,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) @@ -176,7 +173,6 @@ def _setup_fields(self): @classmethod def _map_concrete(cls, dbname, key, target, quiet=False): - # TODO bookkeep according to a key to allow multiple injection contexts if not quiet: _logger.debug(f"{key} ---> {target}") global SPEC_MIXIN_MAPPINGS @@ -212,8 +208,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,40 +222,59 @@ 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): + if hasattr(cls, "_spec_schema"): # when called via _register_hook + schema = cls._spec_schema + version = cls._spec_version.replace(".", "")[:2] + else: + mod = import_module(".".join(cls.__module__.split(".")[:-1])) + schema = mod.spec_schema + version = mod.spec_version.replace(".", "")[:2] + spec_prefix = f"{schema}{version}" + setattr(cls, f"_{spec_prefix}_stacking_points", {}) + stacking_settings = { + "odoo_module": getattr(cls, f"_{spec_prefix}_odoo_module"), # TODO inherit? + "stacking_mixin": getattr(cls, f"_{spec_prefix}_stacking_mixin"), + "stacking_points": getattr(cls, f"_{spec_prefix}_stacking_points"), + "stacking_skip_paths": getattr( + cls, f"_{spec_prefix}_stacking_skip_paths", [] + ), + "stacking_force_paths": getattr( + cls, f"_{spec_prefix}_stacking_force_paths", [] + ), + } # inject all stacked m2o as inherited classes - if cls._stacked: - _logger.info(f"building StackedModel {cls._name} {cls}") - node = cls._odoo_name_to_class(cls._stacked, cls._spec_module) - env = api.Environment(cr, SUPERUSER_ID, {}) - for kind, klass, _path, _field_path, _child_concrete in cls._visit_stack( - env, node - ): - if kind == "stacked" and klass not in cls.__bases__: - cls.__bases__ = (klass,) + cls.__bases__ + _logger.info(f"building StackedModel {cls._name} {cls}") + node = cls._odoo_name_to_class( + 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 name in type(self)._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 - 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,8 +286,8 @@ 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] - SpecModel._map_concrete(env.cr.dbname, node._name, cls._name, quiet=True) + path = stacking_settings["stacking_mixin"].split(".")[-1] + cls._map_concrete(env.cr.dbname, node._name, cls._name, quiet=True) yield "stacked", node, path, None, None fields = OrderedDict() @@ -294,14 +310,19 @@ 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["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 @@ -309,7 +330,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 +340,9 @@ def _visit_stack(cls, env, node, path=None): # then we will STACK the child in the current class child._stack_path = path child_path = f"{path}.{field_path}" - cls._stacking_points[name] = env[node._name]._fields.get(name) - yield from cls._visit_stack(env, child, child_path) + stacking_settings["stacking_points"][name] = env[ + node._name + ]._fields.get(name) + yield from cls._visit_stack(env, child, stacking_settings, child_path) else: yield "many2one", node, path, field_path, child_concrete diff --git a/spec_driven_model/models/spec_view.py b/spec_driven_model/models/spec_view.py index 41714292c4cf..0e30b9469663 100644 --- a/spec_driven_model/models/spec_view.py +++ b/spec_driven_model/models/spec_view.py @@ -133,7 +133,7 @@ def _build_spec_fragment(self, container=None): # TODO required only if visible @api.model def build_arch(self, lib_node, view_node, fields, depth=0): - """Creates a view arch from an generateds lib model arch""" + """Creates a view arch from an xsdata lib model arch""" # _logger.info("BUILD ARCH", lib_node) choices = set() wrapper_group = None diff --git a/spec_driven_model/readme/DESCRIPTION.md b/spec_driven_model/readme/DESCRIPTION.md index 61464b1a17e7..9a42784cb900 100644 --- a/spec_driven_model/readme/DESCRIPTION.md +++ b/spec_driven_model/readme/DESCRIPTION.md @@ -1,11 +1,10 @@ ## Intro This module is a databinding framework for Odoo and XML data: it allows -to go from XML to Odoo objects back and forth. This module started with -the [GenerateDS](https://www.davekuhlman.org/generateDS.html) pure -Python databinding framework and is now being migrated to xsdata. So a -good starting point is to read [the xsdata documentation -here](https://xsdata.readthedocs.io/) +to go from XML to Odoo objects back and forth. While having no hard +dependency with it, it has been designed to be used along with xsdata. +So a good starting point is to read [the xsdata documentation +here](https://xsdata.readthedocs.io/). But what if instead of only generating Python structures from XML files you could actually generate full blown Odoo objects or serialize Odoo @@ -13,7 +12,7 @@ objects back to XML? This is what this module is for! First you should generate xsdata Python binding libraries you would generate for your specific XSD grammar, the Brazilian Electronic -Invoicing for instance, or UBL. +Invoicing for instance, or UBL or any XSD files... Second you should generate Odoo abstract mixins for all these pure Python bindings. This can be achieved using @@ -23,10 +22,12 @@ OCA/l10n-brazil/l10n_br_nfe_spec for the Brazilian Electronic Invoicing. ## SpecModel Now that you have generated these Odoo abstract bindings you should tell -Odoo how to use them. For instance you may want that your electronic -invoice abstract model matches the Odoo res.partner object. This is -fairly easy, you mostly need to define an override like: +Odoo how to use them. For instance you may want that your abstract model +for the recipient of the electronic invoice matches the Odoo +`res.partner` object. This is fairly easy, you mostly need to define an +override like: +```python from odoo.addons.spec_driven_model.models import spec_models @@ -35,12 +36,13 @@ fairly easy, you mostly need to define an override like: 'res.partner', 'partner.binding.mixin', ] +``` Notice you should inherit from spec_models.SpecModel and not the usual models.Model. **Field mapping**: You can then define two ways mapping between fields -by overriding fields from Odoo or from the binding and using \_compute= +by overriding fields from Odoo or from the binding using \_compute= , \_inverse= or simply related=. **Relational fields**: simple fields are easily mapped this way. However @@ -65,33 +67,46 @@ Sadly real life XML is a bit more complex than that. Often XML structures are deeply nested just because it makes it easier for XSD schemas to validate them! for instance an electronic invoice line can be a nested structure with lots of tax details and product details. In a -relational model like Odoo however you often want flatter data -structures. This is where StackedModel comes to the rescue! It inherits +relational model like Odoo however you often want flatter data structures +instead. This is where StackedModel comes to the rescue! It inherits from SpecModel and when you inherit from StackedModel you can inherit from all the generated mixins corresponding to the nested XML tags below some tag (here invoice.line.binding.mixin). All the fields corresponding to these XML tag attributes will be collected in your model and the XML -parsing and serialization will happen as expected: +parsing and serialization will happen as expected. +Here is an example inspired from the Brazilian Electronic Invoice where +the schema is called `nfe` and where we use the 2 digits `40` for its +short version: + +```python from odoo.addons.spec_driven_model.models import spec_models class InvoiceLine(spec_models.StackedModel): _inherit = [ 'account.move.line', - 'invoice.line.binding.mixin', + 'nfe.40.det', ] - _stacked = 'invoice.line.binding.mixin' + _nfe40_spec_settings = { + "module": "odoo.addons.l10n_br_nfe_spec.models.v4_0.leiaute_nfe_v4_00", + "stacking_mixin": "nfe.40.det", + "stacking_points": {}, + # all m2o below this level will be stacked even if not required: + "stacking_force_paths": ("det.imposto.",), + "stacking_skip_paths": ("nfe40_det_infNFe_id",), + } +``` All many2one fields that are required in the XSD (xsd_required=True) will get their model stacked automatically and recursively. You can -force non required many2one fields to be stacked using the -\_force_stack_paths attribute. On the contrary, you can avoid some -required many2one fields to be stacked using the stack_skip attribute. +also force non required many2one fields to be stacked using the +`stacking_force_paths` attribute. On the contrary, you can avoid some +required many2one fields to be stacked using the `stacking_skip_paths` attribute. -## Hooks +## Initialization hook Because XSD schemas can define lot's of different models, -spec_driven_model comes with handy hooks that will automatically make -all XSD mixins turn into concrete Odoo model (eg with a table) if you -didn't inject them into existing Odoo models. +spec_driven_model comes with a handy `_register_hook` override (in `spec.mixin`) +that will automatically make all XSD mixins turn into concrete Odoo model +(eg with a table) if you didn't inject them into existing Odoo models already. diff --git a/spec_driven_model/tests/__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/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 9f01b44f760e..51c7d8aa1485 100644 --- a/spec_driven_model/tests/spec_purchase.py +++ b/spec_driven_model/tests/spec_purchase.py @@ -41,10 +41,9 @@ 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_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 2658e8d6a216..7f5b98cf0268 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_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_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_binding(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