===== factXrech-class.php ===== Die Klasse **factXrech-class.php** basiert im Prinzip auf der Klasse [[print2forms:tips:tip96|control-class.php]]. Sie wurde aber ganz bewusst nicht als Erweiterung formuliert, sondern ist für den Anwendungsfall **XRechnung** und **Factur-X** als ein **Ersatz** für die //control-class.php// zu verstehen. Zu viele der Methoden unterscheiden sich in Details. Als Motivation für die Einführung der Klasse können noch einmal kurz die ersten Kapitel der Beschreibung der Klasse [[print2forms:tips:tip96|control-class.php]] gelesen werden. \\ \\ ===== Quellcode ===== Die im Quellcode nachfolgend dargestellte Klasse //factXrech-class// übernimmt eine Reihe von immer wiederkehrenden Aufgaben im Umgang mit Kontrolldateien. Als ersten Überblick sind dies: * Einlesen der Kontrolldatei in eine interne Struktur für den schnellen, und wahlfreien Zugriff auf die Inhalte der Kontrolldatei. Methoden für den wahlfreien Zugriff auf einzelne Zeilen, sowie Methoden für den sequenziellen Zugriff auf Zeilen. * Methoden zur Extraktion von Einzelinformationen via regulärer Ausdrücke aus der Kontrolldatei, die dann abgespeichert werden können. Zusätzlich zur Klasse //control-class// kann dabei der extrahierte Text noch in spezielle Formate für Factur-X oder XRechnung konvertiert werden. * Methoden zur Konvertierung von Preisen, Mengenangaben und Einheiten in die vorgegebenen Formate für Factur-X und XRechnung. * Eine Methode, um in einer %%XML%%-Vorlage die Platzhalter (durch '@' eingeklammert) gegen aufgesammelte Daten aus der Kontrolldatei auszutauschen. * Eine Methode, um aus einer Vorlage eine XML-Datei zu erzeugen und diese auch gleich zu validieren. ((Dies erfolgt mit dem frei verfügbaren [[https://github.com/itplr-kosit/validator|KoSIT Validator]]. Voraussetzung dafür ist allerdings das Vorhandensein einer Java-Laufzeitumgebung auf dem ausführenden Rechner. Der KoSIT Validator ist lokal installiert, das heisst, die %%XML%%-Datei verlässt nicht (!) den lokalen Rechner! Die Ausführungszeiten des Validators bewegen sich im Bereich von vielen Sekunden, weil viele zeitaufwändige Transformationen von %%XML%%-Dateien mittels XSLT durchgeführt werden. Das senkt den möglichen Durchsatz des Gateways erheblich!\\ \\ )) * Eine Methode, um aus der vom Gateway ebenfalls erzeugten %%PCL%%-Datei eine %%PDF%%-Datei zu erstellen. Dabei können auch die Meta-Daten für das %%PDF%%-Dokument (Autor, Titel, Schlüsselworte, etc) gesetzt werden. Zusätzlich wird die zuvor erzeugte %%XML%%-Datei mit in das %%PDF%%-Dokument mit eingebunden. * Anlegen und Fortschreiben einer Log-Datei mit Fehlermeldungen, um für den Supportfall nachvollziehen zu können, ob bei der Abarbeitung von Skripten Probleme aufgetreten sind. Das Skript sollte vorzugsweise über diesen Link {{print2forms:tips:factXrech-class.zip|factXrech-class.zip}} geladen werden, weil zum einen beim Laden über den Reiter des Quellcodes die Unicode-BOM verlorengeht. ((Für die korrekte Funktion der Skripte ist es unumgänglich, dass Unicode zum Einsatz kommt. Die Kontrolldatei ist eine in UTF-8 kodierte Datei und somit müssen auch die zur Verarbeitung genutzten Skripte Unicode unterstützen. Ansonsten gibt es erhebliche Probleme mit länderspezifischen Zeichen.\\ \\ )) Zum anderen benötigt die Klasse für ihre korrekte Funktion aber noch eine ganze Reihe weiterer Dateien, die in dem Archiv mit enthalten sind. Bevor nun die einzelnen Methoden beschrieben werden, hier die vollständige Klasse: \\ \\ error ("'$ctlPath' not found", 1); if (($type == "F") || ($type == "X")) $this->type = $type; /* remember type of invoice */ else { $this->error ("Illegal type of invoice - using Factur-X", 0); $this->type = "F"; } $path = ($this->type == "F") ? "Factur-X" : "XRechnung"; $this->xmlInvoice = file_get_contents ("xml/$path/invoice.xml"); $this->xmlItems = file_get_contents ("xml/$path/item.xml"); if (($this->xmlInvoice === false) || ($this->xmlItems === false)) $this->error ("Can't read required xml templates", 1); $ctl = file_get_contents ($ctlPath); /* ctl file coded in UTF-8 */ preg_match_all ("/\[([0-9a-zA-Z]{3,10})\](.+)\r/u", $ctl, $this->matches, PREG_SET_ORDER); $this->xmlFile = "converter/" . (($this->type == "F") ? "factur-x.xml" : "xrechnung.xml"); } public function __destruct () { if (file_exists ($this->xmlFile)) unlink ($this->xmlFile); if (file_exists ("temp.pdf")) unlink ("temp.pdf"); if (file_exists ("invoice.pdf")) unlink ("invoice.pdf"); } /*----------------------------------------------------------------------------*/ public function xmlfile () /* name of xml file to be used */ { return $this->xmlFile; /* subdir needed for ghostscript */ } /*----------------------------------------------------------------------------*/ public function error ($msg, $severity = 0) { file_put_contents ("factxrech.log", "[" . date ('Y-m-d H:i:s', time ()) . "] " . $msg . "\n", FILE_APPEND); if ($severity != 0) die (); $this->errorCount++; } /*----------------------------------------------------------------------------*/ public function errorsFound () { return $this->errorCount; } /*----------------------------------------------------------------------------*/ private function readMapFile ($filename) { $result = array (); $handle = fopen ($filename, "r"); if ($handle) { while (($line = fgets ($handle)) !== false) { $parts = explode (":", $line); if (count ($parts) == 2) $result [trim ($parts [0])] = trim ($parts [1]); } fclose ($handle); } else $this->error ("could not open " . $filename, 1); return $result; } /*----------------------------------------------------------------------------*/ private function unitToUnitCode ($unit) { if ($this->unitCodeMap == null) $this->unitCodeMap = $this->readMapFile("mappings/unitCodes.txt"); return $this->unitCodeMap [$unit]; } /*----------------------------------------------------------------------------*/ private function countryToCountryID ($country) { if ($this->countryCodeMap == null) $this->countryCodeMap = $this->readMapFile("mappings/countryCodes.txt"); return $this->countryCodeMap [$country]; } /*----------------------------------------------------------------------------*/ private function langToLangID ($language) { if ($this->languageCodeMap == null) $this->languageCodeMap = $this->readMapFile("mappings/languageCodes.txt"); return $this->languageCodeMap [$language]; } /*----------------------------------------------------------------------------*/ public function formatDate ($datestring) { $d = trim ($datestring); if (preg_match ('/(\d?\d)\.(\d?\d)\.(\d\d)/', $d, $match)) if ($this->type == "X") return sprintf ("20%02d-%02d-%02d", $match [3], $match [2], $match [1]); else return sprintf ("20%02d%02d%02d", $match [3], $match [2], $match [1]); $this->error ("Illegal date string '$datestring'", 0); return "0000.01.01"; /* not a date */ } /*----------------------------------------------------------------------------*/ public function formatQuantity ($quant) { $q = trim ($quant); $q = str_replace ('.', '', $q); /* remove thousands */ $q = str_replace (',', '.', $q); /* convert fraction */ if (preg_match ('/[-\.0-9]+/u', $q)) /* is numeric */ { if (strpos ($q, "-") != false) /* negative */ return sprintf ("-%01.2f", abs ((float) $q)); return sprintf ("%01.2f", (float) $q); } $this->error ("Illegal quantity string '$quant'", 0); return "0.00"; /* not a number */ } /*----------------------------------------------------------------------------*/ public function formatCurrency ($currency) { $c = trim ($currency); $c = str_replace ('.', '', $c); /* remove thousands */ $c = str_replace (',', '.', $c); /* convert fraction */ if (preg_match ('/[-\.0-9]+/u', $c)) /* is numeric */ { if (strpos ($c, "-") != false) /* negative */ return sprintf ("-%01.2f", abs ((float) $c)); return sprintf ("%01.2f", (float) $c); } $this->error ("Illegal currency string '$currency'", 0); return "0.00"; /* not a number */ } /*----------------------------------------------------------------------------*/ public function getIndex () { return $this->matches [$this->index] [1]; } /*----------------------------------------------------------------------------*/ public function setIndex ($lineIndex) { for ($idx = 0; $idx < count ($this->matches); $idx++) if ($this->matches[$idx][1] == $lineIndex) break; if ($idx == count ($this->matches)) return ""; $this->index = $idx; return trim ($this->matches [$idx] [2]); } /*----------------------------------------------------------------------------*/ public function getLine ($lineIndex) { $line = ""; do { for ($idx = 0; $idx < count ($this->matches); $idx++) if ($this->matches [$idx] [1] == $lineIndex) break; if ($idx == count ($this->matches)) return ""; $lineIndex = $this->matches [$idx + 1] [1]; $lineidx = substr ($this->matches [$idx] [1], 0, 6); $line .= $this->matches [$idx] [2]; } while (substr ($lineIndex, 0, 6) == $lineidx); return $line; } /*----------------------------------------------------------------------------*/ public function fetch ($lineIndex, &$target, $regEx, $idx, $fieldType = self::STRING_FIELD) { $line = ($lineIndex == "") ? $this->matches [$this->index++] [2] /* current line */ : $this->setIndex ($lineIndex); /* requested line */ if (preg_match ("/" . $regEx . "/u", $line, $m) == 1) switch ($fieldType) { case self::CURRENCY_FIELD: $target [$idx] = $this->formatCurrency ($m [1]); break; case self::DATE_FIELD: $target [$idx] = $this->formatDate ($m [1]); break; case self::LANGUAGE_FIELD: $target [$idx] = $this->langToLangID ($m [1]); break; case self::COUNTRY_FIELD: $target [$idx] = $this->countryToCountryID ($m [1]); break; case self::UNIT_FIELD: $target [$idx] = $this->unitToUnitCode ($m [1]); break; case self::QUANTITY_FIELD: $target [$idx] = $this->formatQuantity ($m [1]); break; case self::STRING_FIELD: $target [$idx] = $m [1]; break; case self::MAIL_FIELD: if (filter_var ($m [1], FILTER_VALIDATE_EMAIL)) $target [$idx] = $m [1]; else $target [$idx] = "?"; break; default: $target [$idx] = ""; } else $target [$idx] = ""; } /*----------------------------------------------------------------------------*/ public function substitute ($tmpl, $vars) { switch ($tmpl) { case self::INVOICE_TEMPLATE: $line = $this->xmlInvoice; break; case self::ITEM_TEMPLATE: $line = $this->xmlItems; break; default: $line = ""; break; } /* Replace placeholders in template, uppercase $key is already xml */ foreach ($vars as $key => $value) { if ($key != strtoupper ($key)) /* mixed case name */ $value = htmlentities ($value, ENT_XML1, "UTF-8"); $line = str_replace( "@$key@" , $value , $line); } /* Remove empty lines forced by including other xml-file content */ return str_replace ("\r\n\r\n", "\r\n", $line); } /*----------------------------------------------------------------------------*/ public function createXML ($xml, $validate) { /* Write XML file with all the XRechnung data */ file_put_contents ($this->xmlfile (), $xml); if ($validate == 1) /* validation of xml requested */ { exec ("java -jar validator/validationtool-1.5.0-standalone.jar " . "-r validator -s validator/scenarios.xml " . "-h " . $this->xmlfile () . " -o .", $output, $result); file_put_contents ("validation.txt", implode ("\r\n", $output)); if ($this->type == "F") unlink ("factur-x-report.xml"); else unlink ("xrechnung-report.xml"); if ($result != 0) /* validator returns number of invalid invoices */ { $this->error ("Rechnung kann nicht validiert werden", 0); return 0; } } return 1; /* success with writing xml file (including validation) */ } /*----------------------------------------------------------------------------*/ public function createPDF ($pcl, $company, $reference, $keywords) { if (! file_exists ($pcl)) $this->error ("'$pcl' not found", 1); /* Convert PCL file of invoice to PDF/A3 format using GhostPCL */ system ("\"converter/gpcl6win64.exe\" \"-otemp.pdf\" " . "-sPDFCompatibillityPolicy=2 -sDEVICE=pdfwrite -dPDFA=3 " . "-dCompressPages=false -dCompressFonts=false \"$pcl\""); /* Write postscript file to setup creation time of xml document and meta data for pdf file creation */ file_put_contents ("converter/date.ps", "%!PS\n/InvoiceDate (D:" . date ('YmdHis') . substr (date ('P'), 0, 3) . "'00') def\n" . "[ /Title ($company - $reference)\n" . " /Author ($company)\n" . " /Subject (Elektronische Rechnung im Format " . (($this->type == "F") ? "Factur-X \(ZUGFeRD 2.2\)" : "X-Rechnung") . ")\n" . " /Keywords ($keywords)\n" . " /Creator (print2forms)\n" . " /DOCINFO\n" . "pdfmark\n" . "%%EOF"); /* Convert PDF/A3 to ZUGFeRD format using GhostScript */ $internal = "invoice.pdf"; system ("converter\gswin64.exe " . "--permit-file-read=converter/ " . "-sDEVICE=pdfwrite " . "-dPDFA=3 " . "-sColorConversionStrategy=RGB " . "-sZUGFeRDXMLFile=" . $this->xmlfile () . " " . "-sZUGFeRDProfile=converter/iccprofiles_default_rgb.icc " . "-sZUGFeRDVersion=2p1 " . "-sZUGFeRDConformanceLevel=" . (($this->type == "F") ? "ZUGFeRD" : "XRECHNUNG") . " " . "-o $internal " . "temp.pdf " . "converter/date.ps " . "converter/embedding.ps"); return $internal; /* internal name of the pdf file as result */ } } ?> \\ ==== Konstruktor ==== Der Konstruktor der Klasse benötigt als Parameter drei Werte. Zuerst muss festgelegt werden, ob eine Rechnung im Format Factur-X oder im Format XRechnung erzeugt werden soll. Dazu sind zwei Konstanten definiert: //factXrech::FACTURX// und //factXrech::XRECHNUNG//. Als zweiter Parameter wird eine in der jeweiligen %%PHP%%-Installation bekannte Zeitzone angegeben. Die korrekte Zeitzone wird für die Log-Datei benötigt. Falls Fehler auftreten, ist so sichergestellt, dass die in der Log-Datei angegebenen Zeitstempel tatsächlich in der lokalen Zeit sind. Ausserdem werden so korrekte Zeitstempel für das Einbetten der %%XML%%-Datei in die %%PDF%%-Datei garantiert. Als letzten Parameter benötigt der Konstruktor die Kontrolldatei inklusive absolutem Zugriffspfad. Es wird überprüft, ob der Zugriff auf die Kontrolldatei gegeben ist, und wenn ja, wird die Kontrolldatei komplett in einer internen Struktur abgelegt. Alle Methoden der Klasse greifen nur auf diese interne Struktur zu. Es erfolgt kein weiterer Zugriff auf die eigentliche Kontrolldatei. Der Konstruktor liest dann auch gleich die nötigen Vorlagen für die %%XML%%-Dateien ein. $fxr = new factXrech (factXrech::XRECHNUNG, "Europe/Berlin", "$spool\\$document.ctl"); \\ ==== Destruktor ==== Hier werden eventuell angelegte Zwischendatei wieder entfernt. Falls es während der Inbetriebnahme eines Skripts hilfreich ist, die Zwischendateien zu analysieren, müssen die betreffenden Zeilen des Destruktors gegebenenfalls auskommentiert werden. Der Destruktor wird implizit beim Ende des Skriptes aufgerufen. Wahlweise kann das auch explizit erfolgen. $fxr = null; \\ ==== error ==== Die Methode akzeptiert als Parameter eine Zeichenkette, die in die Log-Datei geschrieben wird. Der optionale zweite Parameter sorgt dafür, dass die Ausführung des Skriptes beendet wird, wenn er einen Wert ungleich Null hat. Damit kann bei besonders schwerwiegenden, irreparablen Fehlern (z.B. Fehlen der Kontrolldatei) sofort abgebrochen werden, ohne das jedesmal ausprogrammieren zu müssen. if ($em->sendmail ($data, $message) == 0) $fxr->error ("Sending e-mail failed '$message'"); \\ ==== errorsFound ==== Gibt einen internen Fehlerzähler zurück. der mit jeder Instanziierung eines Objekts der Klasse zurückgesetzt wurde. Wird hier ein Wert ungleich Null zurückgeliefert, ist im bisherigen Verlauf des Skriptes zumindest einmal eine Fehlermeldung in die Log-Datei geschrieben worden. if ($fxr->errorsFound () != 0) exit (-1); \\ ==== setIndex ==== Die Methode setzt einen internen Positionsindex auf den als Parameter übergebenen Wert. Der Parameter muss eine zehnstellig Hexadezimalzahl sein. Dies ist die Vorbereitung für nachfolgende Aufrufe der Methode //fetch// ohne Positionsindex. $fxr->setIndex ("01063100FD"); /* position to start with item table */ while ($fxr->getIndex () != "010D1A00C6") /* loop through list of items */ { /* Process list of invoice items */ $fxr->fetch ("", $vars, "(\d+)", "LineID"); $fxr->fetch ("", $vars, "(.*)", "SellerAssignedID"); /* ... */ } \\ ==== getIndex ==== Die Methode gibt den aktuellen Wert eines internen Positionsindex als zehnstellig Hexadezimalzahl zurück. In der Regel wird damit kontrolliert, ob die Methode //fetch// bereits das Ende des zu lesenden Bereichs erreicht hat. \\ \\ ==== fetch ==== Die Methode braucht als Parameter zunächst den zu suchenden Positionsindex als zehnstellige Hexadezimalzahl. Zur Speicherung des extrahierten Textes wird danach die Adresse eines Arrays übergeben. Ein regulärer Ausdruck sucht innerhalb des Textes an dieser Position die gewünschte Information ((Der reguläre Ausdruck sollte nur eine Klammer zur Extraktion von Texten enthalten. Zusätzliche Klammern werden ignoriert. Die Angabe von Zeilenanfang **'^'** oder Zeilenende **'$'** ist zulässig. Der Ausdruck wird immer als Unicode ausgewertet.\\ \\ )). Der letzte Parameter definiert einen Namen, unter dem der extrahierte Text im Array abgespeichert wird. Wird als Parameter für den Positionsindex eine leere Zeichenkette angegeben, wird der interne Positionsindex als Referenz genommen. Gleichzeitig wird der interne Positionsindex auf die nächste Zeile innerhalb der Kontrolldatei weitergeschoben. Damit kann die Kontrolldatei quasi sequentiell bearbeitet werden. ((Siehe das Beispiel für die Methode //setIndex// weiter oben.\\ \\ )) Als optionaler fünfter Parameter kann eine spezielle Formatierung des extrahierten Textes angefordert werden. Im Normalfall wird der Text unverändert abgespeichert. Wird einer der vordefinierten Werte //factXrech::DATE_FIELD//, //factXrech::CURRENCY_FIELD//, //factXrech::STRING_FIELD// ((Das ist der Standardwert, bei dem der Text einfach übernommen wird.\\ \\ )), //factXrech::COUNTRY_FIELD//, //factXrech::LANGUAGE_FIELD//, //factXrech::UNIT_FIELD//, //factXrech::QUANTITY_FIELD// oder //factXrech::MAIL_FIELD// angegeben, erfolgt eine gemäss den Spezifikationen von Factur-X oder XRechnung entsprechende Formatierung. $fxr->fetch ("0101720846", $vars, " +(\d\d\.\d\d\.\d{4})", "InvoiceDate", factXrech::DATE_FIELD); \\ ==== getLine ==== Die Methode liefert als Resultat die Zeichenkette am als Parameter übergebenen Positionsindex. Eine Besonderheit von //getLine// ist, dass tatsächlich eine Zeile zurückgegeben wird. Das wird dadurch erreicht, dass die Methode alle Positionen mit gleichlautendem vertikalen Wert aufsammelt und zu einer Zeile verbindet. ((Dabei werden die Zeilenfragmente in der Reihenfolge innerhalb der Kontrolldatei aneinandergefügt, was unter bestimmten Umständen nicht unbedingt der Reihenfolge im Druckbild entspricht!\\ \\ )) ... [010E7300C6]innerhalb 30 Tag [010E730281]en netto bis 04.07.2018, innerhalb 14 Tagen 2% Skonto bis 19.06.2018 ... $vars ["PaymentTerms"] = $fxr->getLine ("010E7300C6"); \\ ==== formatDate ==== Die Methode akzeptiert eine Zeichenkette, in der sich ein Datum befindet. Dieses liegt in Form von ein- oder zweistelligen Zahlen für die Tage und den Monat, sowie einer zweistelligen Jahresangabe vor, jeweils durch Punkte voneinander getrennt. Die Jahresangabe wird mit einer vorgestellten '20' ergänzt. Ist das im konkreten Anwendungsfall aufgrund der Daten aus der Kontrolldatei nicht passend, muss der reguläre Ausdruck eventuell angepasst werden. $vars ["DueDate"] = $fxr->formatDate (substr ($vars ["PaymentTerms"], 29, 10)); \\ ==== formatQuantity ==== Die Methode akzeptiert eine Zeichenkette, in der sich ein Mengenangabe befindet. Eine valide Mengenangabe ist eine Zahl mit oder ohne Nachkommastellen. Optional kann die Zahl auch Tausendertrennungen in Form von Punkten beinhalten. Die Methode ist so programmiert, dass sowohl ein voranstehendes Minus als auch nachfolgendes Minus erkannt werden, und dann eine negative Zahl als Resultat erzeugen. $vars ["InvoicedQuantity"] = $fxr->formatQuantity ($quant); \\ ==== formatCurrency ==== Die Methode akzeptiert eine Zeichenkette, in der sich eine Zahl befindet, die in einer Währung angegeben ist. Eine valide Währungsangabe ist eine Zahl mit oder ohne Nachkommastellen. Optional kann die Zahl auch Tausendertrennungen in Form von Punkten beinhalten. Die Methode ist so programmiert, dass sowohl ein voranstehendes Minus als auch nachfolgendes Minus erkannt werden, und dann eine negative Zahl als Resultat erzeugen. $vars ["PriceAmount"] = $fxr->formatCurrency ($price); \\ ==== substitute ==== Die Methode sucht in einer der vorgegebenen Vorlagen für die %%XML%%-Daten nach den Bezeichnern aus einer Liste von extrahierten Texten. Als erster Parameter wird die Vorlage ausgewählt. Es stehen dafür zwei Konstanten zur Verfügung: //factxrech::INVOICE_TEMPLATE// für die Vorlage der gesamten Rechnung und //factXrech::ITEM_TEMPLATE// für die Vorlage einer Rechnungsposition. Die Vorlagen enthalten an den entsprechenden Positionen Platzhalter für die Daten aus der Kontrolldatei. Platzhalter sind Namen, die in '@'-Zeichen eingeklammert sind. Sie entsprechen den Schlüsseln innerhalb des Arrays mit den aufgesammelten Texten. Anschliessend geht die Methode sequentiell durch das Array, welches als zweiter Parameter übergeben wird, und ersetzt die Platzhalter, die genau so heissen wie die Schlüssel im Array, gegen den Text der unter diesem Schlüssel abgelegt worden ist. ((Werden Platzhalter benutzt, zu denen es keine passenden Schlüssel im Array gibt, werden diese nicht ersetzt, und der Platzhalter inklusive der '@'-Zeichen bleibt Bestandteil der zurückgebenen Zeichenkette.\\ \\ )) Eine Besonderheit dabei ist natürlich, dass die aus den Druckdaten extrahierten Texte gemäss den Vorgaben von %%XML%% umkodiert werden müssen ('&' -> '&', ...). Das ist nicht ganz so trivial, da auch der Fall berücksichtigt werden muss, dass der gegen den Platzhalter getauschte Text bereits gültiges %%XML%% ist. Deshalb wird die Umkodierung nur für Platzhalter durchgeführt, die nicht komplett aus Grossbuchstaben bestehen. ((So kann zum Beispiel die bereits als %%XML%% aufgesammelte Liste von Rechnungs(-)positionen durch Wahl des Namens 'ITEMS' davor geschützt werden, beim Einbinden in die eigentliche Rechnung ein zweites mal umkodiert - und damit zerstört - zu werden.\\ \\ )) $vars ["ITEMS"] .= $fxr->substitute (factXrech::ITEM_TEMPLATE, $itemvars); \\ ==== createXML ==== Die Methode schreibt zunächst einmal die als ersten Parameter übergebene Zeichenkette in eine interne %%XML%%-Datei. Ist der zweite Parameter eine Eins, wird diese %%XML%%-Datei auch sogleich validiert. Das Ergebnis der **Validation** mit dem [[https://github.com/itplr-kosit/validator|KoSIT Validator]] wird als Resultat zurückgegeben. Ein Rückgabewert von Null zeigt, dass die Validation fehlgeschlagen ist. ((Die Ausgabe des Validators selbst wird in einer Datei mit dem Namen 'validation.txt' im Skriptverzeichnis abgelegt.\\ \\ )) Im Falle des Fehlschlags kann das **Prüfprotokoll** zur Fehlerbehebung herangezogen werden. Im Skriptverzeichnis stehen dann die Dateien //factur-x-report.html// respektive //xrechnung-report.html//, die mit jedem Browser angezeigt und inspiziert werden können. ((Es ist auch eine gute Idee, diese Prüfprotokolle im Fehlerfall via E-Mail an einen Help-Desk oder an den Erzeuger der Rechnung zu schicken.\\ \\ )) $fxr->createXML ($fxr->substitute (factXrech::INVOICE_TEMPLATE, $vars), 1); \\ ==== createPDF ==== Die Methode erzeugt aus der als erstem Parameter übergebenen %%PCL%%-Datei eine %%PDF%%-Datei. Als zweiter Parameter wird eine Zeichenkette übergeben, die als Autor in den Metadaten der %%PDF%%-Datei eingetragen wird. Der dritte Parameter ist eine Zeichenkette, die als Betreff in den Metadaten der %%PDF%%-Datei eingetragen wird. Der vierte Parameter ist eine Zeichenkette, die als Schlüsselwörter in den Metadaten der %%PDF%%-Datei eingetragen wird. ((Darüber, wie Schlüsselwörter voneinander zu trennen sind, macht die %%PDF%%-Spzifikation keine genauen Vorgaben. Es hängt wohl letztlich auch davon ab, wie später diese Informationen aus der %%PDF%%-Datei extrahiert und weiterverarbeitet werden sollen. Semikolon scheint zumindest keine schlechte Wahl.\\ \\ )) Für die Einbettung der elektronischen Rechnung in die %%PDF%%-Datei erzeugt und benutzt die Methode eine PostScript-Datei. ((Aufgrund von Rechteproblemen bei der Einbindung von PostScript durch GhostScript müssen alle beteiligten Dateien (auch die eigentliche elektronische Rechnung) im Unterverzeichnis //converter// liegen. Das ist durch die Klasse garantiert, und darf nicht verändert werden! \\ \\ )) $title = "Rechnung {$vars ["DocumentID"]} vom $invoicedate"; $keywords = "Muster_GmbH {$vars ["DocumentID"]} $invoicedate {$vars ["CustomerID"]}"; $resultfile = $fxr->createPDF ("$document.pcl", "Muster GmbH", $title, $keywords); \\ === Hinweise === * Für die korrekte Funktion der Klasse //factXrech// werden zusätzlich vier Unterverzeichnisse benötigt. Zum einen ist das das Verzeichnis //converter//. Hier stehen **Farbprofile** und Dateien zur Erzeugung von %%PDF%%-Dateien. Im Verzeichnis //mappings// stehen Textdateien, die **Masseinheiten**, **Länderkodes** und **Mengenangaben** in spezifikationskonforme Formate übersetzen. Im Verzeichnis //xml// befinden sich in den Unterverzeichnissen //Factur-X// und //XRechnung// die **%%XML%%-Vorlagen**. Wie der Name bereits vermuten lässt, befindet sich im Verzeichnis //validator// der **KoSIT Validator**. * Die Konvertierung von %%PCL%% nach %%PDF%% erfordert die Installation zweier Hilfsprogramme. Aus lizenzrechtlichen Gründen dürfen die Programme [[https://www.ghostscript.com/releases/gpcldnld.html|GhostPCL]] und [[https://www.ghostscript.com/releases/gsdnld.html|GhostScript]] der Firma Artifex nicht mit (p2f) zusammen ausgeliefert werden. 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 Unterordner //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) in den Unterordner //converter// kopiert werden. * Die Klasse ist jetzt nicht so programmiert, dass sie alle möglichen Fehlnutzungen ausschliesst. Zum Beispiel werden Positionsindices nicht auf Korrektheit geprüft. Und auch manch anderer falsche Parameter mag schwerwiegende Fehler produzieren. Das ist von uns bewusst in Kauf genommen worden, um einerseits die Komplexität und auch die Laufzeit im Rahmen zu halten. * Sollte das vom Gateway aufgerufene Skript Daten auch aus externen Quellen erfassen, ist aus Gründen der **Betriebssicherheit** eine sehr viel **genauere Kontrolle** der gelesenen Daten angeraten. \\ \\