Files
ns_erpnext_app/ns_app/api/statements.py
Norman King 63bf0b68f5 feat(statements): add overdue-customers query API
Add ns_app/api/statements.py with get_customers_with_overdue_invoices
(one aggregated row per customer with overdue Sales Invoices) plus the
_get_outstanding_invoices / _aging_bucket helpers used to build the
per-customer statement.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 19:01:03 -04:00

108 lines
2.9 KiB
Python

"""Customer account statements.
Generates printable, one-customer-per-page account statements for customers with
overdue invoices, formatted for a standard double-window envelope. Statement
generation also books a late-payment fee to the ledger (see the late-fee helpers
added alongside the generator).
"""
import frappe
from frappe import _
from frappe.utils import getdate, nowdate
# Roles allowed to run collections/statement actions.
ALLOWED_ROLES = [
"System Manager",
"Sales User",
"Sales Manager",
"Accounts User",
"Accounts Manager",
]
@frappe.whitelist()
def get_customers_with_overdue_invoices():
"""Return one row per customer that has at least one overdue Sales Invoice.
A Sales Invoice is overdue when it is submitted, still has an outstanding
balance, and its due date is in the past.
"""
frappe.only_for(ALLOWED_ROLES)
today = nowdate()
rows = frappe.get_all(
"Sales Invoice",
filters={
"docstatus": 1,
"outstanding_amount": [">", 0],
"due_date": ["<", today],
},
fields=[
"customer",
"customer_name",
"count(name) as overdue_count",
"sum(outstanding_amount) as total_outstanding",
"min(due_date) as oldest_due_date",
],
group_by="customer, customer_name",
order_by="total_outstanding desc",
)
for row in rows:
row["max_days_overdue"] = (
(getdate(today) - getdate(row.oldest_due_date)).days
if row.oldest_due_date
else 0
)
return rows
def _aging_bucket(days_overdue):
"""Map days-overdue to a standard aging bucket label."""
if days_overdue <= 0:
return "Current"
if days_overdue <= 30:
return "1-30"
if days_overdue <= 60:
return "31-60"
if days_overdue <= 90:
return "61-90"
return "90+"
def _get_outstanding_invoices(customer):
"""Return all open (submitted, unpaid) Sales Invoices for a customer.
The statement lists the full open balance, so this includes not-yet-due
invoices; each row is annotated with days overdue, an overdue flag, and its
aging bucket.
"""
today = getdate(nowdate())
invoices = frappe.get_all(
"Sales Invoice",
filters={
"customer": customer,
"docstatus": 1,
"outstanding_amount": [">", 0],
},
fields=[
"name",
"posting_date",
"due_date",
"outstanding_amount",
"grand_total",
"company",
],
order_by="due_date asc",
)
for inv in invoices:
due = getdate(inv.due_date) if inv.due_date else None
days = (today - due).days if due else 0
inv["days_overdue"] = days if days > 0 else 0
inv["is_overdue"] = days > 0
inv["aging_bucket"] = _aging_bucket(inv["days_overdue"])
return invoices