Fix invoice/correction-invoice summary: rows now actually sum to Gesamt
Validate e-invoices / mustang (push) Successful in 21s
Validate e-invoices / mustang (push) Successful in 21s
The Netto/MwSt. rows were computed per tax rate with shipping and discount already proportionally folded in (computeTaxBreakdown's scale factor — tax-correct, since ancillary costs are apportioned across rates), but Rabatt/Versand were ALSO shown as their own separate rows on top. Adding up the visible rows never actually matched the printed Gesamt — off by exactly the shipping (and any discount) amount. Confirmed against a real production order (#EP-0006-ZDX6's Stornorechnung). Restructured both templates into a genuinely additive chain (Zwischensumme → Rabatt → Versand → Gesamt, using the raw order fields directly) with the per-rate tax now shown as an "enthält X% MwSt." annotation below Gesamt — informational, not part of the sum. Mirrors the pattern the website's own VatBreakdown component already uses on /cart and /checkout. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
<View style={styles.summary}>
|
||||
<View style={styles.summaryBox}>
|
||||
{/* 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 && (
|
||||
<View style={[styles.summaryRow, styles.summaryCluster]}>
|
||||
<Text style={styles.summaryLabel}>Versand</Text>
|
||||
<Text style={styles.summaryValue}>-{formatPrice(order.shippingCost)}</Text>
|
||||
</View>
|
||||
{/* 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" && (
|
||||
<>
|
||||
<View style={styles.summaryRow}>
|
||||
<Text style={styles.summaryLabel}>Zwischensumme</Text>
|
||||
<Text style={styles.summaryValue}>-{formatPrice(order.subtotal)}</Text>
|
||||
</View>
|
||||
{order.discountAmount > 0 && (
|
||||
<View style={styles.summaryRow}>
|
||||
<Text style={styles.summaryLabel}>Rabatt (entfällt)</Text>
|
||||
<Text style={styles.summaryValue}>+{formatPrice(order.discountAmount)}</Text>
|
||||
</View>
|
||||
)}
|
||||
{order.shippingCost > 0 && (
|
||||
<View style={styles.summaryRow}>
|
||||
<Text style={styles.summaryLabel}>Versand</Text>
|
||||
<Text style={styles.summaryValue}>-{formatPrice(order.shippingCost)}</Text>
|
||||
</View>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{rateGroups.map((g) => (
|
||||
<View style={styles.summaryCluster} key={g.rate}>
|
||||
<View style={styles.summaryRow}>
|
||||
<Text style={styles.summaryLabel}>Netto</Text>
|
||||
<Text style={styles.summaryValue}>-{formatPrice(g.net)}</Text>
|
||||
</View>
|
||||
<View style={styles.summaryRow}>
|
||||
<Text style={styles.summaryLabel}>zzgl. {g.rate}% MwSt.</Text>
|
||||
<Text style={styles.summaryValue}>-{formatPrice(g.tax)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
))}
|
||||
<View style={styles.grandTotalRow}>
|
||||
<Text style={styles.grandTotalLabel}>Gesamt</Text>
|
||||
<Text style={styles.grandTotalValue}>-{formatPrice(grandTotal)}</Text>
|
||||
</View>
|
||||
<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>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
+40
-28
@@ -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
|
||||
|
||||
<View style={styles.summary}>
|
||||
<View style={styles.summaryBox}>
|
||||
<View style={styles.summaryCluster}>
|
||||
{order.discountAmount > 0 && (
|
||||
<View style={styles.summaryRow}>
|
||||
<Text style={styles.summaryLabel}>Rabatt{order.discountCode ? ` (${order.discountCode})` : ""}</Text>
|
||||
<Text style={styles.summaryValue}>-{formatPrice(order.discountAmount)}</Text>
|
||||
</View>
|
||||
)}
|
||||
<View style={styles.summaryRow}>
|
||||
<Text style={styles.summaryLabel}>Versand</Text>
|
||||
<Text style={styles.summaryValue}>{order.shippingCost === 0 ? "Kostenlos" : formatPrice(order.shippingCost)}</Text>
|
||||
</View>
|
||||
{/* 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). */}
|
||||
<View style={styles.summaryRow}>
|
||||
<Text style={styles.summaryLabel}>Zwischensumme</Text>
|
||||
<Text style={styles.summaryValue}>{formatPrice(order.subtotal)}</Text>
|
||||
</View>
|
||||
{rateGroups.map((g) => (
|
||||
<View style={styles.summaryCluster} key={g.rate}>
|
||||
<View style={styles.summaryRow}>
|
||||
<Text style={styles.summaryLabel}>Netto</Text>
|
||||
<Text style={styles.summaryValue}>{formatPrice(g.net)}</Text>
|
||||
</View>
|
||||
<View style={styles.summaryRow}>
|
||||
<Text style={styles.summaryLabel}>zzgl. {g.rate}% MwSt.</Text>
|
||||
<Text style={styles.summaryValue}>{formatPrice(g.tax)}</Text>
|
||||
</View>
|
||||
{order.discountAmount > 0 && (
|
||||
<View style={styles.summaryRow}>
|
||||
<Text style={styles.summaryLabel}>Rabatt{order.discountCode ? ` (${order.discountCode})` : ""}</Text>
|
||||
<Text style={styles.summaryValue}>-{formatPrice(order.discountAmount)}</Text>
|
||||
</View>
|
||||
))}
|
||||
)}
|
||||
<View style={styles.summaryRow}>
|
||||
<Text style={styles.summaryLabel}>Versand</Text>
|
||||
<Text style={styles.summaryValue}>{order.shippingCost === 0 ? "Kostenlos" : formatPrice(order.shippingCost)}</Text>
|
||||
</View>
|
||||
<View style={styles.grandTotalRow}>
|
||||
<Text style={styles.grandTotalLabel}>Gesamt</Text>
|
||||
<Text style={styles.grandTotalValue}>{formatPrice(order.total)}</Text>
|
||||
</View>
|
||||
<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>
|
||||
{paid && (
|
||||
<View style={styles.paidBadgeRow}>
|
||||
|
||||
Reference in New Issue
Block a user