From b89229a93d7b68c89e198efe12dbf7ed233b2cfb Mon Sep 17 00:00:00 2001 From: Diptanil Saha Date: Tue, 11 Aug 2026 22:22:10 +0530 Subject: [PATCH] fix(selling): read overdue amount from payment ledger, not gl tags (backport #57786) (#58057) Co-authored-by: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com> --- erpnext/selling/doctype/customer/customer.py | 25 ++++----- .../selling/doctype/customer/test_customer.py | 52 +++++++++++++++++++ 2 files changed, 65 insertions(+), 12 deletions(-) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 76495a94959..0f776858cb8 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -731,8 +731,8 @@ def get_overdue_billing_threshold(customer: str, company: str) -> float: def get_customer_overdue_amount(customer: str, company: str) -> float: """Amount the customer owes past its due date, in company currency. - Follows the same rule as the Overdue invoice status, so a customer is only - blocked for what the invoice list already shows as overdue. + Reads the Payment Ledger, the same source as `outstanding_amount`, so this agrees + with the Overdue status the invoice list already shows. """ invoices = get_outstanding_invoices_for_customer(customer, company) if not invoices: @@ -745,27 +745,28 @@ def get_customer_overdue_amount(customer: str, company: str) -> float: def get_outstanding_invoices_for_customer(customer: str, company: str) -> list[frappe._dict]: from frappe.query_builder.functions import Sum - gl_entry = frappe.qb.DocType("GL Entry") + ple = frappe.qb.DocType("Payment Ledger Entry") sales_invoice = frappe.qb.DocType("Sales Invoice") - # debit - credit is always booked in company currency, so this is comparable to the overdue limit - outstanding = Sum(gl_entry.debit) - Sum(gl_entry.credit) + # the Payment Ledger, not the GL, carries allocations made after submit (reconciled advances). + # `amount` is booked in company currency, so this is comparable to the overdue limit. + outstanding = Sum(ple.amount) return ( - frappe.qb.from_(gl_entry) + frappe.qb.from_(ple) .inner_join(sales_invoice) - .on(sales_invoice.name == gl_entry.against_voucher) + .on(sales_invoice.name == ple.against_voucher_no) .select( sales_invoice.name, sales_invoice.due_date, sales_invoice.base_grand_total, outstanding.as_("outstanding"), ) - .where(gl_entry.party_type == "Customer") - .where(gl_entry.party == customer) - .where(gl_entry.company == company) - .where(gl_entry.is_cancelled == 0) - .where(gl_entry.against_voucher_type == "Sales Invoice") + .where(ple.party_type == "Customer") + .where(ple.party == customer) + .where(ple.company == company) + .where(ple.delinked == 0) + .where(ple.against_voucher_type == "Sales Invoice") .groupby(sales_invoice.name, sales_invoice.due_date, sales_invoice.base_grand_total) .having(outstanding > 0) ).run(as_dict=True) diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py index e7fcf2519fd..d2c44b63a1c 100644 --- a/erpnext/selling/doctype/customer/test_customer.py +++ b/erpnext/selling/doctype/customer/test_customer.py @@ -422,6 +422,37 @@ class TestCustomer(ERPNextTestSuite): pe.submit() self.assertEqual(get_customer_overdue_amount("_Test Customer", "_Test Company"), baseline) + def test_get_customer_overdue_amount_ignores_advance_reconciled_after_submit(self): + from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry + from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice + + baseline = get_customer_overdue_amount("_Test Customer", "_Test Company") + + # advance received before the invoice exists, so it carries no reference row + pe = create_payment_entry( + company="_Test Company", + party_type="Customer", + party="_Test Customer", + payment_type="Receive", + paid_from="Debtors - _TC", + paid_to="Cash - _TC", + paid_amount=800, + ) + pe.posting_date = add_days(nowdate(), -60) + pe.submit() + + si = create_sales_invoice(qty=1, rate=800, posting_date=add_days(nowdate(), -30)) + self.assertEqual(get_customer_overdue_amount("_Test Customer", "_Test Company"), baseline + 800) + + reconcile_payment_against_invoice(pe, si) + + # reconciliation settles the invoice without re-tagging the payment's GL entries, so an + # overdue amount read off the GL would still count the full 800 here + si.reload() + self.assertEqual(si.outstanding_amount, 0) + self.assertEqual(si.status, "Paid") + self.assertEqual(get_customer_overdue_amount("_Test Customer", "_Test Company"), baseline) + def test_overdue_billing_threshold_on_submit(self): from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice @@ -591,6 +622,27 @@ def set_credit_limit(customer, company, credit_limit): customer.credit_limits[-1].db_insert() +def reconcile_payment_against_invoice(payment_entry, sales_invoice): + """Allocate an unlinked payment against an invoice through the reconciliation tool.""" + pr = frappe.get_doc( + doctype="Payment Reconciliation", + company=sales_invoice.company, + party_type="Customer", + party=sales_invoice.customer, + receivable_payable_account=sales_invoice.debit_to, + ) + pr.get_unreconciled_entries() + pr.allocate_entries( + frappe._dict( + { + "invoices": [d.as_dict() for d in pr.invoices if d.invoice_number == sales_invoice.name], + "payments": [d.as_dict() for d in pr.payments if d.reference_name == payment_entry.name], + } + ) + ) + pr.reconcile() + + def set_overdue_billing_threshold(customer, company, threshold): customer = frappe.get_doc("Customer", customer) for d in customer.credit_limits: