Skip to content

Commit

Permalink
⚡ Sync Order
Browse files Browse the repository at this point in the history
  • Loading branch information
yelizariev committed May 25, 2024
1 parent f42baed commit db6f799
Show file tree
Hide file tree
Showing 17 changed files with 430 additions and 146 deletions.
2 changes: 1 addition & 1 deletion sync/README.rst
Original file line number Diff line number Diff line change
@@ -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
Expand Down
14 changes: 6 additions & 8 deletions sync/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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": "[email protected]",
"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",
Expand All @@ -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",
Expand All @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions sync/doc/MAGIC.rst
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ Libs
* ``MAGIC.timezone``
* ``MAGIC.b64encode``
* ``MAGIC.b64decode``
* ``MAGIC.sha256``

Tools
=====
Expand All @@ -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
==========
Expand Down
14 changes: 14 additions & 0 deletions sync/doc/changelog.rst
Original file line number Diff line number Diff line change
@@ -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`
-------

Expand Down
2 changes: 1 addition & 1 deletion sync/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
101 changes: 26 additions & 75 deletions sync/models/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -66,34 +66,35 @@ 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:
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)
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,
)
Expand All @@ -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
12 changes: 0 additions & 12 deletions sync/models/ir_fields.py

This file was deleted.

24 changes: 14 additions & 10 deletions sync/models/sync_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

import yaml

from odoo import fields, models
from odoo import api, fields, models


class SyncData(models.Model):
Expand All @@ -17,29 +17,33 @@ 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 []

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
2 changes: 1 addition & 1 deletion sync/models/sync_job.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
55 changes: 55 additions & 0 deletions sync/models/sync_order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Copyright 2024 Ivan Yelizariev <https://twitter.com/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
Loading

0 comments on commit db6f799

Please sign in to comment.