Files
ns_erpnext_app/docs/CUSTOMER_STATEMENTS.md
Norman King 0aefdeb5ff fix(statements): set envelope geometry for #9 double-window
Reset the statement window positions to the app's proven #9 (9x4)
double-window geometry, mirroring sales_invoice_ns.html — recipient
window at top:1.5in/left:1.125in. It previously copied the dunning
format's 1.9in, which sits too low for a #9. Tighten the body
padding-top to keep clearance below the higher window, and correct the
stale #10 references in the template comment and docs.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 07:52:07 -04:00

136 lines
5.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Customer Statements & Late Payment Fees
> Branch: `feature/customer-statements`
Generates printable, **one-page-per-customer** account statements — formatted to
fit a standard #9 (9x4) **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.