diff --git a/docs/CUSTOMER_STATEMENTS.md b/docs/CUSTOMER_STATEMENTS.md new file mode 100644 index 0000000..5f746d2 --- /dev/null +++ b/docs/CUSTOMER_STATEMENTS.md @@ -0,0 +1,135 @@ +# Customer Statements & Late Payment Fees + +> Branch: `feature/customer-statements` + +Generates printable, **one-page-per-customer** account statements — formatted to +fit a standard #10 **double-window envelope** — for customers with overdue +invoices, and (optionally) bills a **late-payment fee** that posts to the ledger +and is collectible through the app's existing payment flow. + +--- + +## 1. What it does + +From either the **Customer list** or a **Customer form**, a user can generate +account statements: + +1. **Pick customers.** On the list, *Generate Statements* opens a dialog listing + every customer with overdue invoices (overdue count, max days overdue, total + outstanding) with select-all. On a Customer form, *Generate Statement* targets + that one customer. +2. **Choose whether to bill a late fee** via a checkbox in the popup + (*Generate late payment fee*, on by default). +3. **Get a printable report.** A new browser tab opens with one statement per + page — each showing the customer's open invoices, aging buckets + (Current / 1–30 / 31–60 / 61–90 / 90+), and a **Total Due**. The customer and + company (return) addresses sit in the two envelope-window positions. + +Each generation is recorded on the customer's timeline as an audit-trail entry. + +--- + +## 2. Late payment fees + +### How the fee is calculated +Interest uses ERPNext's own Dunning formula: + +``` +fee = Σ(invoice.outstanding × rate_of_interest/100/365 × days_overdue) + dunning_fee +``` + +### Where the settings live — ERPNext **Dunning Type** +All fee configuration comes from the existing **Dunning Type** doctype +(Accounting ▸ Dunning Type). Nothing is auto-created; generation stops with a +clear error until it is configured. The default (`is_default`) Dunning Type for +the company is used. Fields consumed: + +| Dunning Type field | Purpose | +|--------------------|---------| +| `rate_of_interest` | Annual interest rate (%) | +| `dunning_fee` | Flat fee added per statement | +| `income_account` | Credited when the fee is billed | +| `cost_center` | Cost center for the fee line (falls back to company default) | +| `custom_late_fee_item` | **Late Fee Item** — the Item used to bill the fee (custom field added by this app) | + +### How the fee posts, and how it gets paid +The fee is billed as a **submitted Sales Invoice** (item → the Dunning Type +income account). This is deliberate: because it is a real Sales Invoice it + +- increases the customer's receivable balance immediately, and +- appears in `get_unpaid_invoices` and is charged/settled automatically by the + app's existing payment paths (**Run Payment / AutoPay / multi-invoice** → + `create_payment_entry`), which allocate against Sales Invoices. + +A bare Journal Entry (or an ERPNext Dunning document) would raise the balance but +sit **uncollectible** by those flows — hence the Sales Invoice. + +### Fee invoice specifics +- **Dedicated naming series `LPF-.YYYY.-`** (e.g. `LPF-2026-00001`) so late-fee + invoices are easy to spot and filter. Registered on Sales Invoice's + `naming_series` via `after_migrate`. +- **Never taxed.** A single zero-amount "Actual" tax line keeps ERPNext from + auto-applying company/item tax templates, so the invoice total equals the + computed fee exactly and nothing extra hits the ledger. +- **Idempotent** — at most one fee invoice per customer / company / calendar + month. Prior fee invoices are excluded from the interest base (no fee-on-fee). + +On the statement the fee shows as a normal invoice line tagged **“late fee”**, +folded into a single **Total Due** that equals the customer's balance. + +--- + +## 3. Configuration / prerequisites + +1. **Migrate** the app (`bench --site migrate`) — creates the + `Late Fee Item` custom field on Dunning Type and registers the `LPF-` series. +2. Create an **Item** to represent the fee (a non-stock sales item, e.g. + "Late Payment Fee"). +3. Create/complete a **Dunning Type** for the company with: rate of interest, + dunning fee, **income account**, and the **Late Fee Item**. Mark it default. + +If any of these is missing, statement generation throws a clear, actionable +error and posts nothing. + +--- + +## 4. Audit trail + +Each generated statement adds an *Info* comment to the customer's timeline, e.g. + +> Statement generated — Total Due $557.17 (late fee invoice LPF-2026-00001). + +The note reflects the outcome: the fee invoice raised, *no late fee*, or +*late fee skipped* (when the fee checkbox was cleared). It is attributed to the +generating user. + +--- + +## 5. Files + +| File | Role | +|------|------| +| `ns_app/api/statements.py` | Overdue-customer query, statement builder, printable HTML, late-fee billing (Sales Invoice), audit-trail entry | +| `ns_app/templates/statements/customer_statement.html` | Jinja template for one customer page (envelope windows + aging table) | +| `ns_app/public/js/customer_statements.js` | List action + Customer-form button + selection/fee-toggle popups (loaded globally) | +| `ns_app/setup.py` | `after_migrate`: creates the Late Fee Item custom field, registers the `LPF-` naming series | +| `ns_app/hooks.py` | Wires the JS (`app_include_js`) and `after_migrate` | + +### Server API (`ns_app.api.statements`) +- `get_customers_with_overdue_invoices()` — customers with overdue invoices. +- `generate_statements(customers, skip_late_fee=0)` — bills fees (unless skipped), + renders the printable HTML, records the audit entry. Returns + `{html, rendered, skipped}`. +- `get_statement_data(customer)` — the per-customer statement data (internal). + +Access is restricted to System Manager, Sales User/Manager, Accounts +User/Manager. + +--- + +## 6. Notes / non-goals + +- No persisted "Statement" doctype — statements are generated on demand. +- No email/fax delivery — print only. +- Whether the fee should be taxed and the interest rate/fee amounts are business + settings, controlled entirely through Dunning Type.