Eine konforme elektronische Rechnung ist kein PDF mit einer seitlich angehefteten XML-Datei. Es ist ein einzelnes PDF/A-3-Dokument, das die Rechnung zweimal enthält: einmal als eine Seite, die ein Mensch liest, und einmal als maschinenlesbares Cross-Industry-Invoice-XML, das innerhalb der Datei als zugehörige Datei (Associated File) gespeichert ist. Die beiden Repräsentationen beschreiben dieselbe Rechnung. Diese duale Natur ist der eigentliche Sinn der Formatfamilien, die europäische Mandate heute fordern: Factur-X in Frankreich und Deutschland, ZUGFeRD in den deutschsprachigen Märkten und XRechnung für die Rechnungsstellung im deutschen öffentlichen Sektor. Dieser Artikel geht Schritt für Schritt durch, wie PDFlibPas eine solche hybride Rechnung in Delphi zusammenstellt, wo die Standards Spielraum für Fehler lassen und warum ein Profil im Katalog einen völlig separaten XML-Builder benötigt
Was eine hybride Rechnung eigentlich ist
Die sichtbare Seite und das eingebettete XML bedienen unterschiedliche Leser. Ein Sachbearbeiter, der eine Zahlung freigibt, betrachtet die gerenderte Seite. Ein Kreditorenbuchhaltungssystem erfasst das XML, liest die Summen und die Steueraufschlüsselung als strukturierte Felder und verbucht den Eintrag, ohne dass ein Mensch etwas eintippen muss. Der semantische Inhalt dieses XML wird durch die EN 16931 geregelt, den europäischen Standard, der das Datenmodell der Rechnung definiert: welche Felder existieren, was sie bedeuten und welche obligatorisch sind. EN 16931 ist ein semantisches Modell, kein Dateiformat. Factur-X, ZUGFeRD 2.x und XRechnung setzen alle dieses Modell als ein UN/CEFACT-Cross-Industry-Invoice-Dokument um, die Syntax, die die EN-16931-Felder über die Leitung transportiert
Damit das Dokument sowohl archivierbar als auch selbstbeschreibend ist, ist der Container PDF/A-3, definiert durch ISO 19005-3. PDF/A-3 ist die Konformitätsstufe, die beliebige eingebettete Dateien zulässt, was genau das ist, was ein Rechnungs-XML sein muss. PDF/A-2 verbietet das Einbetten von Dateien, die nicht selbst PDF/A sind, daher kann eine Factur-X-Rechnung kein PDF/A-2 sein. Die Wahl von PDF/A-3 ist somit keine Vorliebe, sondern eine Anforderung, die sich direkt aus dem Wunsch ergibt, Nicht-PDF-Daten in ein Archivdokument einzubetten
Warum die Beziehung „Alternative“ lautet
Das Einbetten der Bytes ist der einfache Teil. ISO 32000 §7.11.4 definiert den eingebetteten Dateistream (Embedded File Stream), das Objekt, das das rohe XML und seine Parameter aufnimmt. Der Teil, der die Datei zu einer gültigen zugehörigen Datei (Associated File) macht, ist §14.13, der das Konzept einer zugehörigen Datei und den Schlüssel /AFRelationship hinzufügt. Dieser Schlüssel gibt an, wie sich die eingebetteten Daten zu dem Inhalt verhalten, an den sie angehängt sind, und der Wert, den Factur-X vorschreibt, lautet Alternative
Die Wahl ist wichtig, weil die anderen Werte etwas Falsches über das Dokument aussagen würden. Source würde bedeuten, dass das XML das Material ist, aus dem der sichtbare Inhalt generiert wurde, ein Master, von dem die Seite abgeleitet ist. Supplement würde bedeuten, dass das XML Informationen hinzufügt, die über das hinausgehen, was die Seite zeigt, ein Extra, das nicht im Rendering enthalten ist. Beides trifft auf eine Factur-X-Rechnung nicht zu. Das XML und die Seite sind zwei gleichwertige Ausdrücke einer einzigen Rechnung, die denselben rechtlichen Inhalt in zwei Formen transportieren. Alternative ist der Wert, der genau das aussagt: eine gleichwertige alternative Repräsentation des sichtbaren Inhalts. Ein Validator, der eine andere Beziehung in einer Factur-X-Datei liest, wird sie ablehnen, und das zu Recht, da die Beziehung eine maschinenlesbare Behauptung darüber ist, wozu der Anhang dient
Der Profilkatalog
Das E-Rechnungs-Beispiel, das mit PDFlibPas ausgeliefert wird, steuert denselben Generierungspfad über sechs Profile hinweg, die als ein Array von Records in InvoiceModel.pas definiert sind. Jedes Profil trägt die Werte, die der Writer benötigt: einen Anzeigenamen, den Namen der eingebetteten Datei, einen Konformitätsgrad, die /AFRelationship, eine Version, einen optionalen Ländercode und die GuidelineID-URN, die das XML innerhalb seines Dokumentenkontextes ankündigt
Die sechs sind Factur-X EN16931, Factur-X BASIC, Factur-X EXTENDED für Frankreich, XRechnung 3.0, ZUGFeRD 1.0 COMFORT und ZUGFeRD 2.0 BASIC. Die GuidelineID ist das Feld, das einem Empfänger genau sagt, welches Profil er erwarten darf, und die Werte sind spezifisch. Factur-X EN16931 kündigt urn:cen.eu:en16931:2017 an. XRechnung 3.0 kündigt urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0 an. ZUGFeRD 2.0 BASIC kündigt urn:cen.eu:en16931:2017#compliant#urn:zugferd.de:2p0:basic an. Der Name der eingebetteten Datei ist ebenfalls Teil des Vertrages. Factur-X-Profile betten factur-x.xml ein, XRechnung bettet xrechnung.xml ein, und die ZUGFeRD-Profile betten ZUGFeRD-invoice.xml oder zugferd-invoice.xml ein. Ein Empfänger durchsucht die Namen der Anhänge, um die Rechnung zu finden, der Dateiname ist also nicht nur Kosmetik
Ein Detail im Katalog ist es wert, genau gelesen zu werden. Die meisten Profile verwenden die Beziehung Alternative, aber der Eintrag für XRechnung 3.0 im Beispielprogramm verwendet Source. Die beiden Formate unterliegen unterschiedlichen Validatoren und Konventionen, und das Beispiel legt die Beziehung jedes Profils aus dem Katalog fest, anstatt einen einzelnen Wert fest zu codieren, weshalb das profilbezogene Feld anstelle einer Konstante existiert
Die ZUGFeRD-1.0-Falle
Es ist verlockend anzunehmen, dass jedes Profil die Cross Industry Invoice nach EN 16931 darstellt, mit geringfügigen Variationen darin, wie viele optionale Felder Sie ausfüllen. Das gilt für fünf der sechs. Es gilt nicht für ZUGFeRD 1.0 COMFORT, und der Grund ist struktureller und nicht kosmetischer Natur
Die modernen Profile emittieren eine UN/CEFACT-Cross-Industry-Invoice mit der Namespace-Version :100, deren Wurzelelement rsm:CrossIndustryInvoice ist. ZUGFeRD 1.0 ist älter als dieses Schema. Es ist das CrossIndustryDocument von 2014 mit der Namespace-Version :1p0, und sein Wurzelelement ist rsm:CrossIndustryDocument. Die Namespace-URNs unterscheiden sich, das Wurzelelement unterscheidet sich, und der Elementbaum weicht durchgehend ab: Das :1p0-Schema gruppiert Daten unter ApplicableSupplyChainTradeAgreement, ApplicableSupplyChainTradeDelivery und ApplicableSupplyChainTradeSettlement, wo :100 die Elemente ApplicableHeaderTradeAgreement, ApplicableHeaderTradeDelivery und ApplicableHeaderTradeSettlement verwendet. Die Benennung ist einander ähnlich genug, um irrezuführen, und unterschiedlich genug, um zu Fehlern zu führen
Das Wort COMFORT im Profilnamen beschreibt, wie reichhaltig die Daten sind, ein automatisierungstaugliches Profil mit vollständigen Positionen, Steueraufschlüsselung und Zahlungsbedingungen, und nicht, welches Schema diese Daten transportiert. Sie können also kein :100-Dokument nehmen und es für ZUGFeRD 1.0 umetikettieren. Das Beispielprogramm handhabt dies mit einem Flag in jedem Profil-Record und zwei separaten Builder-Funktionen, wobei die richtige ausgewählt wird, bevor überhaupt XML generiert wird
function BuildInvoiceXMLText(const AProfile: TeInvoiceProfile;
const Data: TInvoiceData): string;
begin
// XMLFamily = 1 means the legacy ZUGFeRD 1.0 :1p0 schema; every
// other profile is the modern UN/CEFACT :100 Cross Industry Invoice.
if AProfile.XMLFamily = 1 then
Result := BuildZUGFeRD1Text(AProfile, Data)
else
Result := BuildCII100Text(AProfile, Data);
end;
Diese Aufteilung ist keine implementierungstechnische Spielerei. Das Füttern eines :100-Baums an einen ZUGFeRD-1.0-Empfänger führt zu einem Dokument, das die Schemavalidierung bereits beim Wurzelelement nicht besteht, daher müssen die beiden Familien von Code erstellt werden, der weiß, welche der beiden er gerade schreibt
Auswahl der PDF/A-3-Stufe
PDF/A-3 hat drei Konformitätsstufen, und PDFlibPas wählt diese über SetPDFAMode aus. Modus 5 ist PDF/A-3b, die Stufe, die eine zuverlässige visuelle Reproduktion garantiert. Modus 6 ist PDF/A-3a, die die Anforderungen an Tagged Structure und Barrierefreiheit der Stufe a (accessibility) hinzufügt. Modus 7 ist PDF/A-3u, bei der der gesamte Text auf Unicode abgebildet werden muss. Die Aktivierung des Modus bettet auch den in der Bibliothek eingebauten sRGB-Output-Intent ein, die Farbcharakterisierung, die PDF/A fordert, damit die gerenderte Farbe definiert und nicht geräteabhängig ist
Die meisten Rechnungs-Workflows laufen auf Stufe 3b, was für eine originalgetreue sichtbare Seite plus eingebettetem XML völlig ausreicht. Wenn Sie ein explizites ICC-Profil anstelle des eingebauten benötigen, wechselt LoadOutputIntentProfile dieses ein, nachdem der Modus gesetzt wurde. Das Beispiel lädt auf diese Weise das Repository-sRGB-Profil und fällt auf den eingebauten Intent zurück, wenn die Datei nicht erreichbar ist, sodass der Output-Intent immer vorhanden ist
PDF := TPDFlib.Create;
try
// Mode 5 = PDF/A-3b, 6 = PDF/A-3a, 7 = PDF/A-3u.
if PDF.SetPDFAMode(5) <> 1 then
raise Exception.Create('PDF/A-3 mode could not be enabled');
// Optional: swap the built-in sRGB intent for an explicit ICC profile.
if PDF.LoadOutputIntentProfile(ICCFile, 'DeviceRGB') <> 1 then
{ fall back to the built-in sRGB intent that SetPDFAMode embedded };
finally
// ... continue building the document
end;
Zusammenstellen der hybriden Rechnung
Wenn der Container konfiguriert ist, besteht der Rest aus drei aufeinanderfolgenden Schritten: PDF/A-3-Modus setzen, die menschenlesbare Seite zeichnen und dann das XML als zugehörige Datei (Associated File) anhängen. Die sichtbare Seite ist gewöhnlicher Inhalt. Die einzige Einschränkung, die man im Hinterkopf behalten sollte, ist, dass PDF/A die nicht eingebetteten Standard-14-Schriftarten verbietet, daher muss die Seite eine echte Schriftart einbetten, anstatt auf eine eingebaute zu referenzieren
Das Anhängen erfolgt in einem einzigen Aufruf. AddFacturXAssociatedFileFromString nimmt die rohen UTF-8-XML-Bytes plus die Profil-Metadaten, schreibt den eingebetteten Dateistream, registriert ihn in dem für PDF/A-3 erforderlichen /AF-Array des Katalogs, wendet die /AFRelationship an und generiert die XMP-E-Rechnungs-Metadaten, die das Dokument als Factur-X, ZUGFeRD oder XRechnung identifizieren. Die Methode prüft außerdem, ob die Guideline-ID des XML mit dem Konformitätsgrad übereinstimmt, den Sie angefordert haben, sodass eine Nichtübereinstimmung zwischen dem von Ihnen erstellten XML und dem von Ihnen benannten Profil abgefangen und nicht stillschweigend ausgeliefert wird
// 1. PDF/A-3 mode and output intent are already set.
// 2. Draw the visible page (embeds a real TrueType font).
DrawInvoicePage(PDF, AProfile, Data);
// 3. Build the profile-correct XML and attach it as an
// associated file with /AFRelationship = Alternative.
InvoiceXML := BuildInvoiceXML(AProfile, Data); // AnsiString of UTF-8 bytes
FileID := PDF.AddFacturXAssociatedFileFromString(
InvoiceXML,
AProfile.ConformanceLevel, // e.g. 'EN16931'
AProfile.FileName, // 'factur-x.xml'
AProfile.Description,
AProfile.Relationship, // 'Alternative'
AProfile.Version, // '1.0'
AProfile.CountryCode); // '' or 'DE' or 'FR'
if FileID <= 0 then
raise Exception.Create('Invoice XML could not be attached');
PDF.SaveToFile(TargetFile);
Eine subtile Feinheit im Datenpfad ist die Codierung. Das eingebettete XML deklariert encoding="UTF-8", und die Methode nimmt ihre Bytes als AnsiString entgegen, also muss ein Verkäufer- oder Käufername mit Nicht-ASCII-Zeichen den Aufruf als rohe UTF-8-Oktette erreichen. Ein einfacher Cast (Typumwandlung) über die ANSI-Codepage des Systems würde diese Zeichen beschädigen und unbemerkt eine Rechnung erzeugen, deren XML nicht mehr zur eigenen Deklaration passt. Das Beispiel codiert explizit nach UTF-8, bevor es die Bytes übergibt. Das ist der sichere Weg, um jede byte-orientierte PDF-API aus einem Unicode-string zu füttern
Zum Anhängen von XML, das kein erkanntes E-Rechnungs-Profil ist, ist AddPDFA3AssociatedFileFromString das generische Gegenstück. Die Methode übernimmt einen Dateinamen, MIME-Typ, Beschreibung, Beziehung und Bytes und schreibt eine einfache zugehörige PDF/A-3-Datei (Associated File) ohne rechnungsspezifische Metadaten oder Guideline-Checks. Verwenden Sie sie für ergänzende Daten; verwenden Sie die Factur-X-Methode für Rechnungen, damit die Profil-Metadaten und der Abgleich der Richtlinie (Guideline Match) für Sie geschrieben werden
Sobald das Dokument erstellt ist, stellt sich als Nächstes die Frage, ob es die PDF/A- und Barrierefreiheits-Validierung besteht und ob es signiert werden kann, ohne die Konformität zu brechen. Diese Themen werden in unserem Walkthrough zu PDF/A- und PDF/UA-Preflight in Delphi und der Compliance- und Signatur-Workbench behandelt. All dies wird als Teil der PDFlibPas Delphi PDF Library ausgeliefert, zusammen mit den APIs für PDF/A, Tagging und Dokumenteigenschaften, auf denen der E-Rechnungspfad aufbaut