Dieser Artikel ist auch auf Englisch verfügbar.
Gesetzliche Anforderungen an die E-Rechnung
Seit dem 1. Januar 2025 muss jedes Unternehmen in Deutschland in der Lage sein, elektronische Rechnungen für inländische B2B-Umsätze zu empfangen. Ein PDF im E-Mail-Anhang zählt nicht mehr. Eine E-Rechnung im Sinne des Gesetzes ist ein strukturierter Datensatz nach der europäischen Norm EN 16931, sodass das empfangende System Beträge, Steuersätze, Parteien und Zahlungsbedingungen ohne einen Menschen (oder einen OCR-Schritt) dazwischen auslesen kann. Die Pflicht, E-Rechnungen auszustellen, folgt in zwei Stufen: ab dem 1. Januar 2027 für Unternehmen mit einem Vorjahresumsatz über 800.000 EUR und ab dem 1. Januar 2028 für alle anderen. Bis dahin sind Papier und einfaches PDF für ausgehende Rechnungen noch geduldet, allerdings nur, wenn der Empfänger zustimmt.
Ähnliche Regelungen gibt es bereits oder werden gerade in der gesamten Europäischen Union eingeführt. Frankreich betreibt seine eigene Verpflichtung auf Basis der Plattform Chorus Pro für den öffentlichen Sektor, Italien hat seit Jahren SDI, und Belgien, Polen und andere ziehen nach. Alle bauen auf demselben semantischen Modell auf, EN 16931, das eine Liste von Geschäftsbegriffen (die BT-xx-Felder, die in Validator-Ausgaben auftauchen) und einen Satz von Geschäftsregeln (BR-xx) definiert, die eine Rechnung erfüllen muss.
Wer ist betroffen?
- Alle Unternehmen in Deutschland mit B2B-Umsätzen
- Unternehmen, die an öffentliche Auftraggeber fakturieren (B2G) - hier ist die XRechnung bereits seit 2020 Pflicht
- Unternehmen mit Geschäftspartnern in EU-Ländern, die Factur-X/ZUGFeRD verlangen
- Kleinunternehmer und Freiberufler sind eingeschlossen (mit einigen Übergangsfristen)
EN 16931 erlaubt zwei XML-Syntaxen: UN/CEFACT Cross Industry Invoice (CII) und UBL 2.1. Der ZUGFeRD-Standard (und sein französisches Gegenstück Factur-X, das seit Version 2.3 / Factur-X 1.07 identisch mit ZUGFeRD ist) nimmt das CII-XML und bettet es als Anhang in eine PDF/A-3-Datei ein. Das Ergebnis ist ein hybrides Dokument: Die Buchhaltung des Kunden öffnet es in einem beliebigen PDF-Viewer und sieht die Rechnung wie bisher, während das ERP-System das XML extrahiert und automatisch verbucht. Für Entwickler ist das attraktiv, weil das bestehende PDF-Layout erhalten bleibt und nur die strukturierten Daten obendrauf kommen. ZUGFeRD gibt es in mehreren Profilen (MINIMUM, BASIC, EN 16931, EXTENDED, XRECHNUNG); das Profil EN 16931 ist dasjenige, das die deutsche B2B-Pflicht erfüllt, und das, was das unten beschriebene Modul standardmäßig erzeugt.
Erste Schritte mit cbZUGFeRD
Es gibt mehrere Java-Bibliotheken für ZUGFeRD, aber das Mustang Project ist die ausgereifteste und am aktivsten gepflegte; es zieht bei jeder neuen ZUGFeRD- und Factur-X-Version innerhalb weniger Wochen nach und liefert die aktuellen EN-16931-Schematron-Regeln mit. Das Modul cbZUGFeRD kapselt die Mustang-Bibliothek für ColdBox-Anwendungen. Es lädt das Shaded-JAR von Mustang über cbJavaLoader, kommt also nicht mit den JARs in die Quere, die mit der CFML-Engine ausgeliefert werden, und stellt ein kleines MustangFactory-Singleton bereit, das die Java-Objekte erzeugt. Es ist kein externer Dienst, kein Docker-Container und keine REST-API beteiligt, die gesamte Konvertierung läuft in der eigenen JVM. Das Modul wird auf Lucee entwickelt, und seit dem Upgrade auf Mustang 2.20 läuft es auf Java 17 und neuer, womit es auch für BoxLang eine Option ist.
Man kann das Modul entweder von Github in den modules-Ordner klonen oder es per CommandBox von ForgeBox installieren:
box install cbzugferd
Die einzige Abhängigkeit ist cbjavaloader, die CommandBox automatisch auflöst. Modul-Einstellungen sind optional; wer IBAN, BIC und die Pflichtangaben lieber aus dem Handler-Code heraushalten möchte, kann sie in den moduleSettings ablegen.
Verwendung
Der Ablauf besteht aus zwei unabhängigen Schritten. Zuerst rendert die Anwendung die Rechnung wie gewohnt als PDF, typischerweise mit cfdocument aus einem HTML-Template. Im zweiten Schritt wird dieselbe Rechnung als Mustang-Invoice-Objekt beschrieben und zusammen mit dem PDF an den Exporter übergeben, der das PDF nach PDF/A-3 konvertiert, das CII-XML erzeugt und einbettet. Das XML wird ausschließlich aus dem Objektmodell abgeleitet, man ist also selbst dafür verantwortlich, dass die Zahlen im PDF und im Objektmodell übereinstimmen. In der Praxis speist man beides aus demselben Query-Ergebnis.
Hier ein kurzes Beispiel:
// Factory-Singleton holen
var factory = getInstance('MustangFactory@cbzugferd');
// oder als Property injizieren:
// property name="factory" inject="MustangFactory@cbzugferd";
// Rechnung anlegen
var invoice = factory.createInvoice();
// Absender (eigene Firma): Name, Straße, PLZ, Ort, ISO-Ländercode
var sender = factory.createTradeParty(
"Your Company GmbH",
"Main Street 1",
"12345",
"Berlin",
"DE"
);
sender.addVATID("DE123456789");
// Bankverbindung, damit der Empfänger per SEPA überweisen kann
var bankDetails = factory.createBankDetails(
"DE89370400440532013000",
"COBADEFFXXX"
);
bankDetails.setAccountName("Your Company GmbH");
sender.addBankDetails(bankDetails);
// Empfänger anlegen
var recipient = factory.createTradeParty(
"Customer Inc",
"Customer Road 5",
"54321",
"Munich",
"DE"
);
recipient.addVATID("DE987654321");
// Rechnung konfigurieren
invoice.setNumber("INV-2024-001")
.setIssueDate(now())
.setDeliveryDate(now())
.setDueDate(dateAdd("d", 30, now()))
.setSender(sender)
.setRecipient(recipient);
// Positionen hinzufügen: Beschreibung, Name, UN/ECE-Einheitencode, MwSt-Satz
var product = factory.createProduct(
"Consulting Services",
"Professional IT Consulting",
"HUR", // Stunden
19 // MwSt-Satz in Prozent
);
var item = factory.createItem(product, 150.00, 8); // 8 Stunden à 150 EUR
invoice.addItem(item);
Ein paar Dinge, die man hier leicht falsch macht. Die Mengeneinheit muss ein Code aus der UN/ECE Recommendation 20 sein, also HUR für Stunden, C62 für Stück, KGM für Kilogramm und so weiter; ein Freitext wie "Stunden" fällt bei der Validierung durch. Die USt-IdNr. muss dem nationalen Format folgen (zweistelliger Länderpräfix plus neun Ziffern für Deutschland), sonst beschweren sich die Schematron-Regeln. Preise und Mengen werden als CFML-Zahlen übergeben und von der Factory intern nach java.math.BigDecimal gecastet, man muss sich also nicht selbst mit javaCast() herumschlagen. Summen, Steueraufschlüsselung und Bemessungsgrundlage berechnet Mustang aus den Positionen; die setzt man nicht selbst.
Bei 0-Prozent-Positionen verwendet Mustang standardmäßig die UNTDID-5305-Kategorie Z (Nullsatz). Das ist falsch für eine Reverse-Charge-Rechnung in ein anderes EU-Land, dort braucht man die Kategorie AE plus den Befreiungsgrund-Code VATEX-EU-AE am Produkt, oder die Kategorie E mit einem Begründungstext für sonstige nicht steuerbare Leistungen. Die Kategorie O sieht verlockend aus, lässt sich aber nicht mit USt-IdNrn. kombinieren (Regel BR-O-2) und ist damit für B2B praktisch unbrauchbar.
Pflichtangaben
Das deutsche Recht (§ 14 UStG zusammen mit § 35a GmbHG und § 125a HGB) verlangt auf Rechnungen bestimmte Angaben über die reinen Geschäftsdaten hinaus, zum Beispiel die Geschäftsführer und den Handelsregistereintrag. Im XML sind das Freitext-Notizen, die aber den richtigen Subject Code (REG) tragen müssen, damit Validatoren und empfangende Systeme sie von beliebigen Bemerkungen unterscheiden können. Das Modul kümmert sich darum:
// Pflichtangaben hinzufügen
invoice.addRegulatoryNote("Geschäftsführer: Max Mustermann");
invoice.addRegulatoryNote("Handelsregister: Amtsgericht Berlin HRB 12345");
Wer französische Behörden über Chorus Pro fakturiert, braucht für beide Parteien zusätzlich eine Registrierungskennung (BT-30 für den Verkäufer, BT-47 für den Käufer), typischerweise die SIRET mit dem Schema 0009. Chorus Pro lehnt Rechnungen ohne diese Angabe ab und nutzt die SIRET des Käufers, um die Rechnung dem richtigen Schuldner zuzuordnen. Seit Version 1.3.0 gibt es dafür eine Factory-Methode:
// Verkäufer: Handelsregisternummer, ohne ISO-6523-Schema
sender.setLegalOrganisation(factory.createLegalOrganisation("HRB 12345"));
// Käufer: französische SIRET, Schema 0009
recipient.setLegalOrganisation(factory.createLegalOrganisation("12345678900012", "0009"));
PDF mit eingebetteten Schriften erzeugen
Das ist der Teil, an dem die meisten Zeit verlieren. PDF/A-3 verlangt, dass jede im Dokument verwendete Schrift in die Datei eingebettet ist, weil das Archivformat nicht von Schriften abhängen darf, die zufällig auf dem Rechner des Betrachters installiert sind. Lucees cfdocument mit type="modern" nutzt den Flying-Saucer-Renderer, und Flying Saucer bettet eine Schrift nur ein, wenn man es ihm über eine @font-face-Deklaration mit zwei herstellerspezifischen Eigenschaften sagt: -fs-pdf-font-embed: embed und -fs-pdf-font-encoding: Identity-H. Das Attribut fontdirectory allein reicht nicht; die Schrift muss zusätzlich im CSS mit einer absoluten file:///-URL deklariert werden.
// PDF mit eingebetteten Schriften für PDF/A-Konformität erzeugen
cfdocument(
format="PDF"
fontembed="true"
type="modern"
name="pdfContent"
) {
writeOutput('<html><head><style>
@font-face {
font-family: "Nimbus Sans";
src: url("file:///usr/share/fonts/opentype/urw-base35/NimbusSans-Regular.otf");
font-weight: normal;
font-style: normal;
-fs-pdf-font-embed: embed;
-fs-pdf-font-encoding: Identity-H;
}
@font-face {
font-family: "Nimbus Sans";
src: url("file:///usr/share/fonts/opentype/urw-base35/NimbusSans-Bold.otf");
font-weight: bold;
font-style: normal;
-fs-pdf-font-embed: embed;
-fs-pdf-font-encoding: Identity-H;
}
body { font-family: "Nimbus Sans", Arial, Helvetica, sans-serif; }
</style></head><body>');
writeOutput('<h1>Rechnung</h1>...');
writeOutput('</body></html>');
}
fileWrite("/path/to/invoice.pdf", pdfContent);
Wir verwenden bewusst Nimbus Sans statt der verbreiteteren DejaVu Sans. Nimbus Sans ist metrisch kompatibel zu Helvetica, ein Template, das mit Arial oder Helvetica gestaltet wurde, behält also seine Zeilenumbrüche und Spaltenbreiten. DejaVu Sans ist deutlich breiter und lässt gern Tabellenzellen überlaufen, die vorher gepasst haben. Nimbus Sans gehört zum Paket fonts-urw-base35, das auf den meisten Debian- und Ubuntu-Images vorinstalliert ist; falls es fehlt, hilft apt-get install fonts-urw-base35. Was auf dem Server verfügbar ist, prüft man mit fc-list | grep -i nimbus.
Hinweis: Unter macOS liegen TTF-Schriften in /System/Library/Fonts/Supplemental/. Auf Linux-Servern verwendet man /usr/share/fonts/truetype/ oder /usr/share/fonts/opentype/, unter Windows C:/Windows/Fonts/. Die file:///-URLs entsprechend anpassen.
Hinweis: Wer die Schrifteinbettung während der Entwicklung überspringen möchte, kann mit der Methode ignorePDFAErrors() am Exporter auch normale PDFs verarbeiten. Die erzeugte Datei ist dann weiterhin ein gültiger ZUGFeRD-Container und der XML-Teil validiert, aber ein strenger PDF/A-Validator wie veraPDF bemängelt die fehlenden Schriften. Das sollte nicht an Kunden rausgehen.
Das finale PDF erzeugen
Sobald die Rechnungsdaten vorbereitet sind, nimmt der Exporter das bestehende PDF und bettet die XML-Rechnungsdaten ein. createExporterFromA1() erwartet ein PDF/A-1 als Eingabe und hebt es auf PDF/A-3 an; in Kombination mit ignorePDFAErrors() akzeptiert er auch das einfache PDF, das cfdocument erzeugt. Wer bereits ein PDF/A-3 aus einem anderen Werkzeug hat, nimmt stattdessen createExporterFromA3().
// Exporter anlegen und ZUGFeRD-Daten einbetten
var exporter = factory.createExporterFromA1()
.ignorePDFAErrors() // entfernen, sobald die Schrift korrekt eingebettet ist
.load("/path/to/your/invoice.pdf")
.setProducer("Your Application")
.setCreator("cbZUGFeRD");
exporter.setTransaction(invoice);
exporter.export("/path/to/zugferd-invoice.pdf");
Auf dem Weg dorthin gibt es eine Lucee-spezifische Stolperfalle. PDF/A verlangt einen OutputIntent, ein eingebettetes ICC-Farbprofil, und Mustang liefert das Standardprofil sRGB2014 innerhalb seines JARs mit und lädt es über den Context-Classloader des Threads. Weil das JAR per JavaLoader geladen wurde, ist der Context-Classloader des Request-Threads der von Lucee, der die Ressourcen im Classpath von JavaLoader nicht sieht, und der Export schlägt mit einem ICC-Profil-Fehler fehl. Die Lösung ist, den Context-Classloader für die Dauer des Exports auszutauschen und danach wiederherzustellen:
var javaLoader = getInstance("loader@cbjavaloader");
var currentThread = createObject("java", "java.lang.Thread").currentThread();
var originalClassLoader = currentThread.getContextClassLoader();
currentThread.setContextClassLoader(javaLoader.getURLClassLoader());
try {
var exporter = factory.createExporterFromA1()
.ignorePDFAErrors()
.load(sourcePdf)
.setProducer("Your Application")
.setCreator("cbZUGFeRD");
exporter.setTransaction(invoice);
exporter.export(destinationPdf);
} finally {
currentThread.setContextClassLoader(originalClassLoader);
}
Einmal in eine Service-Methode gepackt, muss man darüber nicht mehr nachdenken.
Validierung
Im Entwicklungsprozess will man sicherstellen, dass man konforme Rechnungen verschickt. Die Schematron-Regeln von EN 16931 sind umfangreich, und ein fehlender Begründungstext an einem Rabatt oder ein leeres <ram:URIID/>-Element reicht, damit ein empfangendes System die Rechnung ablehnt. Also erzeugt man ein paar Rechnungen, die die realen Fälle abdecken (Regelsteuersatz, ermäßigter Satz, Reverse Charge, Rabatte, ausländische Kunden), und validiert sie, bevor irgendetwas an Kunden geht. Hier ein paar Online-Dienste, die dabei helfen:
Diese Validatoren prüfen zwei verschiedene Dinge: das XML gegen die Geschäftsregeln von EN 16931 und den PDF-Container gegen PDF/A-3. Eine Rechnung kann das erste bestehen und am zweiten scheitern (typischerweise wegen Schriften oder ICC-Profil), man sollte sich also beide Ergebnisse ansehen. Für automatisierte Tests in der CI bringt Mustang selbst einen Kommandozeilen-Validator mit, und veraPDF deckt die PDF/A-Seite ab.
Wie sich das Modul seit der ersten Version entwickelt hat
Die erste Version von cbZUGFeRD im Oktober 2025 war ein recht dünner Wrapper mit einer CFC pro Mustang-Klasse und, wichtiger, einer Abhängigkeit von einem Gotenberg-Docker-Container, der die cfdocument-Ausgabe nach PDF/A-3b konvertierte, bevor Mustang damit arbeiten konnte. Das funktionierte, bedeutete aber, neben dem CFML-Server noch einen weiteren Dienst zu betreiben und zu überwachen, nur um Rechnungen zu erzeugen. Version 1.1.0 hat Gotenberg komplett entfernt, indem sie auf Mustangs ZUGFeRDExporterFromA1 mit ignorePDFAErrors() umgestellt hat, der die PDF/A-Konvertierung im Prozess erledigt. Gleichzeitig wurden die einzelnen Wrapper-CFCs durch die eine MustangFactory ersetzt, weil das Erzeugen von Java-Objekten über WireBox-Injection und JavaLoader Timing-Probleme hatte, die eine Factory umgeht. Das war ein Breaking Change, aber die API wurde deutlich kleiner.
Die folgenden Point-Releases waren hauptsächlich von Validator-Feedback und Produktiveinsatz getrieben. 1.1.1 brachte createBankDetails() und createContact(). 1.1.2 ist der oben beschriebene Fix für die Schrifteinbettung, nachdem sich herausgestellt hatte, dass der fontdirectory-Ansatz aus der ersten Fassung dieses Artikels mit dem Flying-Saucer-Renderer gar nichts einbettete. 1.1.3 hob Mustang von 2.13 auf 2.20 für Java-17-Kompatibilität und löste den ICC-Profil-Fehler. 1.1.4 wechselte auf Mustang 2.25, das Unterstützung für ZUGFeRD 2.5.2 (identisch mit Factur-X 1.09.2) brachte und zwei veraPDF-CVEs in den gebündelten Abhängigkeiten schloss.
Version 1.2.0 ergänzte createAllowance() für Rabatte auf Dokumentebene, die sich nicht als negative Position ausdrücken lassen, ohne die Geschäftsregeln zu verletzen, und änderte createContact() so, dass Telefon und E-Mail nur geschrieben werden, wenn sie nicht leer sind. Mustang schreibt jeden Nicht-Null-Wert, und die leeren <ram:CompleteNumber/>-Elemente, die aus leeren Strings entstanden, waren ein zuverlässiger Weg, die EN-16931-Validierung zu reißen. 1.2.1 hob Mustang auf 2.26.0, das unter anderem die Fluent-API von Charge und Allowance reparierte und das Schematron auf 1.3.16 aktualisierte.
Die aktuellen Änderungen in der Version 1.3.x vom November 2026 entstanden aus der Anferurderung Rechnungen an Chorus Pro zu liefern. Die README enthält jetzt außerdem eine Tabelle, die klarstellt, wo das Modul in der Familie der e-invoice Standards steht: Es erzeugt das Profil EN 16931 von ZUGFeRD 2.x / Factur-X als CII-XML in PDF/A-3. XRechnung (das reine XML für deutsche Behörden) ist über Mustangs Profilauswahl möglich, aber vom Modul nicht gekapselt, und Peppol BIS / UBL liegen aktuell außerhalb des Scopes.
Wer auf einen Sonderfall stößt, den die Factory nicht abdeckt, sollte im Hinterkopf behalten, dass jedes von ihr zurückgegebene Objekt das reine Mustang-Java-Objekt ist. Man kann darauf jede Methode der Mustang-API direkt aufrufen, und Pull Requests auf Github sind willkommen.
Weiterlesen →