docs: describe the customer statements feature

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-08 19:58:09 -04:00
parent 46967208e9
commit 5181f4a177

135
docs/CUSTOMER_STATEMENTS.md Normal file
View File

@@ -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 / 130 / 3160 / 6190 / 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 <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.