automatisierter UBL-XML-Generator in PHP in Kombination mit FileMaker

Während meiner Arbeit an der serverseitigen PDF-Generierung mit ZUGFeRD wurde mir schnell klar, dass viele Kunden zunehmend auf den UBL-Standard setzen – gerade im internationalen Kontext oder in Verbindung mit elektronischen Rechnungsplattformen. Also habe ich kurzerhand ein eigenes PHP-Skript geschrieben, das auf POST-Daten aus FileMaker oder anderen Quellen reagiert und daraus eine gültige UBL-Rechnung im XML-Format erstellt.
Wie so oft war der Aufbau des XML-Dokuments der anspruchsvollste Teil. Viele Details wie Namespaces, Pflichtfelder und ISO-konforme Datums- und Betragsformate mussten exakt stimmen. Außerdem wollte ich vermeiden, dass mein System bei fehlenden Daten abstürzt – darum habe ich Fallbacks eingebaut und ein eigenes Logging-System integriert.
Das Skript liest die Rechnungsdaten, Kunden- und Lieferantendaten sowie die Rechnungspositionen ein, berechnet die Summen und schreibt daraus ein vollständiges XML-Dokument nach dem UBL 2.1-Standard, das sich z. B. auch für die XRechnung weiterverwenden lässt. Die resultierende Datei ist kompatibel mit Plattformen wie PEPPOL, eRechnung.gv.at oder Zentralplattformen der öffentlichen Hand.
Die Daten werden in FileMaker gesammelt, das ganze klassisch über schleifen. Die Daten werden über ein einfaches application/x-www-form-urlencoded-POST-Request übergeben. Alle Felder werden als Key-Value-Paare übermittelt. Die Rechnungspositionen (line items) sind dabei als kompaktes Raw-String-Feld lineItemsRaw codiert, das einzelne Positionen mit | trennt und innerhalb der Position durch ; strukturiert ist.
FileMaker bietet zwar mittlerweile solide Funktionen für JSON-Manipulation – aber bei 25+ Feldern und einer schlichten Punkt-zu-Punkt-Kommunikation mit meinem PHP-Skript war mir das einfach zu umständlich. Ich wollte keine JSON-Parser-Bastelei, sondern einfach Daten senden. Daher nutze ich application/x-www-form-urlencoded, was mit curl ohnehin besser lesbar ist und mir in PHP direkt über $_POST zur Verfügung steht.
“-X POST " & “–header "Content-Type: application/x-www-form-urlencoded" " & “–data " & Zitat ( “invoiceNumber=” & $invoiceNumber & “&invoiceDate=” & $invoiceDate & “&invoiceCurrencyCode=” & $invoiceCurrencyCode & “&invoiceTypeCode=” & $invoiceTypeCode & “&dueDate=” & $dueDate & “&paymentTerms=” & $paymentTerms & “&deliveryTerms=” & $deliveryTerms &
“&sellerName=” & $sellerName & “&sellerStreet=” & $sellerStreet & “&sellerPostalCode=” & $sellerPostalCode & “&sellerCity=” & $sellerCity & “&sellerCountryCode=” & $sellerCountryCode & “&sellerTaxID=” & $sellerTaxID &
“&lieferschein_nr=” & $lieferschein_nr & “&kunden_nr=” & $kunden_nr &
“&buyerName=” & $buyerName & “&buyerStreet=” & $buyerStreet & “&buyerPostalCode=” & $buyerPostalCode & “&buyerCity=” & $buyerCity & “&buyerCountryCode=” & $buyerCountryCode & “&buyerTaxID=” & $buyerTaxID &
“&paymentMeansCode=” & $paymentMeansCode & “&payeeFinancialInstitution=” & $payeeFinancialInstitution & “&payeeIBAN=” & $payeeIBAN & “&payeeBIC=” & $payeeBIC & “&paymentReference=” & $paymentReference &
“&taxRate=” & $taxRate & “&taxAmount=” & $taxAmount & “&taxableAmount=” & $taxableAmount & “&taxCategoryCode=” & $taxCategoryCode &
“&totalNetAmount=” & $totalNetAmount & “&totalTaxAmount=” & $totalTaxAmount & “&totalGrossAmount=” & $totalGrossAmount & “&lineItemsRaw=” & $lineItemsRaw )
$dom = new DOMDocument('1.0', 'UTF-8'); $dom->formatOutput = true; $invoice = $dom->createElementNS( 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2', 'Invoice' ); $invoice->setAttribute('xmlns:cac', 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2'); $invoice->setAttribute('xmlns:cbc', 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2'); $dom->appendChild($invoice); // Standardfelder setzen $invoice->appendChild($dom->createElement('cbc:UBLVersionID', '2.1')); $invoice->appendChild($dom->createElement('cbc:CustomizationID', 'urn:cen.eu:en16931:2017#compliant#urn:xoev-de:kosit:standard:xrechnung_2.0')); $invoice->appendChild($dom->createElement('cbc:ID', $invoiceNumber)); $invoice->appendChild($dom->createElement('cbc:IssueDate', $invoiceDate)); $invoice->appendChild($dom->createElement('cbc:InvoiceTypeCode', '380')); $invoice->appendChild($dom->createElement('cbc:DocumentCurrencyCode', 'EUR'));
Der Aufbau geht dann weiter über Verkäufer- und Käuferdaten, Zahlungsinformationen, steuerliche Angaben und natürlich die Rechnungspositionen, die als cac:InvoiceLine-Blöcke angelegt werden.
Die Daten werden direkt auf dem Server verarbeitet, im Anschluss kann ich die XML-Datei wieder in ein FileMaker-Feld laden (Aus URL einfügen).
Da ich mit horstoeko/zugferd arbeite, wird in der Folgeversion noch die Validierung erfolgen. Die derzeit händische, zeigt alle Werte, keine Fehler, keine Warnungen.
