Webseiten-Werkzeuge

Benutzer-Werkzeuge


ZUGFeRD

Durch das Steuervereinfachungsgesetz 2011 wurden die Hürden für elektronische Rechnungen gesenkt. Bis dahin musste die Unversehrtheit des Dokuments mittels eines digitalen Zertifikats sichergestellt werden.

Im Zuge dieses Gesetzes wurde ein neues Rechnungsformat mit dem Namen ZUGFeRD entwickelt. Es sieht vor, eine menschenlesbare und maschinenlesbare Repräsentation der Rechnung in einer einzigen Datei zu vereinen. Dazu wird eine PDF/A-3-Datei aus der Rechnung erstellt und in diese wird zusätzlich zum menschenlesbaren Abbild der Rechnung eine maschinenlesbare XML-Repräsentation der Rechnungsdaten eingebettet.

Dies hat den Vorteil, dass die Datei mit jedem gängigen PDF-Anzeigeprogramm geöffnet werden kann und dann wie eine ganz normale PDF-Datei angezeigt oder gedruckt werden kann. Zusätzlich kann die Datei aber auch von ZUGFeRD kompatibler Software automatisch verarbeitet werden. Die ZUGFeRD-Spezifikation enthalten klare Regeln für Aufbau und Inhalt des XML-Teils innerhalb der PDF/A-3-Datei.

Mehr Informationen inklusive der kompletten Spezifikation und ein paar Beispielen finden sich auf der Internetseite des Forums elektronische Rechnung Deutschland.

Im Nachfolgenden wird aufgezeigt, wie ZUGFeRD kompatible Rechnungen durch print2forms erzeugt werden können. Dabei wird voraus­gesetzt, dass der Leser mit dem ZUGFeRD-Standard, insbesondere den dabei genutzten Fachtermini vertraut ist.

Die Beschreibung basiert auf einem Rechnungs­layout, wie es in der nebenstehenden Abbildung zu sehen ist. Es wurde ganz bewusst so gewählt, das die meisten Detailaufgaben im Zusammen­hang mit ZUGFeRD demonstriert werden können.

Die Beispielrechnung wurde auf einem PC mit OpenOffice erstellt und verwendet eine typographische Schrift, was beim direkten Drucken aus Windows heraus zu besonderen Anforderungen führt, auf die im Tip noch eingegangen wird.

Dies ist eine komplett überarbeitete Version des Tips, bei der wir versucht haben, die programmatische Einstiegs­hürde noch weiter zu senken, um die ZUGFERD Anwendung noch leichter realisierbar zu machen.

Grundidee

Um mit print2forms aus gewöhnlichen Druckdaten PDF-Dateien zu erzeugen, wird zuerst einmal ein Gateway benötigt. Mittels eines ganz speziellen Skripts müssen folgenden Aufgaben erledigt werden:

  • Aus der vom Gateway angelieferten Indexdatei werden die Informationen extrahiert, die für das jeweilige ZUGFeRD-Profil (hier Comfort) notwendig sind.
  • Mit diesen Daten wird eine XML-Datei im ZUGFeRD-Standard erstellt.
  • Aus den vom print2forms-Gateway angelieferten PCL-Druckdaten wird eine PDF-Datei (zunächst PDF/A1) erzeugt.
  • Die XML-Datei und die PDF-Datei werden zu einem PDF/A-3-Dokument vereinigt.


Für den ersten Arbeitschritt liest das Skript die Indexdatei der Rechnung. In dieser findet es die gewünschten Informationen. 1)

Über die in der Abbildung rechts grün darge­stellten Indizes können die gewünschten Daten ausgewählt und ausgelesen werden. Die Indizes entsprechen grob den Positionen der jeweiligen Texte im Dokument.

Solange sich die Struktur (Layout) der gedruckten Rechnung nicht ändert, bleiben diese Indizes gleich, sodass man sich in einem Skript auf diese beziehen kann. Man erkennt beispielsweise, dass am Index '[0102410846]' die Kundennummer abzugreifen ist.

...
[01012D0605]Rechnungsnummer
[01012D0846]471102
[0101720605]Rechnungsdatum
[0101720846]05.06.2018
[0101B70605]Leistungsdatum
[0101B70846]03.06.2018
...
[0102410605]Kundennr
[0102410846]202011
[0102860605]Ihre Referenz
[0102860846]Bestellung 123456
[0102FD00C6]Lieferant GmbH – Lieferweg 21 – 80345 Musterstadt    
[01036C00C6]Hans Müller
[0103B100C6]Kundenstrasse 46
[0103F600C6]35268 Musterdorf
...

Im zweiten Arbeitschritt müssen diese Daten jetzt in eine XML-Datei nach dem ZUGFeRD-Standard eingebaut werden.

Dazu nutzt das Skript der Einfachheit halber zwei Vorlagen, die als externe XML-Dateien bereitgestellt werden. Diese Vorlagen enthalten die XML-Tags gemäss dem ZUGFeRD-Standard. Die Inhalte der Tags und auch ein Teil von deren Attribute sind mit Platzhaltern besetzt. Die Platzhalter sind daran erkennbar, dass sie mit einem '@'-Zeichen beginnen und aufhören. Diese Platzhalter wird das Skript gegen Daten aus der empfangenen Rechnung ersetzen.

Ein kurzer Auszug aus einer dieser Dateien zur Illustration:

...
    <ram:SpecifiedSupplyChainTradeAgreement>
        <ram:GrossPriceProductTradePrice>
            <ram:ChargeAmount currencyID="@currencyID@">@ChargeAmount@00</ram:ChargeAmount>
        </ram:GrossPriceProductTradePrice>
        <ram:NetPriceProductTradePrice>
            <ram:ChargeAmount currencyID="@currencyID@">@ChargeAmount@00</ram:ChargeAmount>
        </ram:NetPriceProductTradePrice>
    </ram:SpecifiedSupplyChainTradeAgreement>
    <ram:SpecifiedSupplyChainTradeDelivery>
           <ram:BilledQuantity unitCode="@unitCode@">@BilledQuantity@</ram:BilledQuantity>
    </ram:SpecifiedSupplyChainTradeDelivery>
    <ram:SpecifiedSupplyChainTradeSettlement>
        <ram:ApplicableTradeTax>
            <ram:TypeCode>VAT</ram:TypeCode>
            <ram:CategoryCode>S</ram:CategoryCode>
            <ram:ApplicablePercent>@ApplicablePercent@.00</ram:ApplicablePercent>
        </ram:ApplicableTradeTax>
        <ram:SpecifiedTradeSettlementMonetarySummation>
               <ram:LineTotalAmount currencyID="@currencyID@">@LineTotalAmount@</ram:LineTotalAmount>
        </ram:SpecifiedTradeSettlementMonetarySummation>
    </ram:SpecifiedSupplyChainTradeSettlement>
    <ram:SpecifiedTradeProduct>
        <ram:SellerAssignedID>@SellerAssignedID@</ram:SellerAssignedID>
        <ram:BuyerAssignedID>@BuyerAssignedID@</ram:BuyerAssignedID>
        <ram:Name>@Name@</ram:Name>
    </ram:SpecifiedTradeProduct>
...

Es gibt zwei XML-Dateien, weil zum einen ein Grossteil der Tags nur einmal pro Rechnung gebraucht wird, es dann aber andererseits Tags gibt, die für jeden einzelnen Rechnungsposten wiederholt werden müssen.

Die grössere umfassende Datei für die Rechnung heist comfort.xml und die Datei mit den XML-Daten für jeden Rechnungsposten heist items.xml.

Das Skript liest die beiden Dateien ein und ersetzt die Platzhalter gegen die im ersten Schritt gefundenen Daten aus der Rechnung. Im Skript gibt es dazu natürlich eine Abbildung von Indizes auf Platzhalter.

Der dritte Arbeitschritt ist nicht mit Bordmitteln von print2forms zu erledigen. Es muss ein externes Hilfsprogramm genutzt werden, welches aus PCL-Daten ein PDF-Dokument im Standard PDF/A1 erzeugt.

Dazu kann das kostenpflichtige Programm LincPDF (http://www.lincolnco.com) verwendet werden, aber alternativ kommt auch der Konverter aus dem Open-Source-Projekt ghostpcl (https://www.ghostscript.com) für diese Aufgabe in Frage. Dieser wird auch hier im Tip verwendet.

Der vierte Arbeitschritt schliesslich wird mit einem speziell für diese Anwendung geschriebenen Hilfsprogramm erledigt. Es ist in Java geschrieben und benötigt daher eine aktuelle Java Laufzeitumgebung auf dem Rechner, der das print2forms-Gateway ausführt. Die Aufgabe des Programms besteht darin, aus der im zweiten Schritt erstellten XML-Datei und der im dritten Schritt erstellten PDF-Datei eine PDF/A3-Datei gemäss ZUGFeRD-Standard zu erstellen.

Alle für diesen Tip benötigten Dateien lassen sich hier einfach als Archiv herunterladen, und müssen nicht über die einzelnen Links im Tip aufgesammelt werden.

ZUGFeRD Skript(e)

Das hier verwendete Skript ist eigentlich zweigeteilt. Die geforderten Aufgaben sind nicht ganz trivial und so erschien es uns sinnvoll, das Skript in einen dokumentenabhängigen, kundenspezifischen Teil und in einen mehr technischen, weitestgehend konstanten Teil zu trennen.

Das Skript hat den Name zugferd.php und wird im nachfolgenden schrittweise vorgestellt. Die zweite Datei heist zugferd-class.php und wird im Rahmen des Wikis nur insoweit informal beschrieben, als die Methoden und Eigenschaften der darin definierten Klasse vom ersten Skript genutzt werden.

Die ersten paar Zeilen des Skripts zugferd.php bereiten die eigentliche Arbeit vor. Als allererstes wird das zweite Skript einkopiert (inkludiert), sodass die dort gemachten Definitionen zur Verfügung stehen. 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 zwei Parametern aufgerufen wurde, wobei der erste den Pfad und Namen der zu bearbeitenden Datei enthält und der zweite Parameter ein Lizenzschlüssel für das ZUGFeRD-Hilfsprogramm ist.

Als nächste Prüfung erfolgt ein Existenztest der als Parameter übergebenen Datei. Fällt dieser negativ aus, erfolgt eine Fehlermeldung und das Skript terminiert. Die Fehlermeldungen werden in einer Datei zugferd.log gesammelt, die sich im gleichen Verzeichnis wie das Skript befindet. 2)

<?php
 
/*  ZUGFeRD Support Version 3.0     (c) 2019 SPE Systemhaus GmbH  */
 
require("zugferd-class.php");
 
chdir (dirname(__FILE__));
if ($argc < 3 || strlen ($argv [2]) < 1)
    ZUGFeRD::error ("started without required params." );
 
$document = $argv [1];             /* base name (no extension) including path */
 
if (! file_exists("$document.pcl"))
    ZUGFeRD::error ("$document.pcl file not found.");


Die nächsten Zeilen starten jetzt das Aufsammeln der Rechnungsdaten, die für die ZUGFeRD XML-Datei benötigt werden. Dazu wird als erstes ein Objekt $zf angelegt. Der Konstruktor (new) des Objekts bekommt als Parameter ein Länderkennzeichen und den Namen der Indexdatei. Er wird dann die Indexdatei lesen und für einen schnellen Zugriff in einer internen Struktur abspeichern. Das Länderkennzeichen dient dazu, in multilingualen Umgebungen auf die richtigen Übersetzungstabellen von Ländernamen oder Masseinheiten zugreifen zu können. Davon wird hier im Tip aber kein Gebrauch gemacht.

Die Rechnungsinformationen werden in einem Feld (Array) mit dem Namen $vars gesammelt. Als initialen Inhalt wird lediglich der Platzhalter ITEMS definiert und mit einer leeren Zeichenkette vorbesetzt.

Dann geht es auch schon los mit dem ersten Arbeitschritt. 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.

/* Collect data for template comfort.xml */
 
$zf = new ZUGFeRD ("de", $document . ".CTL");     /* create instance and init */
 
$vars = array ("ITEMS" => "");                 /* start with no items present */
 
$zf->fetch ("0105D20985", $vars, "in ([A-Z]+)", "currencyID");
$zf->fetch ("0100E200C6", $vars, "(.*)", "HeaderExchangedDocumentName");
$zf->fetch ("01012D0846", $vars, "(.*)", "HeaderExchangedDocumentID");


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 ZUGFeRD::DATE_FIELD dafür, dass eine Zeichenkette der Form '04.03.2019' in der Variable $vars als '20190304' (YYYYMMDD) landet.

Entsprechend macht ZUGFeRD::CURRENCY_FIELD das mit Währungsinformationen, die im ZUGFeRD-Standard mit einem Dezimalpunkt und zwei Nachkommastellen erwartet werden. 3)

ZUGFeRD::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 bereitgestellt werden.

$zf->fetch ("0101720846", $vars, "(.*)", "IssueDateTime", ZUGFeRD::DATE_FIELD);
$zf->fetch ("0101B70846", $vars, "(.*)", "OccurrenceDateTime", ZUGFeRD::DATE_FIELD);
$zf->fetch ("0101FC0846", $vars, "(.*)", "PaymentReference");
$zf->fetch ("0102860846", $vars, "(.*)", "CustomerReference");   /* just info */
$zf->fetch ("0102410846", $vars, "(.*)", "CustomerID");          /* just info */
$zf->fetch ("01036C00C6", $vars, "(.*)", "BuyerTradePartyName");
$zf->fetch ("0103B100C6", $vars, "(.*)", "LineOne");
$zf->fetch ("0103F600C6", $vars, "[A-Za-z]*-?([0-9]+) .*", "PostcodeCode");
$zf->fetch ("0103F600C6", $vars, "[A-Za-z]*-?[0-9]+ (.*)", "CityName");
$zf->fetch ("01043B00C6", $vars, "(.*)", "CountryID", ZUGFeRD::COUNTRY_FIELD);
$zf->fetch ("010D1A0889", $vars, "([0-9,]+) .*", "BasisAmount", ZUGFeRD::CURRENCY_FIELD);
$zf->fetch ("010D5F0889", $vars, "([0-9,]+) .*", "TaxTotalAmount", ZUGFeRD::CURRENCY_FIELD);
$zf->fetch ("010DA40889", $vars, "([0-9,]+) .*", "GrandTotalAmount", ZUGFeRD::CURRENCY_FIELD);
$zf->fetch ("010D5F00C6", $vars, ".+ ([0-9,]+)% .*", "ApplicablePercent");


Was jetzt noch fehlt, ist die Information zu der gewünschten Zahlungsweise.

Hier kommt es nun zu einer bereits weiter oben angedeuteten Situation, in der der Windows-Druckertreiber eine längere Zeile durch eine Neupositionierung teilt. 4) 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 ZUGFeRD ein bestimmtes Format vorgeschrieben ist, muss vorher noch die Methode formatDate direkt aufgerufen werden. Neben formatDate gibt es noch die Methoden formatQuantity und formatCurrency, die für eigene Konvertierungen aufgerufen werden können, wenn, so wie hier, die Methode fetch nicht zum Einsatz kommen kann.

/* Special handling of payment information */
 
$vars ["SpecifiedTradePaymentTerms"] = $zf->getLine ("010E7300C6");
$vars ["DueDateDateTime"] = $zf->formatDate (substr ($vars ["SpecifiedTradePaymentTerms"], 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 um eins 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 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 der Tabellenzeile werden die ausgelesenen Daten aus dem Feld $itemvars in die Vorlage items.xml an Stelle der 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.

$zf->setIndex ("01063100FD");              /* position to start of item table */
 
while ($zf->getIndex () != "010D1A00C6") {      /* loop through list of items */
 
    /* walk through line in list of items */
 
    $itemvars = array ("currencyID" => $vars ["currencyID"],
                       "ApplicablePercent" => $vars ["ApplicablePercent"]);
 
    $zf->fetch ("", $itemvars, "(.*)", "LineID");
    $zf->fetch ("", $itemvars, "(.*)", "SellerAssignedID");
    $zf->fetch ("", $itemvars, "(.*)", "BuyerAssignedID");
    $zf->fetch ("", $itemvars, "(.*)", "Name");
    $zf->fetch ("", $itemvars, "(.*)", "ChargeAmount", ZUGFeRD::CURRENCY_FIELD);
    $zf->fetch ("", $itemvars, "(.*)", "unitCode", ZUGFeRD::UNIT_FIELD);
    $zf->fetch ("", $itemvars, "(.*)", "BilledQuantity", ZUGFeRD::QUANTITY_FIELD);
    $zf->fetch ("", $itemvars, "(.*)", "LineTotalAmount", ZUGFeRD::CURRENCY_FIELD);
 
    $vars["ITEMS"] .= $zf->substitute (ZUGFeRD::ITEM_TEMPLATE, $itemvars);
}


Jetzt sind alle Informationen für ZUGFeRD in $vars verfügbar. Der erste Arbeitschritt ist damit abgeschlossen.

Für den zweiten Arbeitschritt werden durch erneuten Aufruf der Methode substitute diese Daten jetzt in die Vorlage comfort.xml eingesetzt. Die resultierende Zeichenkette wird in eine temporäre XML-Datei geschrieben. Der Name dieser Datei ist der des Dokuments erweitert um die Extension '.xml'. Damit ist der Arbeitschritt auch schon abgeschlossen.

Der dritte Arbeitschritt besteht lediglich darin, den vom Gateway erzeugten PCL-Datenstrom in ein PDF/A1-Dokument zu konvertieren. Dafür wird hier das Programm GhostPCL verwendet.

Falls nicht GhostPCL zum Einsatz kommt, muss die Pfadangabe und natürlich die gesamte Parameterzeile entsprechend modifiziert werden. Für den Konverter LincolnPDF finden sich entsprechende Informationen im Tip PDF- oder TIFF-Dateien erzeugen.

file_put_contents ("$document.xml", $zf->substitute (ZUGFeRD::BASIC_TEMPLATE, $vars));
 
system ("\"converter/gpcl6win32.exe\" \"-o$document.temp.pdf\" " .
                 "-sPDFCompatibillityPolicy=2 -sDEVICE=pdfwrite -dPDFA=1 " .
                 "-dCompressPages=false -dCompressFonts=false \"$document.pcl\"");


Fehlt noch der vierte Arbeitschritt, das Zusammenfassen der PDF-Datei und der XML-Datei zu einer ZUGFeRD konformen PDF/A3-Datei.

Hier kommt ein print2forms zur Verfügung gestelltes Hilfsprogramm namens p2fzugferd.jar zum Einsatz. Wie am Namen bereits zu erkennen, handelt es sich um eine Java-Anwendung. Es wird vorausgesetzt, dass der Rechner, auf dem das Gateway läuft, eine entsprechende Laufzeitumgebung (Version 8) bereitstellt. Das Hilfsprogramm erwartet eine Lizenzierung in Form eines Schlüssels, den es vom Gateway als zweiten Parameter des Skriptes zugferd.php übermittelt bekommt.

Der Name des endgültigen PDF-Dokuments sollte natürlich nicht die interne Kennung des Gateways sein, sondern ein für Menschen lesbarer sein. Daher wird die Dokumentenbezeichnung (hier das Wort 'Rechnung') und die Rechnungsnummer als Name gewählt.

Das ZUGFeRD-Dokument steht jetzt zur weiteren Verarbeitung zur Verfügung - es kann zum Beispiel archiviert oder per E-Mail an den Kunden weitergeleitet werden.

Als letztes bleibt die Aufgabe, die erzeugten Zwischendateien zu löschen. Das sollte während des Tests oder während der Fehlersuche verständlicherweise unterbleiben. Im Produktiveinsatz ist es aber wichtig, weil sonst irgendwann die Festplatte vollläuft.

system ("java -jar \"p2fzugferd/p2fzugferd.jar\" --inpdf \"$document.temp.pdf\" " .
            "--xml \"$document.xml\" --outpdf \"" . dirname ($document) . "/" . 
            $vars ["HeaderExchangedDocumentName"] . "_" . 
            $vars ["HeaderExchangedDocumentID"] . ".pdf\" --key \"" . $argv [2] . "\"");
 
//array_map ('unlink', glob ("$document.*"));         /* uncomment after test */



Inbetriebnahme vorbereiten

Gleich vorweg: es ist keine gute Idee, die ZUGFeRD-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 p2fZUGFeRD direkt in der Entwicklungsumgebung geöffnet werden kann. Es sollte sich im Dateiexplorer die rechts abgebildete Struktur ergeben - zunächst aber mit einem leeren Unterordner converter.

Aus lizenzrechtlichen Gründen darf das Programm GhostPCL der Firma Artifex nicht mit print2forms zusammen ausgeliefert werden. Nachdem es von der hier angegebenen Adresse geladen wurde, müssen die beiden Dateien gpcl6win64.exe und gpcl6win64.dll (für 64-Bit Systeme, ansonsten gpcl6win32.exe die gpcl6win32.dll für 32-Bit Systeme) in den Ordner converter 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. 5) Beide Dateien müssen ins Spoolverzeichnis von print2forms kopiert werden - in der Regel C:\Users\Public\Documents\SPE Systemhaus GmbH\print2forms\p2fSpool.

Die Dateien im Unterordner templates dienen der Übersetzung von Länder- und Sprach­bezeichnungen, sowie Masseinheiten in die im ZUGFeRD-Standard vorgegebenen Kodes. Die Dateien liegen in einer deutschen und in einer englischen Version vor. Welche zur Anwendung kommt, wird bei der Initialisierung - eventuell bereits abhängig vom Dokument - der ZUGFeRD-Klasse festgelegt.

Ansonsten findet sich im Verzeichnis noch die eigentliche Office-Datei mit der Rechnung Formular.odt, sowie eine von der ZUGFeRD-Lösung erzeugte und fortgeschriebene Log-Datei mit Fehlermeldungen zugferd.log.

Als erstes wird in der Entwicklungsumgebung die Datei launch.json geöffnet. Diese steuert den Aufruf und die Versorgung mit Parametern des ZUGFeRD-Skriptes. Es muss geprüft werden, ob der Pfad hinter dem Parameter „runtimeExecutable“ für das eigene Testsystem stimmt. Hier muss der Pfad zum PHP-Interpreter (Version 7) stehen.

Im Eintrag für den Argumentvektor „args“ ist als zweites ein Lizenzschlüssel zu erkennen, der natürlich für das Testsystem nicht stimmt und auch längst zeitlich abgelaufen ist. Dieser Lizenzschlüssel muss ersetzt werden gegen einen eigenen, was aber erst zum Tragen kommt, wenn das Hilfsprogramm p2fzugferd.jar zum Einsatz kommt.

An einen eigenen Schlüssel kommt man heran, indem man in einem print2forms-Gateway in der Kommandozeile den Parameter %K mitgibt. Nachdem man dann irgendeine Datei auf dem Gateway ausgegeben hat, kann man sich in der Ablaufverfolgung (natürlich vorher aktivieren) die Zeile mit dem Skriptaufruf anzeigen lassen, und den Schlüssel via Copy and Paste in die Datei launch.json übernehmen. So ein Schlüssel hat lediglich eine Gültigkeit von ein paar Stunden und muss daher von Zeit zu Zeit erneuert werden.

Der erste Wert im Argumentvektor „args“ verweist auf die Testdaten. Wenn später mit Daten vom eigenen Gateway getestet werden soll, ist dieser Wert natürlich entsprechend zu modifizieren.

Damit ist alles eingerichtet, und die ersten Funktionstests können starten.

Test des Skriptes

Anstatt das Skript einfach nur durchlaufen zu lassen, soll zur Verdeutlichung des Ablaufs an bestimmten Stellen angehalten werden, um die bis dahin gesetzten Variablen zu inspizieren. Als erstes wird ein Breakpoint in Zeile 22 unmittelbar nach dem Instanziieren des Objekts $zf gesetzt.

Schaut man sich den Argumentvektor $argv an, erkennt man drei Feldelemente, von denen das erste Element per Definition immer den aktuellen Skriptnamen enthält. Unter dem Index 1 erkennt man den Pfad und den Namen des übergebenen Druckdokuments (ohne Extension). Unter dem Index 2 steht der Lizenzschlüssel. Das sind exakt die Angaben aus der launch.json.

Was auch zu sehen ist, ist, dass die Feldvariable $vars bereits einen ersten Eintrag mit der Kennung ITEMS enthält, dessen Wert aber noch eine leere Zeichenkette ist. In diesem Feld werden nach und nach die anderen für ZUGFeRD notwendigen Informationen auftauchen.



Ein nächster Breakpoint wird in Zeile 45 gesetzt. Das ist die Stelle, an der bereits alle Informationen des Rechnungsformulars inklusive des Textes zu den Zahlungsbedingungen bearbeitet sind.

Eine Inspektion der Feldvariablen $vars zeigt nun insgesamt 20 Einträge. Deutlich ist zu sehen, welchem Platzhalter jetzt welcher Wert zugeordnet wurde. Da bis hierhin allerdings noch kein einziger Rechnungsposten verarbeitet wurde, ist dem Platzhalter ITEMS immer noch die leere Zeichenkette zugewiesen.



Die nächste interessante Stelle ist die, wenn ein Rechnungsposten bearbeitet worden ist. Aus technischen Gründen kann man keinen Breakpoint auf die schliessende Klammer am Ende der Schleife setzen, sondern nur auf die Zeile davor.

Da wir aber auch daran interessiert sind, was diese letzte Zeile verändert hat, lassen wir das Skript zweimal durch die Schleife laufen. Vorher haben wir bei den überwachten Variablen das Feldelement $vars[„ITEMS“] eingetragen, weil wir sehen wollen, was die letzte Zeile der Schleife bewirkt hat.

Nach dem zweiten Schleifendurchlauf sehen wir dann in $itemvars die Daten des zweiten Rechnungspostens. können aber bereits bei den überwachten Variablen erkennen, dass dort die XML-Daten für den ersten Rechnungsposten eingetragen sind.



Lässt man das Skript weiterlaufen bis es terminiert, sollte am Ende im Spoolverzeichnis eine Datei mit dem Namen Rechnung_471102.pdf als Ergebnis der ganzen Bemühungen auftauchen. Dies ist das PDF/A3-konforme ZUGFeRD-Dokument. Es kann mit einem der gängigen Programme - hier Foxit Reader - geöffnet werden. 6)

Die XML-Daten befinden sich im Anhang des Dokuments und können von da aus auch geöffnet und betrachtet werden.



Test des Skriptes als Film

Hier noch einmal die gerade eben statisch präsentierte Debug-Session als Film, um zum einen noch einmal einen Gesamtüberblick zu geben, aber auch um noch einmal zu demonstieren, was eine Entwicklungsumgebung für eine Erleichterung beim Erstellen von solch anspruchsvollen Anwendungen für print2forms-Gateways darstellt.



Eigene Tests

Bisher erfolgte der ganze Test des Skriptes ohne jedes Zutun eines print2forms-Gateways (abgesehen davon, dass es den Lizenzschlüssel liefern musste). Soll jetzt ZUGFeRD mit eigenen Dokumenten genutzt werden, ist als erstes die Kommandozeile des betreffenden Gateways zu konfigurieren.

Das Skript zugferd.php erwartet zwei Parameter: den Namen des zu bearbeitenden Dokuments und den Lizenzschlüssel. 7)

Demzufolge sieht die Kommandozeile in etwa wie folgt aus:

"C:\PHP7\php.exe" "%1/zugferd.php" "%2\%4" "%K"

Der Pfad zum Aufruf des PHP-Interpreters ist der gleiche Pfad wie der in der Datei lauch.json.

Hinweise

  • Die Vorlage comfort.xml ist gemäss dem ZUGFeRD-Profil 'comfort' erstellt worden, enthält aber auch einige Tags aus dem Profil 'extended'. Insofern kann es sein, dass beim Prüfen der Datei (z.B. unter ZUGFERD Validation.Portal) als Profil 'extended' angezeigt wird.
  • Das Skript zugferd.php macht natürlich eine ganze Reihe von Annahmen, um den Einstieg in das Thema nicht komplizierter zu machen als unbedingt notwendig. Eine der Annahmen ist, dass die Rechnung einseitig ist. Es ist klar, dass die Schleife zum Auslesen der Rechnungsposten auch für Folgeseiten (eventuell etwas modifiziert) ausgeführt werden muss. Eine weitere Annahme ist, dass beim Drucken der Rechnungsposten wirklich sequenziell Zeile für Zeile ausgegeben wird. Das ist manchmal nicht der Fall, weil die Anwendung, die die Rechnung erzeugt z.B. spaltenweise vorgeht. Das ist aber durchaus lösbar.



1)
Die Kodierung der Indexdatei ist ab dem Installer Build 289 immer Unicode in UTF-8. Das passt daher zur Kodierung der ZUGFeRD-XML-Datei.

2)
Der Namespace 'ZUGFeRD::' vor dem Aufruf der Funktion ist notwendig, weil zu diesem Zeitpunkt noch kein Objekt der Klasse instanziiert wurde. Die Fehlerfunktion error ist deshalb statisch.

3)
Es gibt auch Stellen, an denen vier Nachkommastellen erwartet werden - im Ausschnitt aus der items.xml oben bei @ChargeAmount@ - aber das wird dadurch geregelt, dass in der Vorlage einfach zwei Nullen angehängt werden.

4)
Hier im Beispiel wurde die Rechnung nicht (!) durch print2forms aufbereitet. Beim Drucken aus Host-Umgebungen (i5/OS, z/OS, Unix) ist die Ausgangslage meist einfacher, weil meist dicktengleiche Schriften zum Einsatz kommen. Diese bedingen keine Repositionierungen zum Ausgleich von Laufweiten­unterschieden wegen fehlender oder unterschiedlicher Kerning-Tabellen. Wenn die reale Rechnung durch print2forms aus Text- oder SML-Daten aufbereitet wurde, werden ebenfalls keine Neupositionierungen eingebaut.

5)
Beispielsweise wenn Microsoft Office statt Open Office verwendet wird. Das führt wegen der Dokumentkonvertierung meist zu einer geringfügig anderen Indexdatei und das Skript müsste somit erst angepasst werden.

6)
Funktioniert mit Acrobat Reader, Foxit Reader, SumatraPDF und sogar mit dem PDF-Plugin von Firefox. Funktioniert nicht mit der Windows PDF-App und den PDF-Plugins anderer Browser.

7)
Der Lizenzschlüssel wird vom Gateway anhand von Freigaben in der Lizenzdatei der Software Management Konsole errechnet. Für ZUGFeRD ist eine spezielle, kostenpflichtige Freigabe erforderlich.

print2forms/tips/tip69.txt · Zuletzt geändert: 2019-03-08 11:05 (Externe Bearbeitung)