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>
97 lines
3.5 KiB
HTML
97 lines
3.5 KiB
HTML
{# 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">1–30</th>
|
||
<th class="c">31–60</th>
|
||
<th class="c">61–90</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 -->
|