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

Open
wants to merge 3 commits into
base: 14.0
Choose a base branch
from
Open
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
2 changes: 1 addition & 1 deletion account_invoice_import_simple_pdf/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"python": [
"regex",
"dateparser",
"pypdf>=3.1.0",
"pypdf>=3.1.0,<5.0",
],
"deb": ["libmupdf-dev", "mupdf", "mupdf-tools", "poppler-utils"],
},
Expand Down
1 change: 1 addition & 0 deletions edi_oca/README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ Contributors

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

Maintainers
~~~~~~~~~~~
Expand Down
2 changes: 2 additions & 0 deletions edi_oca/__manifest__.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,15 @@
"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/menuitems.xml",
"templates/exchange_chatter_msg.xml",
"templates/exchange_mixin_buttons.xml",
Expand Down
1 change: 1 addition & 0 deletions edi_oca/components/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@
from . import base_output
from . import base_input
from . import base_validate
from . import base_listener_config
33 changes: 33 additions & 0 deletions edi_oca/components/base_listener_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Copyright 2024 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

from odoo.addons.component.core import Component


class EDIBackendListenerComponentConfig(Component):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this listener is useless IMO. See below.

_name = "edi.component.listener.config"
_inherit = "base.event.listener"

# Added *_configuration to avoid being called from other create/write actions.
def on_record_write_configuration(self, record, fields=None, **kwargs):
trigger = "on_record_write"
if kwargs.get("vals", False):
for rec in record:
confs = self.env["edi.configuration"].edi_get_conf(
trigger, rec._name, rec.partner_id
)
for conf in confs:
conf.edi_exec_snippet_do(rec, **kwargs)

# Added *_configuration to avoid being called from other create/write actions.
def on_record_create_configuration(self, record, fields=None, **kwargs):
trigger = "on_record_create"
val_list = kwargs.get("vals", False)
if val_list:
for rec, vals in zip(record, val_list):
kwargs["vals"] = {rec.id: vals}
confs = self.env["edi.configuration"].edi_get_conf(
trigger, rec._name, rec.partner_id
)
for conf in confs:
conf.edi_exec_snippet_do(rec, **kwargs)
18 changes: 18 additions & 0 deletions edi_oca/data/edi_configuration.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8" ?>
<odoo>
<record id="edi_conf_send_via_email" model="edi.configuration">
<field name="name">Send Via Email</field>
<field name="active">False</field>
<field name="code">send_via_email</field>
<field name="trigger">on_email_send</field>
<field name="snippet_do">record._edi_send_via_email()</field>
</record>

<!-- Add type_id to use Send Via EDI -->
<record id="edi_conf_send_via_edi" model="edi.configuration">
<field name="name">Send Via EDI</field>
<field name="active">False</field>
<field name="code">send_via_edi</field>
<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
from . import edi_configuration_mixin
21 changes: 14 additions & 7 deletions edi_oca/models/edi_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,18 @@ def _cron_check_output_exchange_sync(self, **kw):
for backend in self:
backend._check_output_exchange_sync(**kw)

def exchange_generate_send(self, recordset, skip_generate=False, skip_send=False):
for rec in recordset:
if skip_generate:
job1 = rec
else:
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()

def _check_output_exchange_sync(
self, skip_send=False, skip_sent=True, record_ids=None
):
Expand All @@ -396,13 +408,8 @@ def _check_output_exchange_sync(
"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
222 changes: 222 additions & 0 deletions edi_oca/models/edi_configuration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
# Copyright 2024 Camptocamp SA
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comes from the auto-type stuff, can you keep the authorship here?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The license must be LGPL

# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl).

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


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)


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)
code = fields.Char(required=True, copy=False, index=True, unique=True)
description = fields.Char(help="Describe what the conf is for")
backend_id = fields.Many2one(string="Backend", comodel_name="edi.backend")
type_id = fields.Many2one(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

type should be mandatory

string="Exchange Type",
comodel_name="edi.exchange.type",
ondelete="cascade",
auto_join=True,
index=True,
)
model = 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.model", store=True)
partner_ids = fields.Many2many(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's no need for a partner_ids field. The conf should be stored on the partner on a specific field per app or module (eg: edi_purchase_conf_ids).
The goal is to have clarity on UI on what confs are used for.
In fact, it's not in the specs.

string="Enabled for partners",
comodel_name="res.partner",
relation="res_partner_edi_configuration_rel",
column1="conf_id",
column2="partner_id",
help="Leave blank to apply for all partners.",
)
trigger = fields.Selection(
[
("on_record_write", "Update Record"),
("on_record_create", "Create Record"),
("on_email_send", "Send Email"),
],
string="Trigger",
default=False,
)
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(
_("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(
_("Backend type must match with exchange type's backend type!")
)

@api.model
def default_get(self, fields):
vals = super().default_get(fields)
model = self.env.context.get("default_model_name", False)
partners = self.env.context.get("default_partner_ids", False)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd choose which model field you want to be the master. If the model name is only used for searches, we don't need this.
Partners as well, can be trashed here.

if model:
vals["model"] = self.env["ir.model"]._get_id(model)
if partners and isinstance(partners, list):
vals["partner_ids"] = [(6, 0, partners)]
return vals

def _code_snippet_valued(self, snippet):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this stuff is copied from edi_exchange_template and the type auto module. Can you please add a TODO to move it to some utils and/or mixin?

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 ""
if utc:
dt = to_utc(dt)
return fields.Date.to_string(dt)

@staticmethod
def _datetime_to_string(dt, utc=True):
if not dt:
return ""
if utc:
dt = to_utc(dt)
return fields.Datetime.to_string(dt)

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 {}
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 {}
return result

def edi_exec_snippet_before_do(self, record, **kwargs):
self.ensure_one()
vals = {
"todo": kwargs.get("todo", True),
}
# Execute snippet before do
vals_before_do = self._evaluate_code_snippet(
self.snippet_before_do, record=record, **kwargs
)

# Prepare data
vals.update(
{
"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()

old_value = kwargs.get("old_vals", {}).get(record.id, {})
new_value = kwargs.get("vals", {}).get(record.id, {})
vals = {
"todo": kwargs.get("todo", True),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do you expect to receive a todo flag here?

"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

@api.model
def edi_get_conf(self, trigger, model_name=None, partners=None, backend=None):
domain = [("trigger", "=", trigger)]
if model_name:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just accept the record and retrieve its name from it.
In fact, you should filter only the configurations that are assigned to the current partner.

I re-write here what has been done for PO to give you an example.

    def on_button_confirm_purchase_order(self, records):
        trigger = "on_button_confirm_purchase_order"
        confs = record.edi_purchase_conf_ids.edi_get_conf(trigger)
        for conf in confs:
             conf.edi_exec_snippet_do(record)

You see? You always have the recordset of configurations set on the partner. You just have to filter them by trigger.
In fact, this is what I described in the specs 😉
And the docstring I provided is "filter current recordset of conf using trigger".

domain += ["|", ("model_name", "=", model_name), ("model_name", "=", False)]
if partners:
domain += [
"|",
("partner_ids", "in", partners.ids),
("partner_ids", "=", False),
]
else:
domain.append(("partner_ids", "=", False))
if backend:
domain.append(("backend_id", "=", backend.id))
return self.search(domain)
Loading
Loading