Skip to content

Commit dca7d59

Browse files
committed
⚡ Sync Order
1 parent f42baed commit dca7d59

17 files changed

+430
-146
lines changed

sync/README.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
.. image:: https://itpp.dev/images/infinity-readme.png
22
:alt: Tested and maintained by IT Projects Labs
3-
:target: https://itpp.dev
3+
:target: https://odoomagic.com
44

55
.. image:: https://img.shields.io/badge/license-MIT-blue.svg
66
:target: https://opensource.org/licenses/MIT

sync/__manifest__.py

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@
77
"name": "Sync 🪬 Studio",
88
"summary": """Join the Amazing 😍 Community ⤵️""",
99
"category": "VooDoo ✨ Magic",
10-
"version": "16.0.11.0.1",
10+
"version": "16.0.13.0.0",
1111
"application": True,
1212
"author": "Ivan Kropotkin",
1313
"support": "[email protected]",
1414
"website": "https://sync_studio.t.me/",
1515
"license": "Other OSI approved licence", # MIT
16-
"depends": ["base_automation", "mail", "queue_job"],
16+
# The `partner_telegram` dependency is not directly needed,
17+
# but it plays an important role in the **Sync 🪬 Studio** ecosystem
18+
# and is added for the quick onboarding of new **Cyber ✨ Pirates**.
19+
"depends": ["base_automation", "mail", "queue_job", "partner_telegram"],
1720
"external_dependencies": {"python": ["markdown", "pyyaml"], "bin": []},
1821
"data": [
1922
"security/sync_groups.xml",
@@ -25,6 +28,7 @@
2528
"views/sync_trigger_automation_views.xml",
2629
"views/sync_trigger_webhook_views.xml",
2730
"views/sync_trigger_button_views.xml",
31+
"views/sync_order_views.xml",
2832
"views/sync_task_views.xml",
2933
"views/sync_link_views.xml",
3034
"views/sync_project_views.xml",
@@ -37,12 +41,6 @@
3741
},
3842
"demo": [
3943
"data/sync_project_unittest_demo.xml",
40-
# Obsolete
41-
# "data/sync_project_context_demo.xml",
42-
# "data/sync_project_telegram_demo.xml",
43-
# "data/sync_project_odoo2odoo_demo.xml",
44-
# "data/sync_project_trello_github_demo.xml",
45-
# "data/sync_project_context_demo.xml",
4644
],
4745
"qweb": [],
4846
"post_load": None,

sync/doc/MAGIC.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ Libs
6969
* ``MAGIC.timezone``
7070
* ``MAGIC.b64encode``
7171
* ``MAGIC.b64decode``
72+
* ``MAGIC.sha256``
7273

7374
Tools
7475
=====
@@ -80,6 +81,8 @@ Tools
8081
* ``MAGIC.type2str``: get type of the given object
8182
* ``MAGIC.DEFAULT_SERVER_DATETIME_FORMAT``
8283
* ``MAGIC.AttrDict``: Extended dictionary that allows for attribute-style access
84+
* ``MAGIC.group_by_lang(partners, default_lang="en_US")``: yields `lang, partners` grouped by lang
85+
* ``MAGIC.gen2csv(generator)``: prepares csv as a string
8386

8487
Exceptions
8588
==========

sync/doc/changelog.rst

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,17 @@
1+
`13.0.0`
2+
-------
3+
4+
- **Fix:** Use `__sync.` for xmlid namespace to avoid data loss on module update.
5+
- **Fix:** Use task ID in xmlid namespace for the task triggers.
6+
- **Fix:** Keep job records (and their logs) on task deletion.
7+
- **New:** Add *Sync Order* — advanced manual trigger with blackjack, partners list, text input, etc.
8+
- **New:** Support `data.markdown` for custom documentation in the `DATA.🐫` tab.
9+
- **New:** Add `MAGIC.group_by_lang` to eval context.
10+
- **New:** Add dynamic Setting update via `PARAMS._update_param`.
11+
- **New:** Add computed field `text` to the model `sync.data`. Usage example in dynamic code: `DATA.restaurant.text`.
12+
- **Improvement:** Add `DATA.*` to the library eval context.
13+
- **Improvement:** Update API for attaching dynamic values: `_set_sync_value`, `_get_sync_value`. No need to use `ir.property`.
14+
115
`11.0.1`
216
-------
317

sync/models/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111
from . import sync_task
1212
from . import sync_job
1313
from . import sync_data
14+
from . import sync_order
1415
from . import ir_logging
1516
from . import ir_actions
1617
from . import ir_attachment
17-
from . import ir_fields
1818
from . import sync_link
1919
from . import base

sync/models/base.py

Lines changed: 26 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def search_links(self, relation_name, refs=None):
1919
._search_links_odoo(self, relation_name, refs)
2020
)
2121

22-
def _create_or_update_by_xmlid(self, vals, code, namespace="XXX", module="sync"):
22+
def _create_or_update_by_xmlid(self, vals, code, namespace="XXX", module="__sync"):
2323
"""
2424
Create or update a record by a dynamically generated XML ID.
2525
Warning! The field `noupdate` is ignored, i.e. existing records are always updated.
@@ -66,34 +66,35 @@ def _create_or_update_by_xmlid(self, vals, code, namespace="XXX", module="sync")
6666
"noupdate": False,
6767
}
6868
)
69-
7069
return record
7170

72-
def _set_sync_property(self, property_name, property_type, property_value):
73-
"""
74-
Set or create a property for the current record. If the property field
75-
does not exist, create it dynamically.
76-
77-
Args:
78-
property_name (str): Name of the property field to set.
79-
property_value (Any): The value to assign to the property.
80-
property_type (str): Type of the property field.
81-
"""
82-
Property = self.env["ir.property"]
71+
def _sync_field_name(self, property_name, property_type):
8372
sync_project_id = self.env.context.get("sync_project_id")
8473

8574
if not sync_project_id:
8675
raise exceptions.UserError(
8776
_("The 'sync_project_id' must be provided in the context.")
8877
)
8978

90-
field_name = "x_sync_%s_%s_%s" % (sync_project_id, property_name, property_type)
79+
return "x_sync_%s_%s_%s" % (sync_project_id, property_name, property_type)
80+
81+
def _set_sync_value(self, property_name, property_type, property_value):
82+
"""
83+
Set or create a property for the current record. If the field
84+
does not exist, create it dynamically.
85+
86+
Args:
87+
property_name (str): Name of the property field to set.
88+
property_type (str): Type of the property field.
89+
property_value (Any): The value to assign to the property.
90+
"""
91+
self.ensure_one()
92+
field_name = self._sync_field_name(property_name, property_type)
9193
field = self.env["ir.model.fields"].search(
9294
[
9395
("name", "=", field_name),
9496
("model", "=", self._name),
9597
("ttype", "=", property_type),
96-
("sync_project_id", "=", sync_project_id),
9798
],
9899
limit=1,
99100
)
@@ -104,73 +105,23 @@ def _set_sync_property(self, property_name, property_type, property_value):
104105
{
105106
"name": field_name,
106107
"ttype": property_type,
107-
"model_id": self.env["ir.model"]
108-
.search([("model", "=", self._name)], limit=1)
109-
.id,
108+
"model_id": self.env["ir.model"]._get_id(self._name),
110109
"field_description": property_name.capitalize().replace("_", " "),
111-
"sync_project_id": sync_project_id, # Link to the sync project
112110
}
113111
)
112+
self[field_name] = property_value
114113

115-
res_id = f"{self._name},{self.id}"
116-
prop = Property.search(
117-
[
118-
("name", "=", property_name),
119-
("res_id", "=", res_id),
120-
("fields_id", "=", field.id),
121-
],
122-
limit=1,
123-
)
124-
125-
vals = {"type": property_type, "value": property_value}
126-
if prop:
127-
prop.write(vals)
128-
else:
129-
vals.update(
130-
{
131-
"name": property_name,
132-
"fields_id": field.id,
133-
"res_id": res_id,
134-
}
135-
)
136-
Property.create(vals)
137-
138-
def _get_sync_property(self, property_name, property_type):
114+
def _get_sync_value(self, property_name, property_type):
139115
"""
140-
Get the value of a property for the current record.
116+
Get the value of a dynamic field for the current record.
141117
142118
Args:
143119
property_name (str): Name of the property field to get.
120+
property_type (str): Type of the property field.
144121
"""
145-
Property = self.env["ir.property"]
146-
sync_project_id = self.env.context.get("sync_project_id")
147-
148-
if not sync_project_id:
149-
raise exceptions.UserError(
150-
_("The 'sync_project_id' must be provided in the context.")
151-
)
152-
153-
field_name = "x_sync_%s_%s_%s" % (sync_project_id, property_name, property_type)
154-
field = self.env["ir.model.fields"].search(
155-
[
156-
("name", "=", field_name),
157-
("model", "=", self._name),
158-
("sync_project_id", "=", sync_project_id),
159-
],
160-
limit=1,
161-
)
162-
163-
if not field:
122+
self.ensure_one()
123+
field_name = self._sync_field_name(property_name, property_type)
124+
try:
125+
return self[field_name]
126+
except KeyError:
164127
return None
165-
166-
res_id = f"{self._name},{self.id}"
167-
prop = Property.search(
168-
[
169-
("name", "=", property_name),
170-
("res_id", "=", res_id),
171-
("fields_id", "=", field.id),
172-
],
173-
limit=1,
174-
)
175-
176-
return prop.get_by_record() if prop else None

sync/models/ir_fields.py

Lines changed: 0 additions & 12 deletions
This file was deleted.

sync/models/sync_data.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
import yaml
88

9-
from odoo import fields, models
9+
from odoo import api, fields, models
1010

1111

1212
class SyncData(models.Model):
@@ -17,29 +17,33 @@ class SyncData(models.Model):
1717
project_id = fields.Many2one("sync.project", ondelete="cascade")
1818
file_name = fields.Char("File Name")
1919
file_content = fields.Binary("File Content")
20+
text = fields.Text("Decoded Text", compute="_compute_text")
21+
22+
@api.depends("file_content")
23+
def _compute_text(self):
24+
for record in self:
25+
if record.file_content:
26+
decoded_content = base64.b64decode(record.file_content)
27+
record.text = decoded_content.decode("utf-8")
28+
else:
29+
record.text = False
2030

2131
def csv(self, *args, **kwargs):
2232
"""Parse CSV file from binary field."""
2333
if self.file_content:
24-
file_content = base64.b64decode(self.file_content)
25-
file_content = file_content.decode("utf-8")
26-
file_like_object = StringIO(file_content)
34+
file_like_object = StringIO(self.text)
2735
reader = csv.DictReader(file_like_object, *args, **kwargs)
2836
return [row for row in reader]
2937
return []
3038

3139
def json(self):
3240
"""Parse JSON file from binary field."""
3341
if self.file_content:
34-
file_content = base64.b64decode(self.file_content)
35-
file_content = file_content.decode("utf-8")
36-
return json.loads(file_content)
42+
return json.loads(self.text)
3743
return {}
3844

3945
def yaml(self):
4046
"""Parse YAML file from binary field."""
4147
if self.file_content:
42-
file_content = base64.b64decode(self.file_content)
43-
file_content = file_content.decode("utf-8")
44-
return yaml.safe_load(file_content)
48+
return yaml.safe_load(self.text)
4549
return None

sync/models/sync_job.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ class SyncJob(models.Model):
3131
trigger_webhook_id = fields.Many2one("sync.trigger.webhook", readonly=True)
3232
trigger_button_id = fields.Many2one("sync.trigger.button", readonly=True)
3333
task_id = fields.Many2one(
34-
"sync.task", compute="_compute_sync_task_id", store=True, ondelete="cascade"
34+
"sync.task", compute="_compute_sync_task_id", store=True, ondelete="set null"
3535
)
3636
project_id = fields.Many2one(
3737
"sync.project", related="task_id.project_id", readonly=True

sync/models/sync_order.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Copyright 2024 Ivan Yelizariev <https://twitter.com/yelizariev>
2+
from odoo import api, fields, models
3+
4+
5+
class SyncOrder(models.Model):
6+
_name = "sync.order"
7+
_description = "Sync Order"
8+
_inherit = ["mail.thread", "mail.activity.mixin"]
9+
_order = "id desc"
10+
11+
name = fields.Char("Title")
12+
body = fields.Text("Order")
13+
sync_project_id = fields.Many2one("sync.project", related="sync_task_id.project_id")
14+
sync_task_id = fields.Many2one(
15+
"sync.task",
16+
ondelete="cascade",
17+
required=True,
18+
)
19+
description = fields.Html(related="sync_task_id.sync_order_description")
20+
record_id = fields.Reference(
21+
string="Blackjack",
22+
selection="_selection_record_id",
23+
help="Optional extra information to perform this task",
24+
)
25+
26+
partner_ids = fields.Many2many("res.partner", string="Partners")
27+
state = fields.Selection(
28+
[
29+
("draft", "Draft"),
30+
("open", "In Progress"),
31+
("done", "Done"),
32+
("cancel", "Canceled"),
33+
],
34+
default="draft",
35+
)
36+
37+
@api.model
38+
def _selection_record_id(self):
39+
mm = self.sync_task_id.sync_order_model_id
40+
if not mm:
41+
return []
42+
return [(mm.model, mm.name)]
43+
44+
def action_done(self):
45+
self.write({"state": "done"})
46+
47+
def action_confirm(self):
48+
self.write({"state": "open"})
49+
50+
def action_cancel(self):
51+
self.write({"state": "cancel"})
52+
53+
def action_refresh(self):
54+
# Magic
55+
pass

0 commit comments

Comments
 (0)