diff --git a/edi_core_oca/models/edi_backend.py b/edi_core_oca/models/edi_backend.py
index d4af65ff6..937a143d5 100644
--- a/edi_core_oca/models/edi_backend.py
+++ b/edi_core_oca/models/edi_backend.py
@@ -370,6 +370,8 @@ def _output_new_records_domain(self, record_ids=None):
("backend_id", "=", self.id),
("type_id.exchange_file_auto_generate", "=", True),
("type_id.direction", "=", "output"),
+ ("type_id.generate_model_id", "!=", False),
+ ("type_id.send_model_id", "!=", False),
("edi_exchange_state", "=", "new"),
("exchange_file", "=", False),
]
@@ -388,6 +390,7 @@ def _output_pending_records_domain(self, skip_sent=True, record_ids=None):
states += ("output_sent",)
domain = [
("type_id.direction", "=", "output"),
+ ("type_id.send_model_id", "!=", False),
("backend_id", "=", self.id),
("edi_exchange_state", "in", states),
]
@@ -575,6 +578,7 @@ def _input_pending_records_domain(self, record_ids=None):
domain = [
("backend_id", "=", self.id),
("type_id.direction", "=", "input"),
+ ("type_id.receive_model_id", "!=", False),
("edi_exchange_state", "=", "input_pending"),
("exchange_file", "=", False),
]
@@ -587,6 +591,7 @@ def _input_pending_process_records_domain(self, record_ids=None):
domain = [
("backend_id", "=", self.id),
("type_id.direction", "=", "input"),
+ ("type_id.process_model_id", "!=", False),
("edi_exchange_state", "in", states),
]
if record_ids:
diff --git a/edi_core_oca/models/edi_exchange_type.py b/edi_core_oca/models/edi_exchange_type.py
index d6a2cab96..2f602ac6a 100644
--- a/edi_core_oca/models/edi_exchange_type.py
+++ b/edi_core_oca/models/edi_exchange_type.py
@@ -240,6 +240,32 @@ def get_settings(self):
def set_settings(self, val):
self.advanced_settings_edit = val
+ @api.constrains(
+ "direction",
+ "send_model_id",
+ "process_model_id",
+ "generate_model_id",
+ "exchange_file_auto_generate",
+ )
+ def _check_direction_handlers(self):
+ for rec in self:
+ if rec.direction == "output":
+ if not rec.send_model_id:
+ raise exceptions.ValidationError(
+ self.env._("Output exchange types require a Sender handler.")
+ )
+ if rec.exchange_file_auto_generate and not rec.generate_model_id:
+ raise exceptions.ValidationError(
+ self.env._(
+ "Output exchange types with 'Auto Generate' enabled "
+ "require a Generator handler."
+ )
+ )
+ elif rec.direction == "input" and not rec.process_model_id:
+ raise exceptions.ValidationError(
+ self.env._("Input exchange types require a Processor handler.")
+ )
+
@api.constrains("backend_id", "backend_type_id")
def _check_backend(self):
for rec in self:
diff --git a/edi_core_oca/tests/common.py b/edi_core_oca/tests/common.py
index fe9bdb9d1..0ed40d75a 100644
--- a/edi_core_oca/tests/common.py
+++ b/edi_core_oca/tests/common.py
@@ -5,6 +5,7 @@
import os
+from odoo.orm.model_classes import add_to_registry
from odoo.tests.common import TransactionCase
@@ -19,6 +20,24 @@ def _setup_context(cls, **kw):
def _setup_env(cls, ctx=None):
ctx = ctx or {}
cls.env = cls.env(context=cls._setup_context(**ctx))
+ # Register EdiTestExecution early so _create_exchange_type can set default
+ # handler models — the new @api.constrains on edi.exchange.type requires
+ # send_model_id (output) and process_model_id (input) to be set.
+ # Guard prevents double-registration when individual test classes also call
+ # add_to_registry; pop() in cleanup is safe even if __delitem__ already ran.
+ if "edi.framework.test.execution" not in cls.registry:
+ from .fake_models import EdiTestExecution
+
+ add_to_registry(cls.registry, EdiTestExecution)
+ cls.registry._setup_models__(cls.env.cr, ["edi.framework.test.execution"])
+ cls.registry.init_models(
+ cls.env.cr, ["edi.framework.test.execution"], {"models_to_check": True}
+ )
+ cls.addClassCleanup(
+ lambda: cls.registry.__delitem__("edi.framework.test.execution")
+ if "edi.framework.test.execution" in cls.registry
+ else None
+ )
@classmethod
def _setup_records(cls):
@@ -84,6 +103,19 @@ def _get_backend(cls):
@classmethod
def _create_exchange_type(cls, **kw):
+ # Mirror the pattern in edi_component_oca/tests/common.py: provide default
+ # handler models so every test exchange type satisfies the new constraint.
+ # Callers can override individual fields by passing explicit values.
+ if "edi.framework.test.execution" in cls.registry:
+ handler_model = cls.env["ir.model"]._get("edi.framework.test.execution")
+ if handler_model:
+ kw.setdefault("receive_model_id", handler_model.id)
+ kw.setdefault("generate_model_id", handler_model.id)
+ kw.setdefault("input_validate_model_id", handler_model.id)
+ kw.setdefault("output_validate_model_id", handler_model.id)
+ kw.setdefault("send_model_id", handler_model.id)
+ kw.setdefault("process_model_id", handler_model.id)
+ kw.setdefault("check_model_id", handler_model.id)
model = cls.env["edi.exchange.type"]
vals = {
"name": "Test CSV exchange",
diff --git a/edi_core_oca/tests/fake_models.py b/edi_core_oca/tests/fake_models.py
index df7d5e25c..badea718a 100644
--- a/edi_core_oca/tests/fake_models.py
+++ b/edi_core_oca/tests/fake_models.py
@@ -33,6 +33,7 @@ def _get_edi_exchange_record_name(self, exchange_record):
class EdiTestExecution(models.AbstractModel):
_name = "edi.framework.test.execution"
_inherit = [
+ "edi.oca.handler.generate",
"edi.oca.handler.process",
"edi.oca.handler.check",
"edi.oca.handler.send",
diff --git a/edi_core_oca/tests/test_backend_base.py b/edi_core_oca/tests/test_backend_base.py
index 87219bea0..dc401f7c0 100644
--- a/edi_core_oca/tests/test_backend_base.py
+++ b/edi_core_oca/tests/test_backend_base.py
@@ -85,9 +85,15 @@ def test_get_handler_avg_user(self):
self._test_get_handler(user=user)
def test_get_handler_no_handler(self):
- self.exchange_type_in.process_model_id = False
- self.exchange_type_in.input_validate_model_id = False
- self.exchange_type_in.receive_model_id = False
+ self.env.cr.execute(
+ """UPDATE edi_exchange_type
+ SET process_model_id = NULL,
+ input_validate_model_id = NULL,
+ receive_model_id = NULL
+ WHERE id = %s""",
+ [self.exchange_type_in.id],
+ )
+ self.exchange_type_in.invalidate_recordset()
vals = {
"model": self.partner._name,
"res_id": self.partner.id,
diff --git a/edi_core_oca/views/edi_exchange_type_views.xml b/edi_core_oca/views/edi_exchange_type_views.xml
index e7f0be5e1..549494b30 100644
--- a/edi_core_oca/views/edi_exchange_type_views.xml
+++ b/edi_core_oca/views/edi_exchange_type_views.xml
@@ -63,6 +63,7 @@