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)