From a7b668995e51378da9590e63764e97579dd282f1 Mon Sep 17 00:00:00 2001 From: Hembert Iregui Date: Tue, 9 Jun 2026 15:53:39 -0400 Subject: [PATCH] [FIX] Auditlog: Maximum recursion error when creating new accounts #3512 --- auditlog/README.rst | 16 ++++++ auditlog/models/auditlog_rule.py | 23 +++++--- auditlog/readme/CONTRIBUTORS.md | 3 + auditlog/static/description/index.html | 4 ++ test_auditlog/tests/__init__.py | 1 + .../tests/test_account_reentrancy.py | 56 +++++++++++++++++++ 6 files changed, 94 insertions(+), 9 deletions(-) create mode 100644 test_auditlog/tests/test_account_reentrancy.py diff --git a/auditlog/README.rst b/auditlog/README.rst index 1d797d593ea..db8e811ec50 100644 --- a/auditlog/README.rst +++ b/auditlog/README.rst @@ -112,19 +112,35 @@ Contributors ------------ - Sebastien Alix + - Holger Brunn + - Holden Rehg + - Eric Lembregts + - Pieter Paulussen + - Alan Ramos + - Stefan Rijnhart + - Bhavesh Odedra + - Hardik Suthar + - Kitti U. + - Bogdan Valentin Gabor + - Dennis Sluijk d.sluijk@onestein.nl + - Adam Heinz +- `OERP Canada `__: + + - Hembert Iregui < + Other credits ------------- diff --git a/auditlog/models/auditlog_rule.py b/auditlog/models/auditlog_rule.py index 8c90c452bd7..137cf766938 100644 --- a/auditlog/models/auditlog_rule.py +++ b/auditlog/models/auditlog_rule.py @@ -365,7 +365,7 @@ def _make_create(self): @api.model_create_multi def create_full(self, vals_list, **kwargs): - self = self.with_context(auditlog_disabled=True) + self = self.with_context(auditlog_disabled_read=True) rule_model = self.env["auditlog.rule"] new_records = create_full.origin(self, vals_list, **kwargs) # Take a snapshot of record values from the cache instead of using @@ -400,7 +400,7 @@ def create_full(self, vals_list, **kwargs): @api.model_create_multi def create_fast(self, vals_list, **kwargs): - self = self.with_context(auditlog_disabled=True) + self = self.with_context(auditlog_disabled_read=True) rule_model = self.env["auditlog.rule"] vals_list = rule_model._update_vals_list(vals_list) vals_list2 = copy.deepcopy(vals_list) @@ -443,9 +443,9 @@ def read(self, fields=None, load="_classic_read", **kwargs): # avoid logs on `read` produced by auditlog during internal # processing: read data of relevant records, 'ir.model', # 'ir.model.fields'... (no interest in logging such operations) - if self.env.context.get("auditlog_disabled"): + if self.env.context.get("auditlog_disabled_read"): return result - self = self.with_context(auditlog_disabled=True) + self = self.with_context(auditlog_disabled_read=True) rule_model = self.env["auditlog.rule"] if self.env.user in users_to_exclude: return result @@ -469,7 +469,12 @@ def _make_write(self): users_to_exclude = self.mapped("users_to_exclude_ids") def write_full(self, vals, **kwargs): - self = self.with_context(auditlog_disabled=True) + guard = self.env.context.get("auditlog_guard", set()) + guard_keys = {(self._name, tuple(self.ids))} + if bool(guard.intersection(guard_keys)): + return write_full.origin(self, vals, **kwargs) + guard.update(guard_keys) + self = self.with_context(auditlog_guard=guard, auditlog_disabled_read=True) rule_model = self.env["auditlog.rule"] fields_list = rule_model.get_auditlog_fields(self) records_write = ( @@ -503,7 +508,7 @@ def write_full(self, vals, **kwargs): return result def write_fast(self, vals, **kwargs): - self = self.with_context(auditlog_disabled=True) + self = self.with_context(auditlog_disabled_read=True) rule_model = self.env["auditlog.rule"] # Log the user input only, no matter if the `vals` is updated # afterwards as it could not represent the real state @@ -535,7 +540,7 @@ def _make_unlink(self): users_to_exclude = self.mapped("users_to_exclude_ids") def unlink_full(self, **kwargs): - self = self.with_context(auditlog_disabled=True) + self = self.with_context(auditlog_disabled_read=True) rule_model = self.env["auditlog.rule"] fields_list = rule_model.get_auditlog_fields(self) old_values = { @@ -558,7 +563,7 @@ def unlink_full(self, **kwargs): return unlink_full.origin(self, **kwargs) def unlink_fast(self, **kwargs): - self = self.with_context(auditlog_disabled=True) + self = self.with_context(auditlog_disabled_read=True) rule_model = self.env["auditlog.rule"] if self.env.user in users_to_exclude: return unlink_fast.origin(self, **kwargs) @@ -583,7 +588,7 @@ def _make_export_data(self): def export_data(self, fields_to_export): res = export_data.origin(self, fields_to_export) - self = self.with_context(auditlog_disabled=True) + self = self.with_context(auditlog_disabled_read=True) rule_model = self.env["auditlog.rule"] if self.env.user in users_to_exclude: return res diff --git a/auditlog/readme/CONTRIBUTORS.md b/auditlog/readme/CONTRIBUTORS.md index 761a38b9368..9aa52f6d905 100644 --- a/auditlog/readme/CONTRIBUTORS.md +++ b/auditlog/readme/CONTRIBUTORS.md @@ -11,3 +11,6 @@ - Bogdan Valentin Gabor \<\> - Dennis Sluijk - Adam Heinz \<\> +- [OERP Canada](https://www.oerp.ca/): + + - Hembert Iregui \< diff --git a/auditlog/static/description/index.html b/auditlog/static/description/index.html index 4d8467d0a1f..72542088afc 100644 --- a/auditlog/static/description/index.html +++ b/auditlog/static/description/index.html @@ -462,6 +462,10 @@

Contributors

  • Bogdan Valentin Gabor <valentin.gabor@bt-group.com>
  • Dennis Sluijk d.sluijk@onestein.nl
  • Adam Heinz <adam.heinz@metricwise.com>
  • +
  • OERP Canada: +
  • diff --git a/test_auditlog/tests/__init__.py b/test_auditlog/tests/__init__.py index 9678814e1ad..10c5f5c6aae 100644 --- a/test_auditlog/tests/__init__.py +++ b/test_auditlog/tests/__init__.py @@ -1,3 +1,4 @@ from . import test_account_bank_statement_line from . import test_account_move_reverse from . import test_product_tax_multicompany +from . import test_account_reentrancy diff --git a/test_auditlog/tests/test_account_reentrancy.py b/test_auditlog/tests/test_account_reentrancy.py new file mode 100644 index 00000000000..a75bde92174 --- /dev/null +++ b/test_auditlog/tests/test_account_reentrancy.py @@ -0,0 +1,56 @@ +from odoo.tests.common import tagged + +from odoo.addons.auditlog.tests.common import AuditLogRuleCommon + + +@tagged("post_install", "-at_install") +class TestAccountAuditlog(AuditLogRuleCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.account_model_id = cls.env.ref("account.model_account_account").id + cls.rule = cls.create_rule( + { + "name": "Account Audit Rule", + "model_id": cls.account_model_id, + "log_create": True, + "log_write": True, + "log_read": True, + "log_unlink": True, + "log_type": "full", + } + ) + cls.rule.set_to_confirmed() + + def test_write_code_reentrancy(self): + """This test verifies the infinite recursion scenario + that occurs when a field defines both _compute_code and _inverse_code. + It ensures that the introduced guard correctly prevents re-entering + the same field's compute/inverse cycle, avoiding recursive execution + while preserving the expected field behavior. + """ + account = self.env["account.account"].create( + { + "name": "Test Account", + "code": "1000", + } + ) + logs = self.env["auditlog.log"].search( + [ + ("model_id", "=", self.account_model_id), + ("method", "=", "write"), + ("res_id", "=", account.id), + ] + ) + self.assertTrue( + logs.line_ids.filtered(lambda log_line: log_line.field_name == "code_store") + ) + account.write({"code": "2000"}) + logs = self.env["auditlog.log"].search( + [ + ("model_id", "=", self.account_model_id), + ("method", "=", "write"), + ("res_id", "=", account.id), + ] + ) + self.assertEqual(len(logs), 2)