diff --git a/auditlog/models/auditlog_rule.py b/auditlog/models/auditlog_rule.py index 137cf766938..964612e78fe 100644 --- a/auditlog/models/auditlog_rule.py +++ b/auditlog/models/auditlog_rule.py @@ -70,6 +70,7 @@ class ThrowAwayCache: ] def __init__(self, env): + self._env = env self._transaction = env.transaction def __enter__(self): @@ -80,6 +81,12 @@ def __enter__(self): to the cursor, so if we want to keep using the same cursor, we need to patch out these properties. """ + # Flush any pending updates and recomputations first. If the cache is + # set aside while recomputations of stored computed fields are still + # pending, they end up being consumed after the swap/read/restore + # cycle without ever being persisted, leaving NULL columns in the + # database (see OCA/server-tools#3635). + self._env.flush_all() for attribute in self.transaction_attributes: instance = getattr(self._transaction, attribute) setattr( diff --git a/test_auditlog/tests/__init__.py b/test_auditlog/tests/__init__.py index 10c5f5c6aae..1b92fbaf3b7 100644 --- a/test_auditlog/tests/__init__.py +++ b/test_auditlog/tests/__init__.py @@ -2,3 +2,4 @@ from . import test_account_move_reverse from . import test_product_tax_multicompany from . import test_account_reentrancy +from . import test_payment_residual diff --git a/test_auditlog/tests/test_payment_residual.py b/test_auditlog/tests/test_payment_residual.py new file mode 100644 index 00000000000..e720b50cc11 --- /dev/null +++ b/test_auditlog/tests/test_payment_residual.py @@ -0,0 +1,57 @@ +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 TestPaymentResidual(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.set_to_confirmed() + + def test_register_payment_computes_residual(self): + """Payment lines keep their stored computed values with a full rule. + + With a full-log rule on account.move, the swapped-cache reads of the + log diff must not cause the pending recomputations of the payment + lines' stored computed fields to be lost, or those columns end up + NULL in the database (issue #3635: the payment is then never offered + as an outstanding credit on the invoice). + """ + invoice = self.init_invoice("out_invoice", products=self.product_a, post=True) + wizard = ( + self.env["account.payment.register"] + .with_context(active_model="account.move", active_ids=invoice.ids) + .create({"journal_id": self.company_data["default_journal_bank"].id}) + ) + payments = wizard._create_payments() + self.env.flush_all() + self.env.cr.execute( + """ + SELECT id, amount_residual, amount_residual_currency, reconciled + FROM account_move_line + WHERE move_id = %s + """, + (payments.move_id.id,), + ) + rows = self.env.cr.fetchall() + self.assertTrue(rows) + for line_id, residual, residual_currency, reconciled in rows: + self.assertIsNotNone(residual, f"line {line_id}: amount_residual is NULL") + self.assertIsNotNone( + residual_currency, + f"line {line_id}: amount_residual_currency is NULL", + ) + self.assertIsNotNone(reconciled, f"line {line_id}: reconciled is NULL")