diff --git a/auditlog/models/rule.py b/auditlog/models/rule.py index 6e9ce506f74..bb895692ff1 100644 --- a/auditlog/models/rule.py +++ b/auditlog/models/rule.py @@ -91,6 +91,19 @@ def __exit__(self, exc_type, exc_val, exc_tb): self._transaction.tocompute = self._original_tocompute +class _Sentinel: + """A falsy sentinel object as a placeholder for absent values.""" + + def __str__(self): + return "" + + def __bool__(self): + return False + + +_SENTINEL = _Sentinel() + + class AuditlogRule(models.Model): _name = "auditlog.rule" _description = "Auditlog - Rule" @@ -506,7 +519,7 @@ def write_fast(self, vals, **kwargs): # afterwards as it could not represent the real state # of the data in the database vals2 = dict(vals) - old_vals2 = dict.fromkeys(list(vals2.keys()), False) + old_vals2 = dict.fromkeys(list(vals2.keys()), _SENTINEL) old_values = {id_: old_vals2 for id_ in self.ids} new_values = {id_: vals2 for id_ in self.ids} result = write_fast.origin(self, vals, **kwargs) diff --git a/auditlog/tests/test_auditlog.py b/auditlog/tests/test_auditlog.py index 0a71a709756..01f2ea35c5c 100644 --- a/auditlog/tests/test_auditlog.py +++ b/auditlog/tests/test_auditlog.py @@ -343,6 +343,61 @@ def tearDown(self): super(TestAuditlogFast, self).tearDown() +class TestAuditlogFastWriteEmptyValue(AuditLogRuleCommon): + @classmethod + def setUpClass(cls): + super().setUpClass() + cls.test_model = cls.env["ir.model"].search([("model", "=", "res.partner")]) + cls.auditlog_rule = cls.create_rule( + { + "name": "test.model_res_partner", + "model_id": cls.test_model.id, + "log_type": "fast", + "log_read": False, + "log_create": False, + "log_write": True, + "log_unlink": False, + } + ) + cls.test_record = ( + cls.env["res.partner"] + .with_context(tracking_disable=True) + .create( + { + "name": "testpartner3", + "phone": "123", + } + ) + ) + cls.auditlog_rule.subscribe() + + def assert_has_one_log_line_with_empty_values(self): + logs = self.env["auditlog.log"].search( + [ + ("res_id", "=", self.test_record.id), + ("model_id", "=", self.test_model.id), + ] + ) + self.assertEqual(len(logs), 1) + self.assertEqual(logs[0].model_name, "Contact") + self.assertEqual(logs[0].model_model, "res.partner") + log_lines = logs.mapped("line_ids") + self.assertEqual(len(log_lines), 1) + self.assertEqual(log_lines[0].field_name, "phone") + self.assertFalse(log_lines[0].old_value) + self.assertFalse(log_lines[0].old_value_text) + self.assertFalse(log_lines[0].new_value) + self.assertFalse(log_lines[0].new_value_text) + + def test_when_removing_field_value_a_log_line_is_created(self): + self.test_record.write({"phone": False}) + self.assert_has_one_log_line_with_empty_values() + + def test_when_using_none_field_value_a_log_line_is_created(self): + self.test_record.write({"phone": None}) + self.assert_has_one_log_line_with_empty_values() + + class TestFieldRemoval(AuditLogRuleCommon): @classmethod def setUpClass(cls):