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>
136 lines
5.8 KiB
Markdown
136 lines
5.8 KiB
Markdown
# 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 / 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 <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.
|