mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-11 13:41:47 +00:00
fix: prevent TimestampMismatchError resolving Dunning with multiple overdue installments
`get_linked_dunnings_as_per_state` joins Dunning to its Overdue Payment child
table without DISTINCT. When a Sales Invoice has more than one overdue
installment, its Dunning holds one Overdue Payment row per installment, so the
query returns the same Dunning name once per row.
`update_linked_dunnings` then loads that Dunning name into a separate document
object for each duplicate row and saves each one. The first save bumps the
`modified` timestamp, so the second (now stale) save fails with
`TimestampMismatchError` ("Document has been modified after you have opened
it"). The error is raised on the Dunning while the user is submitting a Payment
Entry, making it confusing, and payments for such invoices cannot be posted at
all.
Add DISTINCT so each linked Dunning is returned (and saved) exactly once.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -275,6 +275,7 @@ def get_linked_dunnings_as_per_state(sales_invoice, state):
|
||||
.join(overdue_payment)
|
||||
.on(overdue_payment.parent == dunning.name)
|
||||
.select(dunning.name)
|
||||
.distinct()
|
||||
.where(
|
||||
(dunning.status == state)
|
||||
& (dunning.docstatus != 2)
|
||||
|
||||
@@ -123,6 +123,41 @@ class TestDunning(ERPNextTestSuite):
|
||||
self.assertEqual(sales_invoice.status, "Overdue")
|
||||
self.assertEqual(dunning.status, "Unresolved")
|
||||
|
||||
def test_payment_against_invoice_with_multiple_overdue_installments_in_dunning(self):
|
||||
"""
|
||||
When an invoice has more than one overdue installment, its Dunning holds one
|
||||
Overdue Payment row per installment. Submitting a Payment Entry for the invoice
|
||||
must resolve the Dunning without raising a TimestampMismatchError caused by the
|
||||
same Dunning being loaded and saved more than once.
|
||||
"""
|
||||
create_payment_terms_template_for_dunning()
|
||||
# Post far enough in the past that BOTH installments (5 and 10 credit days) are overdue.
|
||||
sales_invoice = create_sales_invoice_against_cost_center(
|
||||
posting_date=add_days(today(), -15),
|
||||
qty=1,
|
||||
rate=100,
|
||||
do_not_submit=True,
|
||||
)
|
||||
sales_invoice.payment_terms_template = "_Test 50-50 for Dunning"
|
||||
sales_invoice.submit()
|
||||
|
||||
dunning = create_dunning_from_sales_invoice(sales_invoice.name)
|
||||
# Two overdue installments -> two overdue payment rows for the same invoice.
|
||||
self.assertEqual(len(dunning.overdue_payments), 2)
|
||||
dunning.submit()
|
||||
self.assertEqual(dunning.status, "Unresolved")
|
||||
|
||||
# Pay the invoice in full. This previously raised TimestampMismatchError on the Dunning.
|
||||
pe = get_payment_entry("Sales Invoice", sales_invoice.name)
|
||||
pe.reference_no, pe.reference_date = "3", nowdate()
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
sales_invoice.reload()
|
||||
dunning.reload()
|
||||
self.assertEqual(sales_invoice.outstanding_amount, 0)
|
||||
self.assertEqual(dunning.status, "Resolved")
|
||||
|
||||
def test_dunning_resolution_from_credit_note(self):
|
||||
"""
|
||||
Test that dunning is resolved when a credit note is issued against the original invoice.
|
||||
|
||||
Reference in New Issue
Block a user