diff --git a/edi_core_oca/__manifest__.py b/edi_core_oca/__manifest__.py
index 2d91d84a0..6274a6c3c 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.3",
+ "version": "18.0.1.6.4",
"website": "https://github.com/OCA/edi-framework",
"development_status": "Beta",
"license": "LGPL-3",
diff --git a/edi_core_oca/migrations/18.0.1.6.4/post-mig.py b/edi_core_oca/migrations/18.0.1.6.4/post-mig.py
new file mode 100644
index 000000000..6b52c0399
--- /dev/null
+++ b/edi_core_oca/migrations/18.0.1.6.4/post-mig.py
@@ -0,0 +1,24 @@
+# Copyright 2026 Camptocamp SA (http://www.camptocamp.com)
+# License LGPL-3.0 or later (http://www.gnu.org/licenses/lgpl).
+
+from logging import getLogger
+
+from openupgradelib import openupgrade
+
+_logger = getLogger(__name__)
+
+
+@openupgrade.migrate()
+def migrate(env, version):
+ xmlid = "edi_core_oca.rule_edi_exchange_record_user"
+ if rule := env.ref(xmlid, False):
+ old_domain = (rule.domain_force or "").strip()
+ new_domain = ["|", ("model", "!=", False), ("res_id", "=", 0)]
+ _logger.info(
+ f"Updating {rule} ({xmlid=}) domain:\n"
+ f" - old: {old_domain}\n"
+ f" - new: {new_domain}"
+ )
+ rule.domain_force = new_domain
+ else:
+ _logger.warning(f"No rule found with XMLID '{xmlid}', skipping...")
diff --git a/edi_core_oca/security/ir_model_access.xml b/edi_core_oca/security/ir_model_access.xml
index 8c85f038b..31c9c73ae 100644
--- a/edi_core_oca/security/ir_model_access.xml
+++ b/edi_core_oca/security/ir_model_access.xml
@@ -122,7 +122,7 @@
['|', ('model','!=', False), ('res_id', '=', False)]
+ >['|', ('model', '!=', False), ('res_id', '=', 0)]
diff --git a/edi_core_oca/tests/test_security.py b/edi_core_oca/tests/test_security.py
index c0f7b1b9b..c26c62e4e 100644
--- a/edi_core_oca/tests/test_security.py
+++ b/edi_core_oca/tests/test_security.py
@@ -303,3 +303,35 @@ def test_search_pagination_with_inaccessible_middle_records(self):
# The records fetched from the second page must be present in the final result
self.assertIn(visible_id_2, records.ids)
+
+ def test_search_no_res_id(self):
+ """Test Exc Rec visibility for internal users when ``res_id`` is False-ish
+
+ Exchange Record's ``res_id`` is a ``Many2onReference`` field, which internally
+ converts False-ish values to 0 before storing them to the cache and the DB.
+ The rule's domain old leaf ``('res_id', '=', False)`` was instead converted to a
+ SQL query clause ``WHERE "edi_exchange_record.res_id" IS NULL``.
+ Since all ``edi_exchange_record`` rows contain a non-negative integer in the
+ ``res_id`` column, the rule old domain leaf always failed to fetch any record.
+
+ Changing the leaf to ``('res_id', '=', 0)`` fixes the issue, making such
+ Exchange Records visible again for internal users.
+ """
+ # Add the test user to the internal users group
+ self.user.write({"groups_id": [(4, self.env.ref("base.group_user").id)]})
+
+ # Create Exchange Records with no model (condition ``('model', '!=', False)``
+ # will fail) and False-ish record ID (to test condition ``('res_id', '=', 0)``):
+ # such False-ish values are all converted to 0 by ``fields.Many2oneReference``
+ # methods (and methods of its superclasses) when updating the cache values and
+ # preparing SQL queries to flush to the DB
+ exc_recs = self.env["edi.exchange.record"]
+ type_code = "test_csv_output"
+ vals = {"model": False}
+ for res_id in (0, 0.00, False, None, "", self.env["base"]):
+ exc_recs += self.backend.create_record(type_code, vals | {"res_id": res_id})
+ self.assertEqual(exc_recs.mapped("res_id"), [0] * len(exc_recs))
+
+ # Check that the test user can actually fetch such records
+ exc_recs_model = self.env["edi.exchange.record"].with_user(self.user)
+ self.assertEqual(exc_recs_model.search([("id", "in", exc_recs.ids)]), exc_recs)