Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[14.0][IMP] edi_oca: Add new model edi.configuration #1035

Merged
merged 7 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions edi_oca/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,7 @@ Contributors

* Simone Orsi <[email protected]>
* Enric Tobella <[email protected]>
* Thien Vo <[email protected]>

Maintainers
~~~~~~~~~~~
Expand Down
3 changes: 3 additions & 0 deletions edi_oca/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,13 +30,16 @@
"data/sequence.xml",
"data/job_channel.xml",
"data/job_function.xml",
"data/edi_configuration.xml",
"security/res_groups.xml",
"security/ir_model_access.xml",
"views/edi_backend_views.xml",
"views/edi_backend_type_views.xml",
"views/edi_exchange_record_views.xml",
"views/edi_exchange_type_views.xml",
"views/edi_exchange_type_rule_views.xml",
"views/edi_configuration_views.xml",
"views/edi_configuration_trigger_views.xml",
"views/res_partner.xml",
"views/menuitems.xml",
"templates/exchange_chatter_msg.xml",
Expand Down
42 changes: 42 additions & 0 deletions edi_oca/data/edi_configuration.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<!-- The `ir_action` parameter must be passed in order to use `send_via_email`
Examlple:
<field name="snippet_do">record._edi_send_via_email(ir_action)</field>
-->

<record id="edi_conf_trigger_record_create" model="edi.configuration.trigger">
<field name="name">On record create</field>
<field name="code">on_record_create</field>
<field name="description">Trigger when a record is created</field>
</record>
<record id="edi_conf_trigger_record_write" model="edi.configuration.trigger">
<field name="name">On record write</field>
<field name="code">on_record_write</field>
<field name="description">Trigger when a record is updated</field>
</record>
<!-- TODO: these 2 have to be triggered somehow -->
<record id="edi_conf_trigger_send_via_email" model="edi.configuration.trigger">
<field name="name">Send via email</field>
<field name="code">on_send_via_email</field>
<field name="description">Send record via email TBD</field>
</record>
<record id="edi_conf_trigger_send_via_edi" model="edi.configuration.trigger">
<field name="name">Send via EDI</field>
<field name="code">on_send_via_edi</field>
<field name="description">Send record via EDI TBD</field>
</record>

<record id="edi_conf_send_via_email" model="edi.configuration">
<field name="name">Send Via Email</field>
<field name="active">False</field>
<field name="trigger_id" ref="edi_conf_trigger_send_via_email" />
<field name="snippet_do">record._edi_send_via_email()</field>
</record>
<record id="edi_conf_send_via_edi" model="edi.configuration">
<field name="name">Send Via EDI</field>
<field name="active">False</field>
<field name="trigger_id" ref="edi_conf_trigger_send_via_edi" />
<field name="snippet_do">record._edi_send_via_edi(conf.type_id)</field>
</record>
</odoo>
2 changes: 2 additions & 0 deletions edi_oca/models/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@
from . import edi_exchange_type
from . import edi_exchange_type_rule
from . import edi_id_mixin
from . import edi_configuration_trigger
from . import edi_configuration
32 changes: 25 additions & 7 deletions edi_oca/models/edi_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,29 @@
for backend in self:
backend._check_output_exchange_sync(**kw)

def exchange_generate_send(self, recordset, skip_generate=False, skip_send=False):
"""Generate and send output files for given records.

If both are False, the record will be generated and sent right away
with chained jobs.

If both `skip_generate` and `skip_send` are True, nothing will be done.
:param recordset: edi.exchange.record recordset
:param skip_generate: only send records
:param skip_send: only generate missing output
"""
for rec in recordset:
if not skip_generate and not skip_send:
job1 = rec.delayable().action_exchange_generate()
# Chain send job.
# Raise prio to max to send the record out as fast as possible.
job1.on_done(rec.delayable(priority=0).action_exchange_send())
job1.delay()
elif skip_send:
rec.with_delay().action_exchange_generate()
elif not skip_send:
rec.with_delay(priority=0).action_exchange_send()

Check warning on line 422 in edi_oca/models/edi_backend.py

View check run for this annotation

Codecov / codecov/patch

edi_oca/models/edi_backend.py#L422

Added line #L422 was not covered by tests

def _check_output_exchange_sync(
self, skip_send=False, skip_sent=True, record_ids=None
):
Expand All @@ -415,13 +438,8 @@
"EDI Exchange output sync: found %d new records to process.",
len(new_records),
)
for rec in new_records:
job1 = rec.delayable().action_exchange_generate()
if not skip_send:
# Chain send job.
# Raise prio to max to send the record out as fast as possible.
job1.on_done(rec.delayable(priority=0).action_exchange_send())
job1.delay()
if new_records:
self.exchange_generate_send(new_records, skip_send=skip_send)

if skip_send:
return
Expand Down
200 changes: 200 additions & 0 deletions edi_oca/models/edi_configuration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,200 @@
# Copyright 2024 Camptocamp SA
simahawk marked this conversation as resolved.
Show resolved Hide resolved
# @author Simone Orsi <[email protected]>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).

import datetime

import pytz

from odoo import _, api, exceptions, fields, models
from odoo.tools import DotDict, safe_eval


def date_to_datetime(dt):
"""Convert date to datetime."""
if isinstance(dt, datetime.date):
return datetime.datetime.combine(dt, datetime.datetime.min.time())
return dt

Check warning on line 17 in edi_oca/models/edi_configuration.py

View check run for this annotation

Codecov / codecov/patch

edi_oca/models/edi_configuration.py#L16-L17

Added lines #L16 - L17 were not covered by tests


def to_utc(dt):
"""Convert date or datetime to UTC."""
# Gracefully convert to datetime if needed 1st
return date_to_datetime(dt).astimezone(pytz.UTC)

Check warning on line 23 in edi_oca/models/edi_configuration.py

View check run for this annotation

Codecov / codecov/patch

edi_oca/models/edi_configuration.py#L23

Added line #L23 was not covered by tests


class EdiConfiguration(models.Model):
_name = "edi.configuration"
_description = """
This model is used to configure EDI (Electronic Data Interchange) flows.
It allows users to create their own configurations, which can be tailored
to meet the specific needs of their business processes.
"""

name = fields.Char(string="Name", required=True)
active = fields.Boolean(default=True)
description = fields.Char(help="Describe what the conf is for")
backend_id = fields.Many2one(string="Backend", comodel_name="edi.backend")
# Field `type_id` is not a mandatory field because we will create 2 common confs
# for EDI (`send_via_email` and `send_via_edi`). So type_id is
# a mandatory field will create unwanted data for users when installing this module.
type_id = fields.Many2one(
simahawk marked this conversation as resolved.
Show resolved Hide resolved
string="Exchange Type",
comodel_name="edi.exchange.type",
ondelete="cascade",
auto_join=True,
index=True,
)
model_id = fields.Many2one(
"ir.model",
string="Model",
help="Model the conf applies to. Leave blank to apply for all models",
)
model_name = fields.Char(
related="model_id.model", store=True, string="Model tech name"
)
trigger_id = fields.Many2one(
string="Trigger",
comodel_name="edi.configuration.trigger",
help="Trigger that activates this configuration",
domain="['|', ('model_id', '=', model_id), ('model_id', '=', False)]",
)
trigger = fields.Char(related="trigger_id.code")
snippet_before_do = fields.Text(
string="Snippet Before Do",
help="Snippet to validate the state and collect records to do",
)
snippet_do = fields.Text(
string="Snippet Do",
help="""Used to do something specific here.
Receives: operation, edi_action, vals, old_vals.""",
)

@api.constrains("backend_id", "type_id")
def _constrains_backend(self):
for rec in self:
if rec.type_id.backend_id:
if rec.type_id.backend_id != rec.backend_id:
raise exceptions.ValidationError(

Check warning on line 78 in edi_oca/models/edi_configuration.py

View check run for this annotation

Codecov / codecov/patch

edi_oca/models/edi_configuration.py#L78

Added line #L78 was not covered by tests
_("Backend must match with exchange type's backend!")
)
else:
if rec.type_id.backend_type_id != rec.backend_id.backend_type_id:
raise exceptions.ValidationError(

Check warning on line 83 in edi_oca/models/edi_configuration.py

View check run for this annotation

Codecov / codecov/patch

edi_oca/models/edi_configuration.py#L83

Added line #L83 was not covered by tests
_("Backend type must match with exchange type's backend type!")
)

# TODO: This function is also available in `edi_exchange_template`.
# Consider adding this to util or mixin
def _code_snippet_valued(self, snippet):
thienvh332 marked this conversation as resolved.
Show resolved Hide resolved
snippet = snippet or ""
return bool(
[
not line.startswith("#")
for line in (snippet.splitlines())
if line.strip("")
]
)

@staticmethod
def _date_to_string(dt, utc=True):
if not dt:
return ""

Check warning on line 102 in edi_oca/models/edi_configuration.py

View check run for this annotation

Codecov / codecov/patch

edi_oca/models/edi_configuration.py#L102

Added line #L102 was not covered by tests
if utc:
dt = to_utc(dt)
return fields.Date.to_string(dt)

Check warning on line 105 in edi_oca/models/edi_configuration.py

View check run for this annotation

Codecov / codecov/patch

edi_oca/models/edi_configuration.py#L104-L105

Added lines #L104 - L105 were not covered by tests

@staticmethod
def _datetime_to_string(dt, utc=True):
if not dt:
return ""

Check warning on line 110 in edi_oca/models/edi_configuration.py

View check run for this annotation

Codecov / codecov/patch

edi_oca/models/edi_configuration.py#L110

Added line #L110 was not covered by tests
if utc:
dt = to_utc(dt)
return fields.Datetime.to_string(dt)

Check warning on line 113 in edi_oca/models/edi_configuration.py

View check run for this annotation

Codecov / codecov/patch

edi_oca/models/edi_configuration.py#L112-L113

Added lines #L112 - L113 were not covered by tests

def _time_utils(self):
return {
"datetime": safe_eval.datetime,
"dateutil": safe_eval.dateutil,
"time": safe_eval.time,
"utc_now": fields.Datetime.now(),
"date_to_string": self._date_to_string,
"datetime_to_string": self._datetime_to_string,
"time_to_string": lambda dt: dt.strftime("%H:%M:%S") if dt else "",
"first_of": fields.first,
}

def _get_code_snippet_eval_context(self):
"""Prepare the context used when evaluating python code

:returns: dict -- evaluation context given to safe_eval
"""
ctx = {
"uid": self.env.uid,
"user": self.env.user,
"DotDict": DotDict,
"conf": self,
}
ctx.update(self._time_utils())
return ctx

def _evaluate_code_snippet(self, snippet, **render_values):
if not self._code_snippet_valued(snippet):
return {}

Check warning on line 143 in edi_oca/models/edi_configuration.py

View check run for this annotation

Codecov / codecov/patch

edi_oca/models/edi_configuration.py#L143

Added line #L143 was not covered by tests
eval_ctx = dict(render_values, **self._get_code_snippet_eval_context())
safe_eval.safe_eval(snippet, eval_ctx, mode="exec", nocopy=True)
result = eval_ctx.get("result", {})
if not isinstance(result, dict):
return {}

Check warning on line 148 in edi_oca/models/edi_configuration.py

View check run for this annotation

Codecov / codecov/patch

edi_oca/models/edi_configuration.py#L148

Added line #L148 was not covered by tests
return result

def edi_exec_snippet_before_do(self, record, **kwargs):
self.ensure_one()
# Execute snippet before do
vals_before_do = self._evaluate_code_snippet(
self.snippet_before_do, record=record, **kwargs
)

# Prepare data
vals = {
"todo": vals_before_do.get("todo", True),
"snippet_do_vars": vals_before_do.get("snippet_do_vars", False),
"event_only": vals_before_do.get("event_only", False),
"tracked_fields": vals_before_do.get("tracked_fields", False),
"edi_action": vals_before_do.get("edi_action", False),
}
return vals

def edi_exec_snippet_do(self, record, **kwargs):
self.ensure_one()
if self.trigger == "disabled":
return False

Check warning on line 171 in edi_oca/models/edi_configuration.py

View check run for this annotation

Codecov / codecov/patch

edi_oca/models/edi_configuration.py#L171

Added line #L171 was not covered by tests

old_value = kwargs.get("old_vals", {}).get(record.id, {})
new_value = kwargs.get("vals", {}).get(record.id, {})
vals = {
"todo": True,
"record": record,
"operation": kwargs.get("operation", False),
"edi_action": kwargs.get("edi_action", False),
"old_value": old_value,
"vals": new_value,
}
if self.snippet_before_do:
before_do_vals = self.edi_exec_snippet_before_do(record, **kwargs)
vals.update(before_do_vals)
if vals["todo"]:
return self._evaluate_code_snippet(self.snippet_do, **vals)
return True

Check warning on line 188 in edi_oca/models/edi_configuration.py

View check run for this annotation

Codecov / codecov/patch

edi_oca/models/edi_configuration.py#L188

Added line #L188 was not covered by tests

def edi_get_conf(self, trigger, backend=None):
domain = [("trigger", "=", trigger)]
if backend:
domain.append(("backend_id", "=", backend.id))

Check warning on line 193 in edi_oca/models/edi_configuration.py

View check run for this annotation

Codecov / codecov/patch

edi_oca/models/edi_configuration.py#L193

Added line #L193 was not covered by tests
else:
# We will only get confs that have backend_id = False
# or are equal to self.type_id.backend_id.id
backend_ids = self.mapped("type_id.backend_id.id")
backend_ids.append(False)
domain.append(("backend_id", "in", backend_ids))
return self.filtered_domain(domain)
24 changes: 24 additions & 0 deletions edi_oca/models/edi_configuration_trigger.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Copyright 2024 Camptocamp SA
# @author Simone Orsi <[email protected]>
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).

from odoo import fields, models


class EdiConfigurationTrigger(models.Model):
_name = "edi.configuration.trigger"
_description = """
Describe what triggers a specific action for a configuration.
"""

name = fields.Char(string="Name", required=True)
code = fields.Char(required=True, copy=False)
active = fields.Boolean(default=True)
description = fields.Char(help="Describe what the conf is for")
model_id = fields.Many2one(
"ir.model",
string="Model",
help="Model the conf applies to. Leave blank to apply for all models",
)

_sql_constraints = [("code_uniq", "unique(code)", "Code must be unique")]
Loading
Loading