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>
This commit is contained in:
Diptanil Saha
2026-08-11 22:22:10 +05:30
committed by GitHub
parent f11d0d5cec
commit b89229a93d
2 changed files with 65 additions and 12 deletions

View File

@@ -731,8 +731,8 @@ def get_overdue_billing_threshold(customer: str, company: str) -> float:
def get_customer_overdue_amount(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. """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 Reads the Payment Ledger, the same source as `outstanding_amount`, so this agrees
blocked for what the invoice list already shows as overdue. with the Overdue status the invoice list already shows.
""" """
invoices = get_outstanding_invoices_for_customer(customer, company) invoices = get_outstanding_invoices_for_customer(customer, company)
if not invoices: 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]: def get_outstanding_invoices_for_customer(customer: str, company: str) -> list[frappe._dict]:
from frappe.query_builder.functions import Sum 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") sales_invoice = frappe.qb.DocType("Sales Invoice")
# debit - credit is always booked in company currency, so this is comparable to the overdue limit # the Payment Ledger, not the GL, carries allocations made after submit (reconciled advances).
outstanding = Sum(gl_entry.debit) - Sum(gl_entry.credit) # `amount` is booked in company currency, so this is comparable to the overdue limit.
outstanding = Sum(ple.amount)
return ( return (
frappe.qb.from_(gl_entry) frappe.qb.from_(ple)
.inner_join(sales_invoice) .inner_join(sales_invoice)
.on(sales_invoice.name == gl_entry.against_voucher) .on(sales_invoice.name == ple.against_voucher_no)
.select( .select(
sales_invoice.name, sales_invoice.name,
sales_invoice.due_date, sales_invoice.due_date,
sales_invoice.base_grand_total, sales_invoice.base_grand_total,
outstanding.as_("outstanding"), outstanding.as_("outstanding"),
) )
.where(gl_entry.party_type == "Customer") .where(ple.party_type == "Customer")
.where(gl_entry.party == customer) .where(ple.party == customer)
.where(gl_entry.company == company) .where(ple.company == company)
.where(gl_entry.is_cancelled == 0) .where(ple.delinked == 0)
.where(gl_entry.against_voucher_type == "Sales Invoice") .where(ple.against_voucher_type == "Sales Invoice")
.groupby(sales_invoice.name, sales_invoice.due_date, sales_invoice.base_grand_total) .groupby(sales_invoice.name, sales_invoice.due_date, sales_invoice.base_grand_total)
.having(outstanding > 0) .having(outstanding > 0)
).run(as_dict=True) ).run(as_dict=True)

View File

@@ -422,6 +422,37 @@ class TestCustomer(ERPNextTestSuite):
pe.submit() pe.submit()
self.assertEqual(get_customer_overdue_amount("_Test Customer", "_Test Company"), baseline) 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): def test_overdue_billing_threshold_on_submit(self):
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice 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() 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): def set_overdue_billing_threshold(customer, company, threshold):
customer = frappe.get_doc("Customer", customer) customer = frappe.get_doc("Customer", customer)
for d in customer.credit_limits: for d in customer.credit_limits: