From 581be1c4b3355c3ced86d6d28d64f5b41cb61f1e Mon Sep 17 00:00:00 2001 From: Arnau Date: Thu, 12 Mar 2026 11:18:28 +0100 Subject: [PATCH 1/9] [REF] edi_core: move back ``_trigger_edi_event_make_name`` to core --- edi_component_oca/models/edi_exchange_record.py | 6 ------ edi_core_oca/models/edi_exchange_record.py | 6 ++++++ 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/edi_component_oca/models/edi_exchange_record.py b/edi_component_oca/models/edi_exchange_record.py index 551d1fa6b..c37c17f93 100644 --- a/edi_component_oca/models/edi_exchange_record.py +++ b/edi_component_oca/models/edi_exchange_record.py @@ -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) diff --git a/edi_core_oca/models/edi_exchange_record.py b/edi_core_oca/models/edi_exchange_record.py index a87b4506e..4cc9d86b7 100644 --- a/edi_core_oca/models/edi_exchange_record.py +++ b/edi_core_oca/models/edi_exchange_record.py @@ -536,6 +536,12 @@ def _trigger_edi_event(self, name, suffix=None, target=None, **kw): """Hook to be implemented in other modules""" pass + 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")) self._trigger_edi_event("done") From f91e4244b7c7d0000c7552f2b658fec72a0da4b3 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Thu, 14 May 2026 10:40:56 +0200 Subject: [PATCH 2/9] [IMP[ edi_core: add option for global edi.configuration Global configurations can be used to catch generic events not bound to specific partner releations. As such, they are the core framework replacement for component events. Co-authored-by: Arnau --- edi_core_oca/models/edi_configuration.py | 7 +++++++ edi_core_oca/views/edi_configuration_views.xml | 1 + 2 files changed, 8 insertions(+) diff --git a/edi_core_oca/models/edi_configuration.py b/edi_core_oca/models/edi_configuration.py index 93d7412f3..795d141ce 100644 --- a/edi_core_oca/models/edi_configuration.py +++ b/edi_core_oca/models/edi_configuration.py @@ -67,6 +67,13 @@ 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): diff --git a/edi_core_oca/views/edi_configuration_views.xml b/edi_core_oca/views/edi_configuration_views.xml index 053db7764..674e88f8b 100644 --- a/edi_core_oca/views/edi_configuration_views.xml +++ b/edi_core_oca/views/edi_configuration_views.xml @@ -74,6 +74,7 @@ name="model_id" options="{'no_create': True, 'no_create_edit': True}" /> + From 4d28237b9b753034c27ef4aedf33eef68d3d3301 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Thu, 14 May 2026 11:02:55 +0200 Subject: [PATCH 3/9] [IMP] edi_core: trigger events w/ global edi.conf Co-authored-by: Arnau --- edi_core_oca/data/edi_configuration.xml | 12 ++++++++++++ edi_core_oca/models/edi_exchange_record.py | 14 ++++++++++++-- 2 files changed, 24 insertions(+), 2 deletions(-) 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/models/edi_exchange_record.py b/edi_core_oca/models/edi_exchange_record.py index 4cc9d86b7..ee86c86d3 100644 --- a/edi_core_oca/models/edi_exchange_record.py +++ b/edi_core_oca/models/edi_exchange_record.py @@ -533,8 +533,18 @@ 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"].search( + [ + ("trigger", "=", event_name), + ("is_global", "=", True), + ] + ) + + 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( From 0f28c663648ba7e9265b42bd00356a51182bc213 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Thu, 14 May 2026 11:15:31 +0200 Subject: [PATCH 4/9] [IMP] edi_core: improve global edi.conf * full matching by type, backend and model * full test coverage --- edi_core_oca/models/edi_configuration.py | 29 +++ edi_core_oca/models/edi_exchange_record.py | 9 +- edi_core_oca/tests/test_edi_configuration.py | 251 +++++++++++++++++++ 3 files changed, 282 insertions(+), 7 deletions(-) diff --git a/edi_core_oca/models/edi_configuration.py b/edi_core_oca/models/edi_configuration.py index 795d141ce..9d08a308c 100644 --- a/edi_core_oca/models/edi_configuration.py +++ b/edi_core_oca/models/edi_configuration.py @@ -208,6 +208,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 ee86c86d3..685a98bb0 100644 --- a/edi_core_oca/models/edi_exchange_record.py +++ b/edi_core_oca/models/edi_exchange_record.py @@ -535,14 +535,9 @@ def _notify_related_record(self, message, level="info"): def _trigger_edi_event(self, name, suffix=None, target=None, **kw): event_name = self._trigger_edi_event_make_name(name, suffix) target = target or self - - global_configs = self.env["edi.configuration"].search( - [ - ("trigger", "=", event_name), - ("is_global", "=", True), - ] + 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) diff --git a/edi_core_oca/tests/test_edi_configuration.py b/edi_core_oca/tests/test_edi_configuration.py index 689a8f6c1..3e06cd944 100644 --- a/edi_core_oca/tests/test_edi_configuration.py +++ b/edi_core_oca/tests/test_edi_configuration.py @@ -152,3 +152,254 @@ def test_edi_code_snippet(self): ) # Check the new vals after execution self.assertEqual(vals, expected_value) + + +class TestEDIConfigurationGlobalEvents(EDIBackendCommonTestCase): + """Test the global event dispatch via edi.configuration. + + `EDIExchangeRecord._trigger_edi_event` looks up all `edi.configuration` + records flagged as `is_global` and matching the event trigger code, + then executes their `snippet_do` against the target record. + These tests verify the dispatch happens for all `notify_*` events + and that the proper target (exchange record vs related record) + is passed to the snippet. + """ + + # Snippet appends a marker per call so we can verify multiple invocations + # against different targets within the same transaction. + _marker_snippet = "conf.write({'description': (conf.description or '') + '|' + record._name})" + + @classmethod + def setUpClass(cls): + super().setUpClass() + vals = { + "model": cls.partner._name, + "res_id": cls.partner.id, + } + cls.record = cls.backend.create_record("test_csv_output", vals) + cls.trigger_model = cls.env["edi.configuration.trigger"] + cls.conf_model = cls.env["edi.configuration"] + # Reuse existing data triggers when available, create the missing ones. + cls.trigger_done = cls.env.ref("edi_core_oca.edi_config_trigger_record_done") + cls.trigger_error = cls.env.ref("edi_core_oca.edi_config_trigger_record_error") + cls.trigger_ack_received = cls._get_or_create_trigger( + "on_edi_exchange_done_ack_received", "On ACK received" + ) + cls.trigger_ack_missing = cls._get_or_create_trigger( + "on_edi_exchange_done_ack_missing", "On ACK missing" + ) + cls.trigger_ack_received_error = cls._get_or_create_trigger( + "on_edi_exchange_done_ack_received_error", "On ACK received error" + ) + cls.trigger_generate_complete = cls._get_or_create_trigger( + "on_edi_exchange_generate_complete", "On generate complete" + ) + + @classmethod + def _get_or_create_trigger(cls, code, name): + trigger = cls.trigger_model.search([("code", "=", code)], limit=1) + if not trigger: + trigger = cls.trigger_model.create({"name": name, "code": code}) + return trigger + + def _make_conf(self, trigger, name, is_global=True, snippet=None, **overrides): + vals = { + "name": name, + "active": True, + "backend_id": self.backend.id, + "type_id": self.exchange_type_out.id, + "trigger_id": trigger.id, + "is_global": is_global, + "snippet_do": snippet or self._marker_snippet, + } + vals.update(overrides) + return self.conf_model.create(vals) + + def test_notify_done_triggers_global_conf(self): + conf = self._make_conf(self.trigger_done, "Global Done") + self.record._notify_done() + self.assertEqual(conf.description, f"|{self.record._name}") + + def test_notify_error_triggers_global_conf(self): + conf = self._make_conf(self.trigger_error, "Global Error") + self.record._notify_error("send_ko") + self.assertEqual(conf.description, f"|{self.record._name}") + + def test_notify_ack_received_triggers_global_conf(self): + conf = self._make_conf(self.trigger_ack_received, "Global ACK received") + self.record._notify_ack_received() + self.assertEqual(conf.description, f"|{self.record._name}") + + def test_notify_ack_missing_triggers_global_conf(self): + conf = self._make_conf(self.trigger_ack_missing, "Global ACK missing") + self.record._notify_ack_missing() + self.assertEqual(conf.description, f"|{self.record._name}") + + def test_notify_ack_received_error_triggers_global_conf(self): + conf = self._make_conf( + self.trigger_ack_received_error, "Global ACK received error" + ) + self.record._notify_ack_received_error() + self.assertEqual(conf.description, f"|{self.record._name}") + + def test_non_global_conf_is_ignored(self): + conf = self._make_conf(self.trigger_done, "Non Global Done", is_global=False) + self.record._notify_done() + self.assertFalse(conf.description) + + def test_inactive_global_conf_is_ignored(self): + conf = self._make_conf(self.trigger_done, "Inactive Global Done") + conf.active = False + self.record._notify_done() + self.assertFalse(conf.description) + + def test_notify_action_complete_dispatches_to_both_targets(self): + """`notify_action_complete` fires the event twice when the related + record exists: once with the exchange record as target, once with the + related record (partner here).""" + conf = self._make_conf( + self.trigger_generate_complete, "Global generate complete" + ) + # Sanity check: the exchange record has a related record. + self.assertTrue(self.record.related_record_exists) + self.record.notify_action_complete("generate") + # The snippet appended one marker per call: exchange record then partner. + self.assertEqual( + conf.description, + f"|{self.record._name}|{self.partner._name}", + ) + + def test_notify_action_complete_no_related_record(self): + """When no related record exists, the event fires only on the + exchange record itself.""" + conf = self._make_conf( + self.trigger_generate_complete, "Global generate complete - no related" + ) + # Create an exchange record with no related record. + orphan_record = self.backend.create_record( + "test_csv_output", {"model": False, "res_id": False} + ) + orphan_record.notify_action_complete("generate") + self.assertEqual(conf.description, f"|{orphan_record._name}") + + def test_snippet_receives_conf_and_record(self): + """The snippet eval context must expose both `conf` (the configuration) + and `record` (the target of the event).""" + snippet = ( + "conf.write({'description': 'conf=%s|record=%s' % " + "(conf.name, record.display_name)})" + ) + conf = self._make_conf(self.trigger_done, "Context check", snippet=snippet) + self.record._notify_done() + self.assertEqual( + conf.description, + f"conf={conf.name}|record={self.record.display_name}", + ) + + def test_multiple_global_confs_all_executed(self): + """All global confs matching the trigger are executed.""" + conf1 = self._make_conf(self.trigger_done, "Global Done 1") + conf2 = self._make_conf(self.trigger_done, "Global Done 2") + self.record._notify_done() + self.assertEqual(conf1.description, f"|{self.record._name}") + self.assertEqual(conf2.description, f"|{self.record._name}") + + # ------------------------------------------------------------------ + # Filtering tests for `edi_get_conf_global` + # ------------------------------------------------------------------ + def test_filter_by_type_mismatch(self): + """A conf bound to a different exchange type must not fire.""" + conf = self._make_conf( + self.trigger_done, + "Wrong type", + type_id=self.exchange_type_in.id, + ) + self.record._notify_done() + self.assertFalse(conf.description) + + def test_filter_by_type_empty_matches(self): + """A conf without a type matches any exchange record's type.""" + conf = self._make_conf(self.trigger_done, "No type", type_id=False) + self.record._notify_done() + self.assertEqual(conf.description, f"|{self.record._name}") + + def test_filter_by_backend_mismatch(self): + """A conf bound to a different backend must not fire.""" + other_backend = self.env["edi.backend"].create( + { + "name": "Other backend", + "backend_type_id": self.backend.backend_type_id.id, + } + ) + # `_constrains_backend` requires backend to be compatible with the type's + # backend if the type has one set. Detach the type from the conf to test + # only the backend filter. + conf = self._make_conf( + self.trigger_done, + "Wrong backend", + backend_id=other_backend.id, + type_id=False, + ) + self.record._notify_done() + self.assertFalse(conf.description) + + def test_filter_by_backend_empty_matches(self): + """A conf without a backend matches any exchange record's backend.""" + conf = self._make_conf( + self.trigger_done, + "No backend", + backend_id=False, + type_id=False, + ) + self.record._notify_done() + self.assertEqual(conf.description, f"|{self.record._name}") + + def test_filter_by_model_mismatch(self): + """A conf bound to a different model must not fire.""" + other_model = self.env["ir.model"]._get("res.users") + conf = self._make_conf( + self.trigger_done, + "Wrong model", + model_id=other_model.id, + ) + self.record._notify_done() + self.assertFalse(conf.description) + + def test_filter_by_model_match(self): + """A conf bound to the related record model fires.""" + partner_model = self.env["ir.model"]._get(self.partner._name) + conf = self._make_conf( + self.trigger_done, + "Matching model", + model_id=partner_model.id, + ) + self.record._notify_done() + self.assertEqual(conf.description, f"|{self.record._name}") + + def test_filter_by_model_orphan_record(self): + """A conf with a model is skipped on records with no related model.""" + partner_model = self.env["ir.model"]._get(self.partner._name) + conf_with_model = self._make_conf( + self.trigger_done, + "Model bound", + model_id=partner_model.id, + ) + conf_no_model = self._make_conf(self.trigger_done, "Model-less") + orphan_record = self.backend.create_record( + "test_csv_output", {"model": False, "res_id": False} + ) + orphan_record._notify_done() + self.assertFalse(conf_with_model.description) + self.assertEqual(conf_no_model.description, f"|{orphan_record._name}") + + def test_edi_get_conf_global_returns_only_matching(self): + """Direct check on the new helper method.""" + matching = self._make_conf(self.trigger_done, "Matching") + wrong_trigger = self._make_conf(self.trigger_error, "Wrong trigger") + non_global = self._make_conf(self.trigger_done, "Non global", is_global=False) + result = self.env["edi.configuration"].edi_get_conf_global( + self.record, self.trigger_done.code + ) + self.assertIn(matching, result) + self.assertNotIn(wrong_trigger, result) + self.assertNotIn(non_global, result) From 9dc23550f3785df93515820e0b25aab445ad4ea0 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Thu, 14 May 2026 11:38:31 +0200 Subject: [PATCH 5/9] [DOC] edi_core: add minimal docs for edi.conf --- edi_core_oca/readme/CONFIGURE.md | 95 ++++++++++++++++++++++++++++++ edi_core_oca/readme/DESCRIPTION.md | 9 ++- 2 files changed, 103 insertions(+), 1 deletion(-) 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. From abeda5ea3c3d3d01be032046ea9a505bb653839e Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Thu, 14 May 2026 11:39:43 +0200 Subject: [PATCH 6/9] [DOC] edi_core: add readme/newsfragments --- edi_core_oca/readme/newsfragments/.gitkeep | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 edi_core_oca/readme/newsfragments/.gitkeep diff --git a/edi_core_oca/readme/newsfragments/.gitkeep b/edi_core_oca/readme/newsfragments/.gitkeep new file mode 100644 index 000000000..e69de29bb From bbc55a344429755122a2229201510a2a9a04df7c Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Thu, 14 May 2026 11:54:21 +0200 Subject: [PATCH 7/9] [DOC] edi_core: update newfragments --- .../global-edi-conf-events.feature | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 edi_core_oca/readme/newsfragments/global-edi-conf-events.feature diff --git a/edi_core_oca/readme/newsfragments/global-edi-conf-events.feature b/edi_core_oca/readme/newsfragments/global-edi-conf-events.feature new file mode 100644 index 000000000..d2678fe28 --- /dev/null +++ b/edi_core_oca/readme/newsfragments/global-edi-conf-events.feature @@ -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``, ``_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. From 7ea42af10a5c6faec7efddb2e99e5e46317ed155 Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Thu, 14 May 2026 13:02:54 +0200 Subject: [PATCH 8/9] [FIX] edi_core: fix edi.configuration._constrains_backend No comparison if backend is not set or type is not set. --- edi_core_oca/models/edi_configuration.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/edi_core_oca/models/edi_configuration.py b/edi_core_oca/models/edi_configuration.py index 9d08a308c..82bade7d5 100644 --- a/edi_core_oca/models/edi_configuration.py +++ b/edi_core_oca/models/edi_configuration.py @@ -78,12 +78,14 @@ class EdiConfiguration(models.Model): @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._( From 40e51cf2942d78ed8dfab208fc3034b7beb61e9b Mon Sep 17 00:00:00 2001 From: Simone Orsi Date: Thu, 14 May 2026 13:03:37 +0200 Subject: [PATCH 9/9] fixup! [IMP] edi_core: improve global edi.conf --- edi_core_oca/tests/test_edi_configuration.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/edi_core_oca/tests/test_edi_configuration.py b/edi_core_oca/tests/test_edi_configuration.py index 3e06cd944..d624b697e 100644 --- a/edi_core_oca/tests/test_edi_configuration.py +++ b/edi_core_oca/tests/test_edi_configuration.py @@ -167,7 +167,9 @@ class TestEDIConfigurationGlobalEvents(EDIBackendCommonTestCase): # Snippet appends a marker per call so we can verify multiple invocations # against different targets within the same transaction. - _marker_snippet = "conf.write({'description': (conf.description or '') + '|' + record._name})" + _marker_snippet = ( + "conf.write({'description': (conf.description or '') + '|' + record._name})" + ) @classmethod def setUpClass(cls):