einvoice / Deutsch / EN 16931 / XRechnung Regel-Referenz / Praxisbeispiel

Vom roten CI-Lauf zur bestandenen Rechnung

Ein durchgerechnetes Beispiel in fünf Minuten. Wir nehmen eine echte deutsche XRechnung (EN 16931, UBL), entfernen zwei Pflichtangaben, lassen den einvoice-Konformitätsprüfer genau so laufen, wie es ein CI-Gate täte, lesen den tatsächlich ausgegebenen Bericht und korrigieren die Rechnung, bis sie besteht. Jeder Befund unten stammt aus der echten Engine — der Bericht wird aus dem Werkzeug neu erzeugt, und ein Test lässt den Build fehlschlagen, sobald diese Seite von der Live-Ausgabe abweicht.

Die Engine hinter diesem Beispiel setzt 297 Geschäftsregeln aus EN 16931 und XRechnung durch — jede auslösbare offizielle BR-*-Regel in beiden CEN-Syntaxwelten (UBL und CII) — jetzt einschließlich jeder auslösbaren Codelisten-Prüfung, die komplette deutsche KoSIT-Schicht und die 21 PEPPOL-EN16931-R*-Regeln, die KoSIT im offiziellen XRechnung-Artefakt mitliefert (nur diese Teilmenge, nicht Peppol BIS Billing 3.0) — jede differentiell gegen die offiziellen Schematron-Artefakte bewiesen, mit 0 Abweichungen. Das vollständige Regelinventar samt ehrlicher Grenzen steht in COVERAGE.md im Repository.

Sie können jeden Schritt selbst nachvollziehen. Eine Installation legt das Kommandozeilenwerkzeug einvoice in Ihren PATH — Python 3.10+, keine Abhängigkeiten, bei der Prüfung kein Netz:

python3 -m pip install verifyhash-einvoice
einvoice validate --profile xrechnung invoice.xml

Alle Befehle unten sind genau dieses Werkzeug. Die beiden Rechnungsdateien, auf die es gerichtet wird (examples/01-missing-fields/broken.xml und examples/01-missing-fields/fixed.xml), liegen im Repository-Checkout — das PyPI-Wheel enthält bewusst nur das Paket einvoice, nicht die Beispiele. Klonen Sie also das Repository, wenn Sie genau diese Pfade nachfahren wollen, und führen Sie die Befehle aus dessen Verzeichnis einvoice/ aus. Auf eine eigene Rechnung angewandt funktioniert jeder Befehl unverändert direkt nach der pip-Installation.

Bis zu einem echten Fehlerbericht müssen Sie dafür aber nichts klonen: Schritt 1 druckt die kaputte Rechnung vollständig ab — speichern Sie diesen Block in eine eigene Datei und führen Sie Schritt 2 gegen Ihren eigenen Dateinamen aus.

1. Die kaputte Rechnung

Ein Lieferant hat diese UBL-Rechnung exportiert, aber zwei Pflichtangaben fehlen: die Käuferreferenz (BT-10, die Leitweg-ID — die Routing-Kennung, die ein deutscher öffentlicher Auftraggeber verlangt) und die Gruppe SELLER CONTACT (BG-6, ein cac:Contact unter der Lieferantenpartei). Alles Übrige ist eine byteweise Kopie eines gültigen KoSIT-Testdokuments, sodass diese zwei Auslassungen der einzige Grund für das Durchfallen sind. Die vollständige Datei ist examples/01-missing-fields/broken.xml aus dem Repository-Checkout:

<?xml version="1.0" encoding="UTF-8"?>
<!--
  DELIBERATELY NON-CONFORMANT onboarding example (broken invoice).

  Provenance: this file is a minimal mutation of the real, valid XRechnung
  corpus invoice corpus/vendored/valid/xr-01.01a_ubl.xml (a KoSIT test
  document, valid under profile=xrechnung). Two required things were removed
  so the engine fires real, catalog-backed FATAL findings:

    1. The 'Buyer reference' (BT-10) element was deleted        -> BR-DE-15
    2. The 'SELLER CONTACT' group (BG-6, cac:Contact) was deleted -> BR-DE-2

  Everything else is byte-for-byte the valid corpus document, so the ONLY
  reason it fails is the two removals. The corrected version lives in
  fixed.xml (the same file with both elements restored).
-->
<ubl:Invoice xmlns:ubl="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
             xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
             xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
    <cbc:CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0</cbc:CustomizationID>
    <cbc:ProfileID>urn:fdc:peppol.eu:2017:poacc:billing:01:1.0</cbc:ProfileID>
    <cbc:ID>123456XX</cbc:ID>
    <cbc:IssueDate>2016-04-04</cbc:IssueDate>
    <cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
    <cbc:Note>#ADU#Es gelten unsere Allgem. Geschäftsbedingungen, die Sie unter […] finden.</cbc:Note>
    <cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
    <cac:AccountingSupplierParty>
        <cac:Party>
            <cbc:EndpointID schemeID="EM">seller@email.de</cbc:EndpointID>
            <cac:PartyName>
                <cbc:Name>[Seller trading name]</cbc:Name>
            </cac:PartyName>
            <cac:PostalAddress>
                <cbc:StreetName>[Seller address line 1]</cbc:StreetName>
                <cbc:CityName>[Seller city]</cbc:CityName>
                <cbc:PostalZone>12345</cbc:PostalZone>
                <cac:Country>
                    <cbc:IdentificationCode>DE</cbc:IdentificationCode>
                </cac:Country>
            </cac:PostalAddress>
            <cac:PartyTaxScheme>
                <cbc:CompanyID>DE 123456789</cbc:CompanyID>
                <cac:TaxScheme>
                    <cbc:ID>VAT</cbc:ID>
                </cac:TaxScheme>
            </cac:PartyTaxScheme>
            <cac:PartyLegalEntity>
                <cbc:RegistrationName>[Seller name]</cbc:RegistrationName>
                <cbc:CompanyID>[HRA-Eintrag]</cbc:CompanyID>
                <cbc:CompanyLegalForm>123/456/7890, HRA-Eintrag in […]</cbc:CompanyLegalForm>
            </cac:PartyLegalEntity>
        </cac:Party>
    </cac:AccountingSupplierParty>
    <cac:AccountingCustomerParty>
        <cac:Party>
            <cbc:EndpointID schemeID="EM">buyer@info.de</cbc:EndpointID>
            <cac:PartyIdentification>
                <cbc:ID>[Buyer identifier]</cbc:ID>
            </cac:PartyIdentification>
            <cac:PostalAddress>
                <cbc:StreetName>[Buyer address line 1]</cbc:StreetName>
                <cbc:CityName>[Buyer city]</cbc:CityName>
                <cbc:PostalZone>12345</cbc:PostalZone>
                <cac:Country>
                    <cbc:IdentificationCode>DE</cbc:IdentificationCode>
                </cac:Country>
            </cac:PostalAddress>
            <cac:PartyLegalEntity>
                <cbc:RegistrationName>[Buyer name]</cbc:RegistrationName>
            </cac:PartyLegalEntity>
        </cac:Party>
    </cac:AccountingCustomerParty>
    <cac:PaymentMeans>
        <cbc:PaymentMeansCode>58</cbc:PaymentMeansCode>
        <cac:PayeeFinancialAccount>
            <!-- dies ist eine nicht existerende aber valide IBAN als test dummy -->
            <cbc:ID>DE79000000001234567890</cbc:ID>
        </cac:PayeeFinancialAccount>
    </cac:PaymentMeans>
    <cac:PaymentTerms>
        <cbc:Note>Zahlbar sofort ohne Abzug.</cbc:Note>
    </cac:PaymentTerms>
    <cac:TaxTotal>
        <cbc:TaxAmount currencyID="EUR">22.04</cbc:TaxAmount>
        <cac:TaxSubtotal>
            <cbc:TaxableAmount currencyID="EUR">314.86</cbc:TaxableAmount>
            <cbc:TaxAmount currencyID="EUR">22.04</cbc:TaxAmount>
            <cac:TaxCategory>
                <cbc:ID>S</cbc:ID>
                <cbc:Percent>7</cbc:Percent>
                <cac:TaxScheme>
                    <cbc:ID>VAT</cbc:ID>
                </cac:TaxScheme>
            </cac:TaxCategory>
        </cac:TaxSubtotal>
    </cac:TaxTotal>
    <cac:LegalMonetaryTotal>
        <cbc:LineExtensionAmount currencyID="EUR">314.86</cbc:LineExtensionAmount>
        <cbc:TaxExclusiveAmount currencyID="EUR">314.86</cbc:TaxExclusiveAmount>
        <cbc:TaxInclusiveAmount currencyID="EUR">336.9</cbc:TaxInclusiveAmount>
        <cbc:PayableAmount currencyID="EUR">336.9</cbc:PayableAmount>
    </cac:LegalMonetaryTotal>
    <cac:InvoiceLine>
        <cbc:ID>Zeitschrift [...]</cbc:ID>
        <cbc:Note>Die letzte Lieferung im Rahmen des abgerechneten Abonnements erfolgt in 12/2016 Lieferung erfolgt / erfolgte direkt vom Verlag</cbc:Note>
        <cbc:InvoicedQuantity unitCode="XPP">1</cbc:InvoicedQuantity>
        <cbc:LineExtensionAmount currencyID="EUR">288.79</cbc:LineExtensionAmount>
        <cac:InvoicePeriod>
            <cbc:StartDate>2016-01-01</cbc:StartDate>
            <cbc:EndDate>2016-12-31</cbc:EndDate>
        </cac:InvoicePeriod>
        <cac:OrderLineReference>
            <cbc:LineID>6171175.1</cbc:LineID>
        </cac:OrderLineReference>
        <cac:Item>
            <cbc:Description>Zeitschrift Inland</cbc:Description>
            <cbc:Name>Zeitschrift [...]</cbc:Name>
            <cac:SellersItemIdentification>
                <cbc:ID>246</cbc:ID>
            </cac:SellersItemIdentification>
            <cac:CommodityClassification>
                <cbc:ItemClassificationCode listID="IB">0721-880X</cbc:ItemClassificationCode>
            </cac:CommodityClassification>
            <cac:ClassifiedTaxCategory>
                <cbc:ID>S</cbc:ID>
                <cbc:Percent>7</cbc:Percent>
                <cac:TaxScheme>
                    <cbc:ID>VAT</cbc:ID>
                </cac:TaxScheme>
            </cac:ClassifiedTaxCategory>
        </cac:Item>
        <cac:Price>
            <cbc:PriceAmount currencyID="EUR">288.79</cbc:PriceAmount>
        </cac:Price>
    </cac:InvoiceLine>
    <cac:InvoiceLine>
        <cbc:ID>Porto + Versandkosten</cbc:ID>
        <cbc:InvoicedQuantity unitCode="XPP">1</cbc:InvoicedQuantity>
        <cbc:LineExtensionAmount currencyID="EUR">26.07</cbc:LineExtensionAmount>
        <cac:Item>
            <cbc:Name>Porto + Versandkosten</cbc:Name>
            <cac:ClassifiedTaxCategory>
                <cbc:ID>S</cbc:ID>
                <cbc:Percent>7</cbc:Percent>
                <cac:TaxScheme>
                    <cbc:ID>VAT</cbc:ID>
                </cac:TaxScheme>
            </cac:ClassifiedTaxCategory>
        </cac:Item>
        <cac:Price>
            <cbc:PriceAmount currencyID="EUR">26.07</cbc:PriceAmount>
        </cac:Price>
    </cac:InvoiceLine>
</ubl:Invoice>

Sie haben nur die pip-Installation zur Hand? Markieren Sie den gesamten Block, speichern Sie ihn als eigene Datei — der Name ist frei wählbar, etwa broken-invoice.xml, in einem beliebigen Verzeichnis — und verwenden Sie diesen Dateinamen überall dort, wo Schritt 2 examples/01-missing-fields/broken.xml schreibt. Weder Verzeichnis noch Dateiname sind Teil einer Regel: der Prüfer liest das Dokument, nicht seinen Ablageort — Sie erhalten denselben Bericht wie in Schritt 3.

2. Den Prüfer laufen lassen (das ist Ihr CI-Gate)

Richten Sie das Werkzeug auf die Rechnung (die Pfade sind relativ zum Verzeichnis einvoice/ des Repository-Checkouts). In einer CI-Pipeline ist das der Befehl, dessen Exit-Code ungleich null den Build fehlschlagen lässt:

$ einvoice validate --profile xrechnung examples/01-missing-fields/broken.xml

Sie haben den Block aus Schritt 1 stattdessen unter einem eigenen Namen gespeichert? Dann richten Sie denselben Befehl auf diesen Namen — alles nach den Optionen ist nur ein Pfad, und das ist der vollständige Befehl für eine reine pip-Installation:

$ einvoice validate --profile xrechnung broken-invoice.xml

--profile xrechnung ist hier nicht optional: das Kommandozeilenwerkzeug arbeitet standardmäßig mit --profile en16931 (dem europäischen Kernregelsatz), und unter diesem Profil besteht die Datei — beide fehlenden Angaben sind deutsche BR-DE-*-Anforderungen. Der Befehl endet mit 1 und gibt ein knappes Urteil aus: FAIL: mit der ersten fatalen Regel-ID, ihrer Meldung und dem betroffenen Element — danach zwei Zeilen, die genau zu dieser Regel führen: how to fix: einvoice --explain BR-DE-2 und die rule page:-URL derselben Regel auf dieser Website. Beide stammen aus der Regel, die dieser Lauf tatsächlich getroffen hat; die Seitenzeile erscheint nur für Regeln, die der mitgelieferte Remediation-Katalog führt. Die vollständige Befundliste — das, was die CI archivieren will — liefert JSON:

$ einvoice validate --profile xrechnung --format json examples/01-missing-fields/broken.xml

Gleicher Exit-Code, alle Befunde in einem Array. Nur fatal-Befunde machen eine Rechnung ungültig (das spiegelt die flag-Semantik des offiziellen Schematron); warning- und information-Befunde sind beratend und lassen den Build nicht scheitern.

Die ältere Modulform python3 -m einvoice.report examples/01-missing-fields/broken.xml --format json wird weiterhin voll unterstützt und leistet dasselbe: Sie ist der Einstiegspunkt, den das mitgelieferte CI-Gate-Skript ci/validate-invoices.sh und die GitHub-Action aufrufen, sie verwendet bereits standardmäßig --profile xrechnung (das Kommandozeilenwerkzeug dagegen en16931), und ihr JSON legt um dieselben Befunde einen versionierten Umschlag mit report_version, schema, profile, fatal_count und warning_count. Aus diesem Umschlag sind die Zusammenfassung und die Befundkarten unten gerendert.

3. Den Bericht lesen

Die Engine meldet valid: false für examples/01-missing-fields/broken.xml unter dem Profil xrechnung: insgesamt 3 Befunde, davon 2 fatal und 0 Warnung. Jeder Befund nennt die verletzte Regel, die betroffenen EN-16931-Geschäftsbegriffe und einen konkreten Korrekturhinweis. Die Regel-ID führt zur ausführlichen Referenzseite.

BR-DE-2 fatal BG-6

SELLER CONTACT (BG-6) must be transmitted.

Add the required element at `/ubl:Invoice/cac:AccountingSupplierParty`: SELLER CONTACT (BG-6) must be transmitted.

BR-DE-15 fatal BT-10

Buyer reference (BT-10) must be transmitted (non-empty).

Add the required element at `cbc:BuyerReference`: Buyer reference (BT-10) must be transmitted (non-empty).

BR-DE-TMP-32 information BG-14 BG-26 BT-72

An invoice should state the delivery/service date via BT-72 (Actual delivery date), BG-14 (Invoicing period) or a BG-26 (Invoice line period) on EVERY line.

Correct `cac:Delivery/cbc:ActualDeliveryDate` so that an invoice should state the delivery/service date via BT-72 (Actual delivery date), BG-14 (Invoicing period) or a BG-26 (Invoice line period) on EVERY line.

Titel und Korrekturhinweis oben sind die unveränderte Maschinenausgabe (Englisch). Denselben Befund gibt die CLI mit --lang de auf Deutsch aus: wo das offizielle KoSIT-Artefakt einen deutschen Text mitliefert, exakt diesen, sonst eine klar als Übersetzung gekennzeichnete Fassung. Die beiden fatalen Befunde (BR-DE-2 und BR-DE-15) sind der Grund für die Ablehnung. Der information-Befund ist beratend — wir lassen ihn stehen, damit dies eine minimale Zwei-Feld-Korrektur bleibt. Für die vollständige Erläuterung einer Regel-ID aus einem Fehlschlag dient einvoice --explain BR-DE-15 — das liest keine Rechnung und zeigt Anforderung, BT-/BG-Begriffe, XML-Position, den Ein-Zeilen-Fix und den wörtlichen offiziellen Schematron-Assert (--lang de für den deutschen Text).

4. Die Korrektur anwenden

Stellen Sie die zwei fehlenden Elemente wieder her. Das ist der exakte Diff von broken.xml zur korrigierten fixed.xml (die Provenienz-Kommentarköpfe sind weggelassen; die Rechnungsrümpfe unterscheiden sich sonst durch nichts):

--- broken.xml
+++ fixed.xml
@@ -9,6 +9,7 @@
     <cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
     <cbc:Note>#ADU#Es gelten unsere Allgem. Geschäftsbedingungen, die Sie unter […] finden.</cbc:Note>
     <cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
+    <cbc:BuyerReference>04011000-12345-03</cbc:BuyerReference>
     <cac:AccountingSupplierParty>
         <cac:Party>
             <cbc:EndpointID schemeID="EM">seller@email.de</cbc:EndpointID>
@@ -34,6 +35,11 @@
                 <cbc:CompanyID>[HRA-Eintrag]</cbc:CompanyID>
                 <cbc:CompanyLegalForm>123/456/7890, HRA-Eintrag in […]</cbc:CompanyLegalForm>
             </cac:PartyLegalEntity>
+            <cac:Contact>
+                <cbc:Name>nicht vorhanden</cbc:Name>
+                <cbc:Telephone>+49 1234-5678</cbc:Telephone>
+                <cbc:ElectronicMail>seller@email.de</cbc:ElectronicMail>
+            </cac:Contact>
         </cac:Party>
     </cac:AccountingSupplierParty>
     <cac:AccountingCustomerParty>

Ein cac:Contact braucht mindestens einen Namen, eine Telefonnummer und/oder eine E-Mail-Adresse; die Käuferreferenz ist die Routing-Kennung (Leitweg-ID), die Ihnen Ihr Empfänger vorgibt.

5. Die korrigierte Rechnung besteht

Denselben Befehl noch einmal auf der korrigierten Datei laufen lassen (examples/01-missing-fields/fixed.xml, ebenfalls aus dem Repository-Checkout):

$ einvoice validate --profile xrechnung --format json examples/01-missing-fields/fixed.xml

Sie endet jetzt mit 0 und meldet "valid": true, ohne verbleibenden fatalen Befund — der beratende BR-DE-TMP-32 steht weiterhin in der Liste, und genau deshalb hängt das Urteil am Schweregrad und nicht an der Anzahl der Befunde. Beide BR-DE-*-Fatalbefunde sind weg, und die Rechnung bestünde diesen Vorab-Check. (Der Test dieser Seite lässt die echte Engine erneut auf fixed.xml laufen und lässt den Build scheitern, wenn sie nicht wirklich mit null fatalen Befunden besteht.)

Ehrliche Grenze: Ein grünes Ergebnis heißt „keine implementierte fatale Regel hat ausgelöst“ — beratende Befunde können wie oben weiterhin in der Liste stehen —, nicht „rechtsverbindlich konform zertifiziert“. Das ist ein schneller Vorab-Check, der die Fehler fängt, an denen die meisten Ersteinreichungen scheitern — lassen Sie vor dem tatsächlichen Einreichen trotzdem den offiziellen Validator Ihres Empfängers laufen.

Weiter

Alle geprüften Regeln stehen im Regel-Index; Installation, erste Prüfung und das CI-Gate-Rezept auf Deutsch stehen im deutschen Schnellstart.