From cb2605089fa960fd424ba785b01a10b173cc5095 Mon Sep 17 00:00:00 2001 From: Ivan Kropotkin Date: Sat, 11 May 2024 20:52:56 +0200 Subject: [PATCH] :zap: sync: add dynamic fields --- sync/doc/changelog.rst | 1 + sync/models/__init__.py | 1 + sync/models/base.py | 123 ++++++++++++++++++++++++++++++++++-- sync/models/ir_fields.py | 12 ++++ sync/tests/__init__.py | 1 + sync/tests/test_property.py | 37 +++++++++++ 6 files changed, 171 insertions(+), 4 deletions(-) create mode 100644 sync/models/ir_fields.py create mode 100644 sync/tests/test_property.py diff --git a/sync/doc/changelog.rst b/sync/doc/changelog.rst index 9bb8ff88..ba973bcc 100644 --- a/sync/doc/changelog.rst +++ b/sync/doc/changelog.rst @@ -4,6 +4,7 @@ - **New:** Use prime numbers for major releases ;-) - **New:** Support data files - **Fix:** Use Project ID for xmlid namespace +- **New:** Support dynamic properties - **Improvement:** make links dependent on project `7.0.0` diff --git a/sync/models/__init__.py b/sync/models/__init__.py index 46411344..80ed362d 100644 --- a/sync/models/__init__.py +++ b/sync/models/__init__.py @@ -14,5 +14,6 @@ from . import ir_logging from . import ir_actions from . import ir_attachment +from . import ir_fields from . import sync_link from . import base diff --git a/sync/models/base.py b/sync/models/base.py index 16079bb6..a895638f 100644 --- a/sync/models/base.py +++ b/sync/models/base.py @@ -1,7 +1,7 @@ -# Copyright 2020 Ivan Yelizariev +# Copyright 2020,2024 Ivan Yelizariev # License MIT (https://opensource.org/licenses/MIT). -from odoo import models +from odoo import _, exceptions, models class Base(models.AbstractModel): @@ -41,14 +41,21 @@ def _create_or_update_by_xmlid(self, vals, code, namespace="XXX", module="sync") data_obj = self.env["ir.model.data"] res_id = data_obj._xmlid_to_res_id(xmlid_full, raise_if_not_found=False) - + record = None if res_id: - # If record exists, update it record = self.browse(res_id) + + if record and record.exists(): record.write(vals) else: # No record found, create a new one record = self.create(vals) + if res_id: + # exceptional case when data record exists, but record is deleted + data_obj.search( + [("module", "=", module), ("name", "=", xmlid_code)] + ).unlink() + # Also create the corresponding ir.model.data record data_obj.create( { @@ -61,3 +68,111 @@ def _create_or_update_by_xmlid(self, vals, code, namespace="XXX", module="sync") ) return record + + def _set_sync_property(self, property_name, property_type, property_value): + """ + Set or create a property for the current record. If the property field + does not exist, create it dynamically. + + Args: + property_name (str): Name of the property field to set. + property_value (Any): The value to assign to the property. + property_type (str): Type of the property field. + """ + Property = self.env["ir.property"] + sync_project_id = self.env.context.get("sync_project_id") + + if not sync_project_id: + raise exceptions.UserError( + _("The 'sync_project_id' must be provided in the context.") + ) + + field_name = "x_sync_%s_%s_%s" % (sync_project_id, property_name, property_type) + field = self.env["ir.model.fields"].search( + [ + ("name", "=", field_name), + ("model", "=", self._name), + ("ttype", "=", property_type), + ("sync_project_id", "=", sync_project_id), + ], + limit=1, + ) + + if not field: + # Dynamically create the field if it does not exist + field = self.env["ir.model.fields"].create( + { + "name": field_name, + "ttype": property_type, + "model_id": self.env["ir.model"] + .search([("model", "=", self._name)], limit=1) + .id, + "field_description": property_name.capitalize().replace("_", " "), + "sync_project_id": sync_project_id, # Link to the sync project + } + ) + + res_id = f"{self._name},{self.id}" + prop = Property.search( + [ + ("name", "=", property_name), + ("res_id", "=", res_id), + ("fields_id", "=", field.id), + ], + limit=1, + ) + + vals = {"type": property_type, "value": property_value} + if prop: + prop.write(vals) + else: + vals.update( + { + "name": property_name, + "fields_id": field.id, + "res_id": res_id, + } + ) + Property.create(vals) + + def _get_sync_property(self, property_name, property_type): + """ + Get the value of a property for the current record. + + Args: + property_name (str): Name of the property field to get. + """ + Property = self.env["ir.property"] + sync_project_id = self.env.context.get("sync_project_id") + + if not sync_project_id: + raise exceptions.UserError( + _("The 'sync_project_id' must be provided in the context.") + ) + + field_name = "x_sync_%s_%s_%s" % (sync_project_id, property_name, property_type) + field = self.env["ir.model.fields"].search( + [ + ("name", "=", field_name), + ("model", "=", self._name), + ("sync_project_id", "=", sync_project_id), + ], + limit=1, + ) + + if not field: + raise exceptions.UserError( + f"Field '{field_name}' not found for the current model '{self._name}'." + ) + + res_id = f"{self._name},{self.id}" + prop = Property.search( + [ + ("name", "=", property_name), + ("res_id", "=", res_id), + ("fields_id", "=", field.id), + ], + limit=1, + ) + + return prop.get_by_record() if prop else None diff --git a/sync/models/ir_fields.py b/sync/models/ir_fields.py new file mode 100644 index 00000000..c1c92932 --- /dev/null +++ b/sync/models/ir_fields.py @@ -0,0 +1,12 @@ +# Copyright 2024 Ivan Yelizariev +# License MIT (https://opensource.org/licenses/MIT). +from odoo import fields, models + + +class IrModelFields(models.Model): + _inherit = "ir.model.fields" + + sync_project_id = fields.Many2one( + "sync.project", + string="Sync Project", + ) diff --git a/sync/tests/__init__.py b/sync/tests/__init__.py index 24f1e441..3b8371af 100644 --- a/sync/tests/__init__.py +++ b/sync/tests/__init__.py @@ -1,5 +1,6 @@ # License MIT (https://opensource.org/licenses/MIT). +from . import test_property from . import test_links from . import test_trigger_db from . import test_default_value diff --git a/sync/tests/test_property.py b/sync/tests/test_property.py new file mode 100644 index 00000000..391810e1 --- /dev/null +++ b/sync/tests/test_property.py @@ -0,0 +1,37 @@ +# Copyright 2024 Ivan Yelizariev +# License MIT (https://opensource.org/licenses/MIT). +from odoo.tests.common import TransactionCase + + +class TestProperty(TransactionCase): + def setUp(self): + super(TestProperty, self).setUp() + self.project = self.env["sync.project"].create({"name": "Test Project"}) + self.env = self.env( + context=dict(self.env.context, sync_project_id=self.project.id) + ) + self.company = self.env.ref("base.main_company") + self.partner = self.env["res.partner"].create({"name": "Test Partner"}) + + def test_basic_types(self): + # Basic types tests included for completeness + self.company._set_sync_property("x_test_prop_char", "char", "Hello, World!") + self.company._set_sync_property("x_test_prop_boolean", "boolean", True) + self.company._set_sync_property("x_test_prop_integer", "integer", 42) + self.company._set_sync_property("x_test_prop_float", "float", 3.14159) + + # Invalidate cache before reading + self.env.cache.invalidate() + + # Retrieval and Assertions + prop_char = self.company._get_sync_property("x_test_prop_char", "char") + prop_boolean = self.company._get_sync_property("x_test_prop_boolean", "boolean") + prop_integer = self.company._get_sync_property("x_test_prop_integer", "integer") + prop_float = self.company._get_sync_property("x_test_prop_float", "float") + + self.assertEqual(prop_char, "Hello, World!", "The char property did not match.") + self.assertEqual(prop_boolean, True, "The boolean property did not match.") + self.assertEqual(prop_integer, 42, "The integer property did not match.") + self.assertAlmostEqual( + prop_float, 3.14159, places=5, msg="The float property did not match." + )