Statement generation raised a fresh LPF invoice every month, so a customer
who never paid accumulated a pile of small invoices, each carrying the flat
dunning_fee again. Interest was also recomputed from each invoice's due date
every run, re-billing periods already charged for.
Now a customer gets one fee invoice per collections episode:
- While an earlier fee invoice still carries a balance, the next run amends
it and appends the new period's interest as a further line item, rather
than creating a second invoice.
- Payment Entries allocated to a partly paid fee invoice are unlinked by the
cancellation and re-applied to the amended invoice via
reconcile_against_document (the primitive Payment Reconciliation uses), so
the outstanding amount and Payment Ledger stay correct.
- Original posting and due dates are carried over. Re-dating to today would
reset the invoice to Current in the statement's aging buckets and hide how
long the balance has been owed.
- Interest accrues from the last run, tracked by a new
custom_late_fee_billed_upto field on Sales Invoice, so no period is billed
twice. Fee invoices predating the field fall back to their posting date,
which is when they were billed, so no migration patch is needed.
- The flat dunning_fee is charged once, when a fee invoice is first raised,
not again on every top-up.
- Unpaid fee invoices are in the interest base on the same terms as any
other overdue receivable, so interest compounds onto the fee balance.
Amending means cancelling, which is only reversible for links we can
restore. If the open fee invoice has a Journal Entry or credit note applied,
a negative payment allocation, or a posting date in a frozen period, it is
left alone, the charge goes on a new invoice, and the reason is recorded on
the customer's timeline.
Verified against nsi.local with two rolled-back integration probes covering
the amend + re-link path (including two consecutive amendments) and the
blocked-amend fallback.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Move the customer address window up to 2.5in per printed proof, and
pull the body padding-top back up to match.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Company (return) address down 0.25in (to 0.8in) and customer address
down 1.5in (to 3.0in) to line up with the #9 double-window envelope,
per a printed proof. Push the body padding-top to clear the lower
customer window.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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>
- Bill late-fee invoices under a dedicated naming series (LPF-.YYYY.-),
registered on Sales Invoice via after_migrate, so they are easy to spot
and filter.
- Late fees are never taxed: a zero 'Actual' tax line keeps the taxes
table non-empty so ERPNext skips auto-applying company/item tax
templates (posts nothing to the ledger). Fee total == computed fee.
- generate_statements(skip_late_fee=...) generates a statement without
billing a fee.
- Record every generation on the customer's timeline (add_comment) as an
audit trail, noting Total Due and the fee invoice raised / skipped.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Charge a late-payment fee when statements are generated so the customer's
receivable reflects it (the gap ERPNext Dunning leaves — it never
increases AR). The fee is billed as a submitted Sales Invoice (item ->
Dunning Type income account, rate = computed fee) rather than a Journal
Entry, so the app's existing payment flow (Run Payment / AutoPay /
multi-invoice) charges and settles it automatically via its Sales Invoice
references — a bare JE would sit uncollected.
Fee schedule/amounts come from the existing Dunning Type settings (yearly
rate_of_interest + flat dunning_fee), interest computed with ERPNext's own
Dunning formula. The fee Item is configured via a new Late Fee Item custom
field on Dunning Type (created in an after_migrate hook; ns_app/setup.py).
Nothing is auto-seeded: generation stops with a clear error if no Dunning
Type is configured or its income account / fee item is unset.
Billing is idempotent per customer/company/month, and prior fee invoices
are excluded from the interest base (no fee-on-fee). The fee invoice shows
on the statement flagged 'late fee', folded into Total Due (which equals
the customer's balance and is fully collectible).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add get_statement_data (open invoices, aging buckets, totals, formatted
customer + company addresses) and generate_statements, which renders one
page per customer via a Jinja template and returns a printable HTML
document. Recipient window geometry (top:1.9in/left:1.125in) mirrors the
existing double-window print formats for #10 envelope compatibility; each
page uses page-break-after:always. Late fee is a zero placeholder here.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Add ns_app/api/statements.py with get_customers_with_overdue_invoices
(one aggregated row per customer with overdue Sales Invoices) plus the
_get_outstanding_invoices / _aging_bucket helpers used to build the
per-customer statement.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>