Skip to content

Commit d8f5dba

Browse files
committed
[ADD] edi_oca: Add new model edi.configuration
1 parent 94addaf commit d8f5dba

21 files changed

+737
-16
lines changed

edi_oca/README.rst

+1
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,7 @@ Contributors
166166

167167
* Simone Orsi <simahawk@gmail.com>
168168
* Enric Tobella <etobella@creublanca.es>
169+
* Thien Vo <thienvh@trobz.com>
169170

170171
Maintainers
171172
~~~~~~~~~~~

edi_oca/__manifest__.py

+2
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,15 @@
2929
"data/sequence.xml",
3030
"data/job_channel.xml",
3131
"data/job_function.xml",
32+
"data/edi_configuration.xml",
3233
"security/res_groups.xml",
3334
"security/ir_model_access.xml",
3435
"views/edi_backend_views.xml",
3536
"views/edi_backend_type_views.xml",
3637
"views/edi_exchange_record_views.xml",
3738
"views/edi_exchange_type_views.xml",
3839
"views/edi_exchange_type_rule_views.xml",
40+
"views/edi_configuration_views.xml",
3941
"views/menuitems.xml",
4042
"templates/exchange_chatter_msg.xml",
4143
"templates/exchange_mixin_buttons.xml",

edi_oca/data/edi_configuration.xml

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?xml version="1.0" encoding="utf-8" ?>
2+
<odoo>
3+
<!-- The `ir_action` parameter must be passed in order to use `send_via_email`
4+
Examlple:
5+
<field name="snippet_do">record._edi_send_via_email(ir_action)</field>
6+
-->
7+
<record id="edi_conf_send_via_email" model="edi.configuration">
8+
<field name="name">Send Via Email</field>
9+
<field name="active">False</field>
10+
<field name="code">send_via_email</field>
11+
<field name="trigger">on_email_send</field>
12+
<field name="snippet_do">record._edi_send_via_email()</field>
13+
</record>
14+
15+
<!-- Add type_id to use Send Via EDI -->
16+
<record id="edi_conf_send_via_edi" model="edi.configuration">
17+
<field name="name">Send Via EDI</field>
18+
<field name="active">False</field>
19+
<field name="code">send_via_edi</field>
20+
<field name="trigger">on_edi_send</field>
21+
<field name="snippet_do">record._edi_send_via_edi(conf.type_id)</field>
22+
</record>
23+
</odoo>

edi_oca/models/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,4 @@
55
from . import edi_exchange_type
66
from . import edi_exchange_type_rule
77
from . import edi_id_mixin
8+
from . import edi_configuration

edi_oca/models/edi_backend.py

+15-7
Original file line numberDiff line numberDiff line change
@@ -377,6 +377,19 @@ def _cron_check_output_exchange_sync(self, **kw):
377377
for backend in self:
378378
backend._check_output_exchange_sync(**kw)
379379

380+
def exchange_generate_send(self, recordset, skip_generate=False, skip_send=False):
381+
for rec in recordset:
382+
if skip_generate:
383+
job1 = rec
384+
else:
385+
job1 = rec.delayable().action_exchange_generate()
386+
if hasattr(job1, "on_done"):
387+
if not skip_send:
388+
# Chain send job.
389+
# Raise prio to max to send the record out as fast as possible.
390+
job1.on_done(rec.delayable(priority=0).action_exchange_send())
391+
job1.delay()
392+
380393
def _check_output_exchange_sync(
381394
self, skip_send=False, skip_sent=True, record_ids=None
382395
):
@@ -396,13 +409,8 @@ def _check_output_exchange_sync(
396409
"EDI Exchange output sync: found %d new records to process.",
397410
len(new_records),
398411
)
399-
for rec in new_records:
400-
job1 = rec.delayable().action_exchange_generate()
401-
if not skip_send:
402-
# Chain send job.
403-
# Raise prio to max to send the record out as fast as possible.
404-
job1.on_done(rec.delayable(priority=0).action_exchange_send())
405-
job1.delay()
412+
if new_records:
413+
self.exchange_generate_send(new_records, skip_send=skip_send)
406414

407415
if skip_send:
408416
return

edi_oca/models/edi_configuration.py

+209
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
# Copyright 2024 Camptocamp SA
2+
# @author Simone Orsi <simahawk@gmail.com>
3+
# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
4+
5+
import datetime
6+
7+
import pytz
8+
9+
from odoo import _, api, exceptions, fields, models
10+
from odoo.tools import DotDict, safe_eval
11+
12+
13+
def date_to_datetime(dt):
14+
"""Convert date to datetime."""
15+
if isinstance(dt, datetime.date):
16+
return datetime.datetime.combine(dt, datetime.datetime.min.time())
17+
return dt
18+
19+
20+
def to_utc(dt):
21+
"""Convert date or datetime to UTC."""
22+
# Gracefully convert to datetime if needed 1st
23+
return date_to_datetime(dt).astimezone(pytz.UTC)
24+
25+
26+
class EdiConfiguration(models.Model):
27+
_name = "edi.configuration"
28+
_description = """
29+
This model is used to configure EDI (Electronic Data Interchange) flows.
30+
It allows users to create their own configurations, which can be tailored
31+
to meet the specific needs of their business processes.
32+
"""
33+
34+
name = fields.Char(string="Name", required=True)
35+
active = fields.Boolean(default=True)
36+
code = fields.Char(required=True, copy=False, index=True, unique=True)
37+
description = fields.Char(help="Describe what the conf is for")
38+
backend_id = fields.Many2one(string="Backend", comodel_name="edi.backend")
39+
# Field `type_id` is not a mandatory field because we will create 2 common confs
40+
# for EDI (`send_via_email` and `send_via_edi`). So type_id is
41+
# a mandatory field will create unwanted data for users when installing this module.
42+
type_id = fields.Many2one(
43+
string="Exchange Type",
44+
comodel_name="edi.exchange.type",
45+
ondelete="cascade",
46+
auto_join=True,
47+
index=True,
48+
)
49+
model_id = fields.Many2one(
50+
"ir.model",
51+
string="Model",
52+
help="Model the conf applies to. Leave blank to apply for all models",
53+
)
54+
model_name = fields.Char(related="model_id.model", store=True)
55+
trigger = fields.Selection(
56+
# The selections below are intended to assist with basic operations
57+
# and are used to setup common configuration.
58+
[
59+
("on_record_write", "Update Record"),
60+
("on_record_create", "Create Record"),
61+
("on_email_send", "Send Email"),
62+
("on_edi_send", "Send EDI"),
63+
("disabled", "Disabled"),
64+
],
65+
string="Trigger",
66+
# The default selection will be disabled.
67+
# which would allow to keep the conf visible but disabled.
68+
required=True,
69+
default="disabled",
70+
ondelete="on default",
71+
)
72+
snippet_before_do = fields.Text(
73+
string="Snippet Before Do",
74+
help="Snippet to validate the state and collect records to do",
75+
)
76+
snippet_do = fields.Text(
77+
string="Snippet Do",
78+
help="""Used to do something specific here.
79+
Receives: operation, edi_action, vals, old_vals.""",
80+
)
81+
82+
@api.constrains("backend_id", "type_id")
83+
def _constrains_backend(self):
84+
for rec in self:
85+
if rec.type_id.backend_id:
86+
if rec.type_id.backend_id != rec.backend_id:
87+
raise exceptions.ValidationError(
88+
_("Backend must match with exchange type's backend!")
89+
)
90+
else:
91+
if rec.type_id.backend_type_id != rec.backend_id.backend_type_id:
92+
raise exceptions.ValidationError(
93+
_("Backend type must match with exchange type's backend type!")
94+
)
95+
96+
# TODO: This function is also available in `edi_exchange_template`.
97+
# Consider adding this to util or mixin
98+
def _code_snippet_valued(self, snippet):
99+
snippet = snippet or ""
100+
return bool(
101+
[
102+
not line.startswith("#")
103+
for line in (snippet.splitlines())
104+
if line.strip("")
105+
]
106+
)
107+
108+
@staticmethod
109+
def _date_to_string(dt, utc=True):
110+
if not dt:
111+
return ""
112+
if utc:
113+
dt = to_utc(dt)
114+
return fields.Date.to_string(dt)
115+
116+
@staticmethod
117+
def _datetime_to_string(dt, utc=True):
118+
if not dt:
119+
return ""
120+
if utc:
121+
dt = to_utc(dt)
122+
return fields.Datetime.to_string(dt)
123+
124+
def _time_utils(self):
125+
return {
126+
"datetime": safe_eval.datetime,
127+
"dateutil": safe_eval.dateutil,
128+
"time": safe_eval.time,
129+
"utc_now": fields.Datetime.now(),
130+
"date_to_string": self._date_to_string,
131+
"datetime_to_string": self._datetime_to_string,
132+
"time_to_string": lambda dt: dt.strftime("%H:%M:%S") if dt else "",
133+
"first_of": fields.first,
134+
}
135+
136+
def _get_code_snippet_eval_context(self):
137+
"""Prepare the context used when evaluating python code
138+
139+
:returns: dict -- evaluation context given to safe_eval
140+
"""
141+
ctx = {
142+
"uid": self.env.uid,
143+
"user": self.env.user,
144+
"DotDict": DotDict,
145+
"conf": self,
146+
}
147+
ctx.update(self._time_utils())
148+
return ctx
149+
150+
def _evaluate_code_snippet(self, snippet, **render_values):
151+
if not self._code_snippet_valued(snippet):
152+
return {}
153+
eval_ctx = dict(render_values, **self._get_code_snippet_eval_context())
154+
safe_eval.safe_eval(snippet, eval_ctx, mode="exec", nocopy=True)
155+
result = eval_ctx.get("result", {})
156+
if not isinstance(result, dict):
157+
return {}
158+
return result
159+
160+
def edi_exec_snippet_before_do(self, record, **kwargs):
161+
self.ensure_one()
162+
# Execute snippet before do
163+
vals_before_do = self._evaluate_code_snippet(
164+
self.snippet_before_do, record=record, **kwargs
165+
)
166+
167+
# Prepare data
168+
vals = {
169+
"todo": vals_before_do.get("todo", True),
170+
"snippet_do_vars": vals_before_do.get("snippet_do_vars", False),
171+
"event_only": vals_before_do.get("event_only", False),
172+
"tracked_fields": vals_before_do.get("tracked_fields", False),
173+
"edi_action": vals_before_do.get("edi_action", False),
174+
}
175+
return vals
176+
177+
def edi_exec_snippet_do(self, record, **kwargs):
178+
self.ensure_one()
179+
if self.trigger == "disabled":
180+
return False
181+
182+
old_value = kwargs.get("old_vals", {}).get(record.id, {})
183+
new_value = kwargs.get("vals", {}).get(record.id, {})
184+
vals = {
185+
"todo": True,
186+
"record": record,
187+
"operation": kwargs.get("operation", False),
188+
"edi_action": kwargs.get("edi_action", False),
189+
"old_value": old_value,
190+
"vals": new_value,
191+
}
192+
if self.snippet_before_do:
193+
before_do_vals = self.edi_exec_snippet_before_do(record, **kwargs)
194+
vals.update(before_do_vals)
195+
if vals["todo"]:
196+
return self._evaluate_code_snippet(self.snippet_do, **vals)
197+
return True
198+
199+
def edi_get_conf(self, trigger, backend=None):
200+
domain = [("trigger", "=", trigger)]
201+
if backend:
202+
domain.append(("backend_id", "=", backend.id))
203+
else:
204+
# We will only get confs that have backend_id = False
205+
# or are equal to self.type_id.backend_id.id
206+
backend_ids = self.mapped("type_id.backend_id.id")
207+
backend_ids.append(False)
208+
domain.append(("backend_id", "in", backend_ids))
209+
return self.filtered_domain(domain)

edi_oca/models/edi_exchange_consumer_mixin.py

+31
Original file line numberDiff line numberDiff line change
@@ -291,3 +291,34 @@ def _edi_set_origin(self, exc_record):
291291
def _edi_get_origin(self):
292292
self.ensure_one()
293293
return self.origin_exchange_record_id
294+
295+
def _edi_send_via_edi(self, exchange_type, **kw):
296+
exchange_record = self._edi_create_exchange_record(exchange_type)
297+
exchange_record.action_exchange_generate_send(**kw)
298+
299+
def _edi_send_via_email(
300+
self, ir_action, subtype_ref=None, partner_method=None, partners=None
301+
):
302+
# Retrieve context and composer model
303+
ctx = ir_action.get("context", {})
304+
composer_model = self.env[ir_action["res_model"]].with_context(ctx)
305+
306+
# Determine subtype and partner_ids dynamically based on model-specific logic
307+
subtype = subtype_ref and self.env.ref(subtype_ref) or None
308+
if not subtype:
309+
return False
310+
311+
composer = composer_model.create({"subtype_id": subtype.id})
312+
composer.onchange_template_id_wrapper()
313+
314+
# Dynamically retrieve partners based on the provided method or fallback to parameter
315+
if partner_method and hasattr(self, partner_method):
316+
composer.partner_ids = getattr(self, partner_method)().ids
317+
elif partners:
318+
composer.partner_ids = partners.ids
319+
else:
320+
return False
321+
322+
# Send the email
323+
composer.send_mail()
324+
return True

edi_oca/models/edi_exchange_record.py

+3
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,9 @@ def action_exchange_generate(self, **kw):
308308
self.ensure_one()
309309
return self.backend_id.exchange_generate(self, **kw)
310310

311+
def action_exchange_generate_send(self, **kw):
312+
return self.backend_id.exchange_generate_send(self, **kw)
313+
311314
def action_exchange_send(self):
312315
self.ensure_one()
313316
return self.backend_id.exchange_send(self)

edi_oca/readme/CONTRIBUTORS.rst

+1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
* Simone Orsi <simahawk@gmail.com>
22
* Enric Tobella <etobella@creublanca.es>
3+
* Thien Vo <thienvh@trobz.com>

edi_oca/security/ir_model_access.xml

+18
Original file line numberDiff line numberDiff line change
@@ -113,4 +113,22 @@
113113
<field name="domain_force">[(1, '=', 1)]</field>
114114
<field name="groups" eval="[(4, ref('base_edi.group_edi_manager'))]" />
115115
</record>
116+
<record model="ir.model.access" id="access_edi_configuration_manager">
117+
<field name="name">access_edi_configuration manager</field>
118+
<field name="model_id" ref="model_edi_configuration" />
119+
<field name="group_id" ref="base_edi.group_edi_manager" />
120+
<field name="perm_read" eval="1" />
121+
<field name="perm_create" eval="1" />
122+
<field name="perm_write" eval="1" />
123+
<field name="perm_unlink" eval="1" />
124+
</record>
125+
<record model="ir.model.access" id="access_edi_configuration_user">
126+
<field name="name">access_edi_configuration user</field>
127+
<field name="model_id" ref="model_edi_configuration" />
128+
<field name="group_id" ref="base.group_user" />
129+
<field name="perm_read" eval="1" />
130+
<field name="perm_create" eval="0" />
131+
<field name="perm_write" eval="0" />
132+
<field name="perm_unlink" eval="0" />
133+
</record>
116134
</odoo>

0 commit comments

Comments
 (0)