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.
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.