Skip to content

Commit

Permalink
⚡ sync: add dynamic fields
Browse files Browse the repository at this point in the history
  • Loading branch information
yelizariev committed May 11, 2024
1 parent f11be23 commit cb26050
Show file tree
Hide file tree
Showing 6 changed files with 171 additions and 4 deletions.
1 change: 1 addition & 0 deletions sync/doc/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
1 change: 1 addition & 0 deletions sync/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
123 changes: 119 additions & 4 deletions sync/models/base.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# Copyright 2020 Ivan Yelizariev <https://twitter.com/yelizariev>
# Copyright 2020,2024 Ivan Yelizariev <https://twitter.com/yelizariev>
# License MIT (https://opensource.org/licenses/MIT).

from odoo import models
from odoo import _, exceptions, models


class Base(models.AbstractModel):
Expand Down Expand Up @@ -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(
{
Expand All @@ -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
12 changes: 12 additions & 0 deletions sync/models/ir_fields.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Copyright 2024 Ivan Yelizariev <https://twitter.com/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",
)
1 change: 1 addition & 0 deletions sync/tests/__init__.py
Original file line number Diff line number Diff line change
@@ -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
37 changes: 37 additions & 0 deletions sync/tests/test_property.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Copyright 2024 Ivan Yelizariev <https://twitter.com/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."
)

0 comments on commit cb26050

Please sign in to comment.