Move §19/§4-Nr.-1b-UStG notice out of the summary card
Validate e-invoices / mustang (push) Successful in 22s
Validate e-invoices / mustang (push) Successful in 22s
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
|
||||
+1
-1
@@ -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",
|
||||
|
||||
@@ -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. */}
|
||||
<View style={styles.vatNotes}>
|
||||
{order.kleinunternehmer ? (
|
||||
<Text style={styles.vatNoteLabel}>Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.</Text>
|
||||
) : order.vatExempt ? (
|
||||
<Text style={styles.vatNoteLabel}>Steuerfreie innergemeinschaftliche Lieferung (§4 Nr. 1b UStG)</Text>
|
||||
) : (
|
||||
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 && (
|
||||
<View style={styles.vatNotes}>
|
||||
{rateGroups.map((g) => (
|
||||
<View style={styles.vatNoteRow} key={g.rate}>
|
||||
<Text style={styles.vatNoteLabel}>enthält {g.rate}% MwSt.</Text>
|
||||
<Text style={styles.vatNoteValue}>-{formatPrice(g.tax)}</Text>
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
{/* 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
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
{/* Sibling of `summary`, not nested inside it — same fix as
|
||||
invoicePdf.tsx's own taxExemptionNoticeRow. */}
|
||||
{(order.kleinunternehmer || order.vatExempt) && (
|
||||
<View style={styles.taxExemptionNoticeRow}>
|
||||
<Text style={styles.taxExemptionNoticeText}>
|
||||
{order.kleinunternehmer
|
||||
? "Gemäß § 19 UStG wird keine Umsatzsteuer berechnet."
|
||||
: "Steuerfreie innergemeinschaftliche Lieferung (§4 Nr. 1b UStG)"}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.footer} fixed>
|
||||
|
||||
+40
-23
@@ -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
|
||||
<Text style={styles.grandTotalLabel}>Gesamt</Text>
|
||||
<Text style={styles.grandTotalValue}>{formatPrice(order.total)}</Text>
|
||||
</View>
|
||||
<View style={styles.vatNotes}>
|
||||
{/* 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 ? (
|
||||
<Text style={styles.vatNoteLabel}>Gemäß § 19 UStG wird keine Umsatzsteuer berechnet.</Text>
|
||||
) : order.vatExempt ? (
|
||||
<Text style={styles.vatNoteLabel}>Steuerfreie innergemeinschaftliche Lieferung (§4 Nr. 1b UStG)</Text>
|
||||
) : (
|
||||
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 && (
|
||||
<View style={styles.vatNotes}>
|
||||
{rateGroups.map((g) => (
|
||||
<View style={styles.vatNoteRow} key={g.rate}>
|
||||
<Text style={styles.vatNoteLabel}>enthält {g.rate}% MwSt.</Text>
|
||||
<Text style={styles.vatNoteValue}>{formatPrice(g.tax)}</Text>
|
||||
</View>
|
||||
))
|
||||
)}
|
||||
</View>
|
||||
))}
|
||||
</View>
|
||||
)}
|
||||
{/* 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
|
||||
</View>
|
||||
)}
|
||||
</View>
|
||||
{/* 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) && (
|
||||
<View style={styles.taxExemptionNoticeRow}>
|
||||
<Text style={styles.taxExemptionNoticeText}>
|
||||
{order.kleinunternehmer
|
||||
? "Gemäß § 19 UStG wird keine Umsatzsteuer berechnet."
|
||||
: "Steuerfreie innergemeinschaftliche Lieferung (§4 Nr. 1b UStG)"}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
{!paid && (
|
||||
<View style={styles.unpaidNoticeRow}>
|
||||
<Text style={styles.unpaidNoticeText}>
|
||||
|
||||
Reference in New Issue
Block a user