Maßgeschneiderte FileMaker-Lösungen & AddOns. FileMaker-experts.de

ZUGFeRD Creator 4 unter der Haube: zwei Scripte, eine Tabelle, kein Umbau

Ein Fachartikel für FileMaker-Entwickler: wie der Creator 4 aufgebaut ist, warum die Einrichtung mit einem Script beginnt, wie Scripte über Dateigrenzen sprechen und wie der Webviewer die Zuordnung live an der fremden Datei auswertet.


12 server waehlen dialog.

Im Blog haben wir den ZUGFeRD Creator 4 als „ein Ordner, zwei Scripte, ein Knopf“ vorgestellt. Das ist die Sicht des Anwenders. Dieser Artikel ist für alle, die wissen wollen, was dahinter passiert, und für alle, die selbst FileMaker-Lösungen an externe Dienste anbinden und vor denselben Fragen stehen:

  • Wie bindet man eine fremde Lösung an, ohne sie zu kennen?
  • Wie ruft man Scripte in einer anderen Datei auf, ohne dass der Anwender Ziele von Hand setzen muss?
  • Wie baut man eine Webviewer-Oberfläche, die live mit FileMaker spricht, ohne sich auf „JavaScript in Webviewer ausführen“ zu verlassen?
  • Und wie liefert man einen PHP-Dienst aus, ohne dass jemand einen Webserver einrichtet?

Der Artikel ist lang. Er ist so geordnet, dass jeder Abschnitt auch für sich gelesen werden kann.


1. Die Ausgangslage: was in Version 3 wehtat

Der ZUGFeRD Creator erzeugt aus FileMaker-Rechnungen zwei Formate:

  • ZUGFeRD / Factur-X: eine PDF/A-3 mit eingebettetem XML nach EN 16931.
  • XRechnung als XML (UBL).

Die eigentliche Arbeit macht ein PHP-Dienst auf Basis der Bibliothek horstoeko/zugferd. FileMaker schickt die Rechnungsdaten per POST, der Dienst baut das XML, bettet es in die Beleg-PDF ein, und FileMaker holt das Ergebnis ab.

In Version 3 sah die Einrichtung beim Kunden so aus:

  1. Den PHP-Ordner auf einen Webserver bringen, PHP mit den nötigen Erweiterungen bereitstellen, Rechte setzen.
  2. Im Creator eine externe Datenquelle auf die Kundendatei anlegen.
  3. Im Creator Tabellenauftreten auf die Rechnungs- und Positionstabelle des Kunden anlegen.
  4. Je Belegart ein Layout im Creator anlegen, auf dem die Werte ausgewertet werden.
  5. Die Server-Adresse von Hand eintragen.

Technisch war das sauber. Für den Kunden war es eine Hürde, und für uns als Anbieter ein Support-Fall pro Installation. Der Creator musste die Kundendatei kennen: ihre Tabellen, ihre Beziehungen, ihre Layouts.

Für Version 4 stand deshalb eine einzige Frage über allem: Was ist das Wenigste, das ein Anwender tun muss?


2. Das Architekturprinzip: die Abhängigkeit umdrehen

Die wichtigste Entscheidung in Version 4 ist keine technische Einzelheit, sondern eine Umkehr der Abhängigkeit:

Nicht der Creator kennt die Kundendatei, sondern die Kundendatei kennt den Creator. Und: Ausgewertet wird dort, wo die Daten liegen.

In V3 hat der Creator über seine eigenen Tabellenauftreten in die Kundendatei hineingegriffen. In V4 bekommt die Kundendatei ein Tabellenauftreten auf den Creator (_preferences_x) und ein Script (ZF_Kundendatei). Dieses Script wertet die Zuordnung im eigenen Kontext aus. Die Ausdrücke der Zuordnung wie RECHNUNG::Datum oder Kunde::Name werden also in der Datei berechnet, in der es diese Tabellenauftreten auch gibt.

Der Creator wird damit zu dem, was er eigentlich ist: Einstellungen plus Server-Anbindung. Er speichert die Zuordnung, liefert sie auf Anfrage aus und spricht mit dem PHP-Dienst. Er braucht keine Tabellenauftreten, keine Beziehungen und keine Layouts des Kunden mehr.

Der Ablauf beim Erzeugen einer E-Rechnung:

Kundendatei                    Creator                Server
-----------                    -------                ------
Knopf auf der Rechnung
ZF_Kundendatei
  |-- ZF_Belegart_holen ------>  liest _belegart
  |<----- Zuordnung (JSON) ----
  |
  |  Arbeitsfenster add_action
  |  Beleg suchen
  |  Kopf: Ausdruecke auswerten
  |  Positionen per SQL
  |  Beleg-PDF -> globaler
  |  Container im Creator
  |
  |-- ZF_Beleg_senden -------->  Upload PDF  -------->  PHP
  |                              POST Werte  -------->  baut XML/PDF
  |                              abholen     <--------
  |<----- Ergebnis im ---------
  |       globalen Container
  |
  |  Ergebnis -> eigener
  |  Zielcontainer

Zwei Dinge fallen auf: Es gibt genau zwei Aufrufe in den Creator, und alle Daten wandern als JSON oder über globale Container. Dazu gleich mehr.


3. Der Server als Ordner: FrankenPHP statt Webserver

Die Entscheidung

Bevor wir an FileMaker gingen, haben wir das größere Problem gelöst: den PHP-Dienst. Diskutiert haben wir drei Wege:

  • Ein FileMaker-Plugin. Verworfen. Die Erzeugung müsste neu geschrieben werden (PDF/A-3 ist der schwere Teil), es bräuchte drei Plattformen plus Signierung, und FileMaker Go wäre außen vor.
  • Neuschreiben in einer anderen Sprache (Go, Rust, Java mit Mustangproject). Verworfen. Der vorhandene PHP-Code ist mit einem Prüfstand aus 13 Testfällen gegen den offiziellen Validator geprüft, das wirft man nicht weg.
  • Den vorhandenen PHP-Code kapseln. Gewählt.

Das Werkzeug dafür ist FrankenPHP: ein Webserver (Caddy) und PHP in einer einzigen ausführbaren Datei, mit allen Erweiterungen, die wir brauchen (sodium, gd, dom, mbstring, intl, zip …). Für Mac gibt es fertige Binärdateien für Apple-Chip und Intel. Für Windows gibt es eine PHP-Distribution, deren Erweiterungen als DLLs über eine eigene php.ini geladen werden.

Was im Ordner liegt

ZUGFeRD_Server/
  zugferd_server.ini    host, port
  Caddyfile             Webserver-Konfiguration
  start_mac.command     bzw. start_windows.bat
  stop_mac.command      bzw. stop_windows.bat
  programm/             FrankenPHP
  app/                  der PHP-Dienst, unveraendert
  logs/                 entsteht beim ersten Start
  VERSION.txt
  LIESMICH.txt

Der PHP-Code ist gegenüber Version 3 unverändert. Belegt haben wir das mit dem Prüfstand: Alle 13 Testfälle laufen über FrankenPHP in beiden Formaten mit HTTP 200, und die 26 erzeugten XML-Dateien sind byte-gleich mit dem Lauf über einen klassischen PHP-Server. Bei der Validierung nach EN 16931 gibt es keine Fehler, bis auf einen Testfall, der absichtlich falsch ist.

Sicherheit ohne .htaccess

Caddy liest keine .htaccess-Dateien. Die Protokolle des Dienstes enthalten aber vollständige Rechnungsdaten bis hin zur IBAN. Deshalb sperrt das Caddyfile die sensiblen Pfade selbst, ohne Rücksicht auf Groß- und Kleinschreibung (das Mac-Dateisystem unterscheidet sie nicht):

@gesperrt path_regexp (?i)(/\.|^/vendor(/|$)|
  ^/ki_mapping/(logs|mappings|csv_uploads)(/|$)|
  ^/uploads/.*\.(txt|log|php\d?|phtml|phar)$|
  \.log$|config\.local\.php$)
respond @gesperrt "403 - gesperrt" 403

(Im Original eine Zeile.) Dazu kommen bind 127.0.0.1, admin off und auto_https off: Der Dienst ist nur vom eigenen Rechner aus erreichbar.

Starten

Das Startscript liest Host und Port aus der zugferd_server.ini und gibt sie als Umgebungsvariablen an das Caddyfile weiter. Auf dem Mac wählt es die passende Binärdatei und entfernt die Quarantäne-Markierung, die macOS beim Herunterladen setzt:

case "$(uname -m)" in
  arm64) PROG=programm/frankenphp-arm64 ;;
  *)     PROG=programm/frankenphp-x86_64 ;;
esac
xattr -dr com.apple.quarantine . 2>/dev/null
nohup "$PROG" run --config Caddyfile \
  > logs/server.log 2>&1 &

Unter Windows setzt start_windows.bat die Variable PHPRC auf den Ordner programm\, damit FrankenPHP die mitgelieferte php.ini findet. Das Erweiterungsverzeichnis kommt als Umgebungsvariable hinein, so funktioniert der Ordner an jedem Ort:

extension_dir = "${ZF_EXT}"

4. Warum die Einrichtung mit einem Script beginnt

Was FileMaker nicht per Script kann

Die Kundendatei braucht genau ein Tabellenauftreten auf den Creator. Tabellenauftreten und externe Datenquellen lassen sich in FileMaker nicht per Script anlegen. Das muss der Anwender in „Datenbank verwalten“ selbst tun.

Wir können ihm diesen Schritt nicht abnehmen. Aber wir können dafür sorgen, dass er ihn nur einmal, an der richtigen Stelle und mit Prüfung macht. Das ist die Aufgabe von ZF_Einrichten:

  1. Ein Dialog sagt in sechs Zeilen, was gleich zu tun ist.
  2. Das Script öffnet „Datenbank verwalten“ (Open Manage Database). Dafür braucht der Anwender volle Zugriffsrechte auf seine Datei.
  3. Danach prüft es selbst, ob alles stimmt, und meldet das Ergebnis.

Prüfen mit den Systemtabellen

Die Prüfung läuft über ExecuteSQL auf die Systemtabelle FileMaker_Tables. Sie liefert zu jedem Tabellenauftreten den Namen der Datei, aus der es stammt:

ExecuteSQL (
  "SELECT BaseFileName FROM FileMaker_Tables
   WHERE TableName = ?" ;
  "" ; "" ; "_preferences_x" )

Damit sind drei Fehlerbilder sauber unterscheidbar:

  • Kein Ergebnis: Das Tabellenauftreten fehlt noch.
  • Es gibt ein _preferences_x 2: FileMaker hat beim Hinzufügen einen Zusatz vergeben, weil der Name schon belegt war. Das Script sucht per LIKE '_preferences_x%' danach und sagt, was umzubenennen ist.
  • Das Auftreten ist da, aber eine Abfrage auf _url liefert ?: Der Creator ist nicht erreichbar.

Ein verworfener Weg

Zuerst hatte ZF_Einrichten zwei Schritte: erst „Externe Datenquellen verwalten“, dann „Datenbank verwalten“. Im Test hat schon der erste Schritt ein Tabellenauftreten im Diagramm angelegt, und der zweite ergab dann _preferences_x 2. Jetzt ist es ein Schritt: Die Datenquelle wird direkt im Klappmenü von „Tabelle hinzufügen“ angelegt. Das ist für den Anwender einfacher und erzeugt genau ein Auftreten.

Eine Beziehung braucht es nicht. _preferences_x steht allein im Graphen. Alles, was die Kundendatei vom Creator braucht, sind globale Felder und Scripte.


5. Scripte in einer anderen Datei aufrufen: nach Name

Das Problem beim Kopieren

Wer schon einmal ein Script von einer Datei in eine andere kopiert hat, kennt das: Ein Script ausführen auf ein Script in einer anderen Datei kommt als <unbekannt> an. Der Anwender müsste das Ziel von Hand neu setzen. Bei zwei Aufrufen ist das zu viel verlangt.

Die Lösung

Script ausführen kann das Ziel nach Name nehmen, als Berechnung. Ein Name der Form Datenquelle::Script ruft ein Script in einer anderen Datei auf. Eine Berechnung übersteht das Kopieren, weil sie nur Text ist.

Bleibt die Frage, wie die Datenquelle heißt. Auch das beantwortet FileMaker_Tables:

Set Variable [ $quelle ; Value: ExecuteSQL (
  "SELECT BaseFileName FROM FileMaker_Tables
   WHERE TableName = ?" ;
  "" ; "" ; "_preferences_x" ) ]

Perform Script [ Specified: By name ;
  $quelle & "::ZF_Belegart_holen" ;
  Parameter: JSONSetElement ( "{}" ;
    "beleg" ; $beleg ; JSONString ) ]

Damit erreichen wir drei Dinge:

  • Nichts nachzutragen: Der Anwender fügt das Script ein und setzt kein einziges Ziel von Hand.
  • Unabhängig vom Dateinamen: Benennt jemand den Creator um, funktioniert der Aufruf weiter, solange das Tabellenauftreten auf ihn zeigt.
  • Reihenfolge egal: Die Namen werden erst zur Laufzeit aufgelöst. ZF_Einrichten und ZF_Kundendatei lassen sich deshalb in einem Schwung einfügen.

Daten über die Dateigrenze

Für das Hin und Her gilt eine einfache Regel: Parameter und Ergebnisse sind JSON, Dateien gehen über globale Container.

Die Beleg-PDF legt die Kundendatei in den globalen Container _preferences_x::pdf_zugferd_container des Creators. Der Creator lädt sie von dort hoch und legt das Ergebnis wieder in einen globalen Container. Die Kundendatei holt es ab und schreibt es in ihren eigenen Zielcontainer.

Globale Felder sind je Sitzung getrennt. Arbeiten zehn Anwender gleichzeitig, sieht jeder nur seinen eigenen Container, und keiner überschreibt die PDF des anderen.


6. ZF_Kundendatei: das eine Script beim Kunden

ZF_Kundendatei enthält keinen einzigen Tabellen- oder Feldnamen der Kundendatei. Alles, was es über die Datei wissen muss, kommt aus der Zuordnung im Creator. Es kennt vier Modi:

  • erzeugen (Standard): E-Rechnung bauen und am Beleg ablegen.
  • felder: Tabellenauftreten, Layouts und Felder dieser Datei für die Zuordnungsseite liefern.
  • werte: eine Zuordnung auswerten, ohne etwas zu erzeugen. Das ist die Live-Vorschau.
  • zuordnung: den Knopf zum Öffnen der Zuordnung im Creator bedienen.

Der Ablauf beim Erzeugen

1. Belegart holen. ZF_Belegart_holen im Creator liest die Belegart per ExecuteSQL aus der Tabelle _belegart und gibt alles als ein JSON-Objekt zurück: Typcode, Layout, Schlüsselfeld, PDF-Quelle, Zielcontainer, Positions-Tabellenauftreten und die Zuordnung selbst.

2. Beleg suchen, im unsichtbaren Fenster. Damit die Fundmenge des Anwenders unberührt bleibt, arbeitet das Script in einem Fenster namens add_action: 1 × 1 Pixel groß, bei −30000/−30000, ohne Menü und Symbolleisten. Das Layout kommt aus der Belegart und wird über eine Variable angesprungen. Gesucht wird mit Set Field By Name:

Go to Layout [ $layout ]
Enter Find Mode [ Pause: Off ]
Set Field By Name [ $schluessel_feld ; $nr ]
Perform Find []
If [ Get ( FoundCount ) > 1 ]
  # exakt suchen
  Set Field By Name [ $schluessel_feld ; "==" & $nr ]
  ...

3. Kopfwerte auswerten. Jeder Wert der Zuordnung ist ein FileMaker-Ausdruck: ein Feld, eine Formel oder ein fester Text in Anführungszeichen. Das Script geht die Schlüssel durch und wertet jeden mit Evaluate aus, im Kontext des gefundenen Belegs. Genau hier zahlt sich die Umkehr aus: Die Ausdrücke nennen Tabellenauftreten der Kundendatei, und sie werden in der Kundendatei berechnet. Beziehungen, Berechnungsfelder, eigene Funktionen: Alles steht zur Verfügung.

4. Positionen per SQL. Die Positionen kommen nicht über ein Portal, sondern über eine ExecuteSQL-Abfrage auf das Positions-Tabellenauftreten, mit neun Spalten in fester Reihenfolge: Positionsnummer, Bezeichnung, Artikelnummer, Menge, Einheit, Nettopreis, Steuersatz, Bestellbezug und Rabatt. Das ist unabhängig vom Layout und schnell, auch über WAN.

Zwei Sonderfälle sind abgefangen:

  • Nennt eine Positionsspalte ein Feld aus dem Kopf (zum Beispiel die Bestellnummer der Rechnung), wird es einmal ausgewertet und als fester Text in jede Zeile gesetzt.
  • Ist die erste Spalte ein fester Text, entfällt das ORDER BY, weil ExecuteSQL das Sortieren nach einem Literal ablehnt.

5. Senden und ablegen. Die Beleg-PDF geht in den globalen Container, ZF_Beleg_senden wird nach Name gerufen. Das Ergebnis schreibt die Kundendatei mit Set Field By Name in ihren Zielcontainer. Dessen Name steht in der Belegart, die Quelle nennt der Creator in seiner Antwort:

Set Field By Name [ $ziel ;
  GetField ( JSONGetElement ( $r ; "container" ) ) ]

6. Aufräumen. Erst wenn das Arbeitsfenster geschlossen ist, darf FileMaker den Anwender ansprechen. Im unsichtbaren Fenster gibt es keine Dialoge. Fehler werden gesammelt und am Ende einmal gemeldet, oder bei "still": true nur als JSON zurückgegeben.


7. ZF_Beleg_senden: der Server-Teil im Creator

Im Creator wird nichts mehr gesucht und nichts mehr ausgewertet. ZF_Beleg_senden bekommt die fertigen Werte und spricht nur noch mit dem Server.

Alle Adressen aus einem Feld. Es gibt nur noch ein maßgebliches Feld, _url. Daraus leitet das Script die Basis ab, egal ob dort die volle Adresse einer .php-Datei steht oder nur der Ordner, und hängt die festen Dateinamen an.

Die PDF hochladen mit Aus URL einfügen und cURL-Optionen. Eine Variable mit Containerinhalt lässt sich direkt hochladen:

--request POST
--upload-file $pdf
--header "Content-Type: application/pdf"

Die Werte senden als application/x-www-form-urlencoded, jeder Schlüssel einmal und URL-kodiert. Die Positionen gehen als eine Zeichenkette mit | zwischen den Zeilen und ; zwischen den Spalten mit.

Das Ergebnis abholen in den globalen Container, im eigenen Arbeitsfenster. Geprüft wird nicht nur der Fehlercode, sondern auch Dateiname und Mindestgröße des Containers.

Verständliche Fehler. Läuft der Server nicht, liefert Aus URL einfügen den Fehler 1631. Statt dieser Zahl bekommt der Anwender den Satz, dass der Server nicht antwortet und wo er ihn startet. Einen automatischen Start beim Erzeugen haben wir bewusst nicht eingebaut: Der Anwender soll wissen, dass da ein Dienst läuft.


8. Server wählen: eine Datei statt einer Adresse

Niemand soll eine Adresse wie http://127.0.0.1:8120/zugferd_import_post_data.php abtippen. Die Idee: Der Anwender zeigt dem Creator den Server-Ordner, und der Creator leitet die Adresse selbst ab.

FileMaker kann keinen Ordner auswählen, nur Dateien. Deshalb liegt im Server-Ordner eine Erkennungsdatei, zugferd_server.ini:

host=127.0.0.1
port=8120
url=

ZF_Server_waehlen macht daraus in zwei Schritten Adresse und Ordner:

  1. Datei einfügen als Verweis in einen globalen Container. Der Dialog ist der normale Dateidialog. Als Verweis enthält der Container keinen Inhalt, sondern den Pfad (filemac:/… bzw. filewin:/…). Daraus ergibt sich der Server-Ordner, den sich der Creator für den Start merkt.
  2. Ein zweites Datei einfügen, diesmal eingebettet und ohne Dialog, aus genau diesem Pfad. Jetzt liegt der Inhalt im Container, und TextDecode macht daraus Text.

Der zweite Schritt ist nötig, weil TextDecode auf einen Verweis nur die Pfadzeilen liefert, nicht den Inhalt der Datei. Ein zweiter Stolperstein: TextDecode gibt die Zeilenenden der Datei als (Zeichen 13) zurück. Wer auf Char ( 10 ) zerlegt, findet nichts.

Danach füllt das Script _url und fragt den Server über install_check.php, ob er antwortet. Läuft er noch nicht, wird die Adresse trotzdem übernommen, denn gestartet wird im nächsten Schritt.


9. Server starten aus FileMaker

ZF_Server_starten prüft zuerst, ob der Server schon antwortet. Dann prüft es, ob er überhaupt auf diesem Rechner liegt: Nur 127.0.0.1 und localhost startet FileMaker selbst. Danach ruft es das Startscript im gemerkten Ordner auf:

  • Mac: AppleScript ausführen mit do shell script. Der Pfad steht in einfachen Anführungszeichen, damit Leerzeichen im Pfad nicht stören.
  • Windows: Ereignis senden mit cmd /c "…\start_windows.bat".

Danach fragt das Script bis zu zehn Sekunden lang alle halbe Sekunde, ob der Server antwortet. Kommt er nicht, verweist die Meldung auf logs/server.log, wo FrankenPHP den Grund hinschreibt.

Beide Startscripte kehren sofort zurück. Der Server läuft im Hintergrund weiter, bis der Rechner neu startet.

Server gestartet, unten rechts der Status auf der Startseite


10. Webviewer I: die Startseite mit Serverstatus

Die Startseite des Creators ist eine HTML-Seite, die in einem Feld liegt. Der Webviewer zeigt sie als data:-URL an:

"data:text/html;base64," & Base64Encode (
  Substitute ( _preferences_x::<Feld> ;
    "/*ZF_URL*/" ; _preferences_x::_url ) )

Vor dem Anzeigen ersetzt die Formel den Platzhalter /*ZF_URL*/ durch die Server-Adresse. Die Seite kennt damit die Adresse, ohne dass ein Script sie hineinschreiben muss.

Der Status ist ein einfacher fetch. Eine Seite unter einer data:-URL darf 127.0.0.1 nicht lesen, das verhindert CORS. Wir müssen den Inhalt aber gar nicht lesen. Mit mode: "no-cors" bekommt die Seite eine undurchsichtige Antwort, und schon dass überhaupt eine Antwort kommt, heißt: Der Server läuft. Kommt keine, scheitert der fetch.

const ab = new AbortController();
setTimeout(() => ab.abort(), 3000);
await fetch(basis + "zf_status_ping", {
  mode: "no-cors", cache: "no-store",
  signal: ab.signal });
// keine Ausnahme -> Server laeuft

Die Seite fragt alle zehn Sekunden, und nur, solange sie sichtbar ist. Der Knopf „Server starten“ ruft per FileMaker.PerformScript das Script ZF_Server_starten auf. Er erscheint nur, wenn die Adresse lokal ist und die Seite in FileMaker läuft.

<Feld> ist das Feld, in dem die Seite liegt. Die Webviewer-Option „JavaScript darf FileMaker-Scripts ausführen“ muss eingeschaltet sein.


11. Die Zuordnung: der schwierigste Teil

Das Problem

Für das Erzeugen hat die Umkehr alles einfacher gemacht. Für die Zuordnung hat sie ein neues Problem geschaffen. Die Zuordnungsseite im Creator braucht nämlich zwei Dinge aus der Kundendatei:

  • die Liste der Tabellenauftreten, Felder und Layouts, damit man beim Tippen auswählen kann,
  • eine Live-Vorschau: den echten Wert jeder Zeile aus einer echten Rechnung.

Der Creator kennt die Kundendatei aber gar nicht mehr.

Die Lösung: eine Datenquelle mit variablem Pfad

FileMaker erlaubt in der Pfadliste einer externen Datenquelle eine globale Variable. Der Creator wird mit genau einer solchen Datenquelle ausgeliefert:

Name:       Kunde
Typ:        FileMaker
Pfadliste:  $$zf_kunde_pfad

Ohne Tabellenauftreten, ohne Beziehung. Sie dient nur dazu, Scripte der Kundendatei nach Name zu rufen: Kunde::ZF_Kundendatei.

Der Ablauf:

  1. In der Kundendatei liegt ein Knopf (nur für die Einrichtung) mit ZF_Kundendatei im Modus zuordnung.
  2. ZF_Kundendatei ruft im Creator ZF_Zuordnung_oeffnen und gibt dabei Get ( FilePath ) mit.
  3. Der Creator setzt $$zf_kunde_pfad auf diesen Pfad und öffnet die Zuordnung in einem eigenen, sichtbaren Fenster.
  4. Ab jetzt ruft der Creator über Kunde::ZF_Kundendatei zurück, mit modus: "felder" für die Feldliste und modus: "werte" für die Vorschau.

Damit kommt die Kette Kunde → Creator → Kunde → Creator zustande, und das ohne einen einzigen Einrichtungsschritt beim Kunden. Ein Pfad der Form file:/Macintosh HD/…/Datei.fmp12 wird als Datenquellenpfad angenommen.

Ein wichtiges Verhalten

FileMaker löst die Datenquelle beim ersten Zugriff auf und behält den Pfad für die Sitzung. Öffnet man die Zuordnung später aus einer anderen Kundendatei, zeigt Kunde weiter auf die erste. ZF_Zuordnung_oeffnen erkennt das, weil sich der neue Pfad vom gemerkten unterscheidet, und sagt in der Statuszeile, dass der Creator neu zu öffnen ist.

Verworfene Wege

  • Vorschau nur beim Öffnen, also alle Werte einmal mitgeben: umständlich, weil jede Änderung an der Zuordnung einen neuen Knopfdruck in der Kundendatei verlangt hätte.
  • Rückruf per fmp://-URL: Dafür braucht das Konto das erweiterte Recht fmurlscript. Das wäre wieder ein Einrichtungsschritt.
  • Eine feste Datenquelle auf die Kundendatei, angelegt in ZF_Einrichten: Das wäre der Rückfallweg gewesen, falls der variable Pfad nicht funktioniert. Wir haben ihn nicht gebraucht.

Mengen: Felder nachladen

Unsere Testdatei, eine gewachsene CRM-Lösung, liefert 36.308 Felder und 316 Layouts. Als ein Block ist das zu viel für den Webviewer und für jedes Auswahlmenü.

Deshalb liefert modus: "felder" ohne weitere Angabe nur die Tabellenauftreten und Layouts. Die Felder kommen je Tabellenauftreten, wenn sie gebraucht werden: erst für die Auftreten, die in der Zuordnung schon vorkommen, und dann für jedes, das der Anwender neu auswählt. Der Creator hält sie in einem globalen Feld zwischen, solange der Pfad gleich bleibt.


12. Webviewer II: eine Seite, die mit FileMaker spricht

Die Seite kommt aus einem Feld

Wie die Startseite liegt auch die Zuordnungsseite als HTML-Vorlage in einem Feld des Creators. Im Script der Seite steht ein Datenblock zwischen zwei Markierungen:

const DATEN = /*DATEN*/{ … }/*ENDE*/;

ZF_Zuordnung_anzeigen ersetzt diesen Block durch die echten Daten: Belegarten, Tabellenauftreten, Layouts, geladene Felder, Vorschau, Statuszeile. Das Ergebnis landet in einem globalen Feld, und der Webviewer zeigt es per data:-URL mit Base64Encode an. Die URL-kodierte Form mit GetAsURLEncoded hat FileMaker 19 im Test als Text angezeigt, deshalb Base64.

Ohne FileMaker läuft die Seite mit Musterdaten im Browser. Das macht die Entwicklung der Oberfläche sehr angenehm.

Hinweg: JavaScript ruft FileMaker

Jede Aktion auf der Seite ruft ein Script im Creator:

function fm(script, param) {
  if (window.FileMaker) {
    FileMaker.PerformScript(script,
      JSON.stringify(param));
    return true;
  }
  return false;   // im Browser: Musterdaten
}

Rückweg: die Seite neu bauen

Für den Rückweg gibt es in FileMaker den Schritt „JavaScript in Webviewer ausführen“. Der hat in unseren Tests nichts bewirkt, ohne Fehlermeldung. Statt auf ihn zu bauen, nehmen wir einen Weg, der immer funktioniert:

FileMaker antwortet, indem es die Seite neu baut.

Damit dabei nichts verloren geht, schickt die Seite bei jedem Aufruf ihren ungespeicherten Zustand als entwurf mit: alle Belegarten mit Änderungen, den aktiven Bereich, die Belegnummer der Vorschau, die KI-Hinweise und sogar das Feld, in dem der Cursor stand. FileMaker fasst den Entwurf nicht an, sondern gibt ihn unverändert in den neuen Datenblock. Die neue Seite liest ihn, stellt den Zustand wieder her und setzt den Cursor zurück:

const ENTWURF = DATEN.entwurf &&
  Array.isArray(DATEN.entwurf.belegarten)
    ? DATEN.entwurf : null;
if (ENTWURF) {
  belegarten = ENTWURF.belegarten;
  bereich = ENTWURF.bereich || bereich;
  geaendert = !!ENTWURF.geaendert;
}

Für den Anwender sieht das aus wie eine Seite, die einfach weiterläuft.

Die Vorschau wertet den Entwurf aus

Ein Detail, das im Alltag viel ausmacht: Die Vorschau wertet den ungespeicherten Stand aus. In Version 3 wurde vor jeder Vorschau gespeichert. Jetzt schickt ZF_Zuordnung_vorschau die Belegart so, wie sie gerade auf der Seite steht, als belegart_json an Kunde::ZF_Kundendatei im Modus werte. Die Kundendatei wertet sie aus, ohne etwas zu erzeugen, und liefert die Werte zurück.

Neben jeder Zeile steht dann der echte Wert: grün, wenn er da ist, rot, wenn er fehlt. Gespeichert wird nur mit dem Knopf Speichern. Man kann also gefahrlos ausprobieren.

Auswahl beim Tippen

Die Auswahl der Felder beim Tippen braucht keine Bibliothek. Die Seite füllt HTML-<datalist>-Elemente mit Tabellenauftreten, Layouts und den geladenen Feldern. Wählt der Anwender ein Tabellenauftreten, dessen Felder noch nicht geladen sind, ruft die Seite ZF_Zuordnung_felder. FileMaker holt die Felder aus der Kundendatei, baut die Seite neu, und der Entwurf kommt wie beschrieben mit zurück.

KI-Vorschlag

Auf Wunsch füllt ein KI-Vorschlag die meisten Zeilen aus. ZF_Zuordnung_ki schickt die geladene Feldliste und die Einstellungen der Belegart an einen Endpunkt des eigenen ZUGFeRD-Servers. Der fragt mit dem hinterlegten API-Schlüssel ein Sprachmodell (Claude). Die Vorschläge werden nicht gespeichert. Sie erscheinen auf der Seite, als „Neu“ oder „Geändert“ markiert, und der Anwender prüft sie mit der Vorschau, bevor er speichert.


13. Was wir bewusst offen sagen

Kein Werkzeug ist ohne Grenzen. Das wissen wir heute:

  • Eine Kundendatei je Sitzung: Wie in Abschnitt 11 beschrieben, behält FileMaker den Pfad der Datenquelle für die Sitzung.
  • Dateizugriffsschutz: Im Creator muss er ausgeschaltet sein, damit die Kundendatei ihn einbinden darf.
  • Windows-Fenster: Unter Windows läuft der Server in einem minimierten Konsolenfenster. Wer es schließt, beendet den Server.
  • Nicht in Cloud-Ordnern: Dropbox, OneDrive und iCloud Drive lagern Dateien aus oder sperren sie beim Abgleich. Der Server-Ordner gehört auf eine lokale Platte.

14. Was wir mitnehmen

Die Technik in diesem Artikel ist kein Selbstzweck. Fast jede Entscheidung folgt aus der Frage vom Anfang, was das Wenigste ist, das ein Anwender tun muss. Ein paar Grundsätze lassen sich auf andere FileMaker-Projekte übertragen:

  • Die Abhängigkeit umdrehen. Ein Werkzeug, das die Kundendatei nicht kennen muss, lässt sich überall einsetzen. Ausgewertet wird dort, wo die Daten liegen.
  • Nach Name aufrufen. Script ausführen nach Name mit Datenquelle::Script, die Datenquelle aus FileMaker_Tables: Damit übersteht ein Script das Kopieren in jede Datei.
  • Systemtabellen nutzen. FileMaker_Tables und FileMaker_Fields beantworten per ExecuteSQL fast jede Frage nach der Struktur einer Datei, ohne Layoutwechsel.
  • Den Einrichtungsschritt prüfen, wenn man ihn nicht abnehmen kann. Was FileMaker nicht per Script erlaubt, kann ein Script wenigstens vorbereiten und danach kontrollieren.
  • Den Webviewer neu bauen statt fernsteuern. Wenn die Seite ihren Zustand selbst mitschickt, ist das Neuzeichnen verlässlicher als jeder Aufruf von außen.
  • Geprüften Code kapseln statt neu schreiben. FrankenPHP macht aus einem PHP-Projekt einen Ordner, ohne eine Zeile PHP zu ändern.

Am Ende steht für den Anwender: ein Ordner, zwei Scripte, ein Knopf. Und für uns ein Werkzeug, das wir mit gutem Gewissen an jede FileMaker-Lösung geben können.


Fragen, Anmerkungen, eigene Erfahrungen? Wir tauschen uns gern mit anderen FileMaker-Entwicklern aus.

Kontakt aufnehmen

FileMaker Experts · MaRo Programmierung