From 8a6fe0d332789347ab5d7326a9affd6991812daf Mon Sep 17 00:00:00 2001 From: Aungkokolin1997 Date: Tue, 13 Oct 2026 07:00:14 +0000 Subject: [PATCH 1/2] [FIX] auditlog: fully remove patched methods on revert Reverting a rule assigned the original method back on the model's registry class. Since that class is reused when further modules are loaded, the assigned method kept shadowing the overrides they define, e.g. the `write` of partner_contact_address_default when its tests run in the same process as the ones of auditlog. Remove the patched attribute instead so that the method resolves through the class hierarchy again. In tests, subscribe rules in `setUp` instead of `setUpClass` and revert them in a cleanup after each test, so that no patch outlives a test, even when `setUp` fails after subscribing. Adapted forward port of the auditlog part of #3526 without the registration in Odoo's patch checker, which does not exist in 19.0. Fixes #3439 Assisted-by: Claude Opus 5.5 --- auditlog/models/auditlog_rule.py | 14 ++++------ auditlog/tests/common.py | 34 +++++++++++----------- auditlog/tests/test_auditlog.py | 48 ++++++++++++++++++++------------ 3 files changed, 52 insertions(+), 44 deletions(-) diff --git a/auditlog/models/auditlog_rule.py b/auditlog/models/auditlog_rule.py index 8c90c452bd7..793e91e7b0f 100644 --- a/auditlog/models/auditlog_rule.py +++ b/auditlog/models/auditlog_rule.py @@ -243,7 +243,7 @@ def _register_hook(self): def _patch_method(self, model, method_name, check_attr): result = new_method = False - model_class = type(model) + model_class = model.env.registry[model._name] if method_name == "create": new_method = self._make_create() elif method_name == "read": @@ -257,7 +257,7 @@ def _patch_method(self, model, method_name, check_attr): if new_method: new_method.origin = getattr(model_class, method_name) setattr(model_class, method_name, new_method) - setattr(type(model), check_attr, True) + setattr(model_class, check_attr, True) result = True return result @@ -300,15 +300,13 @@ def _revert_methods(self): """Restore original ORM methods of models defined in rules.""" updated = False for rule in self: - model_model = self.env[rule.model_id.model or rule.model_model] + model_class = self.env.registry[rule.model_id.model or rule.model_model] for method in ["create", "read", "write", "unlink", "export_data"]: if getattr(rule, f"log_{method}") and hasattr( - getattr(model_model, method), "origin" + getattr(model_class, method), "origin" ): - setattr( - type(model_model), method, getattr(model_model, method).origin - ) - delattr(type(model_model), f"auditlog_ruled_{method}") + delattr(model_class, method) + delattr(model_class, f"auditlog_ruled_{method}") updated = True if updated: self._update_registry() diff --git a/auditlog/tests/common.py b/auditlog/tests/common.py index 085ee422f2f..684fd4d46d2 100644 --- a/auditlog/tests/common.py +++ b/auditlog/tests/common.py @@ -4,30 +4,28 @@ class AuditLogRuleCommon(TransactionCase): - @classmethod - def setUpClass(cls): - super().setUpClass() - cls.models = set() + """Rules must be subscribed in `setUp` or in the test itself, not in + `setUpClass`: patched methods are reverted after each test.""" @classmethod def create_rule(cls, vals): - rule = cls.env["auditlog.rule"].with_context(tracking_disable=True).create(vals) - # Keep track of patched models - cls.models |= set(rule.model_id.mapped("model")) - return rule + return cls.env["auditlog.rule"].with_context(tracking_disable=True).create(vals) - @classmethod - def tearDownClass(cls): - for rule in cls.env["auditlog.rule"].search([]): + def setUp(self): + super().setUp() + # A cleanup also runs when setUp fails after subscribing a rule, + # unlike tearDown. + self.addCleanup(self._unsubscribe_rules) + + def _unsubscribe_rules(self): + for rule in self.env["auditlog.rule"].search([]): try: rule.set_to_draft() except KeyError: # pragma: no cover - continue # Model not loaded yet - + continue # Preexisting rule for model not loaded yet # Assert no patched methods remain - for model in cls.models: - for method in ["create", "read", "write", "unlink"]: - assert not hasattr(getattr(cls.env[model], method), "origin"), ( - f"{model} {method} still patched" + for model_class in self.env.registry.values(): + for method in ["create", "read", "write", "unlink", "export_data"]: + assert not hasattr(model_class, f"auditlog_ruled_{method}"), ( + f"{model_class._name} {method} still patched" ) - super().tearDownClass() diff --git a/auditlog/tests/test_auditlog.py b/auditlog/tests/test_auditlog.py index 127a5982768..aac8dd5229b 100644 --- a/auditlog/tests/test_auditlog.py +++ b/auditlog/tests/test_auditlog.py @@ -4,6 +4,7 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl). from odoo.fields import Command +from odoo.tools import mute_logger from odoo.addons.base.models.ir_model import MODULE_UNINSTALL_FLAG @@ -401,13 +402,15 @@ def setUpClass(cls): } ) - cls.auditlog_rule.set_to_confirmed() + def setUp(self): + super().setUp() + self.auditlog_rule.set_to_confirmed() # Trigger log creation - rec = cls.env["x_test.model"].create({"x_test_field": "test value"}) + rec = self.env["x_test.model"].create({"x_test_field": "test value"}) rec.write({"x_test_field": "test value 2"}) - cls.logs = cls.env["auditlog.log"].search( - [("res_id", "=", rec.id), ("model_id", "=", cls.test_model.id)] + self.logs = self.env["auditlog.log"].search( + [("res_id", "=", rec.id), ("model_id", "=", self.test_model.id)] ) def assert_values(self): @@ -431,13 +434,15 @@ def test_01_field_and_model_removal(self): self.assert_values() # Remove the field - self.test_field.with_context(**{MODULE_UNINSTALL_FLAG: True}).unlink() + with mute_logger("odoo.api"): # Mute 'Too many iterations for flushing fields' + self.test_field.with_context(**{MODULE_UNINSTALL_FLAG: True}).unlink() self.assert_values() # The field should not be linked self.assertFalse(self.logs.mapped("line_ids.field_id")) # Remove the model - self.test_model.with_context(**{MODULE_UNINSTALL_FLAG: True}).unlink() + with mute_logger("odoo.api"): # Mute 'Too many iterations for flushing fields' + self.test_model.with_context(**{MODULE_UNINSTALL_FLAG: True}).unlink() self.assert_values() # The model should not be linked @@ -533,14 +538,16 @@ def setUpClass(cls): # Updating users_to_exclude_ids cls.auditlog_rule.users_to_exclude_ids = [[4, cls.users_to_exclude_ids]] - # Subscribe auditlog.rule - cls.auditlog_rule.set_to_confirmed() - cls.auditlog_log = cls.env["auditlog.log"] + def setUp(self): + super().setUp() + # Subscribe auditlog.rule + self.auditlog_rule.set_to_confirmed() + # Creating new res.partner - cls.testpartner1 = ( - cls.env["res.partner"] + self.testpartner1 = ( + self.env["res.partner"] .with_context(tracking_disable=True) .create( { @@ -551,10 +558,10 @@ def setUpClass(cls): ) # Creating new res.partner from excluded user - cls.testpartner2 = ( - cls.env["res.partner"] + self.testpartner2 = ( + self.env["res.partner"] .with_context(tracking_disable=True) - .with_user(cls.user.id) + .with_user(self.user.id) .create( { "name": "testpartner2", @@ -690,8 +697,11 @@ def setUpClass(cls): cls.group = cls.env.ref("auditlog.group_auditlog_manager") cls.auditlog_log = cls.env["auditlog.log"] + + def setUp(self): + super().setUp() # Subscribe auditlog.rule - cls.auditlog_rule.set_to_confirmed() + self.auditlog_rule.set_to_confirmed() def test_01_AuditlogFull_field_group_write_log(self): """Change group and check successfully created log""" @@ -741,9 +751,6 @@ def setUpClass(cls): # Updating phone in fields_to_exclude_ids cls.auditlog_rule.fields_to_exclude_ids = [[4, cls.fields_to_exclude_ids]] - # Subscribe auditlog.rule - cls.auditlog_rule.set_to_confirmed() - cls.auditlog_log = cls.env["auditlog.log"] # Creating new res.partner @@ -758,6 +765,11 @@ def setUpClass(cls): ) ) + def setUp(self): + super().setUp() + # Subscribe auditlog.rule + self.auditlog_rule.set_to_confirmed() + def test_01_AuditlogFast_field_exclude_write_log(self): # Checking fields_to_exclude_ids self.testpartner1.with_context(tracking_disable=True).write( From ef290478c70f99d6aeea26b165edfa9058a9bb2d Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Mon, 19 Oct 2026 08:33:58 +0000 Subject: [PATCH 2/2] [BOT] post-merge updates --- README.md | 2 +- auditlog/README.rst | 2 +- auditlog/__manifest__.py | 2 +- auditlog/static/description/index.html | 4 ++-- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 5bf93cea150..65f1eb90fc9 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Available addons addon | version | maintainers | summary --- | --- | --- | --- [attachment_unindex_content](attachment_unindex_content/) | 19.0.1.0.0 | moylop260 ebirbe luisg123v | Disable indexing of attachments -[auditlog](auditlog/) | 19.0.2.0.0 | | Audit Log +[auditlog](auditlog/) | 19.0.2.0.1 | | Audit Log [auto_backup](auto_backup/) | 19.0.1.0.1 | | Backups database [base_cron_exclusion](base_cron_exclusion/) | 19.0.1.0.0 | LoisRForgeFlow | Allow you to select scheduled actions that should not run simultaneously. [base_exception](base_exception/) | 19.0.1.0.0 | hparfr sebastienbeau | This module provide an abstract model to manage customizable exceptions to be applied on different models (sale order, invoice, ...) diff --git a/auditlog/README.rst b/auditlog/README.rst index 70aed989a25..20fb5f1f8e9 100644 --- a/auditlog/README.rst +++ b/auditlog/README.rst @@ -11,7 +11,7 @@ Audit Log !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! - !! source digest: sha256:3db0314c73f17e5c63ecb5205a4d29beff97812aca664e1dd624646702f14bee + !! source digest: sha256:620a80526b6f2d225f81287c03ac63b2c4b5f56fe19e2b833b6f2d31b015c093 !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! .. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png diff --git a/auditlog/__manifest__.py b/auditlog/__manifest__.py index cd1ffd914dc..48fbbc40ea4 100644 --- a/auditlog/__manifest__.py +++ b/auditlog/__manifest__.py @@ -3,7 +3,7 @@ { "name": "Audit Log", - "version": "19.0.2.0.0", + "version": "19.0.2.0.1", "author": "ABF OSIELL, Odoo Community Association (OCA)", "license": "AGPL-3", "website": "https://github.com/OCA/server-tools", diff --git a/auditlog/static/description/index.html b/auditlog/static/description/index.html index c2472e1cabf..dc34b219e22 100644 --- a/auditlog/static/description/index.html +++ b/auditlog/static/description/index.html @@ -3,7 +3,7 @@ -README.rst +Audit Log