diff --git a/ns_app/api/statements.py b/ns_app/api/statements.py index 1e121b5..252194c 100644 --- a/ns_app/api/statements.py +++ b/ns_app/api/statements.py @@ -6,9 +6,12 @@ generation also books a late-payment fee to the ledger (see the late-fee helpers added alongside the generator). """ +import json + import frappe from frappe import _ -from frappe.utils import getdate, nowdate +from frappe.contacts.doctype.address.address import get_address_display, get_default_address +from frappe.utils import flt, getdate, nowdate # Roles allowed to run collections/statement actions. ALLOWED_ROLES = [ @@ -105,3 +108,167 @@ def _get_outstanding_invoices(customer): inv["aging_bucket"] = _aging_bucket(inv["days_overdue"]) return invoices + + +def _address_display(doctype, name): + """Return the formatted (HTML) default address for a party, or ''.""" + address_name = get_default_address(doctype, name) + if not address_name: + return "" + return get_address_display(frappe.get_doc("Address", address_name).as_dict()) or "" + + +def _resolve_company(invoices): + """Pick the company for the statement header/return address.""" + if invoices: + return invoices[0].company + return frappe.defaults.get_user_default("Company") or frappe.db.get_single_value( + "Global Defaults", "default_company" + ) + + +def get_statement_data(customer): + """Assemble everything the statement template needs for one customer.""" + cust = frappe.get_doc("Customer", customer) + invoices = _get_outstanding_invoices(customer) + + company = _resolve_company(invoices) + company_doc = frappe.get_doc("Company", company) if company else None + + aging = {"Current": 0.0, "1-30": 0.0, "31-60": 0.0, "61-90": 0.0, "90+": 0.0} + total_outstanding = 0.0 + for inv in invoices: + aging[inv["aging_bucket"]] += flt(inv["outstanding_amount"]) + total_outstanding += flt(inv["outstanding_amount"]) + + # Late fee is booked and populated by generate_statements (later commit); + # get_statement_data on its own reports a zero fee. + late_fee = 0.0 + + return { + "customer": cust.name, + "customer_name": cust.customer_name, + "customer_address": _address_display("Customer", cust.name), + "company": company, + "company_name": company_doc.company_name if company_doc else "", + "return_address": _address_display("Company", company) if company else "", + "currency": (company_doc.default_currency if company_doc else None) + or frappe.db.get_single_value("Global Defaults", "default_currency"), + "invoices": invoices, + "aging": aging, + "total_outstanding": total_outstanding, + "late_fee": late_fee, + "total_due": total_outstanding + late_fee, + "statement_date": nowdate(), + } + + +def _render_page(data): + path = frappe.get_app_path( + "ns_app", "templates", "statements", "customer_statement.html" + ) + with open(path) as f: + template = f.read() + return frappe.render_template(template, {"s": data}) + + +def _wrap_document(pages): + """Wrap rendered per-customer pages in a printable HTML document.""" + body = "\n".join(pages) + return f""" + +
+ +| Invoice | +Date | +Due Date | +Days Overdue | +Aging | +Outstanding | +
|---|---|---|---|---|---|
| {{ inv.name }} | +{{ frappe.utils.formatdate(inv.posting_date, "MM-dd-yyyy") }} | +{{ frappe.utils.formatdate(inv.due_date, "MM-dd-yyyy") }} | +{{ inv.days_overdue if inv.days_overdue else "—" }} | +{{ inv.aging_bucket }} | +{{ fmt(inv.outstanding_amount, currency=s.currency) }} | +
Total Outstanding:{{ fmt(s.total_outstanding, currency=s.currency) }}
+ {% if s.late_fee and s.late_fee > 0 %} +Late Payment Fee:{{ fmt(s.late_fee, currency=s.currency) }}
+ {% endif %} +Total Due:{{ fmt(s.total_due, currency=s.currency) }}
+| Current | +1–30 | +31–60 | +61–90 | +90+ | +
|---|---|---|---|---|
| {{ fmt(s.aging["Current"], currency=s.currency) }} | +{{ fmt(s.aging["1-30"], currency=s.currency) }} | +{{ fmt(s.aging["31-60"], currency=s.currency) }} | +{{ fmt(s.aging["61-90"], currency=s.currency) }} | +{{ fmt(s.aging["90+"], currency=s.currency) }} | +