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>
Payload's company-settings.vatId is no longer hard-required (a
Kleinunternehmer often has no USt-IdNr. at all) — this package's
InvoiceSeller.vatId follows suit, with every renderer now omitting the
USt-IdNr. line/field entirely instead of printing/serializing an empty
value.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
InvoiceOrder/CorrectionInvoiceOrder gain an optional order-level
kleinunternehmer flag, snapshotted per order (same pattern as vatExempt)
so a later toggle of the tenant's setting never rewrites an
already-issued invoice. Takes precedence over vatExempt.
- Visual PDF: "enthält X% MwSt." becomes the §19 UStG notice.
- E-invoice XML: new UNTDID 5305 category "E" with a free-text exemption
reason (no VATEX code — §19 UStG has none, BR-E-10 allows text alone).
- New Mustang CI fixture + buildEInvoiceData unit test for the "E" path.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Was using summaryLabel/summaryValue (fontSize 10, same as Zwischensumme/
Versand — the build-up rows above Gesamt), which visually competed
with Gesamt instead of reading as supplementary detail below it. Now
uses the same small/muted vatNoteLabel/vatNoteValue as the "enthält
X% MwSt." line right above it — both are detail underneath the hero
Gesamt figure, same visual tier.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Order was Gesamt → Netto → "enthält X% MwSt." — reads as if the
annotation describes Netto, which is backwards: Netto already has the
tax removed, Gesamt is what contains it. Reordered to Gesamt →
"enthält X% MwSt." → Netto so the annotation stays unambiguously
attached to the row it actually describes. Verified by re-rendering
and visually inspecting both the original invoice and a Storno.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Netto/Gesamt/MwSt breakdown was gated on !order.vatExempt — on a
vatExempt (innergemeinschaftliche Lieferung) invoice, Netto happens to
equal Gesamt exactly (0% tax), which read as redundant next to the
exemption note, so the row was skipped there. Turns out that's wrong:
the ask was for Netto on every invoice this shop issues, B2B/exempt
included, for layout consistency — not conditional on the tax rate.
Same fix in both invoicePdf.tsx and correctionInvoicePdf.tsx (the
latter covers both Storno and Gutschrift via the same summary code).
Verified by rendering all three cases (normal 19% invoice, exempt B2B
invoice, Storno correction) and visually inspecting the output.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
InvoiceOrder/CorrectionInvoiceOrder gained an optional vatExempt flag
— items already carry 0% rate and de-grossed prices when true (the
caller's responsibility), this only controls display/XML mapping:
- Visual PDF: replaces the "enthält X% MwSt." annotation with
"Steuerfreie innergemeinschaftliche Lieferung (§4 Nr. 1b UStG)" on
both the invoice and its Storno/Gutschrift.
- EN16931 XML (buildEInvoiceData.ts): VAT category 'K' instead of 'S'
on every line/tax-subtotal, plus BT-120/BT-121 exemption reason
(VATEX-EU-IC) — but only on cac:TaxTotal's own TaxSubtotal.TaxCategory,
not on InvoiceLine/AllowanceCharge's TaxCategory, which
@e-invoice-eu/core's schema rejects those same two fields on (caught
locally before Mustang ever saw it — a fully-rendered PDF/A-3 with a
hand-inflated CII XML confirmed CategoryCode/ExemptionReasonCode/
ExemptionReason land correctly).
Also surfaced (not fixed, unrelated to this change): a seller with no
iban configured throws in @e-invoice-eu/core's UBL→CII conversion —
doesn't affect this shop's own production data (iban is always set),
noting for whoever touches this next.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
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>
- The "Bereits beglichen" confirmation is now plain green Text, not a
tinted pill/box — read as too heavy for a status note.
- Item table rows no longer alternate white/tinted (zebra striping);
every row now shares the same tinted background for a more uniform
table, on both the invoice and its Storno/Gutschrift corrections.
- InvoiceOrder/CorrectionInvoiceOrder gained optional companyName/
vatId, rendered in the "An" recipient block when present — the
buyer-side counterpart to the sellers' own fields already shown in
the footer. Both consuming repos need their own commit to actually
pass these through from Orders.companyName/vatId.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The previous placement put it as one more row inside summaryBox, right after Gesamt — the user wanted it visually below/outside the tinted card entirely. Now a sibling of summaryBox instead of a child, verified by rendering to PDF.
- Footer is centered, not left-aligned.
- Summary rows are grouped into visual clusters (Rabatt+Versand; each rate's own Netto/MwSt pair) with a bit of spacing between clusters, tight spacing within — makes it clearer which lines belong together.
- More space between the product image and the article text in each line item.
- "Bereits beglichen" badge moved from the top meta row to directly below "Gesamt", where it reads more naturally against the amount it confirms.
- Packstation number and Postnummer render as two separate lines instead of one "Packstation X · Postnummer Y" line.
Applied identically to both invoicePdf.tsx and correctionInvoicePdf.tsx (Stornorechnung/Gutschrift) so every generated invoice stays visually consistent — verified by rendering both to PDF and comparing.
Dropped by accident when bankDetails split into iban/bic — a bank name
has no fixed format to validate, so it stays free text, but it's still
useful context next to the account numbers in the invoice footer.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
InvoiceSeller.bankDetails becomes iban/bic (EN16931 wants discrete
PaymentMeans data, not a paragraph a human formatted by hand). The
footer's bank-details line also drops the misleading "(für Überweisung)"
qualifier — it was never actually conditional on the order's payment
method (only on whether the field was set at all), just worded as if it
were. Now shown whenever iban or bic is set, regardless of payment
method.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Consolidates code previously hand-duplicated between the einfach-produktiv
frontend and payload backend repos into one canonical implementation,
consumed as a git dependency by both instead of being kept in sync by eye.
While merging the two correction-invoice copies, found and fixed three
real drifts between them:
- variantName was silently dropped in the backend's emailed Stornorechnung/
Gutschrift, but present in the frontend's re-download copy
- the backend's correction-invoice footer wasn't position:fixed, unlike
the original invoice and the frontend's copy
- the backend used a numeric date format (03.07.2025) while the original
invoice and the frontend's copy both used a spelled-out month (03. Juli
2025) — a re-download didn't visually match what was emailed
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>