Neu: ZUGFeRD/UBL Reader Add-on für FileMaker
Elektronische Rechnungen sind längst Standard, insbesondere im öffentlichen Bereich mit UBL/XRechnung und bei Geschäftspartnern mit ZUGFeRD 2.1.1. Für viele FileMaker-Anwender stellt sich die Frage: Wie lassen sich diese Rechnungen direkt in bestehende Systeme integrieren, ohne jede Datei manuell zu erfassen?

Genau hier setzt unser neues ZUGFeRD/UBL Reader Add-on an.
Was kann das Add-on? • ZUGFeRD-PDFs einlesen: XML wird automatisch erkannt und verarbeitet • UBL/XRechnung unterstützen: kompatibel mit den gängigen Behörden- und Portallösungen • Strukturierte JSON-Ausgabe: Kopf-, Positions- und Steuerdaten stehen FileMaker-konform bereit • Beispielskripte und Mapping: sofort loslegen mit vorbereiteten Routinen • Flexibel im Einsatz: Einzelplatz oder Serverumgebung
Warum ist das wichtig? • Zeitersparnis: keine manuelle Eingabe von Rechnungsdaten mehr • Rechtssicherheit: Unterstützung aktueller Standards für E-Rechnung • Offen & erweiterbar: klare Dokumentation, kein Vendor-Lock-in • Praxisnah: entwickelt von FileMaker-Experten mit jahrzehntelanger Projekterfahrung
Für wen ist das interessant? • Unternehmen mit Pflicht zur E-Rechnung (öffentlicher Sektor, Lieferanten) • FileMaker-Entwickler, die Buchhaltung oder ERP-Funktionen integrieren möchten • Teams, die Eingangsrechnungen automatisiert verarbeiten wollen
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.
Endlich ist es fertig: ZUGFeRD in FileMaker – Integration in unter einer Stunde

Nach intensiver Entwicklung ist es soweit: Unser ZUGFeRD-/Factur-X Add-on für FileMaker ist fertig! 🎉 Wer schon einmal versucht hat, ZUGFeRD in eine FileMaker-Anwendung einzubauen, kennt die Herausforderung: Eigenentwicklungen ziehen sich über Tage oder Wochen, kosten Zeit, Geld und Nerven – und am Ende bleibt die Frage nach der Wartbarkeit.
Mit unserem Add-on gehen wir einen anderen Weg. Keine Programmierung, kein kompliziertes Skripten, stattdessen Mapping.
Mapping statt Programmieren
Die Implementierung basiert auf dem offiziellen COMFORT-Profil von ZUGFeRD/Factur-X. Das bedeutet: Alle relevanten Rechnungsdaten werden abgedeckt, ohne dass man sich selbst tief in Spezifikationen und Normdetails einarbeiten muss.
Anstatt eigene Scripts oder Datenmodelle zu schreiben, werden die bestehenden Felder in FileMaker einfach über ein Mapping den benötigten ZUGFeRD-Feldern zugeordnet. Damit bleibt die eigene Struktur unverändert, und trotzdem erfüllt die Anwendung sofort den Standard.
In weniger als einer Stunde einsatzbereit
Der größte Vorteil: Die Integration dauert in der Regel nicht länger als eine Stunde. Das Add-on klinkt sich direkt in die bestehende FileMaker-Datenbank ein, ohne dass das Rechnungslayout oder vorhandene Scripts angepasst werden müssen.
Wer möchte, kann die Integration jederzeit durch eigenes Skripting erweitern – doch die Grundinstallation läuft komplett ohne Programmieraufwand.
Vorteile gegenüber Eigenentwicklung • Zeitersparnis: statt tagelanger Arbeit → unter einer Stunde fertig. • Kostenreduktion: keine teuren Eigenentwicklungen, kein externer Entwickler notwendig. • Zukunftssicherheit: Nutzung des offiziellen ZUGFeRD-COMFORT-Profils, das in vielen Branchen bereits Standard ist. • Flexibilität: Erweiterungen und Anpassungen sind jederzeit möglich, aber nicht zwingend nötig.
Fazit
Endlich verfügbar! Mit unserem ZUGFeRD-Add-on für FileMaker sparen Sie Zeit, Geld und Ressourcen – und bleiben dabei normenkonform.
ZUGFeRD mit PHP: Wie ich das horstoeko/zugferd-Paket lokal vorbereitet und ohne Composer-Zugriff auf den Server gebracht habe
Wer schon einmal versucht hat, das ZUGFeRD-Format mit PHP umzusetzen, wird früher oder später auf das Projekt horstoeko/zugferd stoßen. Es bietet eine mächtige Möglichkeit, ZUGFeRD-konforme Rechnungsdaten zu erstellen und in PDF-Dokumente einzubetten. Doch gerade am Anfang lauern einige Stolpersteine: Composer, Pfadprobleme, Server ohne Shell-Zugriff. Dieser Beitrag zeigt, wie ich mir mit einem lokalen Setup, GitKraken und einem simplen Upload-Trick geholfen habe, um trotz aller Einschränkungen produktiv arbeiten zu können.
Bevor ich das Paket überhaupt einbinden konnte, musste Composer einmal lokal installiert werden – ganz ohne kommt man nicht aus. Ich habe mich für den Weg über die offizielle Installationsanleitung entschieden:
php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" php composer-setup.php php -r "unlink('composer-setup.php');"
Es gibt aber auch fertige Pakete als *.exe für Windows.
GitKraken, Composer & das Terminal
Ich arbeite gerne visuell, und daher ist GitKraken mein bevorzugter Git-Client. Doch ein oft unterschätzter Vorteil: GitKraken bringt ein eigenes Terminal mit. Dieses habe ich genutzt, um Composer lokal zu verwenden – ohne die globale Composer-Installation auf meinem Server-System anfassen zu müssen.

# Im Terminal von GitKraken composer require horstoeko/zugferd
Dabei habe ich mich bewusst für die 1.x
-Version entschieden, da diese eine stabilere und besser dokumentierte Grundlage für den Einsatz ohne komplexes Setup bietet. Zudem ist dort der ZugferdDocumentPdfBuilder
enthalten, der es erlaubt, das gesamte PDF-Handling im PHP-Kosmos zu belassen. Soweit ich gesehen habe, gibt es wohl auch DEV-Versionen, aber ich war mir nicht sicher wie weit diese nutzbar sind.
Der Upload-Trick: Alles lokal vorbereiten
Da mein Zielserver keinen Composer-Zugriff bietet, musste ich alles lokal vorbereiten. Ich nutze für meine Testumgebung einen einfachen Server von AllInk. Das ist extrem kostengünstig, aber eigene Software installieren, Fehlanzeige.
Der Trick: Ich habe den gesamten vendor
-Ordner inklusive composer.json
und composer.lock
gezippt und manuell auf den Server übertragen. Das spart nicht nur Zeit, sondern funktioniert in jeder Hostingumgebung.
# Lokaler Aufbau my-project/ ├── src/ ├── vendor/ ├── composer.json ├── composer.lock
Dann per SFTP oder FTP hochladen und sicherstellen, dass im PHP-Code folgender Autoloader korrekt eingebunden wird:
require __DIR__ . '/vendor/autoload.php';
Vorsicht, Pfade: Die Sache mit dem “/src”-Unterordner
Ein Stolperstein war die Struktur des horstoeko-Pakets. Die Klassen liegen nicht direkt im Projektverzeichnis, sondern verstecken sich unter:
/vendor/horstoeko/zugferd/src/...
Der PSR-4-Autoloader von Composer ist darauf vorbereitet, aber wer manuell Klassen einbindet oder den Autoloader nicht korrekt referenziert, bekommt Fehler. Ein Test mit:
use horstoeko\zugferd\ZugferdDocumentPdfBuilder;
funktionierte erst, nachdem ich sicher war, dass der Autoloader geladen war und keine Pfade fehlten.
Endlich produktiv: Der erste Builder-Lauf
Nachdem alles hochgeladen und die Autoloading-Probleme beseitigt waren, konnte ich mein erstes ZUGFeRD-Dokument bauen:
$builder = new ZugferdDocumentPdfBuilder(); $builder->setDocumentFile("./rechnung.pdf"); $builder->setZugferdXml("./debug_12345.xml"); $builder->saveDocument("./zugferd_12345_final.pdf");
Und siehe da: eine ZUGFeRD-konforme PDF-Datei, direkt aus PHP erzeugt. Kein Java, kein PDF/A-Tool von Adobe, keine Blackbox. Wichtig, das ganze ist per ZIP auf jeden Kundenserver übertragbar.
Warum kein Java?
Ich habe bewusst darauf verzichtet, Java-Tools wie Apache PDFBox oder gar die offizielle ZUGFeRD Java Library zu nutzen – aus einem ganz einfachen Grund: Ich wollte die Lösung so nah wie möglich an meiner bestehenden PHP-Infrastruktur halten. Keine zusätzliche Runtime, keine komplexen Abhängigkeiten, keine Übersetzungsprobleme zwischen Systemen. PHP allein reicht – wenn man die richtigen Werkzeuge nutzt.
Häufige Fehlermeldungen und ihre Lösungen
Gerade beim Einstieg in das horstoeko/zugferd-Paket können einige typische Fehlermeldungen auftreten:
Fehler: Class 'horstoeko\zugferd\ZugferdDocumentPdfBuilder' not found
// Lösung: require_once __DIR__ . '/vendor/autoload.php';
Fehler: Cannot open file ./debug_12345.xml
// Lösung: Pfad prüfen! Gerade bei relativen Pfaden kann es helfen, alles absolut zu machen: $builder->setZugferdXml(__DIR__ . '/debug_12345.xml');
Fehler: Output file cannot be written
// Lösung: Schreibrechte auf dem Zielverzeichnis prüfen! Ein chmod 775 oder 777 (mit Bedacht!) kann helfen.
Fazit: Wer wie ich auf Servern ohne Composer arbeiten muss oder will, kann sich mit einem lokalen Setup, GitKraken und einem Zip-Upload wunderbar behelfen. Wichtig ist, auf die Pfade zu achten, den Autoloader korrekt einzubinden und nicht vor kleinen Hürden zurückzuschrecken. Die Möglichkeiten, die das horstoeko/zugferd-Paket bietet, machen die Mühe mehr als wett.
Zumal das ganze Setup, 1 zu 1, auf einen Kundenserver übertragen werden kann. Die eigentlichen Daten kommen aus FileMaker, dieser holt sich die PDF und das XML auch wieder vom Server ab. Somit ist die Erstellung der ZUGFeRD-PDF und der XML mit einen FileMaker-Script abzudecken. Für die Erstellung auf dem Server bedarf es zweier PHP-Scripte. Dazu das Horstoeko/zugferd-Paket.
ZUGFeRD mit horstoeko/zugferd und FileMaker

Nach unzähligen Versuchen mit TCPDF, FPDI und verschiedensten Merge-Strategien, habe ich mich letztlich für einen pragmatischeren Weg entschieden: Ich lasse sowohl das PDF als auch das XML direkt auf dem Server erzeugen – ohne nachträglichen Merge. Die Lösung basiert auf dem PHP-Paket horstoeko/zugferd, welches sich nach einigen Stolpersteinen als zuverlässig herausgestellt hat – sobald man seine Eigenheiten akzeptiert.
Im ersten Szenario habe ich die fertige PDF aus FileMaker auf den Server, dort dann die Daten als POST-Parameter empfangen. Dann versucht die beiden Dateien, XML und PDF zu verschmelzen. Keine Change, bin fast verzweifelt, kenne die Doku zu Horstoeko/Zugferd aus dem FF. Aber es hat nicht geklappt. Also jetzt der pragmatische Ansatz. Die Daten werden übertragen und dann wird auf dem Server alles erzeugt.
Der FileMaker-Teil übergibt die notwendigen Daten per POST an ein PHP-Skript auf dem Server. Dieses Skript generiert daraus die ZUGFeRD-konforme XML, erzeugt gleichzeitig ein einfaches PDF mit den wichtigsten Rechnungsdaten (z. B. Rechnungstitel, Nummer etc.) – und bindet die XML direkt beim Erzeugen ein. Kein nachträgliches Anhängen mehr nötig. Kein Merge-Objekt, kein Zwischenschritt. Die erzeugte Datei ist PDF/A-3B und enthält die eingebettete Rechnung als XML. Dabei wird die PDF gleich im PHP etwas angepasst. Vermutlich wird das ganze noch etwas schicker mit css Implementierung, aber für den Anfang reicht es so.
Hier ist der komplette PHP-Code, den ich aktuell produktiv im Testsystem einsetze:
$_POST['sellerName'] ?? '',
'street' => $_POST['sellerStreet'] ?? '',
'zip' => $_POST['sellerPostalCode'] ?? '',
'city' => $_POST['sellerCity'] ?? '',
'country' => $_POST['sellerCountryCode'] ?? 'DE',
'tax_id' => $_POST['sellerTaxID'] ?? ''
];
// Rechnungsempfänger-Daten
$buyer = [
'name' => $_POST['buyerName'] ?? '',
'street' => $_POST['buyerStreet'] ?? '',
'zip' => $_POST['buyerPostalCode'] ?? '',
'city' => $_POST['buyerCity'] ?? '',
'country' => $_POST['buyerCountryCode'] ?? 'DE',
'tax_id' => $_POST['buyerTaxID'] ?? ''
];
// Zahlungsinformationen
$payment = [
'means_code' => $_POST['paymentMeansCode'] ?? '',
'financial_institution' => $_POST['payeeFinancialInstitution'] ?? '',
'iban' => $_POST['payeeIBAN'] ?? '',
'bic' => $_POST['payeeBIC'] ?? '',
'reference' => $_POST['paymentReference'] ?? ''
];
// Steuerinformationen
$tax = [
'rate' => floatval(str_replace(',', '.', $_POST['taxRate'] ?? '19')),
'amount' => floatval(str_replace(',', '.', $_POST['taxAmount'] ?? '0')),
'taxable_amount' => floatval(str_replace(',', '.', $_POST['taxableAmount'] ?? '0')),
'category_code' => $_POST['taxCategoryCode'] ?? 'S'
];
// Positionen aus lineItemsRaw extrahieren
$positions = [];
$lineItems = explode('|', $_POST['lineItemsRaw'] ?? '');
foreach ($lineItems as $lineItem) {
if (empty($lineItem)) continue;
$parts = explode(';', $lineItem);
if (count($parts) >= 7) {
$quantity = floatval($parts[3]);
$netPrice = floatval(str_replace(',', '.', $parts[5]));
$total = $quantity * $netPrice;
$orderDate = isset($parts[8]) ? date('d.m.Y', strtotime($parts[8])) : '';
$positions[] = [
'position' => $parts[0],
'description' => $parts[1],
'article_number' => $parts[2],
'quantity' => $quantity,
'unit' => $parts[4],
'net_price' => $netPrice,
'tax_rate' => floatval($parts[6]),
'total' => $total,
'order_date' => $orderDate
];
}
}
// Summen neu berechnen
$totalNet = 0;
$totalTax = 0;
foreach ($positions as $position) {
$totalNet += $position['total'];
$totalTax += $position['total'] * ($position['tax_rate'] / 100);
}
$totals = [
'net' => $totalNet,
'tax' => $totalTax,
'gross' => $totalNet + $totalTax
];
// Pfade definieren
$pdfPath = $uploadDir . $invoiceNumber . '.pdf';
$xmlPath = $uploadDir . 'inv_' . $invoiceNumber . '.xml';
$outputPath = $uploadDir . $invoiceNumber . '_ZUGFeRD.pdf';
// PDF-Datei überprüfen
if (!checkFile($pdfPath, "PDF-Datei")) {
throw new Exception("PDF-Datei nicht verfügbar");
}
// ZUGFeRD-Dokument erstellen
logMessage("Erstelle ZUGFeRD-Dokument...");
$document = ZugferdDocumentBuilder::createNew(ZugferdProfiles::PROFILE_BASIC);
logMessage("DocumentBuilder initialisiert");
// Basisinformationen setzen
logMessage("Setze Basisinformationen...");
$document->setDocumentInformation(
$invoiceTypeCode, // Dokumenttyp (380 = Rechnung)
$invoiceNumber, // Rechnungsnummer
new DateTime($invoiceDate), // Rechnungsdatum
$currency // Währung
);
logMessage("Basisinformationen gesetzt");
// Rechnungssteller setzen
logMessage("Setze Rechnungssteller...");
$document->setDocumentSeller(
$seller['name'], // Name
$seller['zip'], // PLZ
$seller['city'], // Stadt
$seller['street'], // Straße
$seller['country'] // Land
);
logMessage("Rechnungssteller gesetzt");
// Rechnungsempfänger setzen
logMessage("Setze Rechnungsempfänger...");
$document->setDocumentBuyer(
$buyer['name'], // Name
$buyer['zip'], // PLZ
$buyer['city'], // Stadt
$buyer['street'], // Straße
$buyer['country'] // Land
);
logMessage("Rechnungsempfänger gesetzt");
// Positionen hinzufügen
logMessage("Füge Positionen hinzu...");
foreach ($positions as $index => $position) {
$document->addNewPosition((string)($index + 1));
$document->setDocumentPositionProductDetails($position['description']);
$document->setDocumentPositionNetPrice($position['net_price']);
$document->setDocumentPositionQuantity($position['quantity'], $position['unit']);
$document->addDocumentPositionTax("S", "VAT", $position['tax_rate']);
$document->setDocumentPositionLineSummation($position['total']);
logMessage("Position {$position['description']} hinzugefügt");
}
// PDF mit Rechnungsdaten erstellen
logMessage("🔄 Erstelle PDF mit Rechnungsdaten...");
try {
$pdf = new FPDF();
$pdf->AddPage();
$pdf->SetAutoPageBreak(true, 20);
// Schriftarten definieren
$pdf->SetFont('Arial', '', 10);
// Absenderadresse
$pdf->SetXY(20, 20);
$pdf->Cell(0, 5, $seller['name'], 0, 1);
$pdf->Cell(0, 5, 'GmbH & Co. KG', 0, 1);
$pdf->Cell(0, 5, $seller['street'], 0, 1);
$pdf->Cell(0, 5, 'D-' . $seller['zip'] . ' ' . $seller['city'], 0, 1);
// Rechnungsinformationen rechts oben
$pdf->SetXY(120, 20);
$pdf->SetFont('Arial', '', 9);
// Rechte Spalte mit Informationen
$rightColumnData = [
['Nummer', $invoiceNumber],
['Datum', date('d.m.Y', strtotime($invoiceDate))],
['Kunden Nr.', $_POST['customerNumber'] ?? '16105'],
['Lieferschein', $_POST['deliveryNote'] ?? ''],
['Lief. Datum', date('d.m.Y', strtotime($_POST['deliveryDate'] ?? $invoiceDate))]
];
foreach ($rightColumnData as $row) {
$pdf->SetX(120);
$pdf->Cell(30, 5, $row[0], 0, 0);
$pdf->Cell(40, 5, $row[1], 0, 1);
}
// Überschrift "Rechnung"
$pdf->SetFont('Arial', '', 14);
$pdf->SetXY(20, 70);
$pdf->Cell(0, 10, 'Rechnung', 0, 1);
// Positionen Header
$pdf->SetFont('Arial', '', 8);
$pdf->SetFillColor(240, 240, 240);
$pdf->SetY($pdf->GetY() + 5);
// Spaltenbreiten
$colWidths = [
'auftrag' => 25,
'bestellung' => 35,
'kommission' => 35,
'artikel' => 25,
'bezeichnung' => 60,
'menge' => 20,
'preis' => 25,
'gesamt' => 25
];
// Positionen ausgeben
foreach ($positions as $index => $position) {
// Grauer Balken für Auftragskopf
$pdf->SetFillColor(240, 240, 240);
$y = $pdf->GetY();
// Auftragskopf mit Datum
$pdf->Cell($colWidths['auftrag'], 5, 'Auftrag: ' . $position['article_number'] . ' / ' . $position['order_date'], 0, 0, 'L', true);
$pdf->Cell($colWidths['bestellung'], 5, 'Ihre Bestellung: ' . $_POST['orderNumber'], 0, 0, 'L', true);
$pdf->Cell($colWidths['kommission'], 5, 'Ihre Kommission:', 0, 1, 'L', true);
// Positionsdetails
$pdf->SetFont('Arial', '', 8);
$pdf->Cell($colWidths['artikel'], 5, $position['article_number'], 0, 0);
$pdf->Cell($colWidths['bezeichnung'], 5, $position['description'], 0, 0);
$pdf->Cell($colWidths['menge'], 5, $position['quantity'] . ' ' . $position['unit'], 0, 0, 'R');
$pdf->Cell($colWidths['preis'], 5, number_format($position['net_price'], 2, ',', '.') . ' €', 0, 0, 'R');
$pdf->Cell($colWidths['gesamt'], 5, number_format($position['total'], 2, ',', '.') . ' €', 0, 1, 'R');
$pdf->Ln(2);
}
// Summen am Ende
$pdf->SetY(-60);
$pdf->SetFont('Arial', '', 9);
// Zahlungsbedingungen
$pdf->Cell(0, 5, 'Rechnungsbetrag zahlbar bis ' . date('d.m.Y', strtotime($dueDate)), 0, 1);
$pdf->Cell(0, 5, 'Bei Zahlung bis ' . date('d.m.Y', strtotime($dueDate)) . ' Skonto 3 %', 0, 1);
// Summen rechtsbündig
$pdf->SetX(-80);
$pdf->Cell(30, 5, 'Summe netto', 0, 0, 'R');
$pdf->Cell(50, 5, number_format($totals['net'], 2, ',', '.') . ' €', 0, 1, 'R');
$pdf->SetX(-80);
$pdf->Cell(30, 5, 'USt. ' . number_format($tax['rate'], 0) . ' %', 0, 0, 'R');
$pdf->Cell(50, 5, number_format($totals['tax'], 2, ',', '.') . ' €', 0, 1, 'R');
$pdf->SetX(-80);
$pdf->SetFont('Arial', 'B', 9);
$pdf->Cell(30, 5, 'Rechnungsbetrag', 0, 0, 'R');
$pdf->Cell(50, 5, number_format($totals['gross'], 2, ',', '.') . ' €', 0, 1, 'R');
// Bestellnummer
$pdf->SetFont('Arial', '', 9);
$pdf->SetY(-30);
$pdf->Cell(0, 5, 'Zu Kommission: Bestell-Nr.: ' . ($_POST['orderReference'] ?? ''), 0, 1);
// Horizontale Linie am Ende
$pdf->SetY(-20);
$pdf->Line(20, $pdf->GetY(), 190, $pdf->GetY());
$pdf->Output('F', $outputPath);
logMessage("PDF erfolgreich generiert: $outputPath");
if (file_exists($outputPath)) {
logMessage("ZUGFeRD-PDF erfolgreich erstellt: " . basename($outputPath));
logMessage("Dateigröße: " . filesize($outputPath) . " Bytes");
} else {
throw new Exception("ZUGFeRD-PDF wurde nicht erstellt!");
}
} catch (Exception $e) {
logMessage(" Fehler beim PDF-Generieren: " . $e->getMessage());
logMessage(" Stack Trace:\n" . $e->getTraceAsString());
throw $e;
}
} catch (Exception $e) {
logMessage(" Fehler beim Erstellen der ZUGFeRD-PDF: " . $e->getMessage());
logMessage(" Stack Trace:\n" . $e->getTraceAsString());
// Debug-Informationen
logMessage("\nDebug-Informationen:");
logMessage("PHP Version: " . PHP_VERSION);
logMessage("Memory Limit: " . ini_get('memory_limit'));
logMessage("Max Execution Time: " . ini_get('max_execution_time'));
logMessage("Upload Directory Permissions: " . substr(sprintf('%o', fileperms($uploadDir)), -4));
// XML-Datei prüfen
if (file_exists($xmlPath)) {
logMessage("XML-Datei existiert: " . filesize($xmlPath) . " Bytes");
logMessage("XML-Inhalt (erste 200 Zeichen):\n" . substr(file_get_contents($xmlPath), 0, 200));
} else {
logMessage("XML-Datei existiert nicht");
}
}
Natürlich ist das so erst der Anfang, so fehlt die Überprüfung des fertigen PDF-A, aber bei händischen Tests, wurde alles akzeptiert.
Die Vorgehensweise aus FileMaker heraus ist klar. Daten sammeln und als POST übertragen. Der POST schaut in etwa so aus. Aus URL einfügen:
“-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 &
“&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
)
Nun geht es darum, die Form der Rechnung anzupassen, die Rechnung schon auf dem System zu validieren. Im Anschluss muss diese wieder im FileMaker-System zugänglich sein. Was ich aber jetzt schon sagen kann, es geht auch ohne MBS-Plugin und ohne die dort auflaufenden Lizenz-Kosten. Der Weg bis zu diesem Punkt war wirklich sehr steinig, vieles funktionierte nicht so wie dokumentiert. Jetzt bin ich froh,
PDF-Erzeugung per IWP
Jeder der eine Datenbank im Web über IWP freigibt, kommt irgendwann an einen Punkt wo er die Entscheidung IWP zu verwenden bereut.
Nun ich erreichte den Punkt als es darum ging Dateien zu exportieren, oder nur eine PDF zu erzeugen. Viele Script-Schritte funktionieren über IWP nicht. Kein als Excel-Speichern, kein Datensätze als PDF speichern.
Was also tun wenn man z.B. Teilnehmer über IWP erfassen möchte und Diese im Anschluss eine Anmeldebestätigung erhalten sollen?
Die Antwort habe ich im FileMaker-Forum erhalten. Einfach einen Client auf dem Server mitlaufen lassen. Einen Client auf dem Server laufen lassen?
Was steckt dahinter. Nun der Client übernimmt die Aufgaben die der Server nicht kann. Also speichern, exportieren oder gar importieren von Dateien. Alles was eine Client im Netzwerk kann, geht mit diesem Ansatz.
Wie aber sage ich nun dem Client was zu tun ist? Über gesetzte Feldwerte. Die Funktion Feldwert setzen geht auch über IWP basierte Webseiten. Das bedeutet z.B. wenn die Anmeldung eines Teilnehmers durchlaufen ist, wird über ein Script der Feldwert „FLAG.MAIL.Anmeldung_1“ auf den Wert 1 gesetzt.
Jetzt beginnt die Arbeit des Clients. Über ein „BeiTimer-Script“ setzen überprüft der Client regelmäßig ob der Feldwert „FLAG.MAIL.Anmeldung_1“ den Wert 1 enthält. Tut er dies, wird einfach ein Script zu Versenden einer EMail angestossen. Um nun nicht im Minutentakt immer wieder eine Mail an den Teilnehmer zu versenden gibt es noch ein zweites Statusfeld. Dieses erhält nach Versendung der Mail den Status 1. Ist der Wert dieses Feldes auf 1, wird keine Mail versendet.
Auf diese Weise kann ich alle Funktionen die ein Client beherrscht auch über den Server ansteuern.