"""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