diff --git a/src/correctionInvoicePdf.tsx b/src/correctionInvoicePdf.tsx index 1f389d5..9599b51 100644 --- a/src/correctionInvoicePdf.tsx +++ b/src/correctionInvoicePdf.tsx @@ -78,15 +78,22 @@ const styles = StyleSheet.create({ bundleLine: { fontSize: 8, color: TEXT_MUTED, marginTop: 2 }, summary: { alignItems: "flex-end", marginBottom: 24 }, summaryBox: { width: 240, backgroundColor: BG_MUTED, borderRadius: 6, padding: 14 }, - // See invoicePdf.tsx's own comment on this same style — groups rows that - // belong together with a bit of breathing room between groups. - summaryCluster: { marginBottom: 6 }, summaryRow: { flexDirection: "row", justifyContent: "space-between", paddingVertical: 2 }, summaryLabel: { fontSize: 10, color: TEXT_MUTED }, summaryValue: { fontSize: 10 }, grandTotalRow: { flexDirection: "row", justifyContent: "space-between", paddingTop: 8, marginTop: 6, borderTopWidth: 1, borderTopColor: BORDER }, grandTotalLabel: { fontSize: 12, fontFamily: "Helvetica-Bold" }, grandTotalValue: { fontSize: 12, fontFamily: "Helvetica-Bold" }, + // Same "enthält X% MwSt." annotation-under-Gesamt framing as + // invoicePdf.tsx now uses, fixing the identical double-counting bug this + // file had: the old layout showed Versand as its own row AND ALSO + // folded into the per-rate Netto/MwSt rows below it (via + // computeTaxBreakdown's scale factor) — the visible rows never summed + // to the printed Gesamt. Fixed 2026-07-23. + vatNotes: { marginTop: 6 }, + vatNoteRow: { flexDirection: "row", justifyContent: "space-between", paddingVertical: 1 }, + vatNoteLabel: { fontSize: 8, color: TEXT_MUTED }, + vatNoteValue: { fontSize: 8, color: TEXT_MUTED }, // `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 @@ -288,34 +295,49 @@ function CorrectionInvoiceDocument({ kind, order, seller }: { kind: CorrectionIn - {/* Storno reverses the full original invoice, shipping - included (see this file's top-of-file comment on why the - two kinds differ) — shown as its own line instead of - silently folded into the tax-rate groups below, same as - the original invoice's own Versand row. Gutschrift never - reverses shipping, so this never renders for it. */} - {kind === "storno" && order.shippingCost > 0 && ( - - Versand - -{formatPrice(order.shippingCost)} - + {/* Genuinely additive chain for Storno — Zwischensumme, the + discount reversal ("Rabatt (entfällt)", a positive + add-back since the original discount no longer applies + once everything is undone), and Versand always sum + exactly to Gesamt below. Gutschrift never reverses + shipping/discount (see this file's top comment), so its + Gesamt is just the returned lines' own total — nothing to + itemize above it. Previously showed Versand as its own + row AND folded into the per-rate Netto/MwSt rows below + (double-counted, visible rows never summed to Gesamt) — + fixed 2026-07-23, same as invoicePdf.tsx. */} + {kind === "storno" && ( + <> + + Zwischensumme + -{formatPrice(order.subtotal)} + + {order.discountAmount > 0 && ( + + Rabatt (entfällt) + +{formatPrice(order.discountAmount)} + + )} + {order.shippingCost > 0 && ( + + Versand + -{formatPrice(order.shippingCost)} + + )} + )} - {rateGroups.map((g) => ( - - - Netto - -{formatPrice(g.net)} - - - zzgl. {g.rate}% MwSt. - -{formatPrice(g.tax)} - - - ))} Gesamt -{formatPrice(grandTotal)} + + {rateGroups.map((g) => ( + + enthält {g.rate}% MwSt. + -{formatPrice(g.tax)} + + ))} + diff --git a/src/invoicePdf.tsx b/src/invoicePdf.tsx index 092e99e..a8b09f7 100644 --- a/src/invoicePdf.tsx +++ b/src/invoicePdf.tsx @@ -70,18 +70,28 @@ const styles = StyleSheet.create({ bundleLine: { fontSize: 8, color: TEXT_MUTED, marginTop: 2 }, summary: { alignItems: "flex-end", marginBottom: 24 }, summaryBox: { width: 240, backgroundColor: BG_MUTED, borderRadius: 6, padding: 14 }, - // Groups rows that belong together (Rabatt+Versand; each rate's own - // Netto/MwSt pair) with a bit of breathing room between groups, while - // rows *within* a group stay at the original tight spacing — a visual - // cue that e.g. "Netto"/"zzgl. 19% MwSt." are one line item's two halves, - // not just an undifferentiated stack of numbers. - summaryCluster: { marginBottom: 6 }, summaryRow: { flexDirection: "row", justifyContent: "space-between", paddingVertical: 2 }, summaryLabel: { fontSize: 10, color: TEXT_MUTED }, summaryValue: { fontSize: 10 }, grandTotalRow: { flexDirection: "row", justifyContent: "space-between", paddingTop: 8, marginTop: 6, borderTopWidth: 1, borderTopColor: BORDER }, grandTotalLabel: { fontSize: 12, fontFamily: "Helvetica-Bold" }, grandTotalValue: { fontSize: 12, fontFamily: "Helvetica-Bold" }, + // "Enthält X% MwSt.: Y €" annotations under Gesamt — same "contained + // within the total, not an additional deduction" framing the website's + // own VatBreakdown component already uses on /cart and /checkout. + // Deliberately NOT part of the summaryRow additive chain above (Zwischen- + // summe → Rabatt → Versand → Gesamt): every row in that chain sums + // exactly to Gesamt on its own; the tax-rate breakdown is informational + // context about what portion of Gesamt is tax, not a further adjustment. + // A previous version showed the same per-rate net/tax figures as their + // own additive-looking rows alongside Rabatt/Versand, which double- + // counted shipping/discount (already proportionally folded into each + // rate's own net/tax by computeTaxBreakdown) — the visible rows never + // actually summed to the printed Gesamt. Fixed 2026-07-23. + vatNotes: { marginTop: 6 }, + vatNoteRow: { flexDirection: "row", justifyContent: "space-between", paddingVertical: 1 }, + vatNoteLabel: { fontSize: 8, color: TEXT_MUTED }, + vatNoteValue: { fontSize: 8, color: TEXT_MUTED }, // Moved here (below the summary card) from the top meta row — reads more // naturally right next to the amount it's confirming was paid, and keeps // the meta row itself to just the three reference numbers. Deliberately @@ -340,34 +350,36 @@ export function InvoiceDocument({ order, seller }: { order: InvoiceOrder; seller - - {order.discountAmount > 0 && ( - - Rabatt{order.discountCode ? ` (${order.discountCode})` : ""} - -{formatPrice(order.discountAmount)} - - )} - - Versand - {order.shippingCost === 0 ? "Kostenlos" : formatPrice(order.shippingCost)} - + {/* Genuinely additive chain — Zwischensumme (raw item gross, + unscaled) − Rabatt + Versand always equals Gesamt exactly, + unlike the old layout this replaced (see vatNotes' own + comment above). */} + + Zwischensumme + {formatPrice(order.subtotal)} - {rateGroups.map((g) => ( - - - Netto - {formatPrice(g.net)} - - - zzgl. {g.rate}% MwSt. - {formatPrice(g.tax)} - + {order.discountAmount > 0 && ( + + Rabatt{order.discountCode ? ` (${order.discountCode})` : ""} + -{formatPrice(order.discountAmount)} - ))} + )} + + Versand + {order.shippingCost === 0 ? "Kostenlos" : formatPrice(order.shippingCost)} + Gesamt {formatPrice(order.total)} + + {rateGroups.map((g) => ( + + enthält {g.rate}% MwSt. + {formatPrice(g.tax)} + + ))} + {paid && (