From 760311bb0cbc741e54df633de17dc14f01711984 Mon Sep 17 00:00:00 2001 From: Florian da Costa Date: Thu, 3 Sep 2026 09:21:22 +0200 Subject: [PATCH] [FIX] server_environment_data_encryption: keep sibling env fields cache consistent Writing one env-managed field left the cache of the other env fields stale: they are non-stored computed fields with no field dependency on the encrypted storage, so writing (via the inverse) did not recompute them. A constraint validating a sibling field then read a stale/empty value and raised incorrectly (e.g. microsoft_outlook checking smtp_encryption while saving smtp_authentication). After storing a value, refresh the cache of the sibling env fields from the values just written, skipping fields already in cache so that a multi-field write keeps each fields freshly assigned value. --- .../models/server_env_mixin.py | 21 +++++++++++++++++++ .../tests/models.py | 14 +++++++++++++ .../test_server_environment_data_encrypt.py | 18 ++++++++++++++++ 3 files changed, 53 insertions(+) diff --git a/server_environment_data_encryption/models/server_env_mixin.py b/server_environment_data_encryption/models/server_env_mixin.py index 08d8d5d66..58e0b5369 100644 --- a/server_environment_data_encryption/models/server_env_mixin.py +++ b/server_environment_data_encryption/models/server_env_mixin.py @@ -74,6 +74,27 @@ def _inverse_server_env(self, field_name): encrypted_data_obj._encrypted_store_json( encrypted_data_name, values, env=env ) + # The env fields are non-stored computed fields with no field + # dependency on the storage. Their cache is therefore not + # updated when we store here: refresh the *sibling* fields from + # the values we just wrote, so that a constraint validating a + # sibling field (e.g. microsoft_outlook reading smtp_encryption + # while saving smtp_authentication) sees the current values. + # The written field is left untouched so later inverses of the + # same write still read its freshly-assigned value. + record._update_cache( + { + name: value + for name, value in values.items() + if ( + name in record._fields + and name != field_name + and not record.env.cache.contains( + record, record._fields[name] + ) + ) + } + ) def action_change_env_data_encrypted_fields(self): action_id = self.env.context.get("params", {}).get("action") diff --git a/server_environment_data_encryption/tests/models.py b/server_environment_data_encryption/tests/models.py index b2a76f2ba..7ee8cb4ca 100644 --- a/server_environment_data_encryption/tests/models.py +++ b/server_environment_data_encryption/tests/models.py @@ -1,5 +1,6 @@ # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). from odoo import api, models +from odoo.exceptions import ValidationError # pylint: disable=consider-merging-classes-inherited @@ -13,6 +14,8 @@ def _server_env_fields(self): base_fields = super()._server_env_fields partner_fields = { "city": {}, + "street": {}, + "street2": {}, } partner_fields.update(base_fields) return partner_fields @@ -20,3 +23,14 @@ def _server_env_fields(self): @api.model def _server_env_global_section_name(self): return "partner" + + @api.constrains("street") + def _check_street2_when_street(self): + """A constraint reading a sibling env field (``street2``) while another + env field (``street``) is written. It must see the current value of + ``street2``, not a stale one.""" + for partner in self.filtered(lambda p: p.street): + if not partner.street2: + raise ValidationError( + self.env._("Street2 is required when Street is set.") + ) diff --git a/server_environment_data_encryption/tests/test_server_environment_data_encrypt.py b/server_environment_data_encryption/tests/test_server_environment_data_encrypt.py index 71e6043ff..da4a61e69 100644 --- a/server_environment_data_encryption/tests/test_server_environment_data_encrypt.py +++ b/server_environment_data_encryption/tests/test_server_environment_data_encrypt.py @@ -46,6 +46,24 @@ def test_env_dependent_value(self): self.assertEqual(partner.with_context(environment="test").city, "test city") self.assertEqual(partner.with_context(environment="prod").city, "prod city") + def test_constraint_reads_sibling_env_field(self): + """Writing an env field must not corrupt the cache of sibling env + fields read by a constraint in the same transaction. + + Regression test: after a cache clear followed by a re-read (what the + web client does before saving a form), writing ``street`` used to leave + ``street2`` stale, so the constraint raised although ``street2`` was set. + """ + from odoo.tests import Form + + partner = self.env["res.partner"].create({"name": "Fake name"}) + partner.write({"street": "Test street", "street2": "Test street2"}) + # Simulate the web client save: clear the cache and re-read the record + # before writing, as odoo.tests.Form does. + with Form(partner) as form: + form.street = "New street" + self.assertEqual(partner.street2, "Test street2") + def test_view_with_env_update(self): self.maxDiff = None # common class already set test environment (as default)