diff --git a/NEWS.rst b/NEWS.rst index cdec236693eb2b38992d54bbf240a0aa7b0e55c1_TkVXUy5yc3Q=..b4e27f823ace4a230c2cca4b0c95d6a4c8019b20_TkVXUy5yc3Q= 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,6 +1,18 @@ Changelog ========= +18.0.5.0.0 +---------- + +Fix Switch converter to call post_hook. + +Xref converter: + +- Allow prefix on Xref converter +- Add option to include module name in messages. Incoming and outgoing message value have the same comportment. + For example, if __converter__ is used as the module, both generated messages and received message will contain __converter__.<name>. + Previously, generated messages would use the module name while received one would not. + 18.0.4.1.0 ---------- diff --git a/__manifest__.py b/__manifest__.py index cdec236693eb2b38992d54bbf240a0aa7b0e55c1_X19tYW5pZmVzdF9fLnB5..b4e27f823ace4a230c2cca4b0c95d6a4c8019b20_X19tYW5pZmVzdF9fLnB5 100644 --- a/__manifest__.py +++ b/__manifest__.py @@ -21,7 +21,7 @@ "name": "Converter", "license": "AGPL-3", "summary": "Convert odoo records to/from plain data structures.", - "version": "18.0.4.1.0", + "version": "18.0.5.0.0", "category": "Hidden", "author": "XCG Consulting", "website": "https://orbeet.io/", diff --git a/models/ir_model_data.py b/models/ir_model_data.py index cdec236693eb2b38992d54bbf240a0aa7b0e55c1_bW9kZWxzL2lyX21vZGVsX2RhdGEucHk=..b4e27f823ace4a230c2cca4b0c95d6a4c8019b20_bW9kZWxzL2lyX21vZGVsX2RhdGEucHk= 100644 --- a/models/ir_model_data.py +++ b/models/ir_model_data.py @@ -38,5 +38,5 @@ _inherit = "ir.model.data" @api.model - def generate_name(self) -> str: + def generate_name(self, prefix: str = "") -> str: """Generate an xref for odoo record; @@ -42,4 +42,5 @@ """Generate an xref for odoo record; + :param prefix: prefix to use before the name. :return: a UUID from a string of 32 hex digit """ @@ -43,7 +44,7 @@ :return: a UUID from a string of 32 hex digit """ - return uuid.uuid4().hex + return prefix + uuid.uuid4().hex @api.model def object_to_module_and_name( @@ -47,9 +48,12 @@ @api.model def object_to_module_and_name( - self, record_set: models.BaseModel, module: str | None = _XREF_IMD_MODULE + self, + record_set: models.BaseModel, + module: str | None = _XREF_IMD_MODULE, + prefix: str = "", ) -> tuple[str, str]: """Retrieve an xref pointing to the specified Odoo record; create one when missing. :param module: Name of the module to use. None to use any name, if no xmlid exists "" will be used as the module name. @@ -51,8 +55,9 @@ ) -> tuple[str, str]: """Retrieve an xref pointing to the specified Odoo record; create one when missing. :param module: Name of the module to use. None to use any name, if no xmlid exists "" will be used as the module name. + :param prefix: prefix to use before the name. :return: tuple module and name """ record_set.ensure_one() @@ -63,6 +68,8 @@ ] if module is not None: domain.append(("module", "=", module)) + if prefix: + domain.append(("name", "=like", f"{prefix}%")) # Find an existing xref. See class docstring for details. imd = self.sudo().search(domain, limit=1) @@ -70,7 +77,7 @@ return imd.module, imd.name # Could not find an existing xref; create one. - name = self.generate_name() + name = self.generate_name(prefix) if module is None: module = "" self.set_xmlid(record_set, name, module) @@ -78,9 +85,12 @@ @api.model def object_to_xmlid( - self, record_set: models.BaseModel, module: str | None = _XREF_IMD_MODULE + self, + record_set: models.BaseModel, + module: str | None = _XREF_IMD_MODULE, + prefix: str = "", ) -> str: """Retrieve an xref pointing to the specified Odoo record; create one when missing. :param module: Name of the module to use. None to use any name, if no xmlid exists "" will be used as the module name. @@ -82,7 +92,8 @@ ) -> str: """Retrieve an xref pointing to the specified Odoo record; create one when missing. :param module: Name of the module to use. None to use any name, if no xmlid exists "" will be used as the module name. + :param prefix: prefix to use before the name. """ return "{0[0]}.{0[1]}".format( @@ -87,6 +98,6 @@ """ return "{0[0]}.{0[1]}".format( - self.object_to_module_and_name(record_set, module) + self.object_to_module_and_name(record_set, module, prefix) ) @api.model diff --git a/switch.py b/switch.py index cdec236693eb2b38992d54bbf240a0aa7b0e55c1_c3dpdGNoLnB5..b4e27f823ace4a230c2cca4b0c95d6a4c8019b20_c3dpdGNoLnB5 100644 --- a/switch.py +++ b/switch.py @@ -22,7 +22,15 @@ from odoo import api, models # type: ignore[import-untyped] -from .base import Context, ContextBuilder, Converter, NewinstanceType, Skip, SkipType +from .base import ( + Context, + ContextBuilder, + Converter, + NewinstanceType, + PostHookConverter, + Skip, + SkipType, +) from .validate import Validation, Validator @@ -26,7 +34,7 @@ from .validate import Validation, Validator -class Switch(Converter): +class Switch(PostHookConverter): """A converter to handle switch cases. A list of converters are provided with a function. The first function to match is used, any function that is None will be used. @@ -157,3 +165,10 @@ if out_cond is None or out_cond(instance): return converter.odoo_datatype(instance) return super().odoo_datatype(instance) + + def post_hook(self, instance: models.BaseModel, message_data): + for _out_cond, in_cond, converter in self._converters: + if in_cond is None or in_cond(message_data): + if hasattr(converter, "post_hook"): + converter.post_hook(instance, message_data) + return diff --git a/tests/test_converters.py b/tests/test_converters.py index cdec236693eb2b38992d54bbf240a0aa7b0e55c1_dGVzdHMvdGVzdF9jb252ZXJ0ZXJzLnB5..b4e27f823ace4a230c2cca4b0c95d6a4c8019b20_dGVzdHMvdGVzdF9jb252ZXJ0ZXJzLnB5 100644 --- a/tests/test_converters.py +++ b/tests/test_converters.py @@ -100,8 +100,8 @@ return Model( # TODO add a schema to test validation { - "id": Xref(), + "id": Xref(include_module_name=True), "country_code": RelationToOne( "country_id", None, KeyField("code", "res.country") ), "name": Field("name"), @@ -104,8 +104,10 @@ "country_code": RelationToOne( "country_id", None, KeyField("code", "res.country") ), "name": Field("name"), - "partner_id": RelationToOne("partner_id", "res.partner", Xref()), + "partner_id": RelationToOne( + "partner_id", "res.partner", Xref(include_module_name=True) + ), }, __type__="test-type", ) diff --git a/tests/test_relation.py b/tests/test_relation.py index cdec236693eb2b38992d54bbf240a0aa7b0e55c1_dGVzdHMvdGVzdF9yZWxhdGlvbi5weQ==..b4e27f823ace4a230c2cca4b0c95d6a4c8019b20_dGVzdHMvdGVzdF9yZWxhdGlvbi5weQ== 100644 --- a/tests/test_relation.py +++ b/tests/test_relation.py @@ -1,7 +1,7 @@ ############################################################################## # # Converter Odoo module -# Copyright © 2021, 2024 XCG Consulting <https://xcg-consulting.fr> +# Copyright © 2021, 2024, 2025 XCG Consulting <https://xcg-consulting.fr> # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as @@ -36,7 +36,9 @@ @classmethod def setUpClass(cls) -> None: super().setUpClass() - cls.converter1 = RelationToOne("company_id", "res.company", Xref(None)) + cls.converter1 = RelationToOne( + "company_id", "res.company", Xref(None, prefix="main_") + ) cls.converter2 = RelationToOne("action_id", "res.company", Xref(None)) cls.converter3 = RelationToOne( "action_id", "res.company", Xref(None), send_empty=False @@ -52,5 +54,5 @@ def test_many2one_from_odoo(self): message = self.converter1.odoo_to_message(self.user_admin) - self.assertEqual(message, "base.main_company") + self.assertEqual(message, "company") message = self.converter1.odoo_to_message(self.user_root) @@ -56,5 +58,5 @@ message = self.converter1.odoo_to_message(self.user_root) - self.assertEqual(message, "base.main_company") + self.assertEqual(message, "company") def test_many2one_skip_from_odoo(self): message = self.converter4.odoo_to_message(self.user_admin) @@ -58,7 +60,7 @@ def test_many2one_skip_from_odoo(self): message = self.converter4.odoo_to_message(self.user_admin) - self.assertEqual(message, "base.main_company") + self.assertEqual(message, "main_company") def test_empty_many2one_from_odoo(self): message = self.converter2.odoo_to_message(self.user_root) diff --git a/xref.py b/xref.py index cdec236693eb2b38992d54bbf240a0aa7b0e55c1_eHJlZi5weQ==..b4e27f823ace4a230c2cca4b0c95d6a4c8019b20_eHJlZi5weQ== 100644 --- a/xref.py +++ b/xref.py @@ -19,7 +19,8 @@ ############################################################################## import logging import os -from typing import Any +import uuid +from typing import Any, Final from odoo import _, api, models # type: ignore[import-untyped] @@ -43,5 +44,9 @@ """ def __init__( - self, module: str | None = _XREF_IMD_MODULE, is_instance_getter: bool = True + self, + module: str | None = _XREF_IMD_MODULE, + is_instance_getter: bool = True, + include_module_name: bool = False, + prefix: str = "", ): @@ -47,4 +52,8 @@ ): + """ + :param prefix: prefix to use in ir.model.data, nor sent nor received. + Used to prevent duplication if received id is too simple. + """ super().__init__() self._module = module self._is_instance_getter = is_instance_getter @@ -48,7 +57,9 @@ super().__init__() self._module = module self._is_instance_getter = is_instance_getter + self._include_module_name: Final[bool] = include_module_name + self._prefix: Final[str] = prefix def odoo_to_message(self, instance: models.BaseModel, ctx: Context = None) -> Any: if not instance: return "" @@ -51,7 +62,7 @@ def odoo_to_message(self, instance: models.BaseModel, ctx: Context = None) -> Any: if not instance: return "" - return instance.env["ir.model.data"].object_to_xmlid( - instance, module=self._module + module, name = instance.env["ir.model.data"].object_to_module_and_name( + instance, self._module, self._prefix ) @@ -57,6 +68,11 @@ ) + if self._prefix is not None: + name = name[len(self._prefix) :] + if self._include_module_name: + return f"{module}.{name}" + return name def get_instance( self, odoo_env: api.Environment, message_data ) -> models.BaseModel | NewinstanceType | None: if self._is_instance_getter: @@ -58,6 +74,7 @@ def get_instance( self, odoo_env: api.Environment, message_data ) -> models.BaseModel | NewinstanceType | None: if self._is_instance_getter: + module, name = self._module_name(message_data) return odoo_env.ref( @@ -63,8 +80,8 @@ return odoo_env.ref( - ".".join(["" if self._module is None else self._module, message_data]), + f"{module}.{name}", raise_if_not_found=False, ) return None def post_hook(self, instance: models.BaseModel, message_data): # add xmlid to the newly created object @@ -65,7 +82,8 @@ raise_if_not_found=False, ) return None def post_hook(self, instance: models.BaseModel, message_data): # add xmlid to the newly created object + module, name = self._module_name(message_data) instance.env["ir.model.data"].set_xmlid( @@ -71,4 +89,4 @@ instance.env["ir.model.data"].set_xmlid( - instance, message_data, module=self._module, only_when_missing=True + instance, name, module=module, only_when_missing=True ) @@ -73,5 +91,16 @@ ) + def _module_name(self, value: str) -> tuple[str, str]: + """Return module and name depending on options""" + module = "" if self._module is None else self._module + name = value + if self._include_module_name: + module, name = value.split(".", 1) + assert module == self._module + if self._prefix is not None: + name = self._prefix + name + return module, name + @property def is_instance_getter(self) -> bool: return self._is_instance_getter @@ -115,4 +144,16 @@ if not instance: return "" + imds = ( + instance.env["ir.model.data"] + .sudo() + .search( + [ + ("module", "=", self._module), + ("model", "=", instance._name), + ("res_id", "=", instance.id), + ] + ) + ) + ctx = build_context(instance, ctx, self._context) @@ -118,11 +159,4 @@ ctx = build_context(instance, ctx, self._context) - if self._unique_id_field is not None: - name = getattr(instance, self._unique_id_field) - else: - _module, name = instance.env["ir.model.data"].object_to_module_and_name( - instance, self._module - ) - jsonld_id_base_url = ( instance.env["ir.config_parameter"] .sudo() @@ -137,6 +171,7 @@ if self.converter is not None: self._breadcrumb = self.converter.odoo_to_message(instance, ctx) - xref = os.path.join( + # xref does not exist or does not match the jsonld expected format, create it + schema_base = os.path.join( jsonld_id_base_url if self._has_base_url else "", self._breadcrumb if self._breadcrumb is not None else "", @@ -141,4 +176,3 @@ jsonld_id_base_url if self._has_base_url else "", self._breadcrumb if self._breadcrumb is not None else "", - name, ) @@ -144,5 +178,7 @@ ) - instance.env["ir.model.data"].set_xmlid( - instance, xref, module=self._module, only_when_missing=True - ) + if not imds or all(not imd.name.startswith(schema_base) for imd in imds): + if self._unique_id_field is not None: + name = getattr(instance, self._unique_id_field) + else: + name = uuid.uuid4().hex @@ -148,19 +184,8 @@ - # In case of jsonld_id, replace the xref generated by - # `object_to_module_and_name` with jsonld format. - if self._has_base_url: - # Need sudo as typical user does not have the rights on ir.model.data - imd = ( - instance.env["ir.model.data"] - .sudo() - .search( - [ - ("module", "=", self._module), - ("model", "=", instance._name), - ("res_id", "=", instance.id), - ] - ) - ) - imd.write({"name": xref}) - + xref = os.path.join(schema_base, name) + instance.env["ir.model.data"].set_xmlid(instance, xref, module=self._module) + else: + for imd in imds: + if imd.name.startswith(schema_base): + xref = imd.name return xref