Dokumentation

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`);