Webseiten-Werkzeuge

Benutzer-Werkzeuge


factXrech-class.php

Die Klasse factXrech-class.php basiert im Prinzip auf der Klasse 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 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. 1)
  • 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 factXrech-class.zip geladen werden, weil zum einen beim Laden über den Reiter des Quellcodes die Unicode-BOM verlorengeht. 2) 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:

factXrech-class.php
<?php
 
class factXrech
  {
    const DATE_FIELD = 0;
    const CURRENCY_FIELD = 1;
    const STRING_FIELD = 2;
    const COUNTRY_FIELD = 3;
    const LANGUAGE_FIELD = 4;
    const UNIT_FIELD = 5;
    const QUANTITY_FIELD = 6;
    const MAIL_FIELD = 7;
 
    const INVOICE_TEMPLATE = 1;
    const ITEM_TEMPLATE = 2;
 
    const FACTURX = "F";
    const XRECHNUNG = "X";
 
    private $type;                           /* Factur-X (F) vs XRechnung (X) */
    private $errorCount = 0;                 /* number of error messages sent */
    private $index = 0;                       /* line index from control file */
    private $matches = array ();                    /* info from control file */
    private $xmlInvoice = "";         /* xml for factur-X / XRechnung invoice */
    private $xmlItems = "";                           /* xml for billed items */
    private $xmlFile;                           /* name of xml file to create */
 
    private $unitCodeMap = null;           /* unit names to XRechnung mapping */
    private $countryCodeMap = null;     /* country names to XRechnung mapping */
    private $languageCodeMap = null;   /* language names to XRechnung mapping */
 
/*----------------------------------------------------------------------------*/
 
    public function __construct ($type, $zone, $ctlPath)
      {
        date_default_timezone_set ($zone);  /* prepare log file time stamping */
 
        if (! file_exists ($ctlPath))
          $this->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 3). 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. 4)

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 5), 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. 6)

...
[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. 7)

Eine Besonderheit dabei ist natürlich, dass die aus den Druckdaten extrahierten Texte gemäss den Vorgaben von XML umkodiert werden müssen ('&' → '&amp;', …). 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. 8)

$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 KoSIT Validator wird als Resultat zurückgegeben. Ein Rückgabewert von Null zeigt, dass die Validation fehlgeschlagen ist. 9)

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. 10)

$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. 11)

Für die Einbettung der elektronischen Rechnung in die PDF-Datei erzeugt und benutzt die Methode eine PostScript-Datei. 12)

$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 GhostPCL und GhostScript der Firma Artifex nicht mit print2forms 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.



1)
Dies erfolgt mit dem frei verfügbaren 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!

2)
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.

3)
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.

4)
Siehe das Beispiel für die Methode setIndex weiter oben.

5)
Das ist der Standardwert, bei dem der Text einfach übernommen wird.

6)
Dabei werden die Zeilenfragmente in der Reihenfolge innerhalb der Kontrolldatei aneinandergefügt, was unter bestimmten Umständen nicht unbedingt der Reihenfolge im Druckbild entspricht!

7)
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.

8)
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.

9)
Die Ausgabe des Validators selbst wird in einer Datei mit dem Namen 'validation.txt' im Skriptverzeichnis abgelegt.

10)
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.

11)
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.

12)
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!

print2forms/tips/tip97.txt · Zuletzt geändert: 2024-06-26 17:36 (Externe Bearbeitung)