diff --git a/sync/README.rst b/sync/README.rst index 39d00a3b..28fb0c60 100644 --- a/sync/README.rst +++ b/sync/README.rst @@ -1,6 +1,6 @@ .. image:: https://itpp.dev/images/infinity-readme.png :alt: Tested and maintained by IT Projects Labs - :target: https://itpp.dev + :target: https://odoomagic.com .. image:: https://img.shields.io/badge/license-MIT-blue.svg :target: https://opensource.org/licenses/MIT diff --git a/sync/__manifest__.py b/sync/__manifest__.py index bf40e8c9..145ba6d7 100644 --- a/sync/__manifest__.py +++ b/sync/__manifest__.py @@ -7,13 +7,16 @@ "name": "Sync ๐Ÿชฌ Studio", "summary": """Join the Amazing ๐Ÿ˜ Community โคต๏ธ""", "category": "VooDoo โœจ Magic", - "version": "16.0.11.0.1", + "version": "16.0.13.0.0", "application": True, "author": "Ivan Kropotkin", "support": "info@odoomagic.com", "website": "https://sync_studio.t.me/", "license": "Other OSI approved licence", # MIT - "depends": ["base_automation", "mail", "queue_job"], + # The `partner_telegram` dependency is not directly needed, + # but it plays an important role in the **Sync ๐Ÿชฌ Studio** ecosystem + # and is added for the quick onboarding of new **Cyber โœจ Pirates**. + "depends": ["base_automation", "mail", "queue_job", "partner_telegram"], "external_dependencies": {"python": ["markdown", "pyyaml"], "bin": []}, "data": [ "security/sync_groups.xml", @@ -25,6 +28,7 @@ "views/sync_trigger_automation_views.xml", "views/sync_trigger_webhook_views.xml", "views/sync_trigger_button_views.xml", + "views/sync_order_views.xml", "views/sync_task_views.xml", "views/sync_link_views.xml", "views/sync_project_views.xml", @@ -37,12 +41,6 @@ }, "demo": [ "data/sync_project_unittest_demo.xml", - # Obsolete - # "data/sync_project_context_demo.xml", - # "data/sync_project_telegram_demo.xml", - # "data/sync_project_odoo2odoo_demo.xml", - # "data/sync_project_trello_github_demo.xml", - # "data/sync_project_context_demo.xml", ], "qweb": [], "post_load": None, diff --git a/sync/doc/MAGIC.rst b/sync/doc/MAGIC.rst index 092fa23b..dd753432 100644 --- a/sync/doc/MAGIC.rst +++ b/sync/doc/MAGIC.rst @@ -80,6 +80,8 @@ Tools * ``MAGIC.type2str``: get type of the given object * ``MAGIC.DEFAULT_SERVER_DATETIME_FORMAT`` * ``MAGIC.AttrDict``: Extended dictionary that allows for attribute-style access +* ``MAGIC.group_by_lang(partners, default_lang="en_US")``: yields `lang, partners` grouped by lang +* ``MAGIC.gen2csv(generator)``: prepares csv as a string Exceptions ========== diff --git a/sync/doc/changelog.rst b/sync/doc/changelog.rst index 38053cc1..688a2064 100644 --- a/sync/doc/changelog.rst +++ b/sync/doc/changelog.rst @@ -1,3 +1,12 @@ +`13.0.0` +------- + +- **Fix:** use `__sync.` for xmlid namespace to avoid data lose on module update +- **New:** add *Sync Order* โ€” advanced manual trigger with blackjack, partners list, text input, etc. +- **New:** support `data.markdown` for custom documentation at the `DATA.๐Ÿซ` tab +- **New:** add `MAGIC.group_by_lang` to eval context +- **Improvement:** add `DATA.*` to the library eval context + `11.0.1` ------- diff --git a/sync/models/__init__.py b/sync/models/__init__.py index 80ed362d..d6ed5ae3 100644 --- a/sync/models/__init__.py +++ b/sync/models/__init__.py @@ -11,6 +11,7 @@ from . import sync_task from . import sync_job from . import sync_data +from . import sync_order from . import ir_logging from . import ir_actions from . import ir_attachment diff --git a/sync/models/base.py b/sync/models/base.py index 788ebc37..27529f14 100644 --- a/sync/models/base.py +++ b/sync/models/base.py @@ -19,7 +19,7 @@ def search_links(self, relation_name, refs=None): ._search_links_odoo(self, relation_name, refs) ) - def _create_or_update_by_xmlid(self, vals, code, namespace="XXX", module="sync"): + def _create_or_update_by_xmlid(self, vals, code, namespace="XXX", module="__sync"): """ Create or update a record by a dynamically generated XML ID. Warning! The field `noupdate` is ignored, i.e. existing records are always updated. diff --git a/sync/models/sync_order.py b/sync/models/sync_order.py new file mode 100644 index 00000000..4f85b279 --- /dev/null +++ b/sync/models/sync_order.py @@ -0,0 +1,47 @@ +# Copyright 2024 Ivan Yelizariev +from odoo import api, fields, models + + +class SyncOrder(models.Model): + _name = "sync.order" + _description = "Sync Order" + _inherit = ["mail.thread", "mail.activity.mixin"] + + name = fields.Char("Title") + body = fields.Char("Order") + sync_project_id = fields.Many2one("sync.project", ondelete="cascade") + sync_task_id = fields.Many2one( + "sync.task", + ondelete="cascade", + domain="[('project_id', '=', sync_project_id)]", + required=True, + ) + description = fields.Html(related="sync_task_id.description") + record_id = fields.Reference( + string="Record", + selection="_selection_record_id", + help="Optional extra information to perform this task", + ) + + partner_ids = fields.Many2many("res.partner", string="Partners") + state = fields.Selection( + [ + ("draft", "Draft"), + ("open", "In Progress"), + ("done", "Done"), + ("cancel", "Canceled"), + ] + ) + + @api.model + def _selection_record_id(self): + mm = self.sync_task_id.sync_order_model_id + if not mm: + return [] + return [(mm.model, mm.name)] + + def action_confirm(self): + self.write({"state": "open"}) + + def action_cancel(self): + self.write({"state": "cancel"}) diff --git a/sync/models/sync_project.py b/sync/models/sync_project.py index 46b9e7e2..d9cd43f4 100644 --- a/sync/models/sync_project.py +++ b/sync/models/sync_project.py @@ -4,9 +4,13 @@ # License MIT (https://opensource.org/licenses/MIT). import base64 +import csv +import io import logging import os from datetime import datetime +from itertools import groupby +from operator import itemgetter import urllib3 from pytz import timezone @@ -103,6 +107,11 @@ class SyncProject(models.Model): trigger_webhook_count = fields.Integer( compute="_compute_triggers", help="Enabled Webhooks" ) + sync_order_model_id = fields.Many2one("ir.model") + sync_order_ids = fields.One2many( + "sync.order", "project_id", string="Sync Orders", copy=True + ) + sync_order_count = fields.Integer(compute="_compute_sync_order_count") job_ids = fields.One2many("sync.job", "project_id") job_count = fields.Integer(compute="_compute_job_count") log_ids = fields.One2many("ir.logging", "sync_project_id") @@ -110,6 +119,7 @@ class SyncProject(models.Model): link_ids = fields.One2many("sync.link", "project_id") link_count = fields.Integer(compute="_compute_link_count") data_ids = fields.One2many("sync.data", "project_id") + data_description = fields.Html(readonly=True) def copy(self, default=None): default = dict(default or {}) @@ -129,6 +139,11 @@ def _compute_task_count(self): for r in self: r.task_count = len(r.with_context(active_test=False).task_ids) + @api.depends("sync_order_ids") + def _compute_sync_order_count(self): + for r in self: + r.sync_order_count = len(r.sync_order_ids) + @api.depends("job_ids") def _compute_job_count(self): for r in self: @@ -259,6 +274,43 @@ def record2image(record, fname="image_1920"): ) ) + def group_by_lang(partners, default_lang="en_US"): + """ + Yield groups of partners grouped by their language. + + :param partners: recordset of res.partner + :return: generator yielding tuples of (lang, partners) + """ + if not partners: + return + + # Sort the partners by 'lang' to ensure groupby works correctly + partners = partners.sorted(key=lambda p: p.lang) + + # Group the partners by 'lang' + for lang, group in groupby(partners, key=itemgetter("lang")): + partner_group = partners.browse([partner.id for partner in group]) + yield lang or default_lang, partner_group + + def gen2csv(generator): + # Prepare a StringIO buffer to hold the CSV data + output = io.StringIO() + + # Create a CSV writer with quoting enabled + writer = csv.writer(output, quoting=csv.QUOTE_ALL) + + # Write rows from the generator + for row in generator: + writer.writerow(row) + + # Get the CSV content + csv_content = output.getvalue() + + # Close the StringIO buffer + output.close() + + return csv_content + 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() @@ -296,6 +348,8 @@ def record2image(record, fname="image_1920"): "b64decode": base64.b64decode, "type2str": type2str, "record2image": record2image, + "gen2csv": gen2csv, + "group_by_lang": group_by_lang, "DEFAULT_SERVER_DATETIME_FORMAT": DEFAULT_SERVER_DATETIME_FORMAT, "AttrDict": AttrDict, }, @@ -508,11 +562,15 @@ def magic_upgrade(self): vals[field_name] = gist_files[file_name] # [DATA] + file_content = gist_files.get("data.markdown") + if file_content: + vals["data_description"] = compile_markdown_to_html(file_content) + http = urllib3.PoolManager() for file_info in gist_content["files"].values(): # e.g. "data.emoji.csv" file_name = file_info["filename"] - if not file_name.startswith("data."): + if not (file_name.startswith("data.") and file_name != "data.markdown"): continue raw_url = file_info["raw_url"] response = http.request("GET", raw_url) @@ -574,6 +632,18 @@ def magic_upgrade(self): else None, "project_id": self.id, } + # Sync Order Model + if meta.get("SYNC_ORDER_MODEL"): + model = self._get_model(meta.get("SYNC_ORDER_MODEL")) + task_vals["sync_order_model_id"] = model.id + + # Parse docs + description = gist_files.get( + file_name[len("task.") : -len(".py")] + ".markdown" + ) + if description: + task_vals["description"] = compile_markdown_to_html(description) + task = self.env["sync.task"]._create_or_update_by_xmlid( task_vals, task_technical_name, namespace=self.id ) @@ -596,20 +666,23 @@ def create_trigger(model, data): create_trigger("sync.trigger.webhook", data) for data in meta.get("DB_TRIGGERS", []): - model_id = self.env["ir.model"]._get(data["model"]).id - if not model_id: - raise ValidationError( - _( - "Model %s is not available. Check if you need to install an extra module first." - ) - % data["model"] - ) + model = self._get_model(data["model"]) create_trigger( - "sync.trigger.automation", dict(data, model_id=model_id, model=None) + "sync.trigger.automation", dict(data, model_id=model.id, model=None) ) self.update(vals) + def _get_model(self, model_name): + model = self.env["ir.model"]._get(model_name) + if not model: + raise ValidationError( + _( + "Model %s is not available. Check if you need to install an extra module first." + ) + % model_name + ) + class SyncProjectParamMixin(models.AbstractModel): diff --git a/sync/models/sync_task.py b/sync/models/sync_task.py index 09306441..a6296253 100644 --- a/sync/models/sync_task.py +++ b/sync/models/sync_task.py @@ -23,6 +23,7 @@ class SyncTask(models.Model): project_id = fields.Many2one("sync.project", ondelete="cascade") name = fields.Char("Name", help="e.g. Sync Products", required=True) + description = fields.Html(readonly=True) code = fields.Text("Code") code_check = fields.Text("Syntax check", store=False, readonly=True) active = fields.Boolean(default=True) @@ -35,6 +36,9 @@ class SyncTask(models.Model): "sync.trigger.automation", "sync_task_id", copy=True ) webhook_ids = fields.One2many("sync.trigger.webhook", "sync_task_id", copy=True) + # sync_trigger_order_ids = fields.One2many( + # "sync.trigger.order", "sync_task_id", string="Sync Order Triggers", copy=True + # ) active_cron_ids = fields.Many2many( "sync.trigger.cron", string="Enabled Crons", diff --git a/sync/models/sync_trigger_mixin.py b/sync/models/sync_trigger_mixin.py index 0a675bd9..0422bd12 100644 --- a/sync/models/sync_trigger_mixin.py +++ b/sync/models/sync_trigger_mixin.py @@ -39,10 +39,11 @@ def write(self, vals): self._update_name(vals) return res - @api.model - def create(self, vals): - res = super().create(vals) - res._update_name(vals) + @api.model_create_multi + def create(self, vals_list): + for vals in vals_list: + res = super().create(vals) + res._update_name(vals) return res def default_get(self, fields): diff --git a/sync/views/sync_order_views.xml b/sync/views/sync_order_views.xml new file mode 100644 index 00000000..cd141d49 --- /dev/null +++ b/sync/views/sync_order_views.xml @@ -0,0 +1,76 @@ + + + + + sync.order.tree + sync.order + + + + + + + + + + sync.order.form + sync.order + +
+ +
+
+ +
+
+ + + + + + + + + +
+ +
+
+
+ + + +
+ +
+
+ + Button Triggers + sync.trigger.button + tree + [('sync_project_id', '=', active_id)] + +
diff --git a/sync/views/sync_project_views.xml b/sync/views/sync_project_views.xml index 6a04c9f7..ecac451c 100644 --- a/sync/views/sync_project_views.xml +++ b/sync/views/sync_project_views.xml @@ -29,6 +29,18 @@
+