From db6f79949238ba8735017e2825b71e241d345d52 Mon Sep 17 00:00:00 2001 From: Ivan Kropotkin Date: Sun, 19 May 2024 15:47:45 +0200 Subject: [PATCH] :zap: Sync Order --- sync/README.rst | 2 +- sync/__manifest__.py | 14 ++- sync/doc/MAGIC.rst | 3 + sync/doc/changelog.rst | 14 +++ sync/models/__init__.py | 2 +- sync/models/base.py | 101 ++++++--------------- sync/models/ir_fields.py | 12 --- sync/models/sync_data.py | 24 ++--- sync/models/sync_job.py | 2 +- sync/models/sync_order.py | 55 ++++++++++++ sync/models/sync_project.py | 142 ++++++++++++++++++++++++++---- sync/models/sync_task.py | 37 +++++++- sync/models/sync_trigger_mixin.py | 9 +- sync/security/ir.model.access.csv | 3 + sync/tests/test_property.py | 20 +++-- sync/views/sync_order_views.xml | 91 +++++++++++++++++++ sync/views/sync_project_views.xml | 45 ++++++++-- 17 files changed, 430 insertions(+), 146 deletions(-) delete mode 100644 sync/models/ir_fields.py create mode 100644 sync/models/sync_order.py create mode 100644 sync/views/sync_order_views.xml 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..e6426277 100644 --- a/sync/doc/MAGIC.rst +++ b/sync/doc/MAGIC.rst @@ -69,6 +69,7 @@ Libs * ``MAGIC.timezone`` * ``MAGIC.b64encode`` * ``MAGIC.b64decode`` +* ``MAGIC.sha256`` Tools ===== @@ -80,6 +81,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..161ad525 100644 --- a/sync/doc/changelog.rst +++ b/sync/doc/changelog.rst @@ -1,3 +1,17 @@ +`13.0.0` +------- + +- **Fix:** Use `__sync.` for xmlid namespace to avoid data loss on module update. +- **Fix:** Use task ID in xmlid namespace for the task triggers. +- **Fix:** Keep job records (and their logs) on task deletion. +- **New:** Add *Sync Order* — advanced manual trigger with blackjack, partners list, text input, etc. +- **New:** Support `data.markdown` for custom documentation in the `DATA.🐫` tab. +- **New:** Add `MAGIC.group_by_lang` to eval context. +- **New:** Add dynamic Setting update via `PARAMS._update_param`. +- **New:** Add computed field `text` to the model `sync.data`. Usage example in dynamic code: `DATA.restaurant.text`. +- **Improvement:** Add `DATA.*` to the library eval context. +- **Improvement:** Update API for attaching dynamic values: `_set_sync_value`, `_get_sync_value`. No need to use `ir.property`. + `11.0.1` ------- diff --git a/sync/models/__init__.py b/sync/models/__init__.py index 80ed362d..168f4492 100644 --- a/sync/models/__init__.py +++ b/sync/models/__init__.py @@ -11,9 +11,9 @@ 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 -from . import ir_fields from . import sync_link from . import base diff --git a/sync/models/base.py b/sync/models/base.py index 788ebc37..aeef38a0 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. @@ -66,20 +66,9 @@ def _create_or_update_by_xmlid(self, vals, code, namespace="XXX", module="sync") "noupdate": False, } ) - 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"] + def _sync_field_name(self, property_name, property_type): sync_project_id = self.env.context.get("sync_project_id") if not sync_project_id: @@ -87,13 +76,25 @@ def _set_sync_property(self, property_name, property_type, property_value): _("The 'sync_project_id' must be provided in the context.") ) - field_name = "x_sync_%s_%s_%s" % (sync_project_id, property_name, property_type) + return "x_sync_%s_%s_%s" % (sync_project_id, property_name, property_type) + + def _set_sync_value(self, property_name, property_type, property_value): + """ + Set or create a property for the current record. If the field + does not exist, create it dynamically. + + Args: + property_name (str): Name of the property field to set. + property_type (str): Type of the property field. + property_value (Any): The value to assign to the property. + """ + self.ensure_one() + field_name = self._sync_field_name(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, ) @@ -104,73 +105,23 @@ def _set_sync_property(self, property_name, property_type, property_value): { "name": field_name, "ttype": property_type, - "model_id": self.env["ir.model"] - .search([("model", "=", self._name)], limit=1) - .id, + "model_id": self.env["ir.model"]._get_id(self._name), "field_description": property_name.capitalize().replace("_", " "), - "sync_project_id": sync_project_id, # Link to the sync project } ) + self[field_name] = property_value - 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): + def _get_sync_value(self, property_name, property_type): """ - Get the value of a property for the current record. + Get the value of a dynamic field for the current record. Args: property_name (str): Name of the property field to get. + 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), - ("sync_project_id", "=", sync_project_id), - ], - limit=1, - ) - - if not field: + self.ensure_one() + field_name = self._sync_field_name(property_name, property_type) + try: + return self[field_name] + except KeyError: return None - - 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 deleted file mode 100644 index c1c92932..00000000 --- a/sync/models/ir_fields.py +++ /dev/null @@ -1,12 +0,0 @@ -# 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/models/sync_data.py b/sync/models/sync_data.py index e26933b5..d101315b 100644 --- a/sync/models/sync_data.py +++ b/sync/models/sync_data.py @@ -6,7 +6,7 @@ import yaml -from odoo import fields, models +from odoo import api, fields, models class SyncData(models.Model): @@ -17,13 +17,21 @@ class SyncData(models.Model): project_id = fields.Many2one("sync.project", ondelete="cascade") file_name = fields.Char("File Name") file_content = fields.Binary("File Content") + text = fields.Text("Decoded Text", compute="_compute_text") + + @api.depends("file_content") + def _compute_text(self): + for record in self: + if record.file_content: + decoded_content = base64.b64decode(record.file_content) + record.text = decoded_content.decode("utf-8") + else: + record.text = False def csv(self, *args, **kwargs): """Parse CSV file from binary field.""" if self.file_content: - file_content = base64.b64decode(self.file_content) - file_content = file_content.decode("utf-8") - file_like_object = StringIO(file_content) + file_like_object = StringIO(self.text) reader = csv.DictReader(file_like_object, *args, **kwargs) return [row for row in reader] return [] @@ -31,15 +39,11 @@ def csv(self, *args, **kwargs): def json(self): """Parse JSON file from binary field.""" if self.file_content: - file_content = base64.b64decode(self.file_content) - file_content = file_content.decode("utf-8") - return json.loads(file_content) + return json.loads(self.text) return {} def yaml(self): """Parse YAML file from binary field.""" if self.file_content: - file_content = base64.b64decode(self.file_content) - file_content = file_content.decode("utf-8") - return yaml.safe_load(file_content) + return yaml.safe_load(self.text) return None diff --git a/sync/models/sync_job.py b/sync/models/sync_job.py index 5625931d..f6ad2a38 100644 --- a/sync/models/sync_job.py +++ b/sync/models/sync_job.py @@ -31,7 +31,7 @@ class SyncJob(models.Model): trigger_webhook_id = fields.Many2one("sync.trigger.webhook", readonly=True) trigger_button_id = fields.Many2one("sync.trigger.button", readonly=True) task_id = fields.Many2one( - "sync.task", compute="_compute_sync_task_id", store=True, ondelete="cascade" + "sync.task", compute="_compute_sync_task_id", store=True, ondelete="set null" ) project_id = fields.Many2one( "sync.project", related="task_id.project_id", readonly=True diff --git a/sync/models/sync_order.py b/sync/models/sync_order.py new file mode 100644 index 00000000..62348add --- /dev/null +++ b/sync/models/sync_order.py @@ -0,0 +1,55 @@ +# 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"] + _order = "id desc" + + name = fields.Char("Title") + body = fields.Text("Order") + sync_project_id = fields.Many2one("sync.project", related="sync_task_id.project_id") + sync_task_id = fields.Many2one( + "sync.task", + ondelete="cascade", + required=True, + ) + description = fields.Html(related="sync_task_id.sync_order_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"), + ], + default="draft", + ) + + @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_done(self): + self.write({"state": "done"}) + + def action_confirm(self): + self.write({"state": "open"}) + + def action_cancel(self): + self.write({"state": "cancel"}) + + def action_refresh(self): + # Magic + pass diff --git a/sync/models/sync_project.py b/sync/models/sync_project.py index 46b9e7e2..04aef94f 100644 --- a/sync/models/sync_project.py +++ b/sync/models/sync_project.py @@ -4,9 +4,14 @@ # License MIT (https://opensource.org/licenses/MIT). import base64 +import csv +import io import logging import os from datetime import datetime +from hashlib import sha256 +from itertools import groupby +from operator import itemgetter import urllib3 from pytz import timezone @@ -94,6 +99,8 @@ class SyncProject(models.Model): task_ids = fields.One2many("sync.task", "project_id", copy=True) task_count = fields.Integer(compute="_compute_task_count") + task_description = fields.Html(readonly=True) + trigger_cron_count = fields.Integer( compute="_compute_triggers", help="Enabled Crons" ) @@ -103,6 +110,10 @@ class SyncProject(models.Model): trigger_webhook_count = fields.Integer( compute="_compute_triggers", help="Enabled Webhooks" ) + sync_order_ids = fields.One2many( + "sync.order", "sync_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 +121,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 +141,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 +276,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() @@ -294,8 +348,11 @@ def record2image(record, fname="image_1920"): "timezone": timezone, "b64encode": base64.b64encode, "b64decode": base64.b64decode, + "sha256": sha256, "type2str": type2str, "record2image": record2image, + "gen2csv": gen2csv, + "group_by_lang": group_by_lang, "DEFAULT_SERVER_DATETIME_FORMAT": DEFAULT_SERVER_DATETIME_FORMAT, "AttrDict": AttrDict, }, @@ -304,7 +361,21 @@ def record2image(record, fname="image_1920"): for p in self.secret_ids: SECRETS[p.key] = p.value - PARAMS = AttrDict() + def _update_param(key, value): + for p in self.param_ids: + if p.key == key: + p.value = value + return + self.env["sync.project.param"].create( + { + "project_id": self.id, + "key": key, + "value": value, + } + ) + PARAMS[key] = value + + PARAMS = AttrDict(_update_param) for p in self.param_ids: PARAMS[p.key] = p.value @@ -330,15 +401,16 @@ def record2image(record, fname="image_1920"): "SECRETS": SECRETS, "MAGIC": MAGIC, "PARAMS": PARAMS, + "DATA": DATA, } CORE = eval_export(safe_eval__MAGIC, self.core_code, core_eval_context) lib_eval_context = { "MAGIC": MAGIC, "PARAMS": PARAMS, + "DATA": DATA, "CORE": CORE, "WEBHOOKS": WEBHOOKS, - "DATA": DATA, } LIB = eval_export(safe_eval, self.common_code, lib_eval_context) @@ -467,11 +539,16 @@ def magic_upgrade(self): ) # [Documentation] - vals["description"] = ( - compile_markdown_to_html(gist_files.get("README.md")) - if gist_files.get("README.md") - else "

Please add README.md file to place some documentation here

" - ) + for field_name, file_name in ( + ("description", "README.md"), + ("task_description", "tasks.markdown"), + ("data_description", "datas.markdown"), + ): + vals[field_name] = ( + compile_markdown_to_html(gist_files.get(file_name)) + if gist_files.get(file_name) + else f"

Please add {file_name} file to place some documentation here

" + ) # [PARAMS] and [SECRETS] for model, field_name, file_name in ( @@ -512,7 +589,7 @@ def magic_upgrade(self): 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 +651,20 @@ 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 + sync_order_description = gist_files.get( + file_name[: -len(".py")] + ".markdown" + ) + if sync_order_description: + task_vals["sync_order_description"] = compile_markdown_to_html( + sync_order_description + ) + task = self.env["sync.task"]._create_or_update_by_xmlid( task_vals, task_technical_name, namespace=self.id ) @@ -585,7 +676,7 @@ def create_trigger(model, data): trigger_name=data["name"], ) return self.env[model]._create_or_update_by_xmlid( - vals, data["name"], namespace=self.id + vals, data["name"], namespace=f"p{self.id}t{task.id}" ) # Create/Update triggers @@ -596,20 +687,37 @@ 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." + model = self._get_model(data["model"]) + if data.get("trigger_fields"): + trigger_field_ids = [] + for f in data.pop("trigger_fields").split(","): + ff = self.env["ir.model.fields"]._get(model.model, f) + trigger_field_ids.append(ff.id) + data["trigger_field_ids"] = [(6, 0, trigger_field_ids)] + + for field_name in ("filter_pre_domain", "filter_domain"): + if data.get(field_name): + data[field_name] = data[field_name].replace( + "{TASK_ID}", str(task.id) ) - % 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 + ) + return model + class SyncProjectParamMixin(models.AbstractModel): diff --git a/sync/models/sync_task.py b/sync/models/sync_task.py index 09306441..4f4be285 100644 --- a/sync/models/sync_task.py +++ b/sync/models/sync_task.py @@ -26,6 +26,8 @@ class SyncTask(models.Model): code = fields.Text("Code") code_check = fields.Text("Syntax check", store=False, readonly=True) active = fields.Boolean(default=True) + sync_order_model_id = fields.Many2one("ir.model") + sync_order_description = fields.Html(readonly=True) magic_button = fields.Char() button_ids = fields.One2many( "sync.trigger.button", "sync_task_id", string="Manual Triggers", copy=True @@ -35,6 +37,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", @@ -95,7 +100,7 @@ def _compute_active_triggers(self): r.active_webhook_ids = r.with_context(active_test=True).webhook_ids def action_magic_button(self): - # TODO: This should be refactored to delete button_ids + # TODO: This should be refactored, because we use single button per task if not self.button_ids: self.button_ids.create( { @@ -106,6 +111,36 @@ def action_magic_button(self): ) return self.button_ids.start_button() + def _get_current_date_formatted(self): + user_lang = self.env.user.lang or "en_US" + lang = self.env["res.lang"].search([("code", "=", user_lang)], limit=1) + today = fields.Date.context_today(self) + if lang: + date_format = lang.date_format + formatted_date = today.strftime(date_format) + else: + formatted_date = today.strftime("%Y-%m-%d") + + return formatted_date + + def action_super_magic_button(self): + self.ensure_one() + sync_order = self.env["sync.order"].create( + { + "name": self._get_current_date_formatted(), + "sync_task_id": self.id, + } + ) + return { + "name": "Super 🔥 Magic", + "type": "ir.actions.act_window", + "view_type": "form", + "view_mode": "form", + "res_model": "sync.order", + "res_id": sync_order.id, + "target": "self", + } + def start( self, trigger, args=None, with_delay=False, force=False, raise_on_error=True ): 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/security/ir.model.access.csv b/sync/security/ir.model.access.csv index 2ac9e400..fa317b6b 100644 --- a/sync/security/ir.model.access.csv +++ b/sync/security/ir.model.access.csv @@ -5,6 +5,9 @@ access_sync_project_manager,sync.project manager,model_sync_project,sync_group_m access_sync_task_user,sync.task user,model_sync_task,sync_group_user,1,0,0,0 access_sync_task_dev,sync.task dev,model_sync_task,sync_group_dev,1,1,1,1 access_sync_task_manager,sync.task manager,model_sync_task,sync_group_manager,1,1,1,1 +access_sync_order_user,sync.order user,model_sync_order,sync_group_user,1,0,0,0 +access_sync_order_dev,sync.order dev,model_sync_order,sync_group_dev,1,1,1,1 +access_sync_order_manager,sync.order manager,model_sync_order,sync_group_manager,1,1,1,1 access_sync_data_user,sync.data user,model_sync_data,sync_group_user,1,0,0,0 access_sync_data_dev,sync.data dev,model_sync_data,sync_group_dev,1,1,1,1 access_sync_data_manager,sync.data manager,model_sync_data,sync_group_manager,1,1,1,1 diff --git a/sync/tests/test_property.py b/sync/tests/test_property.py index 391810e1..f215cc42 100644 --- a/sync/tests/test_property.py +++ b/sync/tests/test_property.py @@ -14,20 +14,24 @@ def setUp(self): self.partner = self.env["res.partner"].create({"name": "Test Partner"}) def test_basic_types(self): + # Test reading before creating + self.assertFalse(self.company._get_sync_value("test_integer", "integer")) + # 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) + self.company._set_sync_value("test_char", "char", "Hello, World!") + self.company._set_sync_value("test_boolean", "boolean", True) + self.company._set_sync_value("test_integer", "integer", 42) + self.company._set_sync_value("test_float", "float", 3.14159) + self.company.flush_recordset() # 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") + prop_char = self.company._get_sync_value("test_char", "char") + prop_boolean = self.company._get_sync_value("test_boolean", "boolean") + prop_integer = self.company._get_sync_value("test_integer", "integer") + prop_float = self.company._get_sync_value("test_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.") diff --git a/sync/views/sync_order_views.xml b/sync/views/sync_order_views.xml new file mode 100644 index 00000000..1781ffe1 --- /dev/null +++ b/sync/views/sync_order_views.xml @@ -0,0 +1,91 @@ + + + + + sync.order.tree + sync.order + + + + + + + + + + + sync.order.form + sync.order + +
+ +
+
+ +
+
+ + + + + + + + + + + + + +
+ +
+
+
+ + + +
+ +
+
+ + Sync Orders + sync.order + tree,form + [('sync_project_id', '=', active_id)] + +
diff --git a/sync/views/sync_project_views.xml b/sync/views/sync_project_views.xml index 6a04c9f7..48e7b984 100644 --- a/sync/views/sync_project_views.xml +++ b/sync/views/sync_project_views.xml @@ -53,6 +53,18 @@ > +