Dokumentation
PDFs erstellen
PDFs in Vue
PDFs in Nuxt
PDFs in React
PDFs in Angular
Ihr eigenes Layout
Eine ZUGFeRD-Datei ist zwei Dinge in einem: ein PDF, das ein Mensch liest, und ein XML, das eine Maschine liest. Nur das XML muss die EN 16931 erfüllen. Die Seite gehört Ihnen.
jasy hält beides auseinander. Sie können die Rechnung also so weit umgestalten, wie Sie möchten, ohne die
Konformität je zu gefährden - das XML wird aus demselben Invoice-Objekt abgeleitet, ganz gleich, wie die
Seite aussieht.
Die eingebaute Vorlage, in Ihrer Sprache
Das Standard-Layout spricht Deutsch. locale wechselt Beschriftungen, Zahlenformat und Datumsformat in
einem Schritt:
const { bytes, xml } = await renderZugferd(invoice, { locale: "en" });
Einzelne Beschriftungen lassen sich mit labels zusätzlich zur Voreinstellung überschreiben, das reicht für die meisten
Hausstile:
await renderZugferd(invoice, {
labels: { vat: "MwSt.", grandTotal: "Zu zahlen" },
});
Die Schlüssel sind die Teile der Seite, und Sie überschreiben nur die, die Ihnen wichtig sind:
allowance · alreadyPaid · amount · amountDue · amountsIn · bankDetails ·
buyerVatId · charge · contactPerson · contractReference · creditNote ·
customerReference · deliverTo · deliveryDate · description · dueDate · email ·
grandTotal · invoice · invoiceDate · invoiceNumber · itemNumber · netTotal ·
orderNumber · page · pageOf · payableBy · payee · payment · paymentMeans ·
perQuantity · phone · plusVat · position · quantity · registration ·
remittance · servicePeriod · subtotal · taxAmount · taxNumber · taxableBase ·
unitPrice · vat · vatBreakdown · vatId
resolveLabels(locale, overrides) ist ebenfalls exportiert, falls Sie die aufgelösten Beschriftungen für
eine selbst gebaute Seite brauchen.
Ein Layout nach Ihrem eigenen Entwurf
Übergeben Sie pdf ein Document, und jasy verwendet es statt des eingebauten. Darum herum bettet jasy das
XML ein, den Anhang, die PDF/A-3-Metadaten und das Farbprofil. Alles, was die Datei konform macht, wird
weiterhin für Sie erledigt:
import { renderZugferd } from "@jasy/e-invoice";
import { Document, Page, Column, Row, Text, Divider, Image } from "@jasy/pdf";
const { bytes, xml } = await renderZugferd(invoice, {
pdf: Document([
Page({ size: "A4", margin: 48, gap: 14 }, [
Row({ justify: "between", align: "center" }, [
Image("logo.svg", { width: 90 }),
Text("RECHNUNG", { size: 26, bold: true, color: "#1450aa" }),
]),
Divider({ color: "#d0d4da" }),
Column({ gap: 6 }, [
Text(`Nr. ${invoice.number}`, { bold: true }),
Text(`Ausgestellt am ${invoice.issueDate}`, { color: "gray" }),
]),
// ... Ihre Tabelle, Ihre Summen, Ihre Fußzeile
]),
]),
});
Das XML, das zurückkommt, ist Byte für Byte dasselbe wie mit der Standardvorlage. Genau darum geht es bei der Trennung: Das Aussehen ist eine gestalterische Entscheidung, das XML eine rechtliche, und eine Änderung am ersten kann das zweite nicht beschädigen.
Eines bleibt Ihre Verantwortung
Für eines stehen Sie selbst ein, das die Standardvorlage Ihnen abnimmt: Das deutsche Recht (§ 14 UStG)
verlangt bestimmte Angaben sichtbar auf der Seite, nicht nur im XML - Verkäufer und Käufer,
Rechnungsnummer und -datum, Menge und Art der Leistung, das Nettoentgelt je Steuersatz, den Steuersatz
selbst und den Steuerbetrag. Drucken Sie sie, und sorgen Sie dafür, dass ein langer Wert umbricht, statt
abgeschnitten zu werden: Text(value, { breakWord: true }). Eine abgeschnittene Pflichtangabe ist eine
fehlende.
Von der eingebauten Vorlage ausgehen
Beide Hälften der Standardvorlage sind exportiert, Sie müssen also selten bei null anfangen:
import { defaultInvoiceTemplate, bundledFonts } from "@jasy/e-invoice";
defaultInvoiceTemplate baut die Standardseite aus einer Rechnung - nützlich zum Lesen oder zum Umhüllen.
bundledFonts() liefert die Liberation-Schriftfamilien unter den Namen der Standard-14-Schriften, die
sie ersetzen - Helvetica, Times, Courier. Unter diesen Namen registriert, kann eine ZUGFeRD-Datei die
vertrauten Schriftnamen verwenden und trotzdem PDF/A-gültig sein, denn PDF/A verlangt, dass jede Schrift
eingebettet ist, und die Standard-14 sind es nicht.
await renderToBytes(doc, { fonts: await bundledFonts() });
Abrechnungszeiträume
Eine Rechnung für eine laufende Leistung bezieht sich oft auf einen Zeitraum (period) statt auf ein
Lieferdatum (delivery.date), und § 14 UStG erlaubt beides. Beide Ebenen werden unterstützt, period am Beleg und an der Position, und landen im XML als
ram:BillingSpecifiedPeriod (CII) oder cac:InvoicePeriod (UBL):
const invoice = {
// ...
period: { start: "2026-06-01", end: "2026-06-30" }, // für den ganzen Beleg
lines: [
{
name: "Managed Hosting",
period: { start: "2026-06-01", end: "2026-06-30" }, // oder je Position
quantity: 1,
unit: "MON",
netUnitPrice: 240,
vat: { category: "S", ratePercent: 19 },
},
],
};
Die XRechnung-Regel verlangt ein Lieferdatum, oder einen Zeitraum für den Beleg, oder einen
Zeitraum an jeder Position - und die Vorabprüfung setzt genau das durch, every und nicht some. Ein
Zeitraum, der endet, bevor er beginnt, wird ebenfalls abgelehnt.
Was zurückkommt
renderZugferd gibt drei Dinge zurück, nicht zwei:
const { bytes, xml, droppedCharacters } = await renderZugferd(invoice);
droppedCharacters enthält jedes Zeichen, das keine eingebettete Schrift zeichnen konnte. Es wurde
entfernt, statt als .notdef-Kästchen der Schrift geschrieben zu werden, denn PDF/A verbietet, auf
diesen Glyph zu verweisen - so bleibt die Datei konform, und die Rechnung geht trotzdem raus. Bei fast jedem
Dokument ist die Liste leer. Zeigen Sie sie der Person, die die Rechnung erstellt hat, statt sie nur zu
protokollieren:
if (droppedCharacters.length > 0) {
warnUser(`entfernt, keine Schrift kann sie zeichnen: ${droppedCharacters.join(" ")}`);
}
Das ist bewusst ein Rückgabewert und keine Einstellung: Ein fehlendes Häkchen darf nie verhindern, dass eine Rechnung entsteht - aber ein Server-Log ist kein Bericht.
Die Summen selbst berechnen
computeInvoice(invoice) führt dieselbe Rechnung aus, die Vorlage und XML verwenden, und gibt sie zurück:
lineNets und lineTotal, allowanceTotal und chargeTotal, taxBasisTotal, die vatBreakdown je
Kategorie und Satz, taxTotal, grandTotal, paidAmount und duePayable. Jeder Eintrag der
vatBreakdown trägt seine category und ratePercent, den taxableAmount, auf den er sich bezieht, den
taxAmount selbst und einen Befreiungsgrund exemption für die Kategorien, die einen brauchen. Nutzen Sie
das, wenn Sie Ihre eigene Seite zeichnen und die Zahlen per Konstruktion mit dem XML übereinstimmen sollen
statt von Hand.
Jeder Betrag ist ein exakter Decimal, keine JavaScript-Zahl - bei 0.1 + 0.2 liegt nie ein Cent daneben. geändert in beta.3toDecimalString macht daraus Text:
import { computeInvoice, toDecimalString } from "@jasy/e-invoice";
const totals = computeInvoice(invoice);
Text(`Summe ${toDecimalString(totals.grandTotal)} EUR`);