From 15530bbd7657e4b6d19abd505c50c756fa6400ed Mon Sep 17 00:00:00 2001 From: Stefan Rijnhart Date: Thu, 18 Dec 2025 15:32:00 +0100 Subject: [PATCH] [FIX] auditlog: drop tocompute register entirely from ThrowAwayCache Fixes #3424 Previous approach was to create a copy, so that the recomputes were also applied to the real world cache. However, this causes an issue when a field is required that is only available in the real world cache, such as the `date` field in ``` @api.depends('date', 'sequence') def _compute_internal_index(self): for st_line in self.filtered(lambda line: line._origin.id): st_line.internal_index = f'{st_line.date.strftime("%Y%m%d")}' ... ``` It actually makes sense to avoid recomputes in this isolated environment that is meant to retrieve the original values from before the write. Fixes ``` File "/odoo/odoo/addons/account/models/account_bank_statement_line.py", line 427, in create st_line.move_id.write(to_write) File "/odoo/server-tools/auditlog/models/rule.py", line 464, in write_full old_values = {d["id"]: d for d in records_write.read(fields_list)} ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/odoo/server-tools/auditlog/models/rule.py", line 406, in read result = read.origin(self, fields, load, **kwargs) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/odoo/odoo/odoo/models.py", line 3857, in read self._origin.fetch(fields) File "/odoo/odoo/odoo/models.py", line 4153, in fetch fetched = self._fetch_query(query, fields_to_fetch) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/odoo/odoo/odoo/models.py", line 4264, in _fetch_query field.read(fetched) File "/odoo/odoo/odoo/fields.py", line 4654, in read lines = comodel.search_fetch(domain, field_names) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/odoo/odoo/odoo/models.py", line 1781, in search_fetch return self._fetch_query(query, fields_to_fetch) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/odoo/odoo/odoo/models.py", line 4240, in _fetch_query rows = self.env.execute_query(query.select(*sql_terms)) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/odoo/odoo/odoo/api.py", line 992, in execute_query self.flush_query(query) File "/odoo/odoo/odoo/api.py", line 984, in flush_query self[model_name].flush_model(field_names) File "/odoo/odoo/odoo/models.py", line 6768, in flush_model self._recompute_model(fnames) File "/odoo/odoo/odoo/models.py", line 7332, in _recompute_model self._recompute_field(field) File "/odoo/odoo/odoo/models.py", line 7360, in _recompute_field field.recompute(records) File "/odoo/odoo/odoo/fields.py", line 1471, in recompute apply_except_missing(self.compute_value, recs) File "/odoo/odoo/odoo/fields.py", line 1444, in apply_except_missing func(records) File "/odoo/odoo/odoo/fields.py", line 1493, in compute_value records._compute_field_value(self) File "/odoo/odoo/odoo/models.py", line 5297, in _compute_field_value fields.determine(field.compute, self) File "/odoo/odoo/odoo/fields.py", line 110, in determine return needle(*args) ^^^^^^^^^^^^^ File "/odoo/odoo/addons/account/models/account_bank_statement_line.py", line 295, in _compute_internal_index st_line.internal_index = f'{st_line.date.strftime("%Y%m%d")}' \ ^^^^^^^^^^^^^^^^^^^^^ AttributeError: 'bool' object has no attribute 'strftime' ``` --- auditlog/models/rule.py | 10 ++-- test_auditlog/tests/__init__.py | 1 + .../tests/test_account_bank_statement_line.py | 52 +++++++++++++++++++ 3 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 test_auditlog/tests/test_account_bank_statement_line.py diff --git a/auditlog/models/rule.py b/auditlog/models/rule.py index 0336ed3e551..32ff17c746c 100644 --- a/auditlog/models/rule.py +++ b/auditlog/models/rule.py @@ -71,10 +71,12 @@ def __enter__(self): don't swap them all out here. """ self._original_cache = self._transaction.cache - # Copy the sets of records, which are popped on recompute but do not - # copy the keys because they do not match the original field object - # afterwards. - self._original_tocompute = defaultdict(OrderedSet) + # Also swap out the list of fields to recompute. Their compute methods + # may depend on fields in the cache that are not yet flushed, and as is + # the case with account.bank.statement.line's _compute_internal_index, + # may not be resilient to some of the values (c.q. 'date') missing. + self._original_tocompute = self._transaction.tocompute + self._transaction.tocompute = defaultdict(OrderedSet) for key, value in self._transaction.tocompute.items(): self._original_tocompute[key] = OrderedSet(value) temporary_cache = api.Cache() diff --git a/test_auditlog/tests/__init__.py b/test_auditlog/tests/__init__.py index febd48f594d..9678814e1ad 100644 --- a/test_auditlog/tests/__init__.py +++ b/test_auditlog/tests/__init__.py @@ -1,2 +1,3 @@ +from . import test_account_bank_statement_line from . import test_account_move_reverse from . import test_product_tax_multicompany diff --git a/test_auditlog/tests/test_account_bank_statement_line.py b/test_auditlog/tests/test_account_bank_statement_line.py new file mode 100644 index 00000000000..01d724212de --- /dev/null +++ b/test_auditlog/tests/test_account_bank_statement_line.py @@ -0,0 +1,52 @@ +from odoo.tests import tagged + +from odoo.addons.account.tests.common import AccountTestInvoicingCommon +from odoo.addons.auditlog.tests.common import AuditLogRuleCommon + + +@tagged("post_install", "-at_install") +class TestAccountBankStatementLine(AccountTestInvoicingCommon, AuditLogRuleCommon): + def setUp(self): + super().setUp() + self.rule = self.env["auditlog.rule"].create( + { + "name": __name__, + "model_id": self.env.ref("account.model_account_move").id, + "log_read": True, + "log_create": True, + "log_write": True, + "log_unlink": True, + "log_type": "full", + } + ) + self.rule.subscribe() + + def test_create_statement_line(self): + """Statement line can be created with logging on journal entries enabled. + + Because we swap out the cache when fetching previous values during full + logging using the ThrowAwayCache, some values that are assumed by + compute methods (c.q. 'date' in account.bank.statement.line's + _compute_internal_index) might be missing. If a recompute of those fields + is inadvertently triggered when using the ThrowAwayCache, the missing + values will raise an exception (in this case: `AttributeError: 'bool' + object has no attribute 'strftime'`). This test verifies that the queued + recomputes are consistent with the values in the cache such that this + exception does not occur. + """ + partner = self.env["res.partner"].create({"name": "test"}) + stmt = self.env["account.bank.statement"].create( + {"journal_id": self.company_data["default_journal_bank"].id} + ) + line = self.env["account.bank.statement.line"].create( + { + "date": "2023-04-01", + "account_number": "NL45 TRIO 0198100000", + "amount": 5.75, + "journal_id": self.company_data["default_journal_bank"].id, + "payment_ref": "1234", + "partner_id": partner.id, + "statement_id": stmt.id, + }, + ) + line.flush_recordset()