尚未收集仓库信息。
尚未检测到更新。GitHub 发布版本后会显示在这里。
PDF invoice to XRechnung (UBL 2.1) e-invoice, checked against 67 EN 16931 / XRechnung rules with their official IDs.
翻译准备中
取自仓库 README
正在加载完整 README。
查看完整 README ↗▶ Live demo in your browser · Case study · Part of Daniil Dushin's portfolio
Turns a PDF invoice into an XRechnung e-invoice (UBL 2.1 XML) and checks it against
EN 16931 and XRechnung business rules. Every failed rule is shown with its official ID and a
plain explanation, for example BR-CO-10: the lines add up to 882.30, the invoice says 882.80.
Since 1 January 2025, invoices between businesses in Germany have to be e-invoices: a structured format that follows the European standard EN 16931, not a PDF (§ 14 UStG). Paper and other formats are still allowed for a transition period: until 31 December 2026, and until 31 December 2027 for businesses with a previous-year turnover of not more than 800,000 euros (§ 27 Abs. 38 UStG). There is no transition for receiving: every business must be able to receive e-invoices since 1 January 2025 (BMF FAQ). In the EU, the VAT in the Digital Age package (adopted by the Council on 11 March 2025) makes e-invoicing mandatory for intra-EU transactions from 1 July 2030.
Small companies still write invoices in Word or an old accounting tool and send PDFs. A PDF looks fine to a person and can still be wrong for a machine: totals that do not add up, a missing buyer reference, a VAT ID without a country prefix, an IBAN with a typo. This tool reads the PDF, shows what it found, checks the rules a conversion most often breaks, and writes the XML.
Decimal, rounded
half up like the official schematron, never a float.CustomizationID
urn:cen.eu:en16931:2017#compliant#urn:xeinkauf.de:kosit:xrechnung_3.0, element order of
the UBL 2.1 schema, business terms mapped as in the official KoSIT examples.uv run python -m app convert invoice.pdf -o invoice.xml.flowchart LR
PDF[PDF invoice] --> T[pdf_text.py<br/>words to lines and cells]
T --> X{Extractor}
X -->|API key| C[Claude<br/>JSON schema output]
X -->|demo / offline| R[Rule-based<br/>labels, blocks, tables]
C --> M[Typed invoice<br/>Decimal money]
R --> M
M --> D[complete.py<br/>derive missing values,<br/>list each one]
D --> V[validation.py<br/>67 rules with IDs]
D --> U[ubl.py<br/>XRechnung UBL 2.1]
V --> W[Web page / CLI / JSON API]
U --> W
W --> DB[(SQLite history)]
The extractor only reads. The derivation step fills what the PDF does not print and lists it. The validator never changes anything; it only reports.
Requires uv (it installs Python 3.12 itself).
cp .env.example .env
uv sync
uv run python -m app --demo
Open http://127.0.0.1:8105 and click Convert → next to a sample. The last sample,
de-muster-gmbh-with-errors.pdf, has a wrong subtotal and a wrong IBAN digit on purpose.
With Docker: cp .env.example .env && docker compose up --build.
Set ANTHROPIC_API_KEY in .env and DEMO_MODE=false. With EXTRACTOR=auto (the default)
Claude is used whenever a key is set. The request sends the PDF text (not the file) and asks
for JSON that matches a fixed schema (app/extract/claude.py); the reply is then typed,
derived and validated exactly like the offline result.
uv run python -m app --demo convert samples/de-muster-gmbh.pdf -o out.xml
uv run python -m app convert invoice.pdf --buyer-email [email protected]
It prints the rule results and writes the XML. Exit code 0 = no errors, 1 = the XML was
written but has rule errors, 2 = the PDF could not be read or extraction failed.
See .env.example.
| Variable | Default | Meaning |
|---|---|---|
DEMO_MODE | true in the example | Offline rule-based extractor only |
EXTRACTOR | auto | auto, rules or claude |
ANTHROPIC_API_KEY | empty | Enables the Claude extractor |
ANTHROPIC_MODEL | claude-opus-5-5 | Model for extraction |
ANTHROPIC_EFFORT | medium | low … max |
DATABASE_PATH | data/invoices.db | SQLite history |
SAMPLES_DIR | samples | PDFs offered on the start page |
MAX_UPLOAD_MB / MAX_PAGES | 10 / 20 | Upload limits |
HOST / PORT | 127.0.0.1 / 8105 | Web server |
The reference is the official KoSIT validator (Java), which runs the UBL schema, the CEN EN 16931 schematron and the XRechnung schematron. This project implements a subset of those rules in Python, so it can explain each failure next to the extracted values. It does not replace the official validator.
To see how close the subset is, scripts/official_check.py runs the official artefacts on the
XML generated from every sample: the OASIS UBL 2.1 XSD (lxml), the CEN EN 16931 UBL schematron
(validation-1.3.17) and the KoSIT XRechnung schematron (v2.6.0), via Saxon-HE:
uv run --group official python scripts/official_check.py
Result of my last run: all five XML files are valid against the UBL 2.1 schema, and for every
sample the official schematron reports exactly the same rule IDs as this app (none for three
samples; PEPPOL-EN16931-R010 for the public-sector sample without a buyer e-invoice address;
BR-CO-10, BR-CO-15 and the warning BR-DE-19 for the sample with errors). An official
KoSIT example invoice is checked as a control and passes. This comparison found two of the bugs
in BUGFIX.md.
round(), which rounds halves up.
Python's round() rounds halves to even, and floats cannot hold 2.675 exactly. All money is
Decimal and is rounded with ROUND_HALF_UP; SQLite stores it as text.Decimal
without passing through a float; missing values are "" instead of nullable unions, which
keeps the schema within the structured-output limits and easy for the model.app/
pdf_text.py PDF text layer -> lines and cells (from word positions)
extract/
rules.py offline extractor: labels, address blocks, tables, totals
claude.py Claude extractor: JSON schema, prompt, reply -> typed invoice
complete.py derived values, each one listed
parsing.py dates, units (UN/ECE Rec 20), countries, VAT IDs
model.py typed invoice (EN 16931 business terms in comments)
money.py Decimal parsing, rounding, formatting
iban.py IBAN mod-97 check
validation.py 67 business rules with IDs
ubl.py XRechnung UBL 2.1 writer (lxml)
pipeline.py PDF -> extraction -> validation -> XML
db.py SQLite history
web.py, templates/ FastAPI app
main.py web server and CLI
samples/ five sample PDFs and generate.py (fpdf2)
scripts/official_check.py comparison with the official XSD and schematron
tests/ pytest suite
uv run pytest
uv run ruff check . && uv run ruff format --check .
192 tests. They cover extraction on every sample layout (with the expected values written out, not imported from the generator), every one of the 67 rules (it passes on a correct invoice and fails on a broken one, and a test fails if a new rule has no failing case), Decimal rounding against float rounding, the XML parsed back element by element (including element order and two-decimal amounts), the Claude extractor with a stub client (request shape, refusals, malformed replies), the web app (upload, rule results, XML download, the buyer-address fix, bad uploads), the SQLite history and the CLI exit codes.
Offline conversion of one sample (PDF read, extraction, 67 rules, XML) takes 16–29 ms (median of 20 runs per sample on my laptop).
See BUGFIX.md.