Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions auditlog/models/rule.py
Original file line number Diff line number Diff line change
Expand Up @@ -442,6 +442,8 @@ def _make_write(self):
users_to_exclude = self.mapped("users_to_exclude_ids")

def write_full(self, vals, **kwargs):
if self.env.context.get("auditlog_disabled"):
return write_full.origin(self, vals, **kwargs)
self = self.with_context(auditlog_disabled=True)
rule_model = self.env["auditlog.rule"]
fields_list = rule_model.get_auditlog_fields(self)
Expand Down Expand Up @@ -478,6 +480,8 @@ def write_full(self, vals, **kwargs):
return result

def write_fast(self, vals, **kwargs):
if self.env.context.get("auditlog_disabled"):
return write_fast.origin(self, vals, **kwargs)
self = self.with_context(auditlog_disabled=True)
rule_model = self.env["auditlog.rule"]
# Log the user input only, no matter if the `vals` is updated
Expand Down
71 changes: 71 additions & 0 deletions auditlog/tests/test_auditlog.py
Original file line number Diff line number Diff line change
Expand Up @@ -799,3 +799,74 @@ def test_01_AuditlogFast_field_exclude_write_log(self):
]
)
)


class TestAuditlogRecursion(AuditLogRuleCommon):
"""Verify that re-entrant write calls caused by compute/inverse methods
do not produce a RecursionError when auditlog is active."""

@classmethod
def setUpClass(cls):
super().setUpClass()
cls.partner_model_id = cls.env.ref("base.model_res_partner").id
cls.partner_rule_full = cls.env["auditlog.rule"].create(
{
"name": "testrule for partner full",
"model_id": cls.partner_model_id,
"log_create": True,
"log_write": True,
"log_type": "full",
}
)
cls.partner_rule_fast = cls.env["auditlog.rule"].create(
{
"name": "testrule for partner fast",
"model_id": cls.partner_model_id,
"log_create": True,
"log_write": True,
"log_type": "fast",
}
)

def _make_company_with_child(self):
company = self.env["res.partner"].create(
{"name": "Test Company", "is_company": True, "street": "1 Old St"}
)
child = self.env["res.partner"].create(
{"name": "Test Contact", "parent_id": company.id, "type": "contact"}
)
return company, child

def test_no_recursion_write_full_reentrant(self):
"""write_full must not recurse when auditlog_disabled is already set.

Simulate a compute/inverse method calling write() on an audited model
while already inside an auditlog write call (auditlog_disabled=True).
"""
self.partner_rule_full.subscribe()
company, child = self._make_company_with_child()
# Writing company address triggers _fields_sync which writes to child.
# Both are audited — without the guard this re-enters write_full.
company.write({"street": "2 New St"})
self.assertEqual(child.street, "2 New St")

def test_no_recursion_write_fast_reentrant(self):
"""write_fast must not recurse when auditlog_disabled is already set."""
self.partner_rule_fast.subscribe()
company, child = self._make_company_with_child()
company.write({"street": "3 Fast St"})
self.assertEqual(child.street, "3 Fast St")

def test_write_full_guard_skips_logging(self):
"""write_full early-exit must skip logging when auditlog_disabled is set."""
self.partner_rule_full.subscribe()
partner = self.env["res.partner"].create({"name": "Guard Test Partner"})
log_count_before = self.env["auditlog.log"].search_count(
[("model_id", "=", self.partner_model_id), ("res_id", "=", partner.id)]
)
partner.with_context(auditlog_disabled=True).write({"name": "Guard Updated"})
log_count_after = self.env["auditlog.log"].search_count(
[("model_id", "=", self.partner_model_id), ("res_id", "=", partner.id)]
)
self.assertEqual(log_count_before, log_count_after)
self.assertEqual(partner.name, "Guard Updated")
Loading