euinvoice
EN 16931 e-invoicing for Python. You can build European e-invoices as typed models, serialize them to UBL or CII, validate them against the official rule sets, parse them back, and embed them in Factur-X / ZUGFeRD PDFs.
Status: 0.1.0, not yet released. The API can still change before the release; see
CHANGELOG.mdand the known limitations.
Why
EU e-invoicing mandates are arriving country by country, and the reference tooling is mostly Java. euinvoice is a small, typed, MIT-licensed Python library with three principles:
- One semantic model. The EN 16931 business terms form a frozen,
Decimal-only Pydantic model. UBL and CII are just two ways of writing it down. - The official rules are the judge. Validation runs the pinned official XSD and Schematron artifacts (CEN, Peppol, KoSIT XRechnung) on SaxonC-HE, not a reimplementation that drifts after the next rule release.
- Framework-free and side-effect-free. Bytes go in and bytes come out, with no Django, no network and no file system in the core.
What 0.1.0 supports
| Profile | Syntax | Generate | Parse | Validate |
|---|---|---|---|---|
| EN 16931 core | UBL 2.1, CII D16B | ✅ | ✅ | ✅ XSD + CEN |
| Peppol BIS Billing 3.0 | UBL 2.1, CII D16B | ✅ | ✅ | ✅ XSD + CEN + Peppol |
| XRechnung 3.0 (CIUS) | UBL 2.1, CII D16B | ✅ | ✅ | ✅ XSD + CEN + XRechnung |
| XRechnung 3.0 Extension | UBL 2.1, CII D16B | ✅ EN 16931 content only | ✅ extension content listed as unmapped; content outside the CEN code lists (e.g. ICD XR03, testsuite 04.05a CII) raises ParseError |
⚠️ raw flags (#49) |
| XRechnung 3.0 CVD | UBL 2.1, CII D16B | ❌ | ❌ conforming documents (BR-CL-13) | ⚠️ raw flags, see below |
| Factur-X 1.0 / ZUGFeRD 2.1+ EN 16931, XRECHNUNG | CII in PDF/A-3 | ✅ | ✅ | ⚠️ EN 16931 / XRechnung rules only |
| Factur-X 1.0 / ZUGFeRD 2.1+ BASIC, EXTENDED | CII in PDF/A-3 | ❌ | ✅ EXTENDED-only content listed as unmapped | ❌ |
| Factur-X 1.0 / ZUGFeRD 2.1+ MINIMUM, BASIC WL | CII in PDF/A-3 | ❌ | ❌ detected and extracted only | ❌ |
| ZUGFeRD 2.0 MINIMUM, BASIC, EXTENDED | CII in PDF/A-3 | ❌ | ✅ BASIC, EXTENDED (EXTENDED-only content listed as unmapped; corpus EXTENDED samples with codes outside the CEN lists, ABK BR-CL-19 and 9958 BR-CL-25, raise ParseError like their 2.1 twins, #69); ❌ MINIMUM |
❌ |
- Parse means read into the EN 16931 model. Readers never drop input silently: every element or attribute
without a business term is listed in
parse_detailed(...).unmapped. - Validate reports the raw severities of the official rule sets. KoSIT's per-scenario severity overrides are not applied (#49): for example, 2 of the 6 official XRechnung Extension instances get fatal findings that KoSIT downgrades.
- XRechnung CVD: BR-DE-CVD-03 (fatal,
XRechnung-UBL-validation.schlines 560-562) needs an item classification with list idCVD, which BR-CL-13 (fatal, CENEN16931-UBL-codes.schlines 67-68) and therefore the model refuse. No conforming CVD invoice can be built or read.validate()rejects every CVD document: BR-CL-13 when it carries theCVDitem classification, BR-DE-CVD-03 (fatal) when it does not (KoSIT downgrades BR-CL-13, #49). - Factur-X: the rows above cover the Factur-X 1.0 / ZUGFeRD 2.1+ BT-24s.
facturx.embed/facturx.extractwrite and read the PDF container (the[pdf]extra). The Factur-X XSD and Schematron are not pinned yet (#42), sovalidate()checks the embedded XML of the EN 16931 and XRECHNUNG levels against the EN 16931 / XRechnung rules only, and raisesArtifactsNotAvailableErrorfor MINIMUM, BASIC WL, BASIC and EXTENDED. MINIMUM and BASIC WL carry no invoice lines, so they cannot become anInvoice:parse()raisesParseError(#69). The library does not check PDF/A-3 conformance (the test suite runs veraPDF onembed()output). ZUGFeRD 2.0 PDFs are extract-only (not generated). Their MINIMUM, BASIC and EXTENDED BT-24s (urn:zugferd.de:2p0:*) and the colon spellings of BASIC and EXTENDED (urn:cen.eu:en16931:2017:compliant:factur-x.eu:1p0:*) are not registered as profiles, butvalidate()raisesArtifactsNotAvailableErrorfor them as for the levels above (CLI exit 2; #98). A ZUGFeRD 2.0 EN 16931 invoice carries the core BT-24 and is validated as EN 16931 core. ZUGFeRD 1.0 PDFs are extract-only (parse()raisesUnsupportedDocumentError).
Planned later: ebInterface, more national CIUSes (RO, HR, FR, DK, …), FatturaPA, KSeF, Facturae, and clearance/transport integrations.
Install
uv add euinvoice # model + UBL/CII serialize/parse
uv add 'euinvoice[validate]' # + official Schematron validation (SaxonC-HE)
uv add 'euinvoice[pdf]' # + Factur-X / ZUGFeRD PDF embedding
Until 0.1.0 is on PyPI: uv add 'euinvoice @ git+https://github.com/letsrevel/euinvoice'.
The official validation artifacts are downloaded, not bundled, because of their licences. Fetch them once (pinned and sha256-verified):
python -m euinvoice artifacts fetch # cache: $EUINVOICE_ARTIFACTS_DIR or ~/.cache/euinvoice
Usage
draft is an euinvoice.InvoiceDraft: the invoice without its derived totals. These examples run as tests
(tests/_readme.py builds the synthetic XRechnung draft they use); see the quickstart for
building a draft.
>>> from euinvoice import calc, detect, parse, parse_detailed, profiles, to_xml, validate
>>> invoice = calc.complete(draft) # derive line totals, document totals and the VAT breakdown (EN 16931)
>>> xml = to_xml(invoice, profile=profiles.XRECHNUNG, syntax="cii") # raises PreflightError on blocking findings
>>> parse(xml) == profiles.XRECHNUNG.prepare(invoice) # lossless; to_xml wrote the profile's BT-24 (prepare)
True
>>> parse_detailed(xml).unmapped # XPaths of input with no business term; parse() discards them
()
>>> found = detect(xml) # syntax and profile from the root element and BT-24
>>> found.syntax, found.profile.id
(<Syntax.CII: 'cii'>, 'xrechnung')
Validation needs the [validate] extra and the fetched artifacts. validate() takes XML; for a PDF, pass
facturx.extract(pdf).xml or use the CLI.
>>> report = validate(xml) # profile auto-detected from BT-24
>>> report.ok
True
>>> for finding in report.findings:
... print(finding.rule_id, finding.severity, finding.message)
Factur-X / ZUGFeRD needs the [pdf] extra:
>>> from euinvoice import facturx
>>> # your renderer produces the human-readable PDF/A-3 (e.g. WeasyPrint pdf_variant="pdf/a-3b")
>>> hybrid_pdf = facturx.embed(rendered_pdf, invoice, profile=profiles.FACTURX_EN16931)
>>> found = facturx.extract(hybrid_pdf)
>>> found.filename, found.conformance_level, found.profile.id
('factur-x.xml', 'EN 16931', 'facturx-en16931')
>>> parse(hybrid_pdf) == profiles.FACTURX_EN16931.prepare(invoice) # parse() reads PDFs too
True
Command line
python -m euinvoice wraps the same functions. FILE is UBL, CII or a Factur-X / ZUGFeRD PDF (- reads
stdin), and --profile takes a profile id such as xrechnung or peppol.
python -m euinvoice validate invoice.xml [--profile ID] [--json] # one line per finding, then the verdict
python -m euinvoice convert invoice.xml --to ubl|cii [--profile ID] [-o OUT]
python -m euinvoice info invoice.pdf [--json] # syntax, BT-24, profile, container, totals
python -m euinvoice artifacts fetch [--only NAME]
Exit codes: 0 success (warnings allowed); 1 the document was rejected (a fatal or error finding, a
refused conversion, an unreadable invoice) or an artifact failed its integrity check; 2 no verdict (usage
error, unknown profile, unreadable file, failed download, missing artifacts or extras, a Factur-X level whose
rules are not pinned). --help on each subcommand lists them.
Documentation
The docs/ folder holds the guide: quickstart,
concepts, validation, Factur-X, a
mapping guide with synthetic freelancer and ticketing examples, and the
BT mapping. It builds with make docs (mkdocs-material). The GitHub Pages site goes live
once the maintainer enables Pages for the repository and sets the repository variable DOCS_DEPLOY=true.
Known limitations
Open questions waiting for a maintainer decision (needs-human):
- #42: the Factur-X / ZUGFeRD XSD and Schematron have no pinnable official download, so Factur-X levels are not validated against Factur-X rules (see above).
- #69: Factur-X MINIMUM and BASIC WL cannot be read into the model.
- #49: KoSIT's XRechnung severity overrides are not applied, so the verdict can differ from the KoSIT validator in both directions (e.g. BR-CL-21/23, XRechnung Extension and CVD).
- #67: the XRechnung profiles set no default BT-23; the caller must provide it (the pre-flight reports PEPPOL-EN16931-R001 otherwise).
- #40: the hardened parser rejects a single text node over 10,000,000 bytes, i.e. a BT-125 attachment over about 7.5 MB.
Development
make setup # uv sync --all-extras --group dev
make check # ruff + mypy --strict + file length
make test # unit tests, ≥95% branch coverage
make artifacts # fetch official artifacts + corpora
make conformance # run them
See CONTRIBUTING.md. Security reports: SECURITY.md.
Licence
MIT. Official validation artifacts are third-party works under their own licences (EUPL-1.2, Apache-2.0 and others). They are fetched at runtime and never redistributed by this package.
Metadata
Release files for euinvoice 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| euinvoice-0.1.0.tar.gz | 174.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| euinvoice-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 395.4 kB
Release files / euinvoice-0.1.0.tar.gz
| Download URL | euinvoice-0.1.0.tar.gz |
|---|---|
| Size | 174.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
187aee36db00632e03479ebc18154ed9a86f06782bae79ba9821c131be5b8404
|
|
BLAKE2b-256 checksum How to use checksums |
46c784f7ae787e221df21e23962121f5045db7f0b20497e2b80299e511a25bfc
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.11.18 {"installer":{"name":"uv","version":"0.11.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / euinvoice-0.1.0-py3-none-any.whl
| Download URL | euinvoice-0.1.0-py3-none-any.whl |
|---|---|
| Size | 220.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3889344d648473a4e29be200640d3a2dbeee0176d0c2dce17a553ea656dceddc
|
|
BLAKE2b-256 checksum How to use checksums |
cf269be36bcec3cb5224c918819ba925fcfdbaca7c60fb507599a535593aa342
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.11.18 {"installer":{"name":"uv","version":"0.11.18","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|