Das ZUGFeRD-Datenformat basiert auf der Richtlinie 2014/55/EU vom 16. April 2014 über die elektronische Rechnungsstellung bei öffentlichen Aufträgen und auf der am 28. Juni 2017 veröffentlichten Norm EN 16931. Zudem werden die Cross-Industry-Invoice (CII) 1) von UN/CEFACT 2) und die ISO-Norm 19005-3:2012 (PDF/A-3) bei ZUGFeRD 2.2.0 berücksichtigt.
ZUGFeRD 2.2 und Factur-X 1.0 sind vollständig kompatible und technisch identische Formate, die bereits seit dem 24. März 2020 gemeinsam die Kennung Factur-X nutzen. Beide Formate sind grundsätzlich für den Rechnungsaustausch zwischen Unternehmen, der öffentlichen Verwaltung und Verbrauchern geeignet.
In der hybriden Ausprägung beinhaltet das Rechnungsformat ZUGFeRD die strukturierten Rechnungsdaten in einer PDF/A-3 Datei, die die Sichtkomponente der Rechnung bildet. Die strukturierten XML-Rechnungsdaten können vom Rechnungsempfänger ausgelesen und verarbeitet werden.
Die EU-Richtlinie EN 16931 gibt die Verwendung des strukturierten Datenformats XML für den elektronischen Rechnungsaustausch vor, welches eine automatisierte Rechnungsverarbeitung ermöglicht. Ein standardisiertes semantisches Datenmodell beschreibt die Informationselemente einer Rechnung und deren gegenseitige Beziehung und Datentypen. Die Vorgabe der Syntax stellt eine einheitliche technische Umsetzung der E-Rechnung in der EU sicher.
Insofern ist die im Tip ZUGFeRD vorgestellte Lösung nicht mehr aktuell.
Die grundlegenden Komponenten der ZUGFeRD-Lösung können für Factur-X übernommen werden. Deshalb setzen die nachfolgenden Ausführungen die Kenntnisse aus dem Tip ZUGFeRD voraus. Insbesondere die dort vorgestellten Hinweise zur Inbetriebnahme einer Lösung sind auch im Falle der Factur-X gültig. Eine Besonderheit von Factur-X ist, dass es in fünf sogenannten Profilen zur Verfügung steht: Minimum, Basic WL, BASIC, EN16931 und Extended, sowie ein Referenzprofil XRechnung. Da die ersten drei Pofile in Deutschland keine gültige Rechnung darstellen, sondern lediglich eine Buchungshilfe, wurde für die nachfolgenden Ausführungen das Profil EN 16931 als Grundlage gewählt. Gegenüber der bisherigen Lösung wurde das Beispieldokument um die E-Mail-Adressen von Lieferant und Kunden erweitert. Die E-Mail-Adresse des Rechnungsempfängers befindet sich unterhalb des Adressfelds, die des Rechungsausstellers ganz unten rechts bei den Geschäftsangaben. |
Das hier verwendete Skript macht zur Analyse der Kontrolldatei des print2forms-Gateways ausgiebigen Gebrauch von einer Hilfsdatei, in der eine spezielle Klasse factXrech definiert ist. Diese Klasse reduziert den Aufwand des eigentlichen Skripts für die Factur-X erheblich und abstrahiert auch viele technische Details.
Das eigentliche Skript hat den Name factur-x.php und wird im nachfolgenden schrittweise vorgestellt. Die eben genannte Klasse wird im Rahmen dieses Textes nur insoweit informal beschrieben, als die Methoden und Eigenschaften vom Skript factur-x.php genutzt werden. Eine genaue Beschreibung findet sich im Tip factXrech-class.php.
Die ersten paar Zeilen des Skripts factur-x.php bereiten die eigentliche Arbeit vor. Als allererstes wird das zweite Skript einkopiert (inkludiert), sodass die dort gemachte Definition der Klasse factXrech zur Verfügung steht. Anschliessend wird das aktuelle Arbeitsverzeichnis auf das Skriptverzeichnis gesetzt, damit zur Vereinfachung im weiteren auch relative Pfadangaben benutzt werden können.
Es wird geprüft, ob das Skript mit einem Parameter aufgerufen wurde, wobei das der Pfad und Namen der zu bearbeitenden Datei ist. 3) Evenuelle Fehlermeldungen werden in einer Datei factxrech.log gesammelt, die sich im gleichen Verzeichnis wie das Skript befindet. 4)
<?php /* Factur-X Support Version 3.0 (c) 2024 SPE Systemhaus GmbH */ require "factXrech-class.php"; chdir (dirname(__FILE__)); if ($argc < 2) factXrech::error ("started without required param", 1); $document = $argv [1]; /* base name (no extension) including path */
Die nächsten Zeilen starten jetzt das Aufsammeln der Rechnungsdaten, die für die Factur-X XML-Datei benötigt werden. Dazu wird als erstes ein Objekt $fxr angelegt. Der Konstruktor (new) des Objekts bekommt als Parameter ein Kennzeichen für die Factur-X 5), eine Zeitzone und den Namen der Indexdatei. Der Konstruktor wird dann die Indexdatei lesen und für einen schnellen Zugriff in einer internen Struktur abspeichern.
Die Information zur Zeitzone wird benötigt, um beim eventuellen Einbetten des XML-Dokuments in ein PDF eine korrekte Zeitinformation der Einbettung zu erzeugen. Ausserdem sorgt die korrekte Zeitzone für korrekte Zeitstempel in der Log-Datei.
Die Rechnungsinformationen selbst werden in einem Feld (Array) mit dem Namen $vars gesammelt. Als initialen Inhalt wird lediglich der Platzhalter ITEMS 6) definiert und mit einer leeren Zeichenkette vorbesetzt.
Die Methode fetch holt vom angegebenen Index unter Verwendung des angegebenen regulären Ausdrucks den Wert für den als letztes notierten Platzhalter und speichert ihn in der Variablen $vars. Der reguläre Ausdruck wird verwendet, um aus der Indexdatei nur die Daten zu lesen, die wirklich gebraucht werden. Gleich beim ersten Platzhalter (die Währung) wird nur ein Teil der Indexinformation ausgewertet. Konkret wird aus der Tabellenüberschrift der letzten Spalte das 'EUR' geholt.
Mit dem Namen des Dokuments und des Identifikationsnummer wird ganz ähnlich verfahren.
/* Create instance and init, collect data for Factur-X template */ $fxr = new factXrech (factXrech::FACTURX, "Europe/Berlin", "$document.CTL"); $vars = array ("ITEMS" => ""); /* start with no items present */ $fxr->fetch ("0105D20985", $vars, "in ([A-Z]+)", "currencyID"); $fxr->fetch ("0100E200C6", $vars, "(.*)", "HeaderExchangedDocumentName"); $fxr->fetch ("01012D0846", $vars, "(.*)", "DocumentID");
Die nächsten Zeilen holen die restlichen Rechnungsinformationen. Neu ist hier, dass als fünfter, offensichtlich optionaler Parameter für die Methode fetch eine Konstante aus der Klasse übergeben wird. Damit wird die Formatierung der Information gesteuert. Wie der Name bereits suggeriert, sorgt factXrech::DATE_FIELD dafür, dass eine Zeichenkette der Form '04.03.2019' in der Variable $vars als '20190304' (YYYYMMDD) landet. Entsprechend macht factXrech::CURRENCY_FIELD das mit Währungsinformationen, die im Factur-X-Standard mit einem Dezimalpunkt und zwei Nachkommastellen erwartet werden.
factXrech::COUNTRY_FIELD ist deutlich komplexer, weil es zum Beispiel 'Deutschland' in 'DE' übersetzen muss. Das regelt die Klasse unter Zuhilfenahme von entsprechenden Tabellen, die als externe Dateien im Hintergrund automatisch bereitgestellt werden.
Das neue Format factXrech::MAIL_FIELD stellt sicher, dass es sich bei dem zu prüfenden Wert um eine gültige E-Mail-Adresse handelt. 7)
$fxr->fetch ("0101720846", $vars, "(.*)", "IssueDate", factXrech::DATE_FIELD); $fxr->fetch ("0101B70846", $vars, "(.*)", "ActualDeliveryDate", factXrech::DATE_FIELD); $fxr->fetch ("0101FC0846", $vars, "(.*)", "PaymentReference"); $fxr->fetch ("0102860846", $vars, "(.*)", "BuyerReference"); $fxr->fetch ("0102410846", $vars, "(.*)", "CustomerID"); $fxr->fetch ("01036C00C6", $vars, "(.*)", "BuyerRegistrationName"); $fxr->fetch ("0103B100C6", $vars, "(.*)", "BuyerStreetName"); $fxr->fetch ("0103F600C6", $vars, "[A-Za-z]*-?([0-9]+) .*", "BuyerPostcodeCode"); $fxr->fetch ("0103F600C6", $vars, "[A-Za-z]*-?[0-9]+ (.*)", "BuyerCityName"); $fxr->fetch ("01043B00C6", $vars, "(.*)", "CountryIC", factXrech::COUNTRY_FIELD); $fxr->fetch ("010D1A0889", $vars, "([0-9,]+) .*", "TaxableAmount", factXrech::CURRENCY_FIELD); $fxr->fetch ("010D5F0889", $vars, "([0-9,]+) .*", "TaxAmount", factXrech::CURRENCY_FIELD); $fxr->fetch ("010DA40889", $vars, "([0-9,]+) .*", "PayableAmount", factXrech::CURRENCY_FIELD); $fxr->fetch ("010D5F00C6", $vars, ".+ ([0-9,]+)% .*", "TaxPercent"); $fxr->fetch ("01050A0229", $vars, "(.*)", "BuyerEndpointID", factXrech::MAIL_FIELD);
Was jetzt noch fehlt, ist die Information zu der gewünschten Zahlungsweise.
Hier kommt es nun zu einer Situation, in der der Windows-Druckertreiber eine längere Zeile durch eine Neupositionierung teilt. 8) Hier zur Verdeutlichung der Besonderheit der entsprechende Ausschnitt aus der Indexdatei:
... [010E7300C6]innerhalb 30 Tag [010E730281]en netto bis 04.07.2018, innerhalb 14 Tagen 2% Skonto bis 19.06.2018 ...
Für diesen Anwendungsfall gibt es eine Methode getLine, die alle Texte aufsammelt, die über eine Zeile verstreut sind. Das wird anhand der Ziffern eins bis sechs des Indexes erkannt.
Nachdem die Zeile zur Verfügung steht, wird aus ihr das Datum für den Ablauf der Zahlungsfrist extrahiert, und direkt in das Feld $vars eingetragen. Da für ein Datum in Factur-X ein bestimmtes Format vorgeschrieben ist, muss vorher noch die Methode formatDate direkt aufgerufen werden. Neben formatDate (entspricht factXrech::DATE_FIELD) gibt es noch die Methoden formatQuantity (entspricht factXrech::QUANTITY_FIELD) und formatCurrency (entspricht factXrech::CURRENCY_FIELD), die für eigene Konvertierungen aufgerufen werden können, wenn, so wie hier, die Methode fetch nicht zum Einsatz kommen kann.
/* Handling of payment is identical for all items */ $vars ["PaymentTerms"] = $fxr->getLine ("010E7300C6"); $vars ["DueDate"] = $fxr->formatDate (substr ($vars ["PaymentTerms"], 29, 10));
Als nächstes erfolgt die Bearbeitung der Tabelle mit den einzelnen Rechnungsposten.
Dazu werden zwei weitere Methoden benötigt getIndex und setIndex. Innerhalb der Klasse wird ein Zeiger innerhalb der Indexdatei verwaltet, der mit den beiden Methoden abgefragt oder gesetzt werden kann. Wichtig zu wissen ist auch, dass die Methode fetch mit diesem Zeiger arbeitet und ihn immer weiter schiebt, wenn sie ohne einen Index aufgerufen wird.
Im folgenden wird also erst der Zeiger auf die erste Zeile der Indexdatei mit Tabelleninformationen gesetzt. In unserem Fall ist dies die erste Positionsnummer in der Tabelle mit dem Index 01063100FD. Um das Ende der Tabelle zu finden, wird in der Schleife auf die erste Zeile getestet, die nicht mehr zur Tabelle gehört - am Index 010D1A00C6 steht 'Rechnungssumme Netto (exkl. USt.)'.
Für jeden Rechnungsposten wird ein eigenes Feld $itemvars für die ausgelesenen Informationen angelegt. Das Feld wird mit der Währungsbezeichnung und dem Umsatzsteuersatz der Rechnung fest vorbesetzt. Für jede der acht Tabellenspalten wird jetzt einmal die Methode fetch mit einem leeren Index aufgerufen, und so das Feld $itemvars Schritt für Schritt gefüllt.
Am Ende jeder Tabellenzeile werden die ausgelesenen Daten aus dem Feld $itemvars in die Vorlage für Rechnungspositionen an Stelle der dort hinterlegten Platzhalter eingetragen. Dazu dient die Methode substitute, die als Parameter die Kennung der Vorlage (eine vordefinierte Konstante der Klasse) und die Variable mit den Daten erwartet. Die Methode gibt die resultierende XML-Zeichenkette zurück, die einfach an den aktuellen Wert des Platzhalters ITEMS in den Rechnungsinformationen angehängt wird.
Am Ende der Schleife steht in $vars für den Platzhalter ITEMS der gesamte XML-Text für die vorgefundenen Rechnungspositionen.
$fxr->setIndex ("01063100FD"); /* position to start with item table */ while ($fxr->getIndex () != "010D1A00C6") /* loop through list of items */ { /* Process list of invoice items, all items have fix VAT and currency */ $itemvars = array ("currencyID" => $vars ["currencyID"], "TaxPercent" => $vars ["TaxPercent"]); $fxr->fetch ("", $itemvars, "(.*)", "LineID"); $fxr->fetch ("", $itemvars, "(.*)", "SellerAssignedID"); $fxr->fetch ("", $itemvars, "(.*)", "BuyerAssignedID"); $fxr->fetch ("", $itemvars, "(.*)", "Name"); $fxr->fetch ("", $itemvars, "(.*)", "PriceAmount", factXrech::CURRENCY_FIELD); $fxr->fetch ("", $itemvars, "(.*)", "UnitCode", factXrech::UNIT_FIELD); $fxr->fetch ("", $itemvars, "(.*)", "InvoicedQuantity", factXrech::QUANTITY_FIELD); $fxr->fetch ("", $itemvars, "(.*)", "LineExtensionAmount", factXrech::CURRENCY_FIELD); $vars ["ITEMS"] .= $fxr->substitute (factXrech::ITEM_TEMPLATE, $itemvars); }
Jetzt sind alle Informationen für Factur-X in $vars verfügbar.
Jetzt werden durch erneuten Aufruf der Methode substitute diese Daten ihrerseits in die Vorlage für die eigentliche Rechnung eingesetzt. Die resultierende Zeichenkette wird in eine temporäre XML-Datei geschrieben.
Falls gewünscht, kann der zweite Parameter der Methode createXML mit dem Wert '1' dafür sorgen, dass die erzeugte XML-Datei gleich validiert wird. 9) Falls die Validierung scheitert, wird intern eine Fehlermeldung in die Log-Datei geschrieben, und die nachfolgende Abfrage auf Fehler verhindert die weitere Bearbeitung. 10)
Als nächstes muss die Einbettung in eine PDF-Datei erfolgen - das Zusammenfassen der PDF-Datei und der XML-Datei zu einer Factur-X konformen PDF/A3-Datei.
Die Methode createPDF benötigt die PCL-Datei des Gateways, einen Firmennamen, einen Titel für die Datei und Schlüsselworte. Diese Informationen werden in die Meta-Daten der PDF-Datei eingebaut und unterstützen so die automatische Weiterverarbeitung der Datei.
Als allerletzte Aufgabe verbleibt, die intern angelegte PDF-Datei mit einem entsprechenden Namen an den gewünschten Zielort zu kopieren wird.
/* create and validate xml file using external kosit validator (java) */ $fxr->createXML ($fxr->substitute (factXrech::INVOICE_TEMPLATE, $vars), 1); if ($fxr->errorsfound () == 0) /* nothing went wrong so far */ { $invoicedate = substr ($vars ["IssueDate"], 8, 2) . "." . substr ($vars ["IssueDate"], 5, 2) . "." . substr ($vars ["IssueDate"], 0, 4); $title = "Rechnung {$vars ["DocumentID"]} vom $invoicedate"; $keywords = "Muster_GmbH; {$vars ["DocumentID"]}; $invoicedate; {$vars ["CustomerID"]}"; $resultfile = $fxr->createPDF ("$document.pcl", "Muster GmbH", $title, $keywords); /* save and rename just generated pdf file */ copy ($resultfile, "Rechnung_{$vars ["DocumentID"]}_$invoicedate.pdf"); } //array_map ('unlink', glob ("$document.*")); /* uncomment after test */ ?>
Gleich vorweg: es ist keine gute Idee, die Factur-X-Lösung ohne die Hilfe einer entsprechenden Entwicklungsumgebung in Betrieb nehmen zu wollen. Das Skript ist nicht ganz trivial, und spätestens, wenn es an die realen Bedingungen einer konkreten Kundeninstallation angepasst werden soll, ist das mit reinem 'Print'-Debugging extrem ineffizient.
Es wird deshalb noch einmal ausdrücklich auf die Tips Visual Studio Code für PHP installieren, Test von PHP-Skripten und PHP-Skript ohne Gateway testen verwiesen. In den nachfolgenden Ausführungen wird davon ausgegangen, dass die Lösung mit Microsoft Visual Studio Code in Betrieb genommen wird.
Alle benötigten Dateien lassen sich hier einfach als Archiv herunterladen, und müssen nicht im Wiki aufgesammelt werden. Das vermeidet auch eventuelle Probleme mit der Kodierung der einzelnen Textdateien. Vorzugsweise sollte das Archiv im Skriptverzeichnis von print2forms entpackt werden - in der Regel
C:\Users\Public\Documents\SPE Systemhaus GmbH\print2forms\p2fScript
.
Im Archiv sind auch die entsprechenden Konfigurationsdateien für Visual Studio Code enthalten, sodass der jetzt neu erscheinende Ordner p2fFactur-X direkt in der Entwicklungsumgebung geöffnet werden kann. Es sollte sich im Dateiexplorer die rechts abgebildete Struktur ergeben - zunächst aber mit einem unvollständigen Unterordner converter.
Aus lizenzrechtlichen Gründen dürfen die Programme GhostPCL und GhostScript der Firma Artifex nicht mit print2forms zusammen ausgeliefert werden. 11)
Nachdem die Programme von den hier angegebenen Web-Adressen geladen wurden, müssen für GhostPCL die beiden Dateien gpcl6win64.exe und gpcl6win64.dll (für 64-Bit Systeme, ansonsten gpcl6win32.exe und gpcl6win32.dll für 32-Bit Systeme) in den Ordner converter kopiert werden. Für GhostScript müssen die beiden Dateien gswin64.exe und gsdll64.dll (für 64-Bit Systeme, ansonsten gswin32.exe und gsdll32.dll für 32-Bit Systeme) kopiert werden.
Die beiden Dateien im Unterordner test sind bereits von einem print2forms-Gateway erzeugte Testdaten aus der Beispielrechnung. Durch die Verwendung dieser Dateien wird verhindert, dass das Office Paket auf der Testmaschine die Rechnung abweichend druckt. 12)
Die Dateien im Unterordner mappings dienen der Übersetzung von Länder- und Sprachbezeichnungen, sowie Masseinheiten in die im Factur-X-Standard vorgegebenen Kodes. Die Dateien im Unterordner validator werden für die eventuell gewünschte Validation der XML-Dateien benötigt. Ansonsten findet sich im Verzeichnis noch die eigentliche Office-Datei mit der Rechnung Formular.odt.
Damit ist alles eingerichtet, und die ersten Funktionstests mit Visual Studio Code können starten.
Bisher erfolgte der ganze Test des Skriptes ohne jedes Zutun eines print2forms-Gateways. Soll jetzt Factur-X mit eigenen Dokumenten genutzt werden, ist die Kommandozeile des betreffenden Gateways entsprechend zu konfigurieren.
Das Skript factur-x.php erwartet nur einen Parameter: den Namen des zu bearbeitenden Dokuments. Demzufolge sieht die Kommandozeile in etwa wie folgt aus:
"C:\PHP8\php.exe" "%1/factur-x.php" "%2\%4"
Der Pfad zum Aufruf des PHP-Interpreters ist der gleiche Pfad wie der in der Datei lauch.json.
In der Praxis ist das eben entwickelte Beispielskript nur die halbe Miete, weil die erzeugte PDF-Datei noch - möglichst automatisch - weiter an den Rechnungsempfänger übermittelt werden muss. Für den Fall, dass der Rechnungsempfänger die Rechnung über den Upload auf einem speziellen Portal erwartet, ist je nach Portal eine mehr oder minder aufwändige zusätzliche Programmierung notwendig, die weit über den Umfang dieses Wikis hinausgeht.
Für den Fall, dass die Rechnung vom Rechnungsempfänger als E-Mail entgegengenommen wird, kann das Skript jedoch leicht entsprechend erweitert werden. Grundlage dafür liefern die Informationen im Wiki Artikel Dokument via E-Mail in PHP versenden.