mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-14 07:01:56 +00:00
Co-authored-by: Sudharsanan Ashok <135326972+Sudharsanan11@users.noreply.github.com>
This commit is contained in:
@@ -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)
|
||||||
|
|||||||
@@ -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:
|
||||||
|
|||||||
Reference in New Issue
Block a user