mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-13 06:31:48 +00:00
fix(selling): read overdue amount from payment ledger, not gl tags (#57786)
This commit is contained in:
committed by
GitHub
parent
e258921681
commit
b5a3815a64
@@ -632,8 +632,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:
|
||||
@@ -646,27 +646,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)
|
||||
|
||||
@@ -438,6 +438,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
|
||||
|
||||
@@ -632,6 +663,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:
|
||||
|
||||
Reference in New Issue
Block a user