3.39. ZUGFeRD

Überblick

Ein Zusammenschluss von Verbänden und Unternehmen der Wirtschaft und des Öffentlichen Dienstes, das Forum elektronische Rechnung Deutschland (FeRD), hat am 25.06.2014 die Version 1.0 eines XML-Formates für den Austausch elektronischer Rechnungen beschlossen. Die Spezifikation selber wird ZUGFeRD (Zentraler User Guide des Forums elektronische Rechnung Deutschland) genannt. Weitreichende Informationen findet man im Internet bei Wikipedia (ZUGFeRD) , bei 'FeRD' und bei der PDF-Association im 'Leitfaden zu PDF-A3 und ZUGFeRD'.

Viele Validierungstools überprüfen zwar die Übereinstimmung der XML-Daten mit der XML-Schema Spezifikation, nicht aber die Übereinstimmung der XML-Daten (unsichtbar) mit den Daten des gedruckten PDF-Dokumentes (sichtbar). Das ist mit PDFUnit einfach, sofern Sie einen Seitenbereich definieren können, in dem der zu prüfende Wert vorkommen muss.

Für Tests mit ZUGFeRD-Daten stehen diese Methoden zur Verfügung:

// Methods to test ZUGFeRD data:

.hasZugferdData().matchingXPath(xpathExpression)
.hasZugferdData().withNode(xmlNode)

.compliesWith().zugferdSpecification()

Die folgenden Beispiele beziehen sich auf das vom ZUGFeRD-Standard 1.0 mitgelieferte Beispiel-Dokument 'ZUGFeRD_1p0_BASIC_Einfach.pdf'. In jedem Beispiel wird zuerst der Test mit PDFUnit gezeigt, dann der dazugehörende Teil des PDF-Dokumentes und der XML-Daten.

Wenn Sie die ZUGFeRD-Daten eines Dokumentes sehen wollen, exportieren Sie sie am einfachsten, indem Sie das Dokument mit dem Adobe Reader® öffnen und von dort aus die angezeigte Datei 'ZUGFeRD-invoice.xml' speichern.

PDF-Inhalte mit ZUGFeRD-Inhalten vergleichen - IBAN

In diesem Beispiel wird ein Wert für die IBAN sowohl in den XML-Daten, als auch im PDF-Text gesucht. Das geschieht mit zwei AssertThat-Anweisungen. Für die Suche auf der PDF-Seite muss der passende Ausschnitt (regionIBAN) und für die Suche in den XML-Daten der Name des XML-Knotens angegeben werden (nodeIBAN).

@Test
public void validateIBAN() throws Exception {
  String filename = "ZUGFeRD_1p0_BASIC_Einfach.pdf";
  String expectedIBAN = "DE08700901001234567890";
  
  XMLNode nodeIBAN = new XMLNode("ram:IBANID", expectedIBAN);
  PageRegion regionIBAN = createRegionIBAN();
  
  AssertThat.document(filename)
            .hasZugferdData()
            .withNode(nodeIBAN)
  ;
  AssertThat.document(filename)
            .restrictedTo(FIRST_PAGE)
            .restrictedTo(regionIBAN)
            .hasText()
            .containing(expectedIBAN, WhitespaceProcessing.IGNORE)
  ;
}

Ausschnitt der PDF-Seite:

Ausschnitt der ZUGFeRD Daten:

PDF-Inhalte mit ZUGFeRD-Inhalten vergleichen - noch einfacher

Es geht aber noch einfacher, nämlich mit hasText().containingZugferdData(xmlNode). Die Methode extrahiert zuerst den Text aus dem angegebenen ZUGFeRD-Knoten und vergleicht diesen Text dann mit dem sichtbaren Text des angegebenen Seitenausschnitts. Der Vergleich findet intern mit der Funktion containing() statt, d.h. der XML-Text muss irgendwo im Seitenausschnitt gefunden werden. Zusätzlicher Text im Seitenausschnitt ist erlaubt.

@Test
public void validateIBAN_simplified() throws Exception {
  String filename = "ZUGFeRD_1p0_BASIC_Einfach.pdf";
  
  XMLNode nodeIBAN = new XMLNode("ram:IBANID");
  PageRegion regionIBAN = createRegionIBAN();
  
  AssertThat.document(filename)
            .restrictedTo(FIRST_PAGE)
            .restrictedTo(regionIBAN)
            .hasText()
            .containingZugferdData(nodeIBAN)
  ;
}

Wichtig zu wissen: Bei dem Vergleich werden alle Leerzeichen ignoriert. Das ist notwendig, denn Zeilenumbrüche und formatierende Leerzeichen haben im sichtbaren Text auf der PDF-Seite und innerhalb des unsichtbaren XML-Textes eine ganz verschiedene Bedeutung und sind demzufolge selten gleich.

PDF-Inhalte mit ZUGFeRD-Inhalten vergleichen - Rechnungsanschrift

Der vereinfachte Funktionsaufruf des vorigen Beispiels funktioniert aber nur, wenn die XML-Daten als Ganzes im vorgegebenen Seitenausschnitt enthalten sind. Im folgenden Beispiel steht die Postleitzahl in den XML-Daten getrennt vom Städtename getrennt. Deshalb muss für die Überprüfung der Rechnungsanschrift wieder mit zwei AssertThat-Aufrufen gearbeitet werden.

Weil der zu prüfende Text nur ein Teil des XML-Knotens ist, enthält der XPath-Ausdruck die Funktion contains(). Der Knoten ram:PostalTradeAddress enthält zusätzlich noch den Wert 'DE', der aber nicht im sichtbaren Text vorkommt.

@Test
public void validatePostalTradeAddress() throws Exception {
  String filename = "ZUGFeRD_1p0_BASIC_Einfach.pdf";
  String expectedAddressPDF = "Hans Muster "
                            + "Kundenstraße 15 "
                            + "69876 Frankfurt";
  String expectedAddressXML = "Hans Muster "
                            + "Kundenstraße 15 "
                            + "Frankfurt";
  String addressXMLNormalized = WhitespaceProcessing.NORMALIZE.process(expectedAddressXML);
  String xpathWithPlaceholder = 
         "ram:BuyerTradeParty/ram:PostalTradeAddress[contains(normalize-space(.), '%s')]";
  String xpathPostalTradeAddress = String.format(xpathWithPlaceholder, addressXMLNormalized);
  
  XMLNode nodePostalTradeAddress = new XMLNode(xpathPostalTradeAddress);
  PageRegion regionPostalTradeAddress = createRegionPostalAddress();
  
  AssertThat.document(filename)
            .hasZugferdData()
            .withNode(nodePostalTradeAddress)
  ;
  AssertThat.document(filename)
            .restrictedTo(FIRST_PAGE)
            .restrictedTo(regionPostalTradeAddress)
            .hasText()
            .containing(expectedAddressPDF)
  ;
}

In diesem Test unterscheiden sich die Whitespaces im PDF-Dokument von denen in den XML-Daten. Deshalb muss der XPath-Ausdruck die Funktion normalize-space() enthalten.

Ausschnitt der PDF-Seite:

Ausschnitt der ZUGFeRD Daten:

PDF-Inhalte mit ZUGFeRD-Inhalten vergleichen - Artikel

Das Neue am folgenden Beispiel ist die Tatsache, dass sich die Reihenfolge der Textteile des zu prüfenden Textes im PDF und in den XML-Daten unterscheidet. Deshalb kann nicht auf die zusammenhängende Artikelbezeichnung 'Trennblätter A4 GTIN: 4012345001235' geprüft werden. Der Test validiert lediglich 'GTIN: 4012345001235'. Dafür wird in XPath die Funktion contains() und in PDFUnit die Funktion hasText().containing() benutzt.

@Test
public void validateTradeProduct() throws Exception {
  String filename = "ZUGFeRD_1p0_BASIC_Einfach.pdf";
  String expectedTradeProduct = "GTIN: 4012345001235";
  String xpathWithPlaceholder = 
         "ram:SpecifiedTradeProduct/ram:Name[contains(., '%s')]";
  String xpathTradeProduct = String.format(xpathWithPlaceholder, expectedTradeProduct);
  
  XMLNode nodeTradeProduct = new XMLNode(xpathTradeProduct);
  PageRegion regionTradeProduct = createRegionTradeProduct();
  
  AssertThat.document(filename)
            .hasZugferdData()
            .withNode(nodeTradeProduct)
  ;
  AssertThat.document(filename)
            .restrictedTo(FIRST_PAGE)
            .restrictedTo(regionTradeProduct)
            .hasText()
            .containing(expectedTradeProduct)
  ;
}

Ausschnitt der PDF-Seite:

Ausschnitt der ZUGFeRD Daten:

Komplizierte Prüfungen auf ZUGFeRD-Daten

Es gibt gelegentlich den Wunsch, anspruchsvolle Prüfungen auf die XML-Daten anzuwenden. Diesen Wunsch unterstützt PDFUnit, indem es die Methode matchingXPath(..) zur Verfügung stellt, mit der das volle Potential von XPath auf die ZUGFeRD-Daten losgelassen werden kann.

Das folgende Beispiel überprüft, dass die Anzahl der fakturierten Artikel (TradeLineItems) genau '1' ist.

@Test
public void hasZugferdDataMatchingXPath() throws Exception {
  String filename = "ZUGFeRD_1p0_BASIC_Einfach.pdf";
  String xpathNumberOfTradeItems = "count(//ram:IncludedSupplyChainTradeLineItem) = 1";
  XPathExpression exprNumberOfTradeItems = new XPathExpression(xpathNumberOfTradeItems);
  AssertThat.document(filename)
            .hasZugferdData()
            .matchingXPath(exprNumberOfTradeItems)
  ;
}

Ausschnitt der ZUGFeRD Daten:

Noch anspruchsvoller ist es, mit Hilfe von XPath zu prüfen, dass die Summe der Preise aller bestellten Artikel der Gesamtsumme (netto) entspricht, die an anderer Stelle im Dokument gespeichert ist. Ob eine solche Prüfung Gegenstand automatisierter Tests ist oder eher eine Prüfung in der Produktionsumgebung, darf gefragt werden. Aber sie zeigt, wie einfach es ist, komplizierte Prüfungen auf ZUGFeRD-Daten durchzuführen:

@Test
public void hasZugferdData_TotalAmountWithoutTax() throws Exception {
  String filename = "ZUGFeRD_1p0_BASIC_Einfach.pdf";
  String xpTotalAmount = "sum(//ram:IncludedSupplyChainTradeLineItem//ram:LineTotalAmount)"
                          + " = "
                          + "sum(//ram:TaxBasisTotalAmount)";
  XPathExpression exprTotalAmount = new XPathExpression(xpTotalAmount);

  AssertThat.document(filename)
            .hasZugferdData()
            .matchingXPath(exprTotalAmount)
  ;
}

Um einen solchen XPath-Ausdruck zu entwickeln, müssen Ihnen die XML-Daten vorliegen. Sie können die ZUGFeRD entweder aus dem Adobe Reader® speichern (rechte Maustaste) oder Sie extrahieren sie mit dem Hilfsprogramm ExtractZugferdData von PDFUnit. Es ist in Kapitel 9.15: „ZUGFeRD-Daten extrahieren“ beschrieben.

ZUGFeRD-Daten gegen die Spezifikation prüfen

Als Letztes muss noch erwähnt werden, dass PDFUnit auch die Einhaltung der ZUGFeRD-Spezifikation überprüfen kann:

@Test
public void compliesWithZugferdSpecification() throws Exception {
  String filename = "ZUGFeRD_1p0_BASIC_Einfach.pdf";
  AssertThat.document(filename)
            .compliesWith()
            .zugferdSpecification(ZugferdVersion.VERSION10)
  ;
}