From bcdf8d2889bfc87fb2d5f12c8f80a311e491fd21 Mon Sep 17 00:00:00 2001 From: Marco Date: Sat, 25 Jul 2026 19:48:08 +0000 Subject: [PATCH] =?UTF-8?q?Move=20=C2=A719/=C2=A74-Nr.-1b-UStG=20notice=20?= =?UTF-8?q?out=20of=20the=20summary=20card?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same fix invoicePdf.tsx's own unpaid-notice row already got: the exemption/Kleinunternehmer notice used to live inside summaryBox's narrow, right-aligned column, squeezing what's actually a full sentence. Now a full-width sibling row below the card, same treatment in both invoicePdf.tsx and correctionInvoicePdf.tsx, and ordered ahead of the unpaid-notice row when both apply. Co-Authored-By: Claude Sonnet 5 --- README.md | 2 +- package.json | 2 +- src/correctionInvoicePdf.tsx | 35 ++++++++++++++------ src/invoicePdf.tsx | 63 +++++++++++++++++++++++------------- 4 files changed, 67 insertions(+), 35 deletions(-) diff --git a/README.md b/README.md index 11c439a..1890a6d 100644 --- a/README.md +++ b/README.md @@ -70,7 +70,7 @@ Ships raw TypeScript/TSX source (no build step) via `main`/`types` pointing stra `InvoiceOrder.vatExempt`/`CorrectionInvoiceOrder.vatExempt` (optional, default falsy) — set by the consuming app after its own checkout confirms (live VIES lookup) that a sale qualifies as an innergemeinschaftliche Lieferung. This package never decides the exemption itself — by the time an order reaches these renderers, every item's `unitPrice` is already de-grossed (net) and `taxRatePercent` already `0`; `vatExempt` only controls **display**: -- Visual PDF (`invoicePdf.tsx`/`correctionInvoicePdf.tsx`): the "enthält X% MwSt." annotation under Gesamt becomes "Steuerfreie innergemeinschaftliche Lieferung (§4 Nr. 1b UStG)" instead. +- Visual PDF (`invoicePdf.tsx`/`correctionInvoicePdf.tsx`): the "enthält X% MwSt." annotation under Gesamt becomes "Steuerfreie innergemeinschaftliche Lieferung (§4 Nr. 1b UStG)" instead — rendered as its own full-width row below the summary card (same treatment as the Vorkasse/unpaid notice), not squeezed into the card's narrow right-aligned column. Moved out 2026-07-25, at the user's explicit request. - E-invoice XML (`buildEInvoiceData.ts`): VAT category `K` + VATEX-EU-IC exemption reason — see "E-invoicing" above for exactly where those fields are (and aren't) allowed to live. See the frontend repo's own README ("VAT exemption" section) for the actual VIES lookup, de-grossing math, and checkout UI this feeds from, and the Payload backend's README ("B2B checkout & VAT exemption") for the persisted `Orders.vatExempt`/`vatIdValidatedAt` fields and audit-trail reasoning. diff --git a/package.json b/package.json index 59ce3e3..7c3377e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@einfach-produktiv/invoicing", - "version": "0.2.4", + "version": "0.2.5", "private": true, "description": "Shared invoice / correction-invoice (Stornorechnung, Gutschrift) PDF generation and VAT-breakdown math, consumed as a git dependency by both the einfach-produktiv frontend and the payload backend — not published to npm.", "type": "module", diff --git a/src/correctionInvoicePdf.tsx b/src/correctionInvoicePdf.tsx index acb65cb..be6a102 100644 --- a/src/correctionInvoicePdf.tsx +++ b/src/correctionInvoicePdf.tsx @@ -96,6 +96,11 @@ const styles = StyleSheet.create({ vatNoteRow: { flexDirection: "row", justifyContent: "space-between", paddingVertical: 1 }, vatNoteLabel: { fontSize: 8, color: TEXT_MUTED }, vatNoteValue: { fontSize: 8, color: TEXT_MUTED }, + // Same fix as invoicePdf.tsx's own taxExemptionNoticeRow/Text — the + // §19/§4-Nr.-1b-UStG notice moved out of the narrow summaryBox card to + // its own full-width sibling row below it, 2026-07-25. + taxExemptionNoticeText: { fontSize: 9, lineHeight: 1.5, color: TEXT_MUTED, textAlign: "left" }, + taxExemptionNoticeRow: { width: "100%", alignSelf: "stretch", marginTop: 20 }, // `fixed` (on the element, see CorrectionInvoiceDocument below) — always // pinned to the bottom of the page regardless of content height above, // same as invoicePdf.tsx's own footer. (One of the two pre-package @@ -346,20 +351,19 @@ function CorrectionInvoiceDocument({ kind, order, seller }: { kind: CorrectionIn ordering fix as invoicePdf.tsx: "enthält X% MwSt." describes Gesamt, not Netto (which has the tax already taken out), so it must not read as attached to Netto. */} - - {order.kleinunternehmer ? ( - Gemäß § 19 UStG wird keine Umsatzsteuer berechnet. - ) : order.vatExempt ? ( - Steuerfreie innergemeinschaftliche Lieferung (§4 Nr. 1b UStG) - ) : ( - rateGroups.map((g) => ( + {/* Exempt/Kleinunternehmer orders get their own full-width + notice below the card instead (see taxExemptionNoticeRow + below) — same fix as invoicePdf.tsx. */} + {!order.kleinunternehmer && !order.vatExempt && ( + + {rateGroups.map((g) => ( enthält {g.rate}% MwSt. -{formatPrice(g.tax)} - )) - )} - + ))} + + )} {/* Netto — same reasoning as invoicePdf.tsx's own row: a single well-defined figure (Gesamt minus every rate's own tax, summed) regardless of how many rates are present. @@ -383,6 +387,17 @@ function CorrectionInvoiceDocument({ kind, order, seller }: { kind: CorrectionIn )} + {/* Sibling of `summary`, not nested inside it — same fix as + invoicePdf.tsx's own taxExemptionNoticeRow. */} + {(order.kleinunternehmer || order.vatExempt) && ( + + + {order.kleinunternehmer + ? "Gemäß § 19 UStG wird keine Umsatzsteuer berechnet." + : "Steuerfreie innergemeinschaftliche Lieferung (§4 Nr. 1b UStG)"} + + + )} diff --git a/src/invoicePdf.tsx b/src/invoicePdf.tsx index 564c80d..8510cd4 100644 --- a/src/invoicePdf.tsx +++ b/src/invoicePdf.tsx @@ -64,6 +64,16 @@ const styles = StyleSheet.create({ // important instruction than a one-line confirmation, needs to read as // its own section, not crowd the summary card above it. unpaidNoticeRow: { width: "100%", alignSelf: "stretch", marginTop: 20 }, + // Same full-width-below-the-card treatment as unpaidNoticeText/Row + // above, same reasoning: the §19/§4-Nr.-1b-UStG notice used to live + // *inside* summaryBox (see vatNotes' own comment), where its narrow + // right-aligned column squeezed what's actually an important legal + // statement about the invoice as a whole, not a figure belonging to the + // Zwischensumme/Rabatt/Versand/Gesamt build-up. Moved out to its own + // sibling row 2026-07-25, at the user's explicit request, once the + // unpaid-notice fix above had already established the pattern. + taxExemptionNoticeText: { fontSize: 9, lineHeight: 1.5, color: TEXT_MUTED, textAlign: "left" }, + taxExemptionNoticeRow: { width: "100%", alignSelf: "stretch", marginTop: 20 }, table: { borderRadius: 6, overflow: "hidden", borderWidth: 1, borderColor: BORDER, marginTop: 8, marginBottom: 16 }, tableHeader: { flexDirection: "row", backgroundColor: BG_MUTED, paddingVertical: 8, paddingHorizontal: 10 }, // Every item row shares this same tinted background now — no more @@ -416,30 +426,20 @@ export function InvoiceDocument({ order, seller }: { order: InvoiceOrder; seller Gesamt {formatPrice(order.total)} - - {/* Exempt orders already have every item at 0% (see this - file's own InvoiceOrder.vatExempt comment) — "enthält - 0% MwSt.: 0,00 €" would be a meaningless thing to print, - so this replaces the whole per-rate breakdown with the - actual legal basis instead. Kept directly under Gesamt - (not after Netto) — "enthält X% MwSt." describes what's - contained in Gesamt; sandwiching Netto between the two - made it read as if Netto were what contained the tax, - which is backwards (Netto is the figure with tax - already taken out). */} - {order.kleinunternehmer ? ( - Gemäß § 19 UStG wird keine Umsatzsteuer berechnet. - ) : order.vatExempt ? ( - Steuerfreie innergemeinschaftliche Lieferung (§4 Nr. 1b UStG) - ) : ( - rateGroups.map((g) => ( + {/* Exempt/Kleinunternehmer orders get their own full-width + notice below the card instead (see taxExemptionNoticeRow + below) — this per-rate breakdown only ever applies to the + normal, non-exempt case now. */} + {!order.kleinunternehmer && !order.vatExempt && ( + + {rateGroups.map((g) => ( enthält {g.rate}% MwSt. {formatPrice(g.tax)} - )) - )} - + ))} + + )} {/* Netto (Gesamt minus every rate's own tax, summed — a single well-defined figure regardless of how many distinct rates the order actually has) — businesses read @@ -477,9 +477,26 @@ export function InvoiceDocument({ order, seller }: { order: InvoiceOrder; seller )} - {/* Sibling of `summary`, not nested inside it — `summary`'s own - `alignItems: "flex-end"` is meant for the narrow paid-badge - case, not this full-width, multi-sentence instruction. */} + {/* Both siblings of `summary`, not nested inside it — `summary`'s + own `alignItems: "flex-end"` is meant for the narrow + paid-badge case, not these full-width, multi-sentence + notices. Legal notice first, payment instruction second — + when both apply (an unpaid, innergemeinschaftlich-exempt + Vorkasse order), the tax-exemption statement is the more + legally foundational one of the two. Yoga (react-pdf's + flexbox) doesn't collapse margins the way CSS does, so + `summary`'s own marginBottom: 24 plus each row's own + marginTop: 20 below still read as two clearly separated + blocks, not squeezed together. */} + {(order.kleinunternehmer || order.vatExempt) && ( + + + {order.kleinunternehmer + ? "Gemäß § 19 UStG wird keine Umsatzsteuer berechnet." + : "Steuerfreie innergemeinschaftliche Lieferung (§4 Nr. 1b UStG)"} + + + )} {!paid && (