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 */ } } ?>