Skip to content

Commit

Permalink
[IMP] spec_driven_model: multi-schemas compat
Browse files Browse the repository at this point in the history
  • Loading branch information
rvalyi committed Oct 9, 2024
1 parent 304722e commit f9e79ab
Show file tree
Hide file tree
Showing 7 changed files with 118 additions and 79 deletions.
43 changes: 28 additions & 15 deletions spec_driven_model/models/spec_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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():
Expand Down Expand Up @@ -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(
Expand All @@ -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
Expand Down Expand Up @@ -190,20 +192,28 @@ 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
binding constructors. Hence the value can either be simple values or
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

Expand Down Expand Up @@ -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()
10 changes: 7 additions & 3 deletions spec_driven_model/models/spec_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand Down
16 changes: 2 additions & 14 deletions spec_driven_model/models/spec_mixin.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,20 +23,6 @@ class SpecMixin(models.AbstractModel):
_name = "spec.mixin"
_inherit = ["spec.mixin_export", "spec.mixin_import"]

# actually _stacking_points are model and even schema specific
# but the legacy code used it extensively so a first defensive
# action we can take is to use a frozendict so it is readonly
# at least. In the future the whole stacking system should be
# scoped under a given schema and version as in
# https://github.com/OCA/l10n-brazil/pull/3424
_stacking_points = frozendict({})

# _spec_module = 'override.with.your.python.module'
# _binding_module = 'your.pyhthon.binding.module'
# _odoo_module = 'your.odoo_module'
# _field_prefix = 'your_field_prefix_'
# _schema_name = 'your_schema_name'

def _valid_field_parameter(self, field, name):
if name in (
"xsd_type",
Expand Down Expand Up @@ -114,6 +100,8 @@ def _register_hook(self):
"_module": self._odoo_module,
},
)
model_type._schema_name = self._schema_name
model_type._schema_version = self._schema_version
models.MetaModel.module_to_models[self._odoo_module] += [model_type]

# now we init these models properly
Expand Down
77 changes: 52 additions & 25 deletions spec_driven_model/models/spec_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -190,7 +205,6 @@ def spec_module_classes(cls, spec_module):
Cache the list of spec_module classes to save calls to
slow reflection API.
"""

spec_module_attr = f"_spec_cache_{spec_module.replace('.', '_')}"
if not hasattr(cls, spec_module_attr):
setattr(
Expand All @@ -215,8 +229,9 @@ class StackedModel(SpecModel):
By inheriting from StackModel instead, your models.Model can
instead inherit all the mixins that would correspond to the nested xsd
nodes starting from the _stacked node. _stack_skip allows you to avoid
stacking specific nodes.
nodes starting from the stacking_mixin. stacking_skip_paths allows you to avoid
stacking specific nodes while stacking_force_paths will stack many2one
entities even if they are not required.
In Brazil it allows us to have mostly the fiscal
document objects and the fiscal document line object with many details
Expand All @@ -228,24 +243,26 @@ class StackedModel(SpecModel):

_register = False # forces you to inherit StackeModel properly

# define _stacked in your submodel to define the model of the XML tags
# where we should start to
# stack models of nested tags in the same object.
_stacked = False
_stack_path = ""
_stack_skip = ()
# all m2o below these paths will be stacked even if not required:
_force_stack_paths = ()
_stacking_points = {}

@classmethod
def _build_model(cls, pool, cr):
mod = import_module(".".join(cls.__module__.split(".")[:-1]))
if hasattr(cls, "_schema_name"):
schema = cls._schema_name
version = cls._schema_version.replace(".", "")[:2]
else:
mod = import_module(".".join(cls.__module__.split(".")[:-1]))
schema = mod.spec_schema
version = mod.spec_version.replace(".", "")[:2]
spec_prefix = cls._spec_prefix(spec_schema=schema, spec_version=version)
stacking_settings = getattr(cls, "_%s_spec_settings" % (spec_prefix,))
# inject all stacked m2o as inherited classes
_logger.info(f"building StackedModel {cls._name} {cls}")
node = cls._odoo_name_to_class(cls._stacked, cls._spec_module)
node = cls._odoo_name_to_class(
stacking_settings["stacking_mixin"], stacking_settings["module"]
)
env = api.Environment(cr, SUPERUSER_ID, {})
for kind, klass, _path, _field_path, _child_concrete in cls._visit_stack(
env, node
env, node, stacking_settings
):
if kind == "stacked" and klass not in cls.__bases__:
cls.__bases__ = (klass,) + cls.__bases__
Expand All @@ -255,12 +272,15 @@ 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"):
key = f"_{self._spec_prefix(None, self._schema_name, self._schema_version)}_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.
Expand All @@ -272,7 +292,7 @@ def _visit_stack(cls, env, node, path=None):
# https://github.com/OCA/l10n-brazil/pull/1272#issuecomment-821806603
node._description = None
if path is None:
path = cls._stacked.split(".")[-1]
path = stacking_settings["stacking_mixin"].split(".")[-1]
cls._map_concrete(env.cr.dbname, node._name, cls._name, quiet=True)
yield "stacked", node, path, None, None

Expand All @@ -296,10 +316,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)
Expand All @@ -311,7 +336,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
Expand All @@ -320,8 +345,10 @@ def _visit_stack(cls, env, node, path=None):
):
# then we will STACK the child in the current class
child._stack_path = path
child_path = f"{path}.{field_path}"
cls._stacking_points[name] = env[node._name]._fields.get(name)
yield from cls._visit_stack(env, child, child_path)
child_path = "%s.%s" % (path, field_path)
stacking_settings["stacking_points"][name] = env[
node._name
]._fields.get(name)
yield from cls._visit_stack(env, child, stacking_settings, child_path)
else:
yield "many2one", node, path, field_path, child_concrete
3 changes: 3 additions & 0 deletions spec_driven_model/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
from . import test_spec_model

spec_schema = "poxsd"
spec_version = "10"
9 changes: 5 additions & 4 deletions spec_driven_model/tests/spec_purchase.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
Loading

0 comments on commit f9e79ab

Please sign in to comment.