feat(invoice): Rechnungen als E-Rechnung (ZUGFeRD/Factur-X EN 16931)
Jedes Rechnungs-PDF ist jetzt eine hybride E-Rechnung: PDF/A-3b mit eingebettetem factur-x.xml (CII D16B, Profil EN 16931). Netto und Steuer je Satz kommen unveraendert aus der Aufstellung des PDFs, Versand und Rabatt stehen als Zu- und Abschlag je Satz; XML und PDF tragen dieselben Summen. - lib/einvoice/cii.ts: XML-Erzeugung (Rechnung 380, Stornorechnung 381 mit Bezug), ohne USt-IdNr. oder bei Positionen ohne Satz nur PDF mit Warnung - lib/einvoice/facturx-pdf.ts: PDF/A-3b, Factur-X-XMP mit Erweiterungsschema, eingebettete Liberation-Schriften (gleiche Laufweiten wie Helvetica/Times) - invoice-pdf: Darstellung als Stornorechnung (negative Betraege, Bezug) - Geprueft mit XSD, CEN-Schematron EN 16931 1.3.16 und Mustang 2.26 (veraPDF: PDF/A-3b konform, XML valid) Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
1 parent
f28e5e9d84
commit
508ff24aa7
16 files changed
+941
-45
No files matched your search
+4
-34
@@ -3,7 +3,7 @@ import type { Prisma } from "@/generated/prisma/client";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { generateInvoicePdf } from "@/lib/invoice-pdf";
|
||||
import { readStoredInvoicePdf } from "@/lib/invoice-storage";
|
||||
import { vatBreakdown, type VatBreakdownLine } from "@/lib/vat";
|
||||
import { vatLinesWithShares, type VatLineWithShares } from "@/lib/vat";
|
||||
import { isLocale, defaultLocale } from "@/i18n/config";
|
||||
|
||||
/**
|
||||
@@ -57,40 +57,10 @@ const invoiceInclude = {
|
||||
|
||||
type ErpInvoiceRecord = Prisma.InvoiceGetPayload<{ include: typeof invoiceInclude }>;
|
||||
|
||||
export interface ErpVatLine extends VatBreakdownLine {
|
||||
/** Darin enthaltener Versandanteil (brutto). */
|
||||
shippingCents: number;
|
||||
/** Davon abgezogener Rabattanteil (brutto). */
|
||||
discountCents: number;
|
||||
}
|
||||
export type ErpVatLine = VatLineWithShares;
|
||||
|
||||
/**
|
||||
* Umsatzsteuer je Satz wie auf der Rechnung (lib/vat.ts), dazu der Versand-
|
||||
* und der Rabattanteil je Satz. Beide Anteile ergeben sich aus derselben
|
||||
* Rechnung ohne den jeweils anderen Betrag, also ohne eigene Verteilungslogik.
|
||||
* null, wenn eine Position keinen Steuersatz hat (Bestellungen aus der Zeit
|
||||
* vor der Steuerausweisung).
|
||||
*/
|
||||
export function erpVatLines(order: {
|
||||
items: ReadonlyArray<{ totalCents: number; vatRatePercent: number | null }>;
|
||||
shippingCents: number;
|
||||
discountCents: number;
|
||||
}): ErpVatLine[] | null {
|
||||
const items: { totalCents: number; vatRatePercent: number }[] = [];
|
||||
for (const item of order.items) {
|
||||
if (item.vatRatePercent == null) return null;
|
||||
items.push({ totalCents: item.totalCents, vatRatePercent: item.vatRatePercent });
|
||||
}
|
||||
const full = vatBreakdown({ items, shippingCents: order.shippingCents, discountCents: order.discountCents });
|
||||
const plain = vatBreakdown({ items, shippingCents: 0, discountCents: 0 });
|
||||
const withShipping = vatBreakdown({ items, shippingCents: order.shippingCents, discountCents: 0 });
|
||||
const withDiscount = vatBreakdown({ items, shippingCents: 0, discountCents: order.discountCents });
|
||||
return full.map((line, index) => ({
|
||||
...line,
|
||||
shippingCents: withShipping[index].grossCents - plain[index].grossCents,
|
||||
discountCents: plain[index].grossCents - withDiscount[index].grossCents,
|
||||
}));
|
||||
}
|
||||
/** Umsatzsteuer je Satz mit Versand- und Rabattanteil, siehe lib/vat.ts. */
|
||||
export const erpVatLines = vatLinesWithShares;
|
||||
|
||||
/** Rechnung fuer ERPNext; bewusst ohne Telefonnummer, Postnummer und Notizen (nicht buchungsrelevant). */
|
||||
export function toErpInvoice(invoice: ErpInvoiceRecord) {
|
||||
|
||||
Reference in new issue
Block a user