Files
ns_erpnext_app/ns_app/templates/statements/customer_statement.html
Norman King acd7df1129 feat(statements): bill late fee as a collectible Sales Invoice
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>
2026-07-08 19:35:04 -04:00

97 lines
3.5 KiB
HTML
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.
{# One customer account statement = one printed page.
Envelope geometry (recipient window at top:1.9in / left:1.125in) mirrors the
existing double-window print formats so the same #10 double-window envelopes
work. Rendered via frappe.render_template with context key `s`
(see ns_app.api.statements.get_statement_data). #}
{% set fmt = frappe.utils.fmt_money %}
<div class="statement-page">
<!-- Return address (top-left envelope window) -->
<div class="return-window">
<strong>{{ s.company_name }}</strong><br>
{{ s.return_address | safe }}
</div>
<!-- Document header (top-right) -->
<div class="doc-header">
<div class="doc-title">STATEMENT</div>
<div><strong>Date:</strong> {{ frappe.utils.formatdate(s.statement_date, "MM-dd-yyyy") }}</div>
<div><strong>Account:</strong> {{ s.customer }}</div>
</div>
<!-- Recipient address (lower envelope window) -->
<div class="recipient-window">
{{ s.customer_name }}<br>
{{ s.customer_address | safe }}
</div>
<!-- Statement body (starts below the address windows) -->
<div class="statement-body">
<div class="intro">
The following is a summary of your account as of
{{ frappe.utils.formatdate(s.statement_date, "MM-dd-yyyy") }}.
Please remit payment for any past-due balance at your earliest convenience.
</div>
<table class="items">
<thead>
<tr>
<th>Invoice</th>
<th class="c">Date</th>
<th class="c">Due Date</th>
<th class="c">Days Overdue</th>
<th class="c">Aging</th>
<th class="r">Outstanding</th>
</tr>
</thead>
<tbody>
{% for inv in s.invoices %}
<tr class="{{ 'overdue' if inv.is_overdue else '' }}">
<td>{{ inv.name }}{% if inv.is_late_fee %} <span class="tag">late fee</span>{% endif %}</td>
<td class="c">{{ frappe.utils.formatdate(inv.posting_date, "MM-dd-yyyy") }}</td>
<td class="c">{{ frappe.utils.formatdate(inv.due_date, "MM-dd-yyyy") }}</td>
<td class="c">{{ inv.days_overdue if inv.days_overdue else "—" }}</td>
<td class="c">{{ inv.aging_bucket }}</td>
<td class="r">{{ fmt(inv.outstanding_amount, currency=s.currency) }}</td>
</tr>
{% endfor %}
</tbody>
</table>
<!-- Totals -->
<div class="totals">
<p class="grand"><span>Total Due:</span><span>{{ fmt(s.total_due, currency=s.currency) }}</span></p>
</div>
<!-- Aging summary -->
<table class="aging">
<thead>
<tr>
<th class="c">Current</th>
<th class="c">130</th>
<th class="c">3160</th>
<th class="c">6190</th>
<th class="c">90+</th>
</tr>
</thead>
<tbody>
<tr>
<td class="c">{{ fmt(s.aging["Current"], currency=s.currency) }}</td>
<td class="c">{{ fmt(s.aging["1-30"], currency=s.currency) }}</td>
<td class="c">{{ fmt(s.aging["31-60"], currency=s.currency) }}</td>
<td class="c">{{ fmt(s.aging["61-90"], currency=s.currency) }}</td>
<td class="c">{{ fmt(s.aging["90+"], currency=s.currency) }}</td>
</tr>
</tbody>
</table>
<div class="footer">
Prompt payment is always appreciated. We accept payments by check or over
the phone using a debit or credit card. Automatic payment setup is also
available upon request. Please contact us if payment has already been sent.
</div>
</div><!-- /statement-body -->
</div><!-- /statement-page -->