rechnungskit — deutsche E-Rechnung in Python

Eine typisierte Bibliothek zum Erzeugen, Serialisieren, Einbetten und Prüfen von XRechnung und ZUGFeRD / Factur-X auf dem semantischen Modell EN 16931 — mit maschinenlesbaren Prüfberichten.

Ergebnis

217 Regeln umgesetzt — 185 aus EN 16931, 32 BR-DE — mit 214 Tests und mypy strict, und einer benannten Liste dessen, was nicht abgedeckt ist, statt eines stillen Durchwinkens.

Das Problem

Seit Januar 2025 ist der Empfang strukturierter elektronischer Rechnungen im deutschen B2B Pflicht, das Ausstellen wird schrittweise verpflichtend. In der Praxis muss eine Rechnung drei Schichten gleichzeitig erfüllen: das semantische Modell EN 16931, dessen nationale Einschränkung (XRechnung 3.0 mit den BR-DE-*-Regeln) und eine Syntax — dasselbe Modell, geschrieben entweder als UBL 2.1 oder als CII D16B, mit anderen Elementnamen für jedes einzelne Feld. Ankommen kann sie als reines XML oder als hybrides PDF, in der Syntax, die der Absender gewählt hat. Ein Fehler ist dabei kein weiches Scheitern: Die Rechnung wird am Portal von einem automatischen Prüfer abgelehnt, der eine Regel-Kennung wie BR-CO-13 nennt — und man muss wissen, was das heißt und wo es passiert ist.

Ein geschichtetes Paket, dessen Abhängigkeiten nur in eine Richtung zeigen. domain liest keine Datei, öffnet keinen Socket, parst kein XML und schaut auf keine Uhr; syntax, validation und pdf liegen darüber, CLI und HTTP-Service sind dünne Hüllen.

Regeln laufen gegen das Modell, nicht gegen XPath. Die meisten Werkzeuge hier kapseln das offizielle Schematron, das pro Syntax geschrieben ist — dieselbe Regel existiert also zweimal, und geprüft werden kann erst nach dem Serialisieren. Hier ist eine Regel eine kleine reine Funktion von Invoice auf die gefundenen Probleme. Eine Regel bedient UBL und CII, eine Rechnung lässt sich prüfen, bevor es das Dokument gibt, und GET /v1/rules wird aus den Regeln erzeugt statt als zweite Liste gepflegt.

Pflichtfeld zu sein ist eine Eigenschaft der Norm, nicht des Typsystems. Invoice.number ist str | None, obwohl BR-02 es verlangt: Um BR-02 melden zu können, muss man ein Dokument ohne Rechnungsnummer überhaupt halten können. Ein Modell, das sich weigert, könnte nur einen Parse-Fehler werfen — und der sagt nichts darüber, welche Regel gebrochen wurde. Aus demselben Grund sind Codes einfache Strings: Eine Rechnung mit "XX" als Steuerkategorie muss lange genug überleben, um als BR-CL-18 gemeldet zu werden.

Beträge sind Decimal, ein float wird abgelehnt statt umgewandelt. Eine Rechnung, die einen Cent danebenliegt, ist eine abgelehnte Rechnung. Der feinere Grund: Ein Decimal merkt sich die Stellenzahl, mit der es geschrieben wurde — genau das fragen die BR-DEC-*-Regeln ab. Beträge werden deshalb aus der lexikalischen Form gelesen und nie normalisiert; Normalisieren würde den Defekt löschen, den diese Regeln finden sollen.

Jede Syntax ist ein Modul mit beiden Richtungen. Die Zuordnung von Geschäftsbegriff zu Elementpfad ist eine Tatsache; Schreiber und Leser auf zwei Dateien zu verteilen ist der Weg, auf dem sie auseinanderlaufen. Die Round-Trip-Tests vergleichen das ganze Modell und haben sich sofort bezahlt gemacht: Sie fanden ein Feld, das der CII-Writer verlor, und eines, das bei einer UBL-Gutschrift verschwand.

Jede Eingabe gilt als feindlich: <!DOCTYPE> wird vor dem Parsen abgelehnt, kein Netzwerk, keine DTD, Grenzen für Bytes, Tiefe und Elementzahl, und genau ein konfigurierter etree.fromstring-Aufruf im ganzen Paket. Fehler sind Problem Details nach RFC 9457, und zwei Konventionen sind bewusst gesetzt: Die Erzeugung weigert sich, eine nicht konforme Rechnung zu schreiben — die Prüfung weigert sich nie. Fünfzig gefundene Fehler sind eine erfolgreiche Anfrage, also 200 mit valid: false.

Die bekannten Grenzen sind benannt statt dem Zufall überlassen — die nicht umgesetzten Regelfamilien, keine XSD-Prüfung, kein Peppol. Ein Prüfer, der still durchwinkt, was er nie geprüft hat, ist schlimmer als gar keiner.