Skip to content

Commit 4df6c52

Browse files
committed
⚡ Sync Order
1 parent f42baed commit 4df6c52

14 files changed

+359
-39
lines changed

sync/README.rst

+1-1
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

+6-8
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

+3
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

+11
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
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+
- **Improvement:** Add `DATA.*` to the library eval context.
11+
112
`11.0.1`
213
-------
314

sync/models/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
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

sync/models/base.py

+1-1
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.

sync/models/sync_job.py

+1-1
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

+55
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="Record",
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

sync/models/sync_project.py

+108-15
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,14 @@
44
# License MIT (https://opensource.org/licenses/MIT).
55

66
import base64
7+
import csv
8+
import io
79
import logging
810
import os
911
from datetime import datetime
12+
from hashlib import sha256
13+
from itertools import groupby
14+
from operator import itemgetter
1015

1116
import urllib3
1217
from pytz import timezone
@@ -94,6 +99,8 @@ class SyncProject(models.Model):
9499

95100
task_ids = fields.One2many("sync.task", "project_id", copy=True)
96101
task_count = fields.Integer(compute="_compute_task_count")
102+
task_description = fields.Html(readonly=True)
103+
97104
trigger_cron_count = fields.Integer(
98105
compute="_compute_triggers", help="Enabled Crons"
99106
)
@@ -103,13 +110,18 @@ class SyncProject(models.Model):
103110
trigger_webhook_count = fields.Integer(
104111
compute="_compute_triggers", help="Enabled Webhooks"
105112
)
113+
sync_order_ids = fields.One2many(
114+
"sync.order", "sync_project_id", string="Sync Orders", copy=True
115+
)
116+
sync_order_count = fields.Integer(compute="_compute_sync_order_count")
106117
job_ids = fields.One2many("sync.job", "project_id")
107118
job_count = fields.Integer(compute="_compute_job_count")
108119
log_ids = fields.One2many("ir.logging", "sync_project_id")
109120
log_count = fields.Integer(compute="_compute_log_count")
110121
link_ids = fields.One2many("sync.link", "project_id")
111122
link_count = fields.Integer(compute="_compute_link_count")
112123
data_ids = fields.One2many("sync.data", "project_id")
124+
data_description = fields.Html(readonly=True)
113125

114126
def copy(self, default=None):
115127
default = dict(default or {})
@@ -129,6 +141,11 @@ def _compute_task_count(self):
129141
for r in self:
130142
r.task_count = len(r.with_context(active_test=False).task_ids)
131143

144+
@api.depends("sync_order_ids")
145+
def _compute_sync_order_count(self):
146+
for r in self:
147+
r.sync_order_count = len(r.sync_order_ids)
148+
132149
@api.depends("job_ids")
133150
def _compute_job_count(self):
134151
for r in self:
@@ -259,6 +276,43 @@ def record2image(record, fname="image_1920"):
259276
)
260277
)
261278

279+
def group_by_lang(partners, default_lang="en_US"):
280+
"""
281+
Yield groups of partners grouped by their language.
282+
283+
:param partners: recordset of res.partner
284+
:return: generator yielding tuples of (lang, partners)
285+
"""
286+
if not partners:
287+
return
288+
289+
# Sort the partners by 'lang' to ensure groupby works correctly
290+
partners = partners.sorted(key=lambda p: p.lang)
291+
292+
# Group the partners by 'lang'
293+
for lang, group in groupby(partners, key=itemgetter("lang")):
294+
partner_group = partners.browse([partner.id for partner in group])
295+
yield lang or default_lang, partner_group
296+
297+
def gen2csv(generator):
298+
# Prepare a StringIO buffer to hold the CSV data
299+
output = io.StringIO()
300+
301+
# Create a CSV writer with quoting enabled
302+
writer = csv.writer(output, quoting=csv.QUOTE_ALL)
303+
304+
# Write rows from the generator
305+
for row in generator:
306+
writer.writerow(row)
307+
308+
# Get the CSV content
309+
csv_content = output.getvalue()
310+
311+
# Close the StringIO buffer
312+
output.close()
313+
314+
return csv_content
315+
262316
context = dict(self.env.context, log_function=log, sync_project_id=self.id)
263317
env = self.env(context=context)
264318
link_functions = env["sync.link"]._get_eval_context()
@@ -294,8 +348,11 @@ def record2image(record, fname="image_1920"):
294348
"timezone": timezone,
295349
"b64encode": base64.b64encode,
296350
"b64decode": base64.b64decode,
351+
"sha256": sha256,
297352
"type2str": type2str,
298353
"record2image": record2image,
354+
"gen2csv": gen2csv,
355+
"group_by_lang": group_by_lang,
299356
"DEFAULT_SERVER_DATETIME_FORMAT": DEFAULT_SERVER_DATETIME_FORMAT,
300357
"AttrDict": AttrDict,
301358
},
@@ -467,11 +524,16 @@ def magic_upgrade(self):
467524
)
468525

469526
# [Documentation]
470-
vals["description"] = (
471-
compile_markdown_to_html(gist_files.get("README.md"))
472-
if gist_files.get("README.md")
473-
else "<h1>Please add README.md file to place some documentation here</h1>"
474-
)
527+
for field_name, file_name in (
528+
("description", "README.md"),
529+
("task_description", "tasks.markdown"),
530+
("data_description", "datas.markdown"),
531+
):
532+
vals[field_name] = (
533+
compile_markdown_to_html(gist_files.get(file_name))
534+
if gist_files.get(file_name)
535+
else f"<h1>Please add {file_name} file to place some documentation here</h1>"
536+
)
475537

476538
# [PARAMS] and [SECRETS]
477539
for model, field_name, file_name in (
@@ -512,7 +574,7 @@ def magic_upgrade(self):
512574
for file_info in gist_content["files"].values():
513575
# e.g. "data.emoji.csv"
514576
file_name = file_info["filename"]
515-
if not file_name.startswith("data."):
577+
if not (file_name.startswith("data.") and file_name != "data.markdown"):
516578
continue
517579
raw_url = file_info["raw_url"]
518580
response = http.request("GET", raw_url)
@@ -574,6 +636,20 @@ def magic_upgrade(self):
574636
else None,
575637
"project_id": self.id,
576638
}
639+
# Sync Order Model
640+
if meta.get("SYNC_ORDER_MODEL"):
641+
model = self._get_model(meta.get("SYNC_ORDER_MODEL"))
642+
task_vals["sync_order_model_id"] = model.id
643+
644+
# Parse docs
645+
sync_order_description = gist_files.get(
646+
file_name[: -len(".py")] + ".markdown"
647+
)
648+
if sync_order_description:
649+
task_vals["sync_order_description"] = compile_markdown_to_html(
650+
sync_order_description
651+
)
652+
577653
task = self.env["sync.task"]._create_or_update_by_xmlid(
578654
task_vals, task_technical_name, namespace=self.id
579655
)
@@ -585,7 +661,7 @@ def create_trigger(model, data):
585661
trigger_name=data["name"],
586662
)
587663
return self.env[model]._create_or_update_by_xmlid(
588-
vals, data["name"], namespace=self.id
664+
vals, data["name"], namespace=f"p{self.id}t{task.id}"
589665
)
590666

591667
# Create/Update triggers
@@ -596,20 +672,37 @@ def create_trigger(model, data):
596672
create_trigger("sync.trigger.webhook", data)
597673

598674
for data in meta.get("DB_TRIGGERS", []):
599-
model_id = self.env["ir.model"]._get(data["model"]).id
600-
if not model_id:
601-
raise ValidationError(
602-
_(
603-
"Model %s is not available. Check if you need to install an extra module first."
675+
model = self._get_model(data["model"])
676+
if data.get("trigger_fields"):
677+
trigger_field_ids = []
678+
for f in data.pop("trigger_fields").split(","):
679+
ff = self.env["ir.model.fields"]._get(model.model, f)
680+
trigger_field_ids.append(ff.id)
681+
data["trigger_field_ids"] = [(6, 0, trigger_field_ids)]
682+
683+
for field_name in ("filter_pre_domain", "filter_domain"):
684+
if data.get(field_name):
685+
data[field_name] = data[field_name].replace(
686+
"{TASK_ID}", str(task.id)
604687
)
605-
% data["model"]
606-
)
688+
607689
create_trigger(
608-
"sync.trigger.automation", dict(data, model_id=model_id, model=None)
690+
"sync.trigger.automation", dict(data, model_id=model.id, model=None)
609691
)
610692

611693
self.update(vals)
612694

695+
def _get_model(self, model_name):
696+
model = self.env["ir.model"]._get(model_name)
697+
if not model:
698+
raise ValidationError(
699+
_(
700+
"Model %s is not available. Check if you need to install an extra module first."
701+
)
702+
% model_name
703+
)
704+
return model
705+
613706

614707
class SyncProjectParamMixin(models.AbstractModel):
615708

0 commit comments

Comments
 (0)