Drop the Netto row for Kleinunternehmer invoices
Validate e-invoices / mustang (push) Successful in 24s

Netto = Gesamt is structural under §19 UStG (no tax component ever
existed), not the coincidental 0%-this-one-sale case vatExempt orders
are in — showing it right under the §19 notice just duplicated Gesamt.
At the user's own request after reviewing a real invoice.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Marco
2026-07-24 15:18:36 +00:00
parent 439165c98d
commit ceaa437724
4 changed files with 41 additions and 23 deletions
+1
View File
@@ -79,6 +79,7 @@ Unlike `vatExempt`, this package does **not** expect `unitPrice` to be de-grosse
- Visual PDF (`invoicePdf.tsx`/`correctionInvoicePdf.tsx`): the "enthält X% MwSt." annotation under Gesamt becomes "Gemäß § 19 UStG wird keine Umsatzsteuer berechnet." instead — takes precedence over the `vatExempt` note if (implausibly) both were ever set.
- E-invoice XML (`buildEInvoiceData.ts`): VAT category `E` + a free-text exemption reason, no VATEX code — see "E-invoicing" above.
- **No Netto row** (added 2026-07-24, at the user's own request) — every other invoice type still shows Netto directly under the tax note/annotation, including `vatExempt` ones, where Netto = Gesamt is a *coincidence* worth keeping for layout consistency (0% happens to apply to that one sale). For `kleinunternehmer` it isn't a coincidence — §19 UStG means no tax component ever existed on any of this tenant's invoices, so the row would just repeat Gesamt directly under the §19 notice above it instead of adding information.
`InvoiceSeller.vatId` is now optional (`string | null`, was a plain required `string`) for the same reason — a Kleinunternehmer commonly never registers for an USt-IdNr. at all (no intra-EU trade). Every renderer treats a missing value as "omit the USt-IdNr. line/field" rather than printing an empty one: the visual PDF footer drops the whole `· USt-IdNr. …` segment, and `buildEInvoiceData.ts`'s `sellerParty()` omits `cac:PartyTaxScheme` entirely rather than emitting one with a blank `CompanyID`.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@einfach-produktiv/invoicing",
"version": "0.2.1",
"version": "0.2.2",
"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",
+17 -9
View File
@@ -364,15 +364,23 @@ function CorrectionInvoiceDocument({ kind, order, seller }: { kind: CorrectionIn
single well-defined figure (Gesamt minus every rate's own
tax, summed) regardless of how many rates are present.
Shown on every correction invoice (Storno and Gutschrift
alike), not just non-exempt ones. Same small/muted
vatNoteLabel/vatNoteValue styling as the MwSt annotation
above it, not the larger summaryLabel/summaryValue the
build-up rows use — both are supplementary detail below
the hero Gesamt figure, same visual tier. */}
<View style={styles.nettoRow}>
<Text style={styles.vatNoteLabel}>Netto</Text>
<Text style={styles.vatNoteValue}>-{formatPrice(rateGroups.reduce((sum, g) => sum + g.net, 0))}</Text>
</View>
alike), not just non-exempt ones — except `kleinunternehmer`
(built 2026-07-24, at the user's own request), same
exception as invoicePdf.tsx's own row for the same reason:
Netto = Gesamt there isn't a coincidence to preserve for
layout consistency, it's structural (§19 UStG means no tax
component ever existed to subtract), so the row would just
duplicate Gesamt right under the §19 notice. Same
small/muted vatNoteLabel/vatNoteValue styling as the MwSt
annotation above it, not the larger summaryLabel/
summaryValue the build-up rows use — both are supplementary
detail below the hero Gesamt figure, same visual tier. */}
{!order.kleinunternehmer && (
<View style={styles.nettoRow}>
<Text style={styles.vatNoteLabel}>Netto</Text>
<Text style={styles.vatNoteValue}>-{formatPrice(rateGroups.reduce((sum, g) => sum + g.net, 0))}</Text>
</View>
)}
</View>
</View>
</View>
+22 -13
View File
@@ -425,19 +425,28 @@ export function InvoiceDocument({ order, seller }: { order: InvoiceOrder; seller
by hand. Shown on every invoice, not just B2B/non-exempt
ones — on a vatExempt order Netto and Gesamt happen to be
the same figure (0% tax), but the row stays for layout
consistency across every invoice this shop issues. Placed
last (after the MwSt annotation, not before it) — see
that annotation's own comment above for why the order
matters here. Styled with the same small/muted
vatNoteLabel/vatNoteValue as that annotation, not the
larger summaryLabel/summaryValue the build-up rows above
Gesamt use — Netto is supplementary detail *below* the
hero Gesamt figure, same visual tier as the tax
breakdown, not another primary line competing with it. */}
<View style={styles.nettoRow}>
<Text style={styles.vatNoteLabel}>Netto</Text>
<Text style={styles.vatNoteValue}>{formatPrice(rateGroups.reduce((sum, g) => sum + g.net, 0))}</Text>
</View>
consistency across every invoice this shop issues. The one
exception is `kleinunternehmer` (built 2026-07-24, at the
user's own request): there Netto = Gesamt for a
structural reason, not a coincidental one (§19 UStG means
there was never a tax component to subtract in the first
place), and the row would just look like a second,
redundant total sitting directly under the §19 notice
above rather than genuinely new information. Placed last
(after the MwSt annotation, not before it) — see that
annotation's own comment above for why the order matters
here. Styled with the same small/muted vatNoteLabel/
vatNoteValue as that annotation, not the larger
summaryLabel/summaryValue the build-up rows above Gesamt
use — Netto is supplementary detail *below* the hero
Gesamt figure, same visual tier as the tax breakdown, not
another primary line competing with it. */}
{!order.kleinunternehmer && (
<View style={styles.nettoRow}>
<Text style={styles.vatNoteLabel}>Netto</Text>
<Text style={styles.vatNoteValue}>{formatPrice(rateGroups.reduce((sum, g) => sum + g.net, 0))}</Text>
</View>
)}
</View>
{paid && (
<View style={styles.paidBadgeRow}>