diff --git a/erpnext/accounts/doctype/dunning/test_dunning.py b/erpnext/accounts/doctype/dunning/test_dunning.py index 0110877ce90..4508738a471 100644 --- a/erpnext/accounts/doctype/dunning/test_dunning.py +++ b/erpnext/accounts/doctype/dunning/test_dunning.py @@ -12,6 +12,7 @@ from erpnext.accounts.doctype.sales_invoice.mapper import ( create_dunning as create_dunning_from_sales_invoice, ) from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import ( + create_sales_invoice, create_sales_invoice_against_cost_center, ) from erpnext.tests.utils import ERPNextTestSuite @@ -152,6 +153,37 @@ class TestDunning(ERPNextTestSuite): dunning.reload() self.assertEqual(dunning.status, "Unresolved") + @ERPNextTestSuite.change_settings( + "Accounts Settings", {"allow_multi_currency_invoices_against_single_party_account": 1} + ) + def test_dunning_outstanding_uses_transaction_currency(self): + """ + Regression for #56006: dunning outstanding must be in the invoice transaction + currency, not in the party account currency. + + A USD invoice posted against an INR receivable account stores + outstanding_amount in INR (party account currency). The overdue payment + row on the resulting Dunning must carry the USD amount, not the INR amount. + """ + si = create_sales_invoice( + posting_date=add_days(today(), -10), + currency="USD", + conversion_rate=50, + rate=100, + debit_to="Debtors - _TC", + ) + + # Sanity-check the invoice state before creating the dunning + self.assertEqual(si.currency, "USD") + self.assertEqual(si.outstanding_amount, 5000.0) # INR (party account currency) + self.assertEqual(si.payment_schedule[0].outstanding, 100.0) # USD (transaction currency) + + dunning = create_dunning_from_sales_invoice(si.name) + + self.assertEqual(len(dunning.overdue_payments), 1) + # Must reflect 100 USD, not 5000 INR mislabelled as USD + self.assertEqual(dunning.overdue_payments[0].outstanding, 100.0) + def test_dunning_not_affected_by_standalone_credit_note(self): """ Test that dunning is NOT resolved when a credit note has update_outstanding_for_self checked. diff --git a/erpnext/accounts/doctype/sales_invoice/mapper.py b/erpnext/accounts/doctype/sales_invoice/mapper.py index 8a8d07dea1d..46ce4753a85 100644 --- a/erpnext/accounts/doctype/sales_invoice/mapper.py +++ b/erpnext/accounts/doctype/sales_invoice/mapper.py @@ -594,7 +594,15 @@ def create_dunning( if source.payment_schedule and len(source.payment_schedule) == 1: for row in target.overdue_payments: if row.payment_schedule == source.payment_schedule[0].name: - row.outstanding = source.get("outstanding_amount") + # outstanding_amount is in the party account currency, but the Overdue Payment + # row is in the invoice's transaction currency. When they differ, use the + # payment schedule's own outstanding — it is kept in transaction currency and + # updated as payments are allocated, so it stays correct even when the invoice + # and its payments post at different exchange rates (#56006). + if source.party_account_currency and source.party_account_currency != source.currency: + row.outstanding = source.payment_schedule[0].outstanding + else: + row.outstanding = source.get("outstanding_amount") target.validate()