feat(crm): Mahngebühr on a separate Mahnung document; invoice stays immutable
Corrected dunning model (Mara): a Mahnung is a reminder LETTER showing the new total (original + Mahngebühr), NOT a separate invoice and NOT a mutation of the issued invoice. - The invoice PDF no longer shows the fee (buildInvoiceRenderContext reports lateFeeAmountMinor 0) and is NEVER re-rendered by a reminder — it stays immutable (§14/§11). - applyReminder now: tracks the fee as dunning state on the row (gross late_fee_amount_minor + new late_fee_vat_minor for the VAT portion, migration 144), renders a separate MAHNUNG PDF (pdfService 'mahnung' kind — reuses the invoice layout: same lines + Mahngebühr row + new total, 'Mahnung' title, no QR), stored under storage/business-docs/mahnung/, and attaches BOTH the unchanged original invoice + the Mahnung to the reminder email. - Fee resolvers split into net + VAT-rate (toggle + org-rate gated); a gross wrapper feeds the payment-check preview. en + de PDF title. Outstanding/collections still read late_fee_amount_minor (now dunning state). P3 (tax-report/Banana booking of the Mahngebühr VAT) stays Treuhänder-gated. Syntax + 17/17 workflow/invoice tests green. NOTE: the Mahnung PDF render path isn't unit-tested (PDF rendering is flaky in the test env) — eyeball on the dev box: fire a level-2 reminder, confirm the Mahnung PDF shows the new total and the original invoice PDF is unchanged.
This commit is contained in:
@@ -1477,6 +1477,10 @@ function renderDocument(type, context) {
|
||||
// family — Storni share the invoice renderer surface, only
|
||||
// the cosmetic + accounting-sign branches differ.
|
||||
const isStorno = type === 'invoice' && ctx.doc.kind === 'storno';
|
||||
// Mahnung (reminder letter) reuses the invoice surface: same line items +
|
||||
// a Mahngebühr row + the new grand total, but a "Mahnung" title and NO
|
||||
// QR (the QR would encode the original amount, not the new total).
|
||||
const isMahnung = type === 'invoice' && ctx.doc.kind === 'mahnung';
|
||||
|
||||
// ---- document number (above) + date (below), both right-aligned
|
||||
// The number sits directly under the sender address block so the
|
||||
@@ -1516,7 +1520,9 @@ function renderDocument(type, context) {
|
||||
? t(ctx.locale, 'quote_title')
|
||||
: isStorno
|
||||
? t(ctx.locale, 'storno_title')
|
||||
: t(ctx.locale, 'invoice_title');
|
||||
: isMahnung
|
||||
? t(ctx.locale, 'mahnung_title')
|
||||
: t(ctx.locale, 'invoice_title');
|
||||
y = drawTitle(doc, title, leftX, y + 2);
|
||||
|
||||
// Mandatory Storno reference line — "Bezug: Storno zu Rechnung
|
||||
@@ -1700,7 +1706,7 @@ function renderDocument(type, context) {
|
||||
// Both append a fresh page; 'none' is a no-op.
|
||||
// Suppressed on Stornorechnungen — negative-amount QR codes
|
||||
// aren't a defined construct in either spec.
|
||||
if (type === 'invoice' && !isStorno) {
|
||||
if (type === 'invoice' && !isStorno && !isMahnung) {
|
||||
if (ctx.qrFormat === 'swiss') {
|
||||
appendSwissQrBill(doc, ctx);
|
||||
} else if (ctx.qrFormat === 'epc') {
|
||||
|
||||
Reference in New Issue
Block a user