From ff49dfbcaf732725e4d6277c97dd06eb2259d099 Mon Sep 17 00:00:00 2001
From: Ivan Kropotkin
Date: Sat, 11 May 2024 16:44:37 +0200
Subject: [PATCH 1/5] :sparkles: sync: make links dependent on project
---
sync/__manifest__.py | 4 ++--
sync/doc/changelog.rst | 6 ++++++
sync/models/sync_link.py | 11 ++++++++---
sync/models/sync_project.py | 9 ++++++++-
sync/tests/test_links.py | 4 +++-
sync/views/sync_link_views.xml | 13 ++++++++++++-
sync/views/sync_project_views.xml | 14 ++++----------
7 files changed, 43 insertions(+), 18 deletions(-)
diff --git a/sync/__manifest__.py b/sync/__manifest__.py
index 6fbd62d9..9a093f7b 100644
--- a/sync/__manifest__.py
+++ b/sync/__manifest__.py
@@ -7,7 +7,7 @@
"name": "Sync ๐ชฌ Studio",
"summary": """Join the Amazing ๐ Community โคต๏ธ""",
"category": "VooDoo โจ Magic",
- "version": "16.0.7.0.0",
+ "version": "16.0.11.0.0",
"application": True,
"author": "Ivan Kropotkin",
"support": "info@odoomagic.com",
@@ -26,8 +26,8 @@
"views/sync_trigger_webhook_views.xml",
"views/sync_trigger_button_views.xml",
"views/sync_task_views.xml",
- "views/sync_project_views.xml",
"views/sync_link_views.xml",
+ "views/sync_project_views.xml",
"data/queue_job_function_data.xml",
],
"assets": {
diff --git a/sync/doc/changelog.rst b/sync/doc/changelog.rst
index 1515164c..6ff2ee99 100644
--- a/sync/doc/changelog.rst
+++ b/sync/doc/changelog.rst
@@ -1,3 +1,9 @@
+`11.0.0`
+-------
+
+- **New:** Use prime numbers for major releases ;-)
+- **Improvement:** make links dependent on project
+
`7.0.0`
-------
diff --git a/sync/models/sync_link.py b/sync/models/sync_link.py
index f711dfc6..704ca036 100644
--- a/sync/models/sync_link.py
+++ b/sync/models/sync_link.py
@@ -1,4 +1,4 @@
-# Copyright 2020 Ivan Yelizariev
+# Copyright 2020,2024 Ivan Yelizariev
# License MIT (https://opensource.org/licenses/MIT).
import logging
@@ -23,6 +23,7 @@ class SyncLink(models.Model):
_description = "Resource Links"
_order = "id desc"
+ project_id = fields.Char("Project")
relation = fields.Char("Relation Name", required=True)
system1 = fields.Char("System 1", required=True)
# index system2 only to make search "Odoo links"
@@ -40,7 +41,7 @@ def _auto_init(self):
self._cr,
"sync_link_refs_uniq_index",
self._table,
- ["relation", "system1", "system2", "ref1", "ref2", "model"],
+ ["project_id", "relation", "system1", "system2", "ref1", "ref2", "model"],
)
return res
@@ -81,6 +82,7 @@ def _set_link_external(
self, relation, external_refs, sync_date=None, allow_many2many=False, model=None
):
vals = self.refs2vals(external_refs)
+ vals["project_id"] = self.env.context.get("sync_project_id")
# Check for existing records
if allow_many2many:
existing = self._search_links_external(relation, external_refs)
@@ -142,7 +144,10 @@ def _search_links_external(
self, relation, external_refs, model=None, make_logs=False
):
vals = self.refs2vals(external_refs)
- domain = [("relation", "=", relation)]
+ domain = [
+ ("relation", "=", relation),
+ ("project_id", "=", self.env.context.get("sync_project_id")),
+ ]
if model:
domain.append(("model", "=", model))
for k, v in vals.items():
diff --git a/sync/models/sync_project.py b/sync/models/sync_project.py
index 91a70c11..d4a8885f 100644
--- a/sync/models/sync_project.py
+++ b/sync/models/sync_project.py
@@ -105,6 +105,8 @@ class SyncProject(models.Model):
job_count = fields.Integer(compute="_compute_job_count")
log_ids = fields.One2many("ir.logging", "sync_project_id")
log_count = fields.Integer(compute="_compute_log_count")
+ link_ids = fields.One2many("sync.link", "project_id")
+ link_count = fields.Integer(compute="_compute_link_count")
def copy(self, default=None):
default = dict(default or {})
@@ -134,6 +136,11 @@ def _compute_log_count(self):
for r in self:
r.log_count = len(r.log_ids)
+ @api.depends("link_ids")
+ def _compute_link_count(self):
+ for r in self:
+ r.link_count = len(r.link_ids)
+
def _compute_triggers(self):
for r in self:
r.trigger_cron_count = len(r.mapped("task_ids.cron_ids"))
@@ -249,7 +256,7 @@ def record2image(record, fname="image_1920"):
)
)
- context = dict(self.env.context, log_function=log)
+ context = dict(self.env.context, log_function=log, sync_project_id=self.id)
env = self.env(context=context)
link_functions = env["sync.link"]._get_eval_context()
MAGIC = AttrDict(
diff --git a/sync/tests/test_links.py b/sync/tests/test_links.py
index f11ca733..8088d890 100644
--- a/sync/tests/test_links.py
+++ b/sync/tests/test_links.py
@@ -1,4 +1,4 @@
-# Copyright 2020 Ivan Yelizariev
+# Copyright 2020,2024 Ivan Yelizariev
# License MIT (https://opensource.org/licenses/MIT).
import uuid
@@ -17,6 +17,8 @@ def generate_ref():
class TestLink(TransactionCase):
def setUp(self):
super(TestLink, self).setUp()
+ project = self.env["sync.project"].create({"name": "Test Project"})
+ self.env = self.env(context=dict(self.env.context, sync_project_id=project.id))
funcs = self.env["sync.link"]._get_eval_context()
self.get_link = funcs["get_link"]
self.set_link = funcs["set_link"]
diff --git a/sync/views/sync_link_views.xml b/sync/views/sync_link_views.xml
index 10e499c9..a050d006 100644
--- a/sync/views/sync_link_views.xml
+++ b/sync/views/sync_link_views.xml
@@ -1,5 +1,5 @@
-
@@ -7,6 +7,7 @@
sync.link
+
@@ -25,6 +26,7 @@
+
@@ -79,6 +81,15 @@
tree,form
+
+ Links
+ sync.link
+ tree,form
+ [('project_id', '=', active_id)]
+
+ {'default_project_id': active_id, 'active_test': False}
+
+
-
@@ -275,6 +253,26 @@
+
+
+
+
+
+
+
+
+
+
+ Hint:
+
+ Documentation
+
+
+
+
diff --git a/sync/views/sync_task_views.xml b/sync/views/sync_task_views.xml
index 9d661c97..823afafd 100644
--- a/sync/views/sync_task_views.xml
+++ b/sync/views/sync_task_views.xml
@@ -1,5 +1,5 @@
-
@@ -68,6 +68,13 @@
Hint: Updating this code won't change the triggers.
Instead, update the gist file.
+
+
+ Documentation
+
From f11be23b4e7cced0340585c6b7f85348e97435b8 Mon Sep 17 00:00:00 2001
From: Ivan Kropotkin
Date: Sat, 11 May 2024 20:45:58 +0200
Subject: [PATCH 3/5] :ambulance: Use Project ID for xmlid namespace
Otherwise we get a mess on making multiple projects or on switching to new gist
---
sync/doc/changelog.rst | 1 +
sync/models/sync_project.py | 9 ++++-----
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/sync/doc/changelog.rst b/sync/doc/changelog.rst
index 08d2165a..9bb8ff88 100644
--- a/sync/doc/changelog.rst
+++ b/sync/doc/changelog.rst
@@ -3,6 +3,7 @@
- **New:** Use prime numbers for major releases ;-)
- **New:** Support data files
+- **Fix:** Use Project ID for xmlid namespace
- **Improvement:** make links dependent on project
`7.0.0`
diff --git a/sync/models/sync_project.py b/sync/models/sync_project.py
index 34cfc5cf..ae26066a 100644
--- a/sync/models/sync_project.py
+++ b/sync/models/sync_project.py
@@ -452,7 +452,6 @@ def magic_upgrade(self):
raise UserError(_("Please provide url to the gist page"))
gist_content = fetch_gist_data(self.source_url)
- gist_id = gist_content["id"]
gist_files = {}
for file_name, file_info in gist_content["files"].items():
gist_files[file_name] = file_info["content"]
@@ -496,7 +495,7 @@ def magic_upgrade(self):
"project_id": self.id,
}
self.env[model]._create_or_update_by_xmlid(
- param_vals, f"PARAM_{key}", namespace=gist_id
+ param_vals, f"PARAM_{key}", namespace=self.id
)
# [CORE] and [LIB]
@@ -536,7 +535,7 @@ def magic_upgrade(self):
"file_content": file_content,
}
self.env["sync.data"]._create_or_update_by_xmlid(
- data_vals, file_name, namespace=gist_id
+ data_vals, file_name, namespace=self.id
)
# Tasks ๐ฆ
@@ -575,7 +574,7 @@ def magic_upgrade(self):
"project_id": self.id,
}
task = self.env["sync.task"]._create_or_update_by_xmlid(
- task_vals, task_technical_name, namespace=gist_id
+ task_vals, task_technical_name, namespace=self.id
)
def create_trigger(model, data):
@@ -585,7 +584,7 @@ def create_trigger(model, data):
trigger_name=data["name"],
)
return self.env[model]._create_or_update_by_xmlid(
- vals, data["name"], namespace=gist_id
+ vals, data["name"], namespace=self.id
)
# Create/Update triggers
From cb2605089fa960fd424ba785b01a10b173cc5095 Mon Sep 17 00:00:00 2001
From: Ivan Kropotkin
Date: Sat, 11 May 2024 20:52:56 +0200
Subject: [PATCH 4/5] :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."
+ )
From 067207fc1ee914e4ca6d861d894eda139612adb9 Mon Sep 17 00:00:00 2001
From: Ivan Kropotkin
Date: Sat, 11 May 2024 20:57:00 +0200
Subject: [PATCH 5/5] :rainbow: rename context key
WARNING devel odoo.addons.queue_job.utils: `test_queue_job_no_delay` ctx key
found. NO JOB scheduled. Note that this key is deprecated: please use
`queue_job__no_delay`
---
sync/models/sync_task.py | 2 +-
sync/tests/test_trigger_db.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/sync/models/sync_task.py b/sync/models/sync_task.py
index 6be3e04c..09306441 100644
--- a/sync/models/sync_task.py
+++ b/sync/models/sync_task.py
@@ -125,7 +125,7 @@ def start(
queue_job_or_result = run(
job, trigger._sync_handler, args, raise_on_error=raise_on_error
)
- if with_delay and not self.env.context.get("test_queue_job_no_delay"):
+ if with_delay and not self.env.context.get("queue_job__no_delay"):
job.queue_job_id = queue_job_or_result.db_record()
return job
else:
diff --git a/sync/tests/test_trigger_db.py b/sync/tests/test_trigger_db.py
index fcea7f23..ae45379e 100644
--- a/sync/tests/test_trigger_db.py
+++ b/sync/tests/test_trigger_db.py
@@ -17,7 +17,7 @@ def setUp(self):
self.env = self.env(
context=dict(
self.env.context,
- test_queue_job_no_delay=True, # no jobs thanks
+ queue_job__no_delay=True, # no jobs thanks
)
)
funcs = self.env["sync.link"]._get_eval_context()