diff --git a/README.md b/README.md index c5400d8..8d68d01 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,8 @@ 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. +`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`. + ## CI validation (Mustang) Every push/PR runs `.gitea/workflows/validate-einvoice.yml` against git.mk360.de's own self-hosted Gitea Actions runner (`vps-runner`, registered on the same VPS as Gitea/Payload — see `/home/marco/dev/docker/docker-compose.yml`'s `act_runner` service): diff --git a/package.json b/package.json index 435b9aa..e1db368 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@einfach-produktiv/invoicing", - "version": "0.2.0", + "version": "0.2.1", "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", diff --git a/src/correctionInvoicePdf.tsx b/src/correctionInvoicePdf.tsx index c52af08..4c6b833 100644 --- a/src/correctionInvoicePdf.tsx +++ b/src/correctionInvoicePdf.tsx @@ -379,8 +379,10 @@ function CorrectionInvoiceDocument({ kind, order, seller }: { kind: CorrectionIn - {seller.sellerName} · {seller.sellerStreet}, {seller.sellerZip} {seller.sellerCity} · {seller.sellerEmail} · USt-IdNr.{" "} - {seller.vatId} + {seller.sellerName} · {seller.sellerStreet}, {seller.sellerZip} {seller.sellerCity} · {seller.sellerEmail} + {/* Same "omit entirely when unset" as invoicePdf.tsx's own + footer — see seller.ts's own comment on vatId. */} + {seller.vatId ? ` · USt-IdNr. ${seller.vatId}` : ""} {seller.registerCourt && seller.registerNumber ? ` · ${seller.registerCourt} · ${seller.registerNumber}` : ""} {seller.managingDirector ? ` · Geschäftsführung: ${seller.managingDirector}` : ""} diff --git a/src/einvoice/buildEInvoiceData.ts b/src/einvoice/buildEInvoiceData.ts index ddcb8d9..7ec54a6 100644 --- a/src/einvoice/buildEInvoiceData.ts +++ b/src/einvoice/buildEInvoiceData.ts @@ -185,12 +185,21 @@ function sellerParty(seller: InvoiceSeller): UblInvoice["cac:AccountingSupplierP "cbc:PostalZone": seller.sellerZip, "cac:Country": { "cbc:IdentificationCode": countryCode(seller.sellerCountry) as SellerCountryCode }, }, - "cac:PartyTaxScheme": [ - { - "cbc:CompanyID": seller.vatId, - "cac:TaxScheme": { "cbc:ID": "VAT" }, - }, - ], + // Omitted entirely (not a PartyTaxScheme with an empty CompanyID) + // when unset — a Kleinunternehmer (§19 UStG) often has no USt-IdNr. + // at all, see seller.ts's own comment. Same "always present in this + // shop's real production data today, but must degrade safely" spirit + // as paymentMeans()'s own `!seller.iban` check above. + ...(seller.vatId + ? { + "cac:PartyTaxScheme": [ + { + "cbc:CompanyID": seller.vatId, + "cac:TaxScheme": { "cbc:ID": "VAT" }, + }, + ], + } + : {}), "cac:PartyLegalEntity": { "cbc:RegistrationName": seller.sellerName }, "cac:Contact": { "cbc:ElectronicMail": seller.sellerEmail }, }, diff --git a/src/invoicePdf.tsx b/src/invoicePdf.tsx index 1139de5..60ab31c 100644 --- a/src/invoicePdf.tsx +++ b/src/invoicePdf.tsx @@ -449,8 +449,11 @@ export function InvoiceDocument({ order, seller }: { order: InvoiceOrder; seller - {seller.sellerName} · {seller.sellerStreet}, {seller.sellerZip} {seller.sellerCity} · {seller.sellerEmail} · USt-IdNr.{" "} - {seller.vatId} + {seller.sellerName} · {seller.sellerStreet}, {seller.sellerZip} {seller.sellerCity} · {seller.sellerEmail} + {/* Omitted entirely (not "USt-IdNr. " with a blank value) when + unset — a Kleinunternehmer (§19 UStG) often has no USt-IdNr. + at all, see seller.ts's own comment. */} + {seller.vatId ? ` · USt-IdNr. ${seller.vatId}` : ""} {seller.registerCourt && seller.registerNumber ? ` · ${seller.registerCourt} · ${seller.registerNumber}` : ""} {seller.managingDirector ? ` · Geschäftsführung: ${seller.managingDirector}` : ""} diff --git a/src/seller.ts b/src/seller.ts index 4a2e90d..c7442a9 100644 --- a/src/seller.ts +++ b/src/seller.ts @@ -9,7 +9,14 @@ export type InvoiceSeller = { sellerCity: string; sellerCountry: string; sellerEmail: string; - vatId: string; + // Optional as of the Kleinunternehmerregelung (§19 UStG) addition — a + // Kleinunternehmer often only has a Steuernummer, never registers for an + // USt-IdNr. at all (no intra-EU trade), so company-settings.vatId is no + // longer a hard-required field on the Payload side (see that + // collection's own comment). Every renderer here already treats a + // missing vatId as "omit the USt-IdNr. line/field" rather than printing + // an empty value. + vatId?: string | null; taxRatePercent: number; // Structured (Phase 2 of the e-invoicing migration — EN16931 wants // discrete PaymentMeans data, not a free-text paragraph a human