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 convert4print erzeugt werden können.

ZUGFeRD Beispiel

Um mit convert4print aus gewöhnlichen Druckdaten PDF-Dateien zu erzeugen, wird ein Gateway benötigt. Mit einem ganz speziellen Skript und zwei Hilfsprogrammen kann ein convert4print-Gateway auch PDF/A-3-Dateien nach dem ZUGFeRD-Standard erstellen.

In einem ersten Schritt konvertiert ein Hilfsprogramm die Druckdaten (PCL5-Daten) in ein normales PDF-Dokument. Dazu kann wie im Tip PDF- oder TIFF-Dateien erzeugen beschrieben das kommerzielle Programm LincPDF (http://www.lincolnco.com) verwendet werden. 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 Beispiel verwendet.

Im zweiten Schritt wird aus dem Inhalt der Rechnung eine XML-Datei nach dem ZUGFeRD-Schema erzeugt und mit einem weiteren Hilfsprogramm zusammen mit der im ersten Schritt erzeugten PDF-Datei zu einer Datei im Format PDF/A-3 vereinigt.

Das zweite Hilfsprogramm hat den Namen p2fzugferd und ist speziell für diese Anwendung erstellt worden. Es ist in Java geschrieben und benötigt daher eine aktuelle Java Laufzeitumgebung auf dem Rechner, der das convert4print-Gateway ausführt.

Im Folgenden wird anhand eines Beispiels der Aufbau eines convert4print-Gateways gezeigt, das ZUGFeRD-Rechnungen erzeugt. Die Projektdateien können Sie als Archiv herunterladen.



ZUGFeRD XML Vorlagen

Ein zentrales Element einer ZUGFeRD-Rechnung ist die XML-Datei mit den für Maschinen lesbaren Inhalten der Rechnung. Diese XML-Datei hat einen durch die Spezifikation fest vorgegebenen Aufbau. Daher liegt es nahe, dass ein Gerüst dieser Datei quasi als Vorlage zum Ausfüllen durch das Gateway-Skript Teil unserer Lösung ist.

Diese Vorlage-XML-Datei befindet sich im Projektarchiv unter dem Namen in.xml. In dieser Datei finden sich durch @-Zeichen eingeschlossene Texte, die als Platzhalter für Informationen aus der Rechnung dienen. Diese Platzhalter werden vom Gateway-Skript gegen die konkreten Informationen aus der aktuellen Rechnung ersetzt.

Die Platzhalter in der Übersicht:

PlatzhalternameErklärung
DOC_ID Eine eindeutige Kennung der Rechnung (Rechnungsnummer)
DOC_NAME Der Name des Dokuments
DOC_ISSUE_DATE_TIME Datum der Erzeugung des Dokuments (Rechnungsdatum)
PAYMENT_INFO Zahlungsinformationen als unstrukturierter Text
CUSTOMER_REFERENCE Kundenrefferenznummer
POSITIONS Rechnungspositionen als unstrukturierter Text
SELLER_NAME Name des Verkäufers
SELLER_PLZ Postleitzahl des Verkäufers
SELLER_LINE_ONE Straße und Hausnummer des Verkäufers
SELLER_CITY_NAME Stadt des Verkäufers
SELLER_COUNTRY_ID Länderkennung des Verkäufers
SELLER_VA_ID Umsatzsteuernummer des Verkäufers
BUYER_NAME Name des Käufers
BUYER_PLZ Postleitzahl des Käufers
BUYER_LINE_ONE Straße und Hausnummer des Käufers
BUYER_CITY_NAME Stadt des Käufers
BUYER_COUNTRY_ID Länderkennung des Käufers
ACTUAL_DELIVERY_DATE_TIME Datum, an dem die in Rechnung gestellte Leistung erbracht wurde.
CURRENCY_ID Währungskennung der in der Rechnung verwendeten Währung (Das Beispiel unterstützt nur eine einzige Währung in einer einzelnen Rechnung)
VAT_PERCENTAGE Verwendeter Steuersatz (Beispiel unterstützt nur einen Steuersatz in einer einzelnen Rechnung)
TOTAL_AMOUNTGesamtbetrag der Positionen
TAX_BASIS_TOTAL_AMOUNTRechnungssumme ohne USt.
TAX_TOTAL_AMOUNTSteuergesamtbetrag (Umsatzsteuerbetrag)
GRAND_TOTAL_AMOUNTGesamtbetrag der Rechnung (Bruttosumme inkl. USt.)

Der Platzhalter POSITIONS_XML steht nicht innerhalb von irgendwelchen XML-Tags, weil er nicht direkt gegen eine einzelne Information aus der Rechnung ersetzt wird. Der Platzhalter steht für die variabel lange Liste von Rechnungsposition auf der Rechnung.

Der Platzhalter markiert die Position, an der eine weitere XML-Vorlage einkopiert werden muss, die sich in der Datei position.xml befindet.

Bedeutung der Platzhalter:

PlatzhalternameErklärung
UNIT_CODE Zur Mengenangabe gehörige Einheitskennung (z.B. MTK für m²)
QUANTITY Menge der in Rechnung gestellten Position
TOTAL Gesamtpreis der Position
NAME Produktbezeichnung

Für jede Rechnungsposition der Rechnung wird diese Vorlage ausgefüllt. Die ausgefüllten Vorlagen werden dann in die Vorlage in.xml im Austausch gegen den Platzhalter POSITIONS_XML hineinkopiert.

Indexdatei

Die Werte für die obigen Platzhalter müssen nun durch ein Skript aus der von convert4print erzeugten Indexdatei (CTL-Datei) extrahiert werden. Für das oben abgebildete Beispiel könnte dies wie in der example.CTL im Projektarchiv aussehen. Ein Beispielhafter Ausschnitt:

[01016F00C6]Rechnung
[01020000C6]Rechnungsnummer
[01020002DA]471102
[01024500C6]Rechnungsdatum
[01024502DA]05.06.2013
[01028A00C6]Leistungsdatum
[01028A02DA]03.06.2013

Eine solche Indexdatei fasst den Inhalt des Druckdatenstroms in einer Art und Weise zusammen, die eine maschinelle Verarbeitung erleichtert. Jede Zeile ist dabei mit einem Index versehen, der die Zeile eindeutig kennzeichnet. Solange sich die Struktur des gedruckten Formulars nicht ändert bleiben diese Zeilen gleich, sodass man sich in einem Skript auf diese beziehen kann. Wir sehen beispielsweise, dass sich die Umsatzsteuernummer in der Zeile mit dem Index [010F4B043C] befindet.

Gatewaykonfiguration

Wir möchten nun ein PHP-Skript zugferd.php erstellen, das von einem Gateway aufgerufen wird, wann immer es druckt. Dazu stellen wir im convert4print Server die Befehlszeile des Gateways auf:

"$$PHP_PFAD$$" "%1/zugferd.php" "%2\%4" "%K"

Wobei $$PHP_PFARD$$ durch den Pfad zur php.exe ersetzt werden muss.

Der unbedingt erforderliche dritte Parameter „%K“ zeigt schon an, dass für die Nutzung des Programm p2fzugferd ein besonderer Eintrag in der Lizenz für das print2forms-Gateway benötigt wird. Dieser Lizenzschlüssel muss vom Gateway beim Aufruf des Skriptes immer mitgegeben werden.

Die print2forms-Gateways stellen zu diesem Zweck einen verschlüsselten Wert zur Verfügung, der mit dem Platzhalter '%K' in der Kommandozeile des Gateways an das Skript, dass diesen dann an p2fzugferd weitergibt. Eine typische Kommandozeile für ein Gateway sähe etwa so aus:

"C:\Programme\PHP\bin\php.exe" "%1/zugferd.php" "%2\%4" "%K"

Stellen Sie das Skriptverzeichnis des Gateways auf den Ordner, in dem die zugferd.php liegen soll und das Spool-Verzeichnis auf das Verzeichnis, in dem die PCL-, CTL- und PDF Dateien erzeugt werden sollen.

Abhängigkeiten vorbereiten

Platzieren Sie die gpcl6win32.exe und die gpcl6win32.dll aus ghostpcl im Ordner p2fzugferd.

Skript

Im Folgenden wird das Beispielskript aus dem Projektarchiv Schritt für Schritt entwickelt.

Zunächst wird eine Hilfsfunktion erstellt, die zum protokollieren von Fehlern in eine Logdatei verwendet wird:

function error($msg){
	echo $msg."\n";
	file_put_contents("zugferd.log", "[" . date('Y-m-d h:i:s', time()) . "] ". $msg."\n", FILE_APPEND);
	die();
}

Um sicherzustellen, dass das aktuelle Arbeitsverzeichnis auch das Verzeichnis ist, in dem sich die zugferd.php befindet kann folgende Codezeile verwendet werden:

chdir(dirname(__FILE__));

Anschließend wird geprüft, ob das Skript auch mit dem erforderlichen Parameter (siehe Abschnitt Gatewaykonfiguration) gestartet wurde:

if($argc < 2)
  error("started without required params.");

Als nächstes werden Variablen für alle wichtigen Ein- und Ausgabedateien angelegt und geprüft, ob die Eingabedateien existieren.

$xmlpath = "in.xml";
$xmllinepath = "position.xml";
$ctlpath = $argv[1].".CTL";
$pclpath = $argv[1].".PCL";
$zugferdXMLpath = $argv[1].".xml";
$pdfpath = $argv[1].".pdf";
 
if(!file_exists($xmllinepath))
  log_error("$xmllinepath not found.. die()");
 
if(!file_exists($xmlpath))
  log_error("$xmlpath not found.. die()");
 
if(!file_exists($ctlpath))
  log_error("$ctlpath not found.. die()");
 
if(!file_exists($pclpath))
  log_error("$pclpath file not found.. die()");

Außerdem wird der vom convert4print-Gateway erzeugte Lizenzschlüssel eingelesen:

$license = $argv[2];

Nun wird der Inhalt der Eingabedateien eingelesen und eine Variable namens $vars vom Typ Array angelegt. Dieses Array wird später die Platzhalter als Indizes verwenden um die passenden Werte abzulegen.

$xml = file_get_contents($xmlpath);
$ctl = utf8_encode(file_get_contents($ctlpath));
$linexml = utf8_encode(file_get_contents($xmllinepath));
$vars = array();

Nun können mittels eines regulären Ausdrucks alle Indexdatei-Zeilen in Index und Wert zerlegt werden:

preg_match_all("/\[([0-9][0-9a-zA-Z]+)\](.+)/", $ctl, $matches, PREG_SET_ORDER);

$matches ist nun ein Array, in dem für jede Zeile der Indexdatei ein Element vorhanden ist. Jedes dieser Elemente ist wiederum ein Array, in dem in Element 1 der Index und in Element 2 der Wert steht. Das heißt, in $matches[0] liegt ein Array, so dass $matches[0][1] == „01016F00C6“ und $matches[0][2] == „Rechnung“ ist (vergleiche die oben abgebildete CTL-Datei). $matches[1][1] ist dagegen „01020000C6“ und $matches[1][2] == „Rechnungsnummer“.

Nun können wir dieses Array Zeilenweise traversieren und das Platzhalterarray $vars füllen. Dazu benötigen wir allerdings noch einige Hilfsfunktionen.

Diese Funktion übersetzt Einheitenabkürzungen, wie sie in unseren Rechnungen verwendet werden, in eine Darstellung, die ZUGFeRD-konform ist:

function unitToUnitCode($unit){
  $ids = array("m²" => "MTK", "kg" => "KGM");
  return $ids[trim($unit)];
}

Genauso müssen auch Länderbezeichnungen in ZUGFeRD-Codes übersetzt werden:

function countryToCountryID($country){
  $ids = array("Deutschland" => "de");
  return $ids[trim($country)];
}

Diese Übersetzungsfunktionen sind nicht vollständig, da die konkrete Übersetzung Firmenspezifisch ist, ist dies an dieser Stelle auch nicht sinnvoll.

ZUGFeRD Rechnungen verwenden einen Punkt als Dezimaltrennzeichen anstelle eines Kommas. Unsere Beispielrechnung verwendet aber Kommas. Daher benötigen wir auch hier eine Übersetzungsfunktion:

function formatDecimal($currency){
  return str_replace(",", ".", $currency);
}

Beachten Sie, dass ein solch einfaches Ersetzen nur funktioniert, da wir keine Tausendertrennzeichen verwenden. Für Ihre Rechnung müsste diese Funktion also unter Umständen weitere Ersetzungen vornehmen.

Neben dem Dezimaltrennzeichen ist in ZUGFeRD auch die Anzahl an Nachkommastellen festgelegt. Diese Funktion formatiert eine Quanitität entsprechend:

function formatQuantity($quant){
  $quant = explode(" ", trim($quant))[0];
  $quant = formatCurrency(trim($quant));
  $parts = explode(".", $quant);
  for($i = strlen($parts[count($parts)-1]); $i<4; $i++)
    $quant .= "0";
  return $quant;
}

Auch Datumsangaben müssen nach ZUGFeRD-Spezifikation mit vierstelligem Jahr, zweistelligem Monat und zweistelligem Tag (yyyymmdd) umformatiert werden:

function formatDate($key){
  global $vars;
  $date = date_parse($vars[$key]);
  $out = $date["year"];
  $out .= strlen($date["month"]) < 2 ? "0".$date["month"] : $date["month"];
  $out .= strlen($date["day"]) < 2 ? "0".$date["day"] : $date["day"];
  $vars[$key] = $out;
}

Dies waren die verwendeten Hilfsfunktionen zur Formatierung und Umwandlung ins ZUGFeRD-Format. Nun gibt es noch zwei weitere Hilfsfunktionen zum Durchlaufen des Arrays $matches. Die Idee ist, sequenziell durch die Indize zu traversieren. Dazu wird in einer globalen Variablen $i die aktuelle Position in $matches gespeichert.

Die folgende Funktion erlaubt es uns, zu einer bestimmten Zeile in der CTL-Datei voranzuschreiten und den Wert dieser Zeile zurückzuerhalten. Gibt es die angeforderte Zeile in der CTL-Datei nicht oder befindet sich diese vor der Stelle $i, so bricht das Programm mit Fehler ab.

function processGet($lineIndex){
	global $i;
	global $matches;
	while(trim($matches[$i][1]) != trim($lineIndex)){
		$i++;
		if($i>=count($matches))
			error("Failed to find " . $lineIndex);
	}
	return trim($matches[$i][2]);
}

Die zweite Hilfsfunktion erweitert die Erste indem ihr neben der zu suchenden Zeile auch der zugehörige Platzhalter und (optional) ein Typ (DATE_FIELD, CURRENCY_FIELD, STRING_FIELD) mitgegeben werden kann, so dass der Wert dieser Zeile direkt Formatiert (also an die passende bereits vorgestellte Hilfsfunktion gegeben wird) und in $vars an die richtige Stelle geschrieben wird:

function process($lineIndex, $targetKey, $fieldType = STRING_FIELD){
  global $vars;
  $vars[$targetKey] = processGet($lineIndex);
  if($fieldType == CURRENCY_FIELD)
    $vars[$targetKey] = formatCurrency($vars[$targetKey]);
  else if($fieldType == DATE_FIELD)
    formatDate($targetKey);
  return $vars[$targetKey];
}

Nun definieren wir noch die in den Rechnungen zu verwendende Währung als „EUR“ für Euro. Verwenden sie verschiedene Währungen in verschiedenen Rechnungen, kann dies auch aus dem Dokument extrahiert werden.

$vars["CURRENCY_ID"] = "EUR";

Dies waren alle Hilfsfunktionen. Nun können wir process() verwenden, um z.B. Den Wert aus Zeile „01016F00C6“ dem Paltzhalter @DOC_NAME@ und Zeile „01020002DA“ @DOC_ID@ zuzuordnen:

process("01016F00C6","DOC_NAME");
process("01020002DA", "DOC_ID");

Wichtig ist, dass Zeile „01016F00C6“ in der CTL-Datei oberhalb von Zeile „01020002DA“ liegt. Bei den folgenden Zeilen wird der optionale Parameter von process() verwendet, der angibt, dass der Wert noch formatiert werden muss. In diesem Fall als Datum:

process("01024502DA", "DOC_ISSUE_DATE_TIME", DATE_FIELD);
process("01028A02DA", "ACTUAL_DELIVERY_DATE_TIME", DATE_FIELD);

Im folgenden Block müssen mehrere Zeilen für einen Platzhalter zusammengesetzt und dann manuell in das Platzhalterarray $vars geschrieben werden. Dazu kann processGet() statt process() verwendet werden, da processGet() den Wert der Zeile nur zurückgibt ohne diesen zu formatieren und in $vars zu schreiben:

$vars["CUSTOMER_REFERENCE"] = processGet("010200057B") . ": " 
	. processGet("010200078F") . "\n" . processGet("010245057B") . ": ";
$vars["CUSTOMER_ID"] = processGet("010245078F");
$vars["CUSTOMER_REFERENCE"] .= $vars["CUSTOMER_ID"];

Die nächsten Platzhalter sind wieder einfach:

process("01039E00C6", "SELLER_NAME");
process("0103E300C6", "SELLER_LINE_ONE");

In Zeile „01042800C6“ stehen sowohl PLZ als auch Ort, diese müssen auseinander genommen werden und „händisch“ in $vars geschrieben werden:

$plzcity = explode(" ", processGet("01042800C6"));
$vars["SELLER_PLZ"] = $plzcity[0];
$vars["SELLER_CITY_NAME"] = $plzcity[1];

Das Land des Verkäufers muss mit countryToCountryID() ZUGFeRD kompatibel übersetzt werden:

$sellerCountry = processGet("01046D00C6");
$vars["SELLER_COUNTRY_ID"] = countryToCountryID($sellerCountry);

Genau wie die Verkäuferdaten werden auch die Käuferdaten ausgelesen. Nur die Zeilenindizes verändern sich:

process("01039E057B", "BUYER_NAME");
process("0103E3057B", "BUYER_LINE_ONE");
$plzcity = explode(" ", processGet("010428057B"));
$vars["BUYER_PLZ"] = $plzcity[0];
$vars["BUYER_CITY_NAME"] = $plzcity[1];
$buyerCountry = processGet("01046D057B");
$vars["BUYER_COUNTRY_ID"] = countryToCountryID($buyerCountry);

Zusätzlich zur strukturierten Angabe der Rechnungspositionen als XML geben wir diese noch als Freitext an. Dazu konstruieren wir nun die Kopfzeile:

$positions = processGet("01054000DC") . " " . processGet("01057A00EE") . "\t" .
  processGet("010540017D") . " " . processGet("01057A017F") . "\t" .
  processGet("0105400285") . " " . processGet("01057A0262") . "\t" .
  processGet("01057A046B") . "\t" .
  processGet("0105400717") . processGet("01057A070A") . "\t" .
  processGet("01057A07DE") . "\t" .
  processGet("01057A08B3") . " " . processGet("010540098A") . " " . processGet("01057A0985");

Anschließend folgen die Positionsdaten. Da es eine beliebige Anzahl an Positionen geben kann, müssen diese mit einer Schleife durchlaufen werden. Kopieren wir daher zunächst das XML für die Positionen in eine neue Variable:

$line = $linexml;

In diese Variable werden wir nach jeder verarbeiteten Position wieder die Vorlage laden, um diese dann mit neuen Daten zu füllen.

Desweiteren brauchen wir eine Variable, in der wir die gefundenen Platzhalterwerte für diese Position ablegen können:

$keys = array("CURRENCY_ID" => $vars["CURRENCY_ID"]);

Der Wert für die Währung wurde bereits an anderer Stelle ermittelt und daher hier direkt in das Array eingesetzt.

Nun benötigen wir eine weitere Variable, in der wir das gesamte Postions-XML konkatenieren können:

$positionmxml = "";

Die Positionsdaten bestehen aus 8 Daten. Wir führen also einen Zähler ein:

$j = 0;

Abhängig von dem Wert dieses Zählers wissen wir dann, um welches Datum unserer Position es sich gerade handelt.

Wir wissen, dass sich das erste Datum, das nicht mehr zu den Positionsdaten gehört in Zeile „010D2200C6“ befindet. Wir können daher eine Schleife formulieren, die so lange Zeilenweise läuft, bis wir diese Zeile erreicht haben:

while($matches[++$i][1] != "010D2200C6"){
  // TODO...
}

Innerhalb dieser Schleife erhöhen wir nun $j und bestimmen den Modulo dieses neuen Wertes zu 8. Der Modulo zweier Zahlen a und b ist so definiert, dass er den Rest der Division von a durch b zurück gibt. Dadurch wissen wir, bei welchem Element einer Position wir gerade sind. Gibt der Modulo 0 zurück, so sind wir beim ersten Element. Gibt er eins zurück, sind wir beim zweiten Element. Sobald er sieben zurück gibt, sind wir beim letzten Elemente und der nächste Schleifendurchlauf wird wieder 0 zurückgeben. Dadurch können wir nun eine einfache Fallunterscheidung machen:

while($matches[++$i][1] != "010D2200C6"){
	$mod = $j++ % 8;
	switch($mod){
		case 0: $positions .= "\n" . trim($matches[$i][2]) . "\t"; break;
		case 1: $keys["NAME"] = trim($matches[$i][2]); $keys["ID"] = $keys["NAME"]; $positions .= trim($matches[$i][2]) . "\t"; break;
		case 2: $keys["NAME"] .= " " . trim($matches[$i][2]); $positions .= trim($matches[$i][2]) . "\t";break;
		case 3: $keys["NAME"] .= " " . trim($matches[$i][2]); $positions .= trim($matches[$i][2]) . "\t";break;
		case 4: $positions .= trim($matches[$i][2]) . "\t"; break;
		case 5: $keys["UNIT_CODE"] = unitToUnitCode($matches[$i][2]); $positions .= trim($matches[$i][2]) . "\t";break;
		case 6: $keys["QUANTITY"] = formatQuantity(trim($matches[$i][2])); $positions .= trim($matches[$i][2]) . "\t";break;
		case 7: $keys["TOTAL"] =  formatCurrency(trim($matches[$i][2]));
      $positions .= trim($matches[$i][2]);
			//replace placeholder in template
			foreach ($keys as $key => $value){
			 	$line = str_replace( "@$key@" , trim($value) , $line );
			}
			$positionmxml .= $line;
			$keys = array("CURRENCY_ID" => $vars["CURRENCY_ID"]);
			$line = $linexml;
			break;
		default: break;
	}
}

Case 0 ist nur für die textuelle Positionsbeschreibung interessant, da Positionsnummern für ZUGFeRD keine Relevanz haben. Case 1-3 bauen den Positionsnamen aus mehreren Zeilen zusammen, case 4 ist wieder nur für die Textbeschreibung wichtig, Case 5 beschreibt die Einheit in der das Produkt der Position gemessen wird, Case 6 die Quantität. Interessant wird es in Case 7: Hier ist der Gesamtpreis der Position abgelegt. Vor allem ist es aber auch der letzte Wert der Position und beinhaltet daher die Verarbeitung der Position als Gesamtes:

foreach ($keys as $key => $value){
	$line = str_replace( "@$key@" , trim($value) , $line );
}

Dieser Teil von Case 7 geht das gesamte $keys-Array (also alle Positionsplatzhalter) durch und ersetzt alle Vorkomnisse dieser Platzhalter im in $line abgelegten XML durch die entsprechenden Werte. Das Ergebnis wird dann an das Gesamtpositionsxml $positionxml angehängt und $line für die nächste Position auf die XML-Vorlage zurückgesetzt.

Nachdem die Schleife durchgelaufen ist, steht in $positionxml das ZUGFeRD-XML zu den Positionen und in $positions die textuelle Beschreibung der Positionen. Diese werden nun noch den entsprechenden Platzhaltern im $vars-Array zugewiesen:

$vars["POSITIONS_XML"] = $positionmxml;
$vars["POSITIONS"] = $positions;

Nachdem der schwierigste Teil nun hinter uns liegt, werden noch verschiedene weitere Platzhalter aus der CTL in $vars extrahiert, wobei an einigen Stellen manche Werte noch mittels explode() von EUR- oder %-Suffixen befreit werden:

process("010D220864", "TOTAL_AMOUNT", CURRENCY_FIELD);
$vars["TOTAL_AMOUNT"] = trim(explode(" ",$vars["TOTAL_AMOUNT"])[0]);
$vars["TAX_BASIS_TOTAL_AMOUNT"] = formatCurrency($vars["TOTAL_AMOUNT"]);
$vars["TAX_BASIS_TOTAL_AMOUNT"] = trim(explode(" ",$vars["TAX_BASIS_TOTAL_AMOUNT"])[0]);
$vatLine = processGet("010D6700C6");
$vars["VAT_PERCENTAGE"] = substr(explode(" ", $vatLine)[1], 0, -1) . ".00"; // extract vat percentage, then cut % char
process("010D670864", "TAX_TOTAL_AMOUNT", CURRENCY_FIELD);
$vars["TAX_TOTAL_AMOUNT"] = trim(explode(" ",$vars["TAX_TOTAL_AMOUNT"])[0]);
process("010DAC0864", "GRAND_TOTAL_AMOUNT", CURRENCY_FIELD);
$vars["GRAND_TOTAL_AMOUNT"] = trim(explode(" ",$vars["GRAND_TOTAL_AMOUNT"])[0]);

Der Rest der CTL Datei sind Zahlungsdaten, die alle im Platzhalter „PAYMENT_INFO“ gesammelt werden und daher mit einer Schleife verarbeitet werden. Zusätzlich steht in den Zahlungsdaten auch in Zeile „010F4B0229“ die Umsatzsteuernummer, die sowohl in „PAYMENT_INFO“ als auch in den Platzhalter „SELLER_VA_ID“ gehört. Die Schleife beinhaltet also eine Prüfung auf diese Zeile und setzt dann diesen Platzhalter.

$vars["PAYMENT_INFO"] = "";
while(++$i < count($matches)){
	$vars["PAYMENT_INFO"] .= $matches[$i][2] . "\n";
	if($matches[$i][1] == "010F4B0229"){
		$vars["SELLER_VA_ID"] = $matches[$i][2];
	}
}

Nun müssen noch wie bei den Positionsdaten die Platzhalterersetzungen in der XML-Vorlage vorgenommen werden:

foreach ($vars as $key => $value){
	$xml = str_replace( "@$key@" , trim($value) , $xml );
}

In $xml steht nun der fertige Inhalt für die XML-Datei, die wir mit folgendem Befehl unter gleichem Namen wie die PCL- und CTL- Datei aber mit der Endung .xml auf die Festplatte schreiben:

file_put_contents($zugferdXMLpath, $xml);

Zum Schluss wird nur noch p2fzugferd aufgerufen, um die PCL-Datei mit ghostpcl nach PDF/A-3 zu konvertieren und dann die erzeugte ZUGFeRD-XML-Datei anzuhängen:

system("java -jar \"p2fzugferd\p2fzugferd.jar\" --inpcl \"$pclpath\" --gpdl \"p2fzugferd\gpcl6win32.exe\" --xml \"$zugferdXMLpath\" --outpdf \"$pdfpath\" --key \"$license\"");

Sollten Sie statt ghostpcl eine andere Software wie etwa LincolnPDF verwenden wollen, müssen Sie diese zunächst mit system() und den entsprechenden Parametern aufrufen, um aus der PCL-Datei eine PDF/A Datei zu machen. Schreiben Sie den Pfad zu der erzeugten PDF-Datei in die Variable $inpdfpath und rufen Sie anschließend p2fzugferd wie folgt auf:

system("java -jar \"p2fzugferd\p2fzugferd.jar\" --inpdf \"$inpdfpath\" --xml \"$zugferdXMLpath\" --outpdf \"$pdfpath\" --key \"$key\"");

Zum Schluss können nun noch unbenötigte Dateien gelöscht werden:

unlink($zugferdXMLpath);
unlink($pclpath);
unlink($ctlpath);


Hinweis