Skip to content
Open
6 changes: 0 additions & 6 deletions edi_component_oca/models/edi_exchange_record.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,6 @@
class EdiExchangeRecord(models.Model):
_inherit = "edi.exchange.record"

def _trigger_edi_event_make_name(self, name, suffix=None):
return "on_edi_exchange_{name}{suffix}".format(
name=name,
suffix=("_" + suffix) if suffix else "",
)

def _trigger_edi_event(self, name, suffix=None, target=None, **kw):
"""Trigger a component event linked to this backend and edi exchange."""
name = self._trigger_edi_event_make_name(name, suffix=suffix)
Expand Down
12 changes: 12 additions & 0 deletions edi_core_oca/data/edi_configuration.xml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,18 @@
<field name="code">on_record_write</field>
<field name="description">Trigger when a record is updated</field>
</record>

<record id="edi_config_trigger_record_done" model="edi.configuration.trigger">
<field name="name">On record exchange done</field>
<field name="code">on_edi_exchange_done</field>
<field name="description">Trigger when a record exchange is done</field>
</record>
<record id="edi_config_trigger_record_error" model="edi.configuration.trigger">
<field name="name">On record exchange error</field>
<field name="code">on_edi_exchange_error</field>
<field name="description">Trigger when a record exchange has an error</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>
Expand Down
40 changes: 39 additions & 1 deletion edi_core_oca/models/edi_configuration.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,16 +67,25 @@ class EdiConfiguration(models.Model):
help="""Used to do something specific here.
Receives: operation, edi_action, vals, old_vals.""",
)
# You can use this to avoid component events ;)
is_global = fields.Boolean(
string="Global Configuration",
help="If checked, this configuration will be executed for all records, "
"regardless of the partner relation.",
default=False,
)

@api.constrains("backend_id", "type_id")
def _constrains_backend(self):
for rec in self:
if not rec.backend_id:
continue
if rec.type_id.backend_id:
if rec.type_id.backend_id != rec.backend_id:
raise exceptions.ValidationError(
self.env._("Backend must match with exchange type's backend!")
)
else:
elif rec.type_id:
if rec.type_id.backend_type_id != rec.backend_id.backend_type_id:
raise exceptions.ValidationError(
self.env._(
Expand Down Expand Up @@ -201,6 +210,35 @@ def edi_get_conf(self, trigger, backend=None):
domain.append(("backend_id", "in", backend_ids))
return self.filtered_domain(domain)

@api.model
def edi_get_conf_global(self, exchange_record, trigger):
"""Return active global configurations matching the given event.

Unlike :meth:`edi_get_conf` -- which runs on a recordset of
configurations already linked to a partner -- global configurations
are not bound to any partner. We therefore have to derive the
filtering keys from the originating exchange record:

* ``trigger`` must match the event code
* ``is_global`` must be True
* ``type_id`` must match the exchange type or be empty (applies to all)
* ``backend_id`` must match the backend or be empty (applies to all)
* ``model_name`` must match the related record model or be empty
(applies to all)
"""
related_model = exchange_record.model
model_options = [False]
if related_model:
model_options.append(related_model)
domain = [
("trigger", "=", trigger),
("is_global", "=", True),
("type_id", "in", [exchange_record.type_id.id, False]),
("backend_id", "in", [exchange_record.backend_id.id, False]),
("model_name", "in", model_options),
]
return self.search(domain)

def action_view_partners(self):
# TODO: add tests
partner_model = self.env["res.partner"]
Expand Down
15 changes: 13 additions & 2 deletions edi_core_oca/models/edi_exchange_record.py
Original file line number Diff line number Diff line change
Expand Up @@ -533,8 +533,19 @@ def _notify_related_record(self, message, level="info"):
rec._notify_related_record(message, level)

def _trigger_edi_event(self, name, suffix=None, target=None, **kw):
"""Hook to be implemented in other modules"""
pass
event_name = self._trigger_edi_event_make_name(name, suffix)
target = target or self
global_configs = self.env["edi.configuration"].edi_get_conf_global(
self, event_name
)
for conf in global_configs:
conf.edi_exec_snippet_do(target, **kw)

def _trigger_edi_event_make_name(self, name, suffix=None):
return "on_edi_exchange_{name}{suffix}".format(
name=name,
suffix=("_" + suffix) if suffix else "",
)

def _notify_done(self):
self._notify_related_record(self._exchange_status_message("process_ok"))
Expand Down
95 changes: 95 additions & 0 deletions edi_core_oca/readme/CONFIGURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,3 +63,98 @@ backend to be used for the exchange.

In case of "Custom" kind, you'll have to define your own logic to do
something.

## Custom event handlers via `edi.configuration`

The framework can dispatch EDI lifecycle events to user-defined
configurations, providing a declarative alternative to component events.
Each `edi.configuration` record links a **trigger** (an
`edi.configuration.trigger` code) to a **snippet** (`snippet_do`) that is
executed every time the matching event fires on an exchange record.

Built-in events fired by `EDIExchangeRecord` include:

- `on_edi_exchange_done` — exchange processed successfully
- `on_edi_exchange_error` — exchange ended in error
- `on_edi_exchange_done_ack_received` — ACK file received
- `on_edi_exchange_done_ack_missing` — expected ACK not received
- `on_edi_exchange_done_ack_received_error` — ACK received with errors
- `on_edi_exchange_<action>_complete` — generic action completion (e.g.
`generate_complete`, `send_complete`), fired once on the exchange
record and once on its related record when present

The snippet receives at least two variables in its evaluation context:

- `conf` — the current `edi.configuration` record
- `record` — the target of the event (either the `edi.exchange.record`
itself or its related business record)

Plus the standard `edi_exec_snippet_do` extras (`operation`,
`edi_action`, `old_value`, `vals`, ...).

Two complementary lookup modes are available, and they can be combined
freely on the same flow.

### Global event configurations

Use this mode when you want a configuration to react to events on **any
business record** that travels through EDI, with no per-partner setup.

Tick **Global Configuration** (`is_global`) on the `edi.configuration`.
When an event fires, the framework calls
`edi.configuration.edi_get_conf_global(exchange_record, trigger)` which
selects all active global configurations whose `trigger` matches the
event code, filtered by the originating exchange record:

- **Exchange type** (`type_id`): must match the exchange record's type,
or be left empty to apply to every type
- **Backend** (`backend_id`): must match the exchange record's backend,
or be left empty to apply to every backend
- **Model** (`model_id` / `model_name`): must match the related record
model (e.g. `sale.order`, `account.move`), or be left empty to apply
to every model

Empty values mean "applies to all". Inactive configurations and
non-global configurations are ignored. All matching configurations are
executed in sequence.

Typical use cases:

- Posting a generic chatter message on every exchange that ends in error
- Pushing a notification to an external system every time an ACK is
received for a given backend
- Logging extra audit information for every exchange of a given type

### Partner-specific (relation-based) event configurations

Use this mode when the reaction must depend on the partner (or any
other related record) involved in the exchange.

In this case configurations are **not** marked as global. Instead, the
business record exposes an `edi_config_ids` relation (via
`edi.exchange.consumer.mixin._edi_config_field_relation`, which by
default returns `self.env["edi.configuration"]` and can be overridden,
for example to point at `self.partner_id.edi_config_ids`). When an
event fires on the business record (e.g. on create, on write,
on send-via-email/EDI), the framework calls
`edi_confs.edi_get_conf(trigger)` on that relation and runs the
matching snippets.

Compared with global configurations:

- **Discovery** comes from the record's own relation, not from a
database-wide search; this is the right place to model "this partner
wants this behaviour" rules
- **Filtering** is reduced to `trigger` and (optionally) `backend_id`,
since the recordset is already narrowed by the relation
- The same `snippet_do` API applies, so a snippet can be reused
verbatim between global and partner-specific configurations

Typical use cases:

- Sending a specific EDI flow only for a subset of partners
- Customising the document generation per customer (e.g. different
email template, different transport)
- Switching between EDI and email delivery based on partner
preferences

9 changes: 8 additions & 1 deletion edi_core_oca/readme/DESCRIPTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,11 @@ Provides following models:
3. EDI Exchange Type, to define file types of exchange
4. EDI Exchange Record, to define a record exchanged between systems

Also define a mixin to be inherited by records that will generate EDIs
Also define a mixin to be inherited by records that will generate EDIs.

In addition, the module ships an ``edi.configuration`` mechanism that lets
users react to EDI events declaratively, by writing small Python snippets
attached to event triggers. This can be used as a lightweight alternative
to component event listeners: configurations can react globally (on any
exchange) or be scoped to a specific partner (or any related record),
exchange type, backend and target model. See ``CONFIGURE.md`` for details.
Empty file.
20 changes: 20 additions & 0 deletions edi_core_oca/readme/newsfragments/global-edi-conf-events.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
Introduce a new system for **global EDI events** based on ``edi.configuration``
that can replace the use of component events.

Any ``edi.configuration`` flagged as ``is_global`` is now picked up by
``EDIExchangeRecord._trigger_edi_event`` and its ``snippet_do`` is executed
whenever the matching event fires (``done``, ``error``, ``ack_received``,
``ack_missing``, ``ack_received_error``, ``<action>_complete``, ...).

Filtering is performed via the new ``edi.configuration.edi_get_conf_global``
model method, which selects active global configurations matching the event
trigger code and, when set, the exchange type, the backend and the related
record model carried by the exchange record (empty values still mean "applies
to all"). This lets integrators subscribe to EDI events declaratively from
the UI instead of writing component listeners.

Full test coverage is included for the dispatch on all ``notify_*`` events
(both on the exchange record and on the related record target) and for the
new filtering rules.

Last but not lease: add minimal docs for edi.configuration.
Loading
Loading