ZUGFeRD nicht nur erstellen, sondern auch lesen

Die meisten Entwickler verbinden ZUGFeRD oder Factur-X mit dem klassischen Anwendungsfall:
Eine Rechnung in FileMaker erzeugen, die dann als PDF mit eingebettetem XML an den Kunden geht. Doch im täglichen Einsatz ergibt sich oft das umgekehrte Szenario: Eingehende ZUGFeRD-Rechnungen sollen in das eigene System importiert und weiterverarbeitet werden. Genau hier zeigt sich die eigentliche Stärke des Standards, denn er erlaubt eine strukturiert maschinenlesbare Übergabe von Rechnungsdaten ohne dass man auf PDF-Parsing oder OCR-Erkennung angewiesen wäre.
Der Ansatz:
Mein Ziel war es, neben der Erzeugung auch das Einlesen von ZUGFeRD-Dateien in eine FileMaker-Lösung zu integrieren. Der Workflow ist dabei erstaunlich klar: 1. Der Anwender zieht eine ZUGFeRD-PDF in ein Containerfeld. 2. Ein FileMaker-Script übernimmt den Upload und prüft die Datei. 3. Die eingebettete XML-Datei wird mit Hilfe des PHP-Pakets horstoeko/zugferd extrahiert und ausgewertet. 4. Als Rückgabe erhalte ich ein kompaktes JSON, das sich in FileMaker direkt weiterverarbeiten lässt.
Damit ist der Kreis geschlossen: Eingehende Rechnungen können automatisch gelesen, geprüft und in die eigene Datenbank übernommen werden.
Technische Umsetzung
Im einfachsten Fall genügt ein FileMaker-Skript, das den Containerinhalt temporär exportiert und per curl an das Server-Skript übergibt:
Variable setzen [ $filename ; HoleContainerAttribute ( Rechnung::g_pdf ; "filename" ) ] Variable setzen [ $tmpFS ; Hole ( TemporärerPfad ) & $filename ] Exportiere Feldinhalt [ Rechnung::g_pdf ; "file:" & $tmpFS ] Variable setzen [ $url ; "[meine-domain.de/mc/receiv...](https://meine-domain.de/mc/receive_pdf_upload.php)" ] Variable setzen [ $curl ; "--request POST --upload-file " & Quote ( $tmpFS ) & " --header \"Content-Type: application/pdf\"" ] Aus URL einfügen [ Auswahl ; Mit Dialog: Aus ; Ziel: $$response ; $url ; SSL-Zertifikate verifizieren ; cURL-Optionen: $curl ]
Das PHP-Skript wiederum nimmt die hochgeladene PDF entgegen, übergibt sie an horstoeko und liefert ein strukturiertes JSON zurück:
{ "ok": true, "mode": "raw", "file": "/www/htdocs/w01da32b/maps.maro-testserver.de/mc/uploads/upload.pdf", "data": { "profile": "EN16931", "profileId": 2, "invoiceNumber": "INV-2025-001", "typeCode": "380", "issueDateTime": "2025-08-01 18:57:53", "currency": "EUR", "taxCurrency": null, "language": null, "isCopy": false, "isTest": false, "seller": { "name": "MaRo-Programmierung GbR", "globalId": "16547", "description": "Birkenwerder", "address": { "lineOne": null, "lineTwo": null, "lineThree": null, "postCode": null, "city": null, "country": null, "subdivision": null } }, "buyer": { "name": "Fensterhaus Ansbach GmbH", "globalId": "91522", "description": "Ansbach", "address": { "lineOne": null, "lineTwo": null, "lineThree": null, "postCode": null, "city": null, "country": null, "subdivision": null } }, "totals": { "grandTotal": 1249.5, "duePayable": 1249.5, "lineTotal": 1050, "taxTotal": 199.5, "taxBasisTotal": 1050, "chargeTotal": null, "allowanceTotal": null, "roundingAmount": null, "totalPrepaidAmount": null }, "payment": { "typeCode": "58", "information": null, "payeeIban": "DE12345678901234567890", "payeeBic": "GENODEF1XYZ", "accountName": "Testbank", "buyerIban": null, "cardType": null, "cardId": null, "cardHolderName": null }, "tax": [ { "categoryCode": "S", "typeCode": "VAT", "basisAmount": 1050, "calculatedAmount": 199.5, "rateApplicablePercent": 19, "exemptionReason": null, "exemptionReasonCode": null, "lineTotalBasisAmount": 0, "allowanceChargeBasisAmount": 0, "taxPointDate": null, "dueDateTypeCode": null } ], "lines": [ { "lineId": "1", "name": "Entwicklung und Anpassungen", "description": null, "sellerId": null, "buyerId": null, "globalIdType": null, "globalId": null, "quantity": 10, "unitCode": "HUR", "unitPrice": 90, "netAmount": 900, "basisQuantity": null, "basisQuantityUnitCode": null, "chargeFreeQuantity": null, "chargeFreeQuantityUnitCode": null, "allowanceChargeAmount": null, "taxPercent": 19, "taxCategory": "S", "taxTypeCode": "VAT", "taxCalculatedAmount": null, "allTaxes": [ { "categoryCode": "S", "typeCode": "VAT", "rateApplicablePercent": 19, "calculatedAmount": null, "exemptionReason": null, "exemptionReasonCode": null } ] }, { "lineId": "2", "name": "Remote-Support pauschal", "description": null, "sellerId": null, "buyerId": null, "globalIdType": null, "globalId": null, "quantity": 1, "unitCode": "C62", "unitPrice": 150, "netAmount": 150, "basisQuantity": null, "basisQuantityUnitCode": null, "chargeFreeQuantity": null, "chargeFreeQuantityUnitCode": null, "allowanceChargeAmount": null, "taxPercent": 19, "taxCategory": "S", "taxTypeCode": "VAT", "taxCalculatedAmount": null, "allTaxes": [ { "categoryCode": "S", "typeCode": "VAT", "rateApplicablePercent": 19, "calculatedAmount": null, "exemptionReason": null, "exemptionReasonCode": null } ] } ] } }
Dieses JSON ist das ideale Bindeglied: In FileMaker genügt ein Loop über data.lines[], um die einzelnen Positionen anzulegen. Kopf und Summenfelder lassen sich direkt in Variablen schreiben und anschließend auf beliebige Felder mappen.
Warum das spannend ist • Automatisierte Buchung: Eingehende Lieferantenrechnungen können ohne Medienbruch erfasst werden. • Plausibilitätsprüfung: Brutto = Netto + Steuer lässt sich sofort abgleichen. • Flexibilität: Egal ob Erzeugung oder Import dieselbe Technik (PHP + horstoeko) kann beides.
Fazit
Während viele nur an die Ausgabe denken, ist gerade das Einlesen der eigentliche Schlüssel zur durchgängigen Digitalisierung von Rechnungsprozessen. Mit ZUGFeRD lassen sich Daten beidseitig austauschen und mit ein paar Skriptzeilen in FileMaker hat man plötzlich nicht nur ein Export, sondern auch eine Import-Funktion in der Hand.