diff --git a/README.md b/README.md
index 7eec74965..5406a0b2f 100644
--- a/README.md
+++ b/README.md
@@ -24,8 +24,8 @@ addon | version | maintainers | summary
--- | --- | --- | ---
[edi_account_core_oca](edi_account_core_oca/) | 18.0.1.1.1 |
| Define EDI Configuration for Account Moves
[edi_account_oca](edi_account_oca/) | 18.0.1.1.1 |
| Define some component listeners for Account Moves
-[edi_component_oca](edi_component_oca/) | 18.0.1.0.3 |
| Allow to use Connector as a source in EDI
-[edi_core_oca](edi_core_oca/) | 18.0.1.6.6 |
| Define backends, exchange types, exchange records, basic automation and views for handling EDI exchanges.
+[edi_component_oca](edi_component_oca/) | 18.0.1.1.0 |
| Allow to use Connector as a source in EDI
+[edi_core_oca](edi_core_oca/) | 18.0.1.7.0 |
| Define backends, exchange types, exchange records, basic automation and views for handling EDI exchanges.
[edi_endpoint_oca](edi_endpoint_oca/) | 18.0.1.0.3 | | Base module allowing configuration of custom endpoints for EDI framework.
[edi_exchange_template_oca](edi_exchange_template_oca/) | 18.0.1.3.3 |
| Allows definition of exchanges via templates.
[edi_exchange_template_party_data](edi_exchange_template_party_data/) | 18.0.1.0.1 |
| Glue module between edi_exchange_template and edi_party_data
@@ -33,6 +33,7 @@ addon | version | maintainers | summary
[edi_oca](edi_oca/) | 18.0.1.5.2 |
| Integrate all EDI modules together
[edi_party_data_oca](edi_party_data_oca/) | 18.0.1.0.1 |
| Allow to configure and retrieve party information for EDI exchanges.
[edi_product_oca](edi_product_oca/) | 18.0.1.0.0 | | EDI framework configuration and base logic for products and products packaging
+[edi_purchase_oca](edi_purchase_oca/) | 18.0.1.0.0 | | Define EDI Configuration for Purchase Orders
[edi_queue_oca](edi_queue_oca/) | 18.0.1.0.2 | | Set Queue Jobs on EDI
[edi_record_metadata_oca](edi_record_metadata_oca/) | 18.0.1.0.5 |
| Allow to store metadata for related records.
[edi_sale_endpoint](edi_sale_endpoint/) | 18.0.1.0.0 |
| Glue module between edi_sale_oca and edi_endpoint_oca.
diff --git a/edi_component_oca/README.rst b/edi_component_oca/README.rst
index db2cffdd7..06d07a3d0 100644
--- a/edi_component_oca/README.rst
+++ b/edi_component_oca/README.rst
@@ -11,7 +11,7 @@ Edi Connector Oca
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
- !! source digest: sha256:81c11c0d670f363513d25e5d2d6cb038f1fc56580f20c837c2d2a7665798018d
+ !! source digest: sha256:6c5e69ae42fdaaaf428ee2b9d0a8e14ba1b5515b6fd5daa7ca926cb5800567b4
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
diff --git a/edi_component_oca/__manifest__.py b/edi_component_oca/__manifest__.py
index fa8111718..c589696e7 100644
--- a/edi_component_oca/__manifest__.py
+++ b/edi_component_oca/__manifest__.py
@@ -4,7 +4,7 @@
{
"name": "Edi Connector Oca",
"summary": """Allow to use Connector as a source in EDI""",
- "version": "18.0.1.0.3",
+ "version": "18.0.1.1.0",
"license": "LGPL-3",
"author": "ACSONE,Dixmit,Camptocamp,Odoo Community Association (OCA)",
"maintainers": ["simahawk", "etobella"],
diff --git a/edi_component_oca/models/edi_exchange_record.py b/edi_component_oca/models/edi_exchange_record.py
index 551d1fa6b..b6105c0d5 100644
--- a/edi_component_oca/models/edi_exchange_record.py
+++ b/edi_component_oca/models/edi_exchange_record.py
@@ -8,15 +8,9 @@
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)
+ event_name = self._trigger_edi_event_make_name(name, suffix=suffix)
target = target or self
- target._event(name).notify(self, **kw)
+ target._event(event_name).notify(self, **kw)
return super()._trigger_edi_event(name, suffix=suffix, target=target, **kw)
diff --git a/edi_component_oca/static/description/index.html b/edi_component_oca/static/description/index.html
index e6128b6fa..f256369c0 100644
--- a/edi_component_oca/static/description/index.html
+++ b/edi_component_oca/static/description/index.html
@@ -372,7 +372,7 @@
Edi Connector Oca
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
-!! source digest: sha256:81c11c0d670f363513d25e5d2d6cb038f1fc56580f20c837c2d2a7665798018d
+!! source digest: sha256:6c5e69ae42fdaaaf428ee2b9d0a8e14ba1b5515b6fd5daa7ca926cb5800567b4
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

This module allows to use components to handle code to execute on EDI
diff --git a/edi_core_oca/README.rst b/edi_core_oca/README.rst
index bb985bd0d..55bdc1e55 100644
--- a/edi_core_oca/README.rst
+++ b/edi_core_oca/README.rst
@@ -11,7 +11,7 @@ EDI
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
- !! source digest: sha256:c609033733302fa71a3c01c11e2729fd2b47ccde0b9a1d0619bed03cc26db4fe
+ !! source digest: sha256:27258fb23153f2660be19d7c76b04c4d09d35d1e240b779076c7f4a9fa66d9f2
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
@@ -42,7 +42,15 @@ 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.
**Table of contents**
@@ -130,6 +138,104 @@ 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__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
+
Usage
=====
@@ -182,6 +288,39 @@ Components dependancy has been removed and set on a new dependant module
``edi_component_oca``. Module ``edi_oca`` has been_renamed to
``edi_core_oca``.
+Changelog
+=========
+
+18.0.1.7.0 (2026-05-20)
+-----------------------
+
+Features
+~~~~~~~~
+
+- 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``,
+ ``_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.
+ (`#global-edi-conf-events `__)
+
Bug Tracker
===========
diff --git a/edi_core_oca/__manifest__.py b/edi_core_oca/__manifest__.py
index a2929a7ab..c3a09f07b 100644
--- a/edi_core_oca/__manifest__.py
+++ b/edi_core_oca/__manifest__.py
@@ -9,7 +9,7 @@
Define backends, exchange types, exchange records,
basic automation and views for handling EDI exchanges.
""",
- "version": "18.0.1.6.6",
+ "version": "18.0.1.7.0",
"website": "https://github.com/OCA/edi-framework",
"development_status": "Beta",
"license": "LGPL-3",
diff --git a/edi_core_oca/data/edi_configuration.xml b/edi_core_oca/data/edi_configuration.xml
index 565919c74..867212198 100644
--- a/edi_core_oca/data/edi_configuration.xml
+++ b/edi_core_oca/data/edi_configuration.xml
@@ -15,6 +15,18 @@
on_record_write
Trigger when a record is updated
+
+
+ On record exchange done
+ on_edi_exchange_done
+ Trigger when a record exchange is done
+
+
+ On record exchange error
+ on_edi_exchange_error
+ Trigger when a record exchange has an error
+
+
Send via email
diff --git a/edi_core_oca/i18n/edi_core_oca.pot b/edi_core_oca/i18n/edi_core_oca.pot
index 436bf77b7..35b6d0b94 100644
--- a/edi_core_oca/i18n/edi_core_oca.pot
+++ b/edi_core_oca/i18n/edi_core_oca.pot
@@ -1005,6 +1005,11 @@ msgstr ""
msgid "Generator"
msgstr ""
+#. module: edi_core_oca
+#: model:ir.model.fields,field_description:edi_core_oca.field_edi_configuration__is_global
+msgid "Global Configuration"
+msgstr ""
+
#. module: edi_core_oca
#: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_exchange_record_view_search
#: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_exchange_type_rule_view_search
@@ -1069,6 +1074,13 @@ msgstr ""
msgid "If checked, some messages have a delivery error."
msgstr ""
+#. module: edi_core_oca
+#: model:ir.model.fields,help:edi_core_oca.field_edi_configuration__is_global
+msgid ""
+"If checked, this configuration will be executed for all records, regardless "
+"of the partner relation."
+msgstr ""
+
#. module: edi_core_oca
#: model:ir.model.fields,help:edi_core_oca.field_edi_exchange_type__exchange_filename_sequence_id
msgid ""
diff --git a/edi_core_oca/i18n/es.po b/edi_core_oca/i18n/es.po
index 6aa91ccbc..ae4534243 100644
--- a/edi_core_oca/i18n/es.po
+++ b/edi_core_oca/i18n/es.po
@@ -1014,6 +1014,11 @@ msgstr ""
msgid "Generator"
msgstr ""
+#. module: edi_core_oca
+#: model:ir.model.fields,field_description:edi_core_oca.field_edi_configuration__is_global
+msgid "Global Configuration"
+msgstr ""
+
#. module: edi_core_oca
#: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_exchange_record_view_search
#: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_exchange_type_rule_view_search
@@ -1078,6 +1083,13 @@ msgstr ""
msgid "If checked, some messages have a delivery error."
msgstr ""
+#. module: edi_core_oca
+#: model:ir.model.fields,help:edi_core_oca.field_edi_configuration__is_global
+msgid ""
+"If checked, this configuration will be executed for all records, regardless "
+"of the partner relation."
+msgstr ""
+
#. module: edi_core_oca
#: model:ir.model.fields,help:edi_core_oca.field_edi_exchange_type__exchange_filename_sequence_id
msgid ""
diff --git a/edi_core_oca/i18n/fr.po b/edi_core_oca/i18n/fr.po
index 28a967be4..b09c20623 100644
--- a/edi_core_oca/i18n/fr.po
+++ b/edi_core_oca/i18n/fr.po
@@ -1026,6 +1026,11 @@ msgstr ""
msgid "Generator"
msgstr "Générer"
+#. module: edi_core_oca
+#: model:ir.model.fields,field_description:edi_core_oca.field_edi_configuration__is_global
+msgid "Global Configuration"
+msgstr ""
+
#. module: edi_core_oca
#: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_exchange_record_view_search
#: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_exchange_type_rule_view_search
@@ -1092,6 +1097,13 @@ msgstr ""
msgid "If checked, some messages have a delivery error."
msgstr ""
+#. module: edi_core_oca
+#: model:ir.model.fields,help:edi_core_oca.field_edi_configuration__is_global
+msgid ""
+"If checked, this configuration will be executed for all records, regardless "
+"of the partner relation."
+msgstr ""
+
#. module: edi_core_oca
#: model:ir.model.fields,help:edi_core_oca.field_edi_exchange_type__exchange_filename_sequence_id
msgid ""
diff --git a/edi_core_oca/i18n/it.po b/edi_core_oca/i18n/it.po
index 14ad64c81..3147d8f34 100644
--- a/edi_core_oca/i18n/it.po
+++ b/edi_core_oca/i18n/it.po
@@ -6,7 +6,7 @@ msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 17.0\n"
"Report-Msgid-Bugs-To: \n"
-"PO-Revision-Date: 2026-04-30 10:45+0000\n"
+"PO-Revision-Date: 2026-05-25 19:57+0000\n"
"Last-Translator: mymage \n"
"Language-Team: none\n"
"Language: it\n"
@@ -1099,6 +1099,11 @@ msgstr "Genera record di scambio"
msgid "Generator"
msgstr "Generatore"
+#. module: edi_core_oca
+#: model:ir.model.fields,field_description:edi_core_oca.field_edi_configuration__is_global
+msgid "Global Configuration"
+msgstr "Configurazione globale"
+
#. module: edi_core_oca
#: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_exchange_record_view_search
#: model_terms:ir.ui.view,arch_db:edi_core_oca.edi_exchange_type_rule_view_search
@@ -1170,6 +1175,15 @@ msgstr "Se selezionata, nuovi messaggi richiedono attenzione."
msgid "If checked, some messages have a delivery error."
msgstr "Se selezionata, alcuni messaggi hanno un errore di consegna."
+#. module: edi_core_oca
+#: model:ir.model.fields,help:edi_core_oca.field_edi_configuration__is_global
+msgid ""
+"If checked, this configuration will be executed for all records, regardless "
+"of the partner relation."
+msgstr ""
+"Se selezionata, questa configurazione verrà eseguita per tutti i record, "
+"indipendentemente dalla relazione con il partner."
+
#. module: edi_core_oca
#: model:ir.model.fields,help:edi_core_oca.field_edi_exchange_type__exchange_filename_sequence_id
msgid ""
diff --git a/edi_core_oca/models/edi_backend.py b/edi_core_oca/models/edi_backend.py
index 54c845480..037ead3e9 100644
--- a/edi_core_oca/models/edi_backend.py
+++ b/edi_core_oca/models/edi_backend.py
@@ -10,6 +10,8 @@
import traceback
from io import StringIO
+from psycopg2 import IntegrityError, OperationalError
+
from odoo import exceptions, fields, models
from odoo.exceptions import UserError
@@ -249,6 +251,11 @@ def exchange_send(self, exchange_record):
_logger.debug(
"%s send failed. Marked as errored.", exchange_record.identifier
)
+ except (OperationalError, IntegrityError):
+ # We don't want the finally block to be executed in this case as
+ # the cursor is already in an aborted state and any query will fail.
+ res = "__sql_error__"
+ raise
else:
# TODO: maybe the send handler should return desired message and state
message = exchange_record._exchange_status_message("send_ok")
@@ -260,16 +267,18 @@ def exchange_send(self, exchange_record):
)
res = message
finally:
- exchange_record.write(
- {
- "edi_exchange_state": state,
- "exchange_error": error,
- "exchange_error_traceback": traceback,
- # FIXME: this should come from _compute_exchanged_on
- # but somehow it's failing in send tests (in record tests it works).
- "exchanged_on": fields.Datetime.now(),
- }
- )
+ if res != "__sql_error__":
+ exchange_record.write(
+ {
+ "edi_exchange_state": state,
+ "exchange_error": error,
+ "exchange_error_traceback": traceback,
+ # FIXME: this should come from _compute_exchanged_on
+ # but somehow it's failing in send tests
+ # (in record tests it works).
+ "exchanged_on": fields.Datetime.now(),
+ }
+ )
exchange_record.notify_action_complete("send", message=message)
return res
@@ -458,20 +467,27 @@ def exchange_process(self, exchange_record):
error = _get_exception_msg(err)
state = "input_processed_error"
res = f"Error: {error}"
+ except (OperationalError, IntegrityError):
+ # We don't want the finally block to be executed in this case as
+ # the cursor is already in an aborted state and any query will fail.
+ res = "__sql_error__"
+ raise
else:
error = traceback = None
state = "input_processed"
finally:
- exchange_record.write(
- {
- "edi_exchange_state": state,
- "exchange_error": error,
- "exchange_error_traceback": traceback,
- # FIXME: this should come from _compute_exchanged_on
- # but somehow it's failing in send tests (in record tests it works).
- "exchanged_on": fields.Datetime.now(),
- }
- )
+ if res != "__sql_error__":
+ exchange_record.write(
+ {
+ "edi_exchange_state": state,
+ "exchange_error": error,
+ "exchange_error_traceback": traceback,
+ # FIXME: this should come from _compute_exchanged_on
+ # but somehow it's failing in send tests
+ # (in record tests it works).
+ "exchanged_on": fields.Datetime.now(),
+ }
+ )
if (
state == "input_processed_error"
and old_state != "input_processed_error"
@@ -519,22 +535,29 @@ def exchange_receive(self, exchange_record):
state = "input_receive_error"
message = exchange_record._exchange_status_message("receive_ko")
res = f"Input error: {error}"
+ except (OperationalError, IntegrityError):
+ # We don't want the finally block to be executed in this case as
+ # the cursor is already in an aborted state and any query will fail.
+ res = "__sql_error__"
+ raise
else:
message = exchange_record._exchange_status_message("receive_ok")
error = traceback = None
state = "input_received"
res = message
finally:
- exchange_record.write(
- {
- "edi_exchange_state": state,
- "exchange_error": error,
- "exchange_error_traceback": traceback,
- # FIXME: this should come from _compute_exchanged_on
- # but somehow it's failing in send tests (in record tests it works).
- "exchanged_on": fields.Datetime.now(),
- }
- )
+ if res != "__sql_error__":
+ exchange_record.write(
+ {
+ "edi_exchange_state": state,
+ "exchange_error": error,
+ "exchange_error_traceback": traceback,
+ # FIXME: this should come from _compute_exchanged_on
+ # but somehow it's failing in send tests
+ # (in record tests it works).
+ "exchanged_on": fields.Datetime.now(),
+ }
+ )
exchange_record.notify_action_complete("receive", message=message)
return res
diff --git a/edi_core_oca/models/edi_configuration.py b/edi_core_oca/models/edi_configuration.py
index 93d7412f3..82bade7d5 100644
--- a/edi_core_oca/models/edi_configuration.py
+++ b/edi_core_oca/models/edi_configuration.py
@@ -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._(
@@ -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"]
diff --git a/edi_core_oca/models/edi_exchange_record.py b/edi_core_oca/models/edi_exchange_record.py
index a87b4506e..685a98bb0 100644
--- a/edi_core_oca/models/edi_exchange_record.py
+++ b/edi_core_oca/models/edi_exchange_record.py
@@ -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"))
diff --git a/edi_core_oca/readme/CONFIGURE.md b/edi_core_oca/readme/CONFIGURE.md
index 48045deba..a1957bbe0 100644
--- a/edi_core_oca/readme/CONFIGURE.md
+++ b/edi_core_oca/readme/CONFIGURE.md
@@ -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__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
+
diff --git a/edi_core_oca/readme/DESCRIPTION.md b/edi_core_oca/readme/DESCRIPTION.md
index 8cfcb472b..f9aee7085 100644
--- a/edi_core_oca/readme/DESCRIPTION.md
+++ b/edi_core_oca/readme/DESCRIPTION.md
@@ -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.
diff --git a/edi_core_oca/readme/HISTORY.md b/edi_core_oca/readme/HISTORY.md
new file mode 100644
index 000000000..755aa4d94
--- /dev/null
+++ b/edi_core_oca/readme/HISTORY.md
@@ -0,0 +1,24 @@
+## 18.0.1.7.0 (2026-05-20)
+
+### Features
+
+- 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``, ``_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. ([#global-edi-conf-events](https://github.com/OCA/edi-framework/issues/global-edi-conf-events))
diff --git a/edi_core_oca/readme/newsfragments/.gitkeep b/edi_core_oca/readme/newsfragments/.gitkeep
new file mode 100644
index 000000000..e69de29bb
diff --git a/edi_core_oca/static/description/index.html b/edi_core_oca/static/description/index.html
index 0273741fe..de6475a29 100644
--- a/edi_core_oca/static/description/index.html
+++ b/edi_core_oca/static/description/index.html
@@ -372,7 +372,7 @@ EDI
!! This file is generated by oca-gen-addon-readme !!
!! changes will be overwritten. !!
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
-!! source digest: sha256:c609033733302fa71a3c01c11e2729fd2b47ccde0b9a1d0619bed03cc26db4fe
+!! source digest: sha256:27258fb23153f2660be19d7c76b04c4d09d35d1e240b779076c7f4a9fa66d9f2
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->

Base EDI backend.
@@ -384,7 +384,14 @@ EDI
EDI Exchange Type, to define file types of exchange
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.
Table of contents
+
+
+
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.
+
+
+
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
+
+
+
+
+
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
+
+
+
-
+
After certain operations or manual execution, Exchange records will be
generated. This Exchange records might be input records or outputs
records.
The change of state can be manually executed by the system or be managed
through by ir.cron.
-
+
An output record is intended to be used for exchange information from
Odoo to another system.
The flow of an output record should be:
@@ -509,7 +619,7 @@
-
+
-
+
The module name has been changed from edi to edi_oca.
-
+
Components dependancy has been removed and set on a new dependant module
edi_component_oca. Module edi_oca has been_renamed to
edi_core_oca.
+
+
+
+
+
+
+
+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.
+(#global-edi-conf-events)
+
+
+
+
+
-
+
Bugs are tracked on GitHub Issues.
In case of trouble, please check there if your issue has already been reported.
If you spotted it first, help us to smash it by providing a detailed and welcomed
@@ -543,9 +684,9 @@
Do not contact contributors directly about support or help with technical issues.