euinvoice 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. euinvoice-0.1.0/LICENSE +21 -0
  2. euinvoice-0.1.0/PKG-INFO +212 -0
  3. euinvoice-0.1.0/README.md +181 -0
  4. euinvoice-0.1.0/pyproject.toml +138 -0
  5. euinvoice-0.1.0/src/euinvoice/__init__.py +64 -0
  6. euinvoice-0.1.0/src/euinvoice/__main__.py +305 -0
  7. euinvoice-0.1.0/src/euinvoice/_api.py +154 -0
  8. euinvoice-0.1.0/src/euinvoice/_syntax.py +16 -0
  9. euinvoice-0.1.0/src/euinvoice/_xml.py +296 -0
  10. euinvoice-0.1.0/src/euinvoice/calc/__init__.py +74 -0
  11. euinvoice-0.1.0/src/euinvoice/calc/_categories.py +369 -0
  12. euinvoice-0.1.0/src/euinvoice/calc/_check.py +243 -0
  13. euinvoice-0.1.0/src/euinvoice/calc/_common.py +182 -0
  14. euinvoice-0.1.0/src/euinvoice/calc/_complete.py +230 -0
  15. euinvoice-0.1.0/src/euinvoice/detection.py +159 -0
  16. euinvoice-0.1.0/src/euinvoice/errors.py +101 -0
  17. euinvoice-0.1.0/src/euinvoice/facturx/__init__.py +10 -0
  18. euinvoice-0.1.0/src/euinvoice/facturx/_embed.py +231 -0
  19. euinvoice-0.1.0/src/euinvoice/facturx/_extract.py +215 -0
  20. euinvoice-0.1.0/src/euinvoice/facturx/_pypdf.py +39 -0
  21. euinvoice-0.1.0/src/euinvoice/facturx/xmp.py +149 -0
  22. euinvoice-0.1.0/src/euinvoice/model/__init__.py +94 -0
  23. euinvoice-0.1.0/src/euinvoice/model/_base.py +127 -0
  24. euinvoice-0.1.0/src/euinvoice/model/allowances.py +106 -0
  25. euinvoice-0.1.0/src/euinvoice/model/amounts.py +110 -0
  26. euinvoice-0.1.0/src/euinvoice/model/bt_index.py +110 -0
  27. euinvoice-0.1.0/src/euinvoice/model/codes/__init__.py +81 -0
  28. euinvoice-0.1.0/src/euinvoice/model/codes/_generated.py +559 -0
  29. euinvoice-0.1.0/src/euinvoice/model/codes/derived.py +16 -0
  30. euinvoice-0.1.0/src/euinvoice/model/codes/enums.py +67 -0
  31. euinvoice-0.1.0/src/euinvoice/model/datatypes.py +399 -0
  32. euinvoice-0.1.0/src/euinvoice/model/delivery.py +61 -0
  33. euinvoice-0.1.0/src/euinvoice/model/documents.py +21 -0
  34. euinvoice-0.1.0/src/euinvoice/model/invoice.py +168 -0
  35. euinvoice-0.1.0/src/euinvoice/model/lines.py +149 -0
  36. euinvoice-0.1.0/src/euinvoice/model/parties.py +180 -0
  37. euinvoice-0.1.0/src/euinvoice/model/payment.py +68 -0
  38. euinvoice-0.1.0/src/euinvoice/model/tax.py +31 -0
  39. euinvoice-0.1.0/src/euinvoice/model/totals.py +38 -0
  40. euinvoice-0.1.0/src/euinvoice/profiles/__init__.py +41 -0
  41. euinvoice-0.1.0/src/euinvoice/profiles/_base.py +162 -0
  42. euinvoice-0.1.0/src/euinvoice/profiles/en16931.py +23 -0
  43. euinvoice-0.1.0/src/euinvoice/profiles/facturx.py +198 -0
  44. euinvoice-0.1.0/src/euinvoice/profiles/peppol.py +360 -0
  45. euinvoice-0.1.0/src/euinvoice/profiles/registry.py +74 -0
  46. euinvoice-0.1.0/src/euinvoice/profiles/xrechnung.py +455 -0
  47. euinvoice-0.1.0/src/euinvoice/py.typed +0 -0
  48. euinvoice-0.1.0/src/euinvoice/report.py +66 -0
  49. euinvoice-0.1.0/src/euinvoice/syntax/__init__.py +9 -0
  50. euinvoice-0.1.0/src/euinvoice/syntax/_marks.py +192 -0
  51. euinvoice-0.1.0/src/euinvoice/syntax/_read_errors.py +55 -0
  52. euinvoice-0.1.0/src/euinvoice/syntax/cii/__init__.py +12 -0
  53. euinvoice-0.1.0/src/euinvoice/syntax/cii/_build.py +188 -0
  54. euinvoice-0.1.0/src/euinvoice/syntax/cii/_lines.py +137 -0
  55. euinvoice-0.1.0/src/euinvoice/syntax/cii/_parties.py +142 -0
  56. euinvoice-0.1.0/src/euinvoice/syntax/cii/_read.py +207 -0
  57. euinvoice-0.1.0/src/euinvoice/syntax/cii/_read_common.py +69 -0
  58. euinvoice-0.1.0/src/euinvoice/syntax/cii/_read_lines.py +168 -0
  59. euinvoice-0.1.0/src/euinvoice/syntax/cii/_read_parties.py +193 -0
  60. euinvoice-0.1.0/src/euinvoice/syntax/cii/_read_settlement.py +287 -0
  61. euinvoice-0.1.0/src/euinvoice/syntax/cii/_reader.py +171 -0
  62. euinvoice-0.1.0/src/euinvoice/syntax/cii/_settlement.py +211 -0
  63. euinvoice-0.1.0/src/euinvoice/syntax/cii/_write.py +136 -0
  64. euinvoice-0.1.0/src/euinvoice/syntax/result.py +25 -0
  65. euinvoice-0.1.0/src/euinvoice/syntax/ubl/__init__.py +13 -0
  66. euinvoice-0.1.0/src/euinvoice/syntax/ubl/_build.py +182 -0
  67. euinvoice-0.1.0/src/euinvoice/syntax/ubl/_cursor.py +292 -0
  68. euinvoice-0.1.0/src/euinvoice/syntax/ubl/_lines.py +186 -0
  69. euinvoice-0.1.0/src/euinvoice/syntax/ubl/_parties.py +183 -0
  70. euinvoice-0.1.0/src/euinvoice/syntax/ubl/_read.py +424 -0
  71. euinvoice-0.1.0/src/euinvoice/syntax/ubl/_read_lines.py +283 -0
  72. euinvoice-0.1.0/src/euinvoice/syntax/ubl/_read_parties.py +277 -0
  73. euinvoice-0.1.0/src/euinvoice/syntax/ubl/_write.py +378 -0
  74. euinvoice-0.1.0/src/euinvoice/validation/__init__.py +8 -0
  75. euinvoice-0.1.0/src/euinvoice/validation/artifacts.py +446 -0
  76. euinvoice-0.1.0/src/euinvoice/validation/manifest.toml +127 -0
  77. euinvoice-0.1.0/src/euinvoice/validation/orchestration.py +190 -0
  78. euinvoice-0.1.0/src/euinvoice/validation/schematron.py +239 -0
  79. euinvoice-0.1.0/src/euinvoice/validation/xsd.py +158 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Biagio Distefano
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,212 @@
1
+ Metadata-Version: 2.4
2
+ Name: euinvoice
3
+ Version: 0.1.0
4
+ Summary: EN 16931 e-invoicing for Python: build, serialize (UBL/CII), validate, parse and embed (Factur-X) European e-invoices.
5
+ Keywords: e-invoicing,en16931,peppol,xrechnung,factur-x,zugferd,ubl,cii,einvoice
6
+ Author: Biagio Distefano
7
+ Author-email: Biagio Distefano <biagio@biagiodistefano.io>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 2 - Pre-Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.12
15
+ Classifier: Programming Language :: Python :: 3.13
16
+ Classifier: Programming Language :: Python :: 3.14
17
+ Classifier: Topic :: Office/Business :: Financial :: Accounting
18
+ Classifier: Topic :: Text Processing :: Markup :: XML
19
+ Classifier: Typing :: Typed
20
+ Requires-Dist: lxml>=5.3
21
+ Requires-Dist: pydantic>=2.11
22
+ Requires-Dist: pypdf>=6.1 ; extra == 'pdf'
23
+ Requires-Dist: saxonche>=12.9,!=13.0.0 ; extra == 'validate'
24
+ Requires-Python: >=3.12
25
+ Project-URL: Homepage, https://github.com/letsrevel/euinvoice
26
+ Project-URL: Issues, https://github.com/letsrevel/euinvoice/issues
27
+ Project-URL: Changelog, https://github.com/letsrevel/euinvoice/blob/main/CHANGELOG.md
28
+ Provides-Extra: pdf
29
+ Provides-Extra: validate
30
+ Description-Content-Type: text/markdown
31
+
32
+ # euinvoice
33
+
34
+ **EN 16931 e-invoicing for Python.** You can build European e-invoices as typed models, serialize them
35
+ to **UBL** or **CII**, validate them against the **official** rule sets, parse them back, and embed them
36
+ in **Factur-X / ZUGFeRD** PDFs.
37
+
38
+ > **Status: 0.1.0, not yet released.** The API can still change before the release; see
39
+ > [`CHANGELOG.md`](CHANGELOG.md) and the [known limitations](#known-limitations).
40
+
41
+ ## Why
42
+
43
+ EU e-invoicing mandates are arriving country by country, and the reference tooling is mostly Java. euinvoice
44
+ is a small, typed, MIT-licensed Python library with three principles:
45
+
46
+ - **One semantic model.** The EN 16931 business terms form a frozen, `Decimal`-only Pydantic model. UBL and CII
47
+ are just two ways of writing it down.
48
+ - **The official rules are the judge.** Validation runs the pinned official XSD and Schematron artifacts (CEN,
49
+ Peppol, KoSIT XRechnung) on SaxonC-HE, not a reimplementation that drifts after the next rule release.
50
+ - **Framework-free and side-effect-free.** Bytes go in and bytes come out, with no Django, no network and no
51
+ file system in the core.
52
+
53
+ ## What 0.1.0 supports
54
+
55
+ | Profile | Syntax | Generate | Parse | Validate |
56
+ |---|---|---|---|---|
57
+ | EN 16931 core | UBL 2.1, CII D16B | ✅ | ✅ | ✅ XSD + CEN |
58
+ | Peppol BIS Billing 3.0 | UBL 2.1, CII D16B | ✅ | ✅ | ✅ XSD + CEN + Peppol |
59
+ | XRechnung 3.0 (CIUS) | UBL 2.1, CII D16B | ✅ | ✅ | ✅ XSD + CEN + XRechnung |
60
+ | 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](https://github.com/letsrevel/euinvoice/issues/49)) |
61
+ | XRechnung 3.0 CVD | UBL 2.1, CII D16B | ❌ | ❌ conforming documents (BR-CL-13) | ⚠️ raw flags, see below |
62
+ | Factur-X 1.0 / ZUGFeRD 2.1+ EN 16931, XRECHNUNG | CII in PDF/A-3 | ✅ | ✅ | ⚠️ EN 16931 / XRechnung rules only |
63
+ | Factur-X 1.0 / ZUGFeRD 2.1+ BASIC, EXTENDED | CII in PDF/A-3 | ❌ | ✅ EXTENDED-only content listed as unmapped | ❌ |
64
+ | Factur-X 1.0 / ZUGFeRD 2.1+ MINIMUM, BASIC WL | CII in PDF/A-3 | ❌ | ❌ detected and extracted only | ❌ |
65
+ | 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](https://github.com/letsrevel/euinvoice/issues/69)); ❌ MINIMUM | ❌ |
66
+
67
+ - **Parse** means read into the EN 16931 model. Readers never drop input silently: every element or attribute
68
+ without a business term is listed in `parse_detailed(...).unmapped`.
69
+ - **Validate** reports the raw severities of the official rule sets. KoSIT's per-scenario severity overrides are
70
+ not applied ([#49](https://github.com/letsrevel/euinvoice/issues/49)): for example, 2 of the 6 official XRechnung Extension instances get fatal findings
71
+ that KoSIT downgrades.
72
+ - **XRechnung CVD:** BR-DE-CVD-03 (fatal, `XRechnung-UBL-validation.sch` lines 560-562) needs an item
73
+ classification with list id `CVD`, which BR-CL-13 (fatal, CEN `EN16931-UBL-codes.sch` lines 67-68) and
74
+ therefore the model refuse. No *conforming* CVD invoice can be built or read. `validate()` rejects every CVD
75
+ document: BR-CL-13 when it carries the `CVD` item classification, BR-DE-CVD-03 (fatal) when it does not
76
+ (KoSIT downgrades BR-CL-13, [#49](https://github.com/letsrevel/euinvoice/issues/49)).
77
+ - **Factur-X:** the rows above cover the Factur-X 1.0 / ZUGFeRD 2.1+ BT-24s. `facturx.embed` / `facturx.extract`
78
+ write and read the PDF container (the `[pdf]` extra). The
79
+ Factur-X XSD and Schematron are not pinned yet ([#42](https://github.com/letsrevel/euinvoice/issues/42)), so
80
+ `validate()` checks the embedded XML of the EN 16931 and XRECHNUNG levels against the EN 16931 / XRechnung
81
+ rules only, and raises `ArtifactsNotAvailableError` for MINIMUM, BASIC WL, BASIC and EXTENDED. MINIMUM and
82
+ BASIC WL carry no invoice lines, so they cannot become an `Invoice`: `parse()` raises `ParseError`
83
+ ([#69](https://github.com/letsrevel/euinvoice/issues/69)). The library does not check PDF/A-3 conformance
84
+ (the test suite runs veraPDF on `embed()` output). ZUGFeRD 2.0 PDFs are extract-only (not generated). Their
85
+ MINIMUM, BASIC and EXTENDED BT-24s (`urn:zugferd.de:2p0:*`) and the colon spellings of BASIC and EXTENDED
86
+ (`urn:cen.eu:en16931:2017:compliant:factur-x.eu:1p0:*`) are not registered as profiles, but `validate()` raises
87
+ `ArtifactsNotAvailableError` for them as for the levels above (CLI exit 2;
88
+ [#98](https://github.com/letsrevel/euinvoice/issues/98)). A ZUGFeRD 2.0 EN 16931 invoice carries the core BT-24
89
+ and is validated as EN 16931 core.
90
+ ZUGFeRD 1.0 PDFs are extract-only (`parse()` raises `UnsupportedDocumentError`).
91
+
92
+ Planned later: ebInterface, more national CIUSes (RO, HR, FR, DK, …), FatturaPA, KSeF, Facturae, and
93
+ clearance/transport integrations.
94
+
95
+ ## Install
96
+
97
+ ```bash
98
+ uv add euinvoice # model + UBL/CII serialize/parse
99
+ uv add 'euinvoice[validate]' # + official Schematron validation (SaxonC-HE)
100
+ uv add 'euinvoice[pdf]' # + Factur-X / ZUGFeRD PDF embedding
101
+ ```
102
+
103
+ Until 0.1.0 is on PyPI: `uv add 'euinvoice @ git+https://github.com/letsrevel/euinvoice'`.
104
+
105
+ The official validation artifacts are **downloaded, not bundled**, because of their licences. Fetch them
106
+ once (pinned and sha256-verified):
107
+
108
+ ```bash
109
+ python -m euinvoice artifacts fetch # cache: $EUINVOICE_ARTIFACTS_DIR or ~/.cache/euinvoice
110
+ ```
111
+
112
+ ## Usage
113
+
114
+ `draft` is an `euinvoice.InvoiceDraft`: the invoice without its derived totals. These examples run as tests
115
+ (`tests/_readme.py` builds the synthetic XRechnung draft they use); see the [quickstart](docs/quickstart.md) for
116
+ building a draft.
117
+
118
+ ```python
119
+ >>> from euinvoice import calc, detect, parse, parse_detailed, profiles, to_xml, validate
120
+ >>> invoice = calc.complete(draft) # derive line totals, document totals and the VAT breakdown (EN 16931)
121
+ >>> xml = to_xml(invoice, profile=profiles.XRECHNUNG, syntax="cii") # raises PreflightError on blocking findings
122
+ >>> parse(xml) == profiles.XRECHNUNG.prepare(invoice) # lossless; to_xml wrote the profile's BT-24 (prepare)
123
+ True
124
+ >>> parse_detailed(xml).unmapped # XPaths of input with no business term; parse() discards them
125
+ ()
126
+ >>> found = detect(xml) # syntax and profile from the root element and BT-24
127
+ >>> found.syntax, found.profile.id
128
+ (<Syntax.CII: 'cii'>, 'xrechnung')
129
+ ```
130
+
131
+ Validation needs the `[validate]` extra and the fetched artifacts. `validate()` takes XML; for a PDF, pass
132
+ `facturx.extract(pdf).xml` or use the CLI.
133
+
134
+ <!-- readme-doctest: needs-artifacts -->
135
+ ```python
136
+ >>> report = validate(xml) # profile auto-detected from BT-24
137
+ >>> report.ok
138
+ True
139
+ >>> for finding in report.findings:
140
+ ... print(finding.rule_id, finding.severity, finding.message)
141
+ ```
142
+
143
+ Factur-X / ZUGFeRD needs the `[pdf]` extra:
144
+
145
+ ```python
146
+ >>> from euinvoice import facturx
147
+ >>> # your renderer produces the human-readable PDF/A-3 (e.g. WeasyPrint pdf_variant="pdf/a-3b")
148
+ >>> hybrid_pdf = facturx.embed(rendered_pdf, invoice, profile=profiles.FACTURX_EN16931)
149
+ >>> found = facturx.extract(hybrid_pdf)
150
+ >>> found.filename, found.conformance_level, found.profile.id
151
+ ('factur-x.xml', 'EN 16931', 'facturx-en16931')
152
+ >>> parse(hybrid_pdf) == profiles.FACTURX_EN16931.prepare(invoice) # parse() reads PDFs too
153
+ True
154
+ ```
155
+
156
+ ## Command line
157
+
158
+ `python -m euinvoice` wraps the same functions. `FILE` is UBL, CII or a Factur-X / ZUGFeRD PDF (`-` reads
159
+ stdin), and `--profile` takes a profile id such as `xrechnung` or `peppol`.
160
+
161
+ ```bash
162
+ python -m euinvoice validate invoice.xml [--profile ID] [--json] # one line per finding, then the verdict
163
+ python -m euinvoice convert invoice.xml --to ubl|cii [--profile ID] [-o OUT]
164
+ python -m euinvoice info invoice.pdf [--json] # syntax, BT-24, profile, container, totals
165
+ python -m euinvoice artifacts fetch [--only NAME]
166
+ ```
167
+
168
+ Exit codes: `0` success (warnings allowed); `1` the document was rejected (a `fatal` or `error` finding, a
169
+ refused conversion, an unreadable invoice) or an artifact failed its integrity check; `2` no verdict (usage
170
+ error, unknown profile, unreadable file, failed download, missing artifacts or extras, a Factur-X level whose
171
+ rules are not pinned). `--help` on each subcommand lists them.
172
+
173
+ ## Documentation
174
+
175
+ The [`docs/`](docs/index.md) folder holds the guide: [quickstart](docs/quickstart.md),
176
+ [concepts](docs/concepts.md), [validation](docs/validation.md), [Factur-X](docs/facturx.md), a
177
+ [mapping guide](docs/mapping/index.md) with synthetic freelancer and ticketing examples, and the
178
+ [BT mapping](docs/reference/bt-mapping.md). It builds with `make docs` (mkdocs-material). The GitHub Pages site goes live
179
+ once the maintainer enables Pages for the repository and sets the repository variable `DOCS_DEPLOY=true`.
180
+
181
+ ## Known limitations
182
+
183
+ Open questions waiting for a maintainer decision (`needs-human`):
184
+
185
+ - [#42](https://github.com/letsrevel/euinvoice/issues/42): the Factur-X / ZUGFeRD XSD and Schematron have no
186
+ pinnable official download, so Factur-X levels are not validated against Factur-X rules (see above).
187
+ - [#69](https://github.com/letsrevel/euinvoice/issues/69): Factur-X MINIMUM and BASIC WL cannot be read into the
188
+ model.
189
+ - [#49](https://github.com/letsrevel/euinvoice/issues/49): KoSIT's XRechnung severity overrides are not applied,
190
+ so the verdict can differ from the KoSIT validator in both directions (e.g. BR-CL-21/23, XRechnung Extension
191
+ and CVD).
192
+ - [#67](https://github.com/letsrevel/euinvoice/issues/67): the XRechnung profiles set no default BT-23; the caller
193
+ must provide it (the pre-flight reports PEPPOL-EN16931-R001 otherwise).
194
+ - [#40](https://github.com/letsrevel/euinvoice/issues/40): the hardened parser rejects a single text node over
195
+ 10,000,000 bytes, i.e. a BT-125 attachment over about 7.5 MB.
196
+
197
+ ## Development
198
+
199
+ ```bash
200
+ make setup # uv sync --all-extras --group dev
201
+ make check # ruff + mypy --strict + file length
202
+ make test # unit tests, ≥95% branch coverage
203
+ make artifacts # fetch official artifacts + corpora
204
+ make conformance # run them
205
+ ```
206
+
207
+ See [`CONTRIBUTING.md`](CONTRIBUTING.md). Security reports: [`SECURITY.md`](SECURITY.md).
208
+
209
+ ## Licence
210
+
211
+ MIT. Official validation artifacts are third-party works under their own licences (EUPL-1.2,
212
+ Apache-2.0 and others). They are fetched at runtime and never redistributed by this package.
@@ -0,0 +1,181 @@
1
+ # euinvoice
2
+
3
+ **EN 16931 e-invoicing for Python.** You can build European e-invoices as typed models, serialize them
4
+ to **UBL** or **CII**, validate them against the **official** rule sets, parse them back, and embed them
5
+ in **Factur-X / ZUGFeRD** PDFs.
6
+
7
+ > **Status: 0.1.0, not yet released.** The API can still change before the release; see
8
+ > [`CHANGELOG.md`](CHANGELOG.md) and the [known limitations](#known-limitations).
9
+
10
+ ## Why
11
+
12
+ EU e-invoicing mandates are arriving country by country, and the reference tooling is mostly Java. euinvoice
13
+ is a small, typed, MIT-licensed Python library with three principles:
14
+
15
+ - **One semantic model.** The EN 16931 business terms form a frozen, `Decimal`-only Pydantic model. UBL and CII
16
+ are just two ways of writing it down.
17
+ - **The official rules are the judge.** Validation runs the pinned official XSD and Schematron artifacts (CEN,
18
+ Peppol, KoSIT XRechnung) on SaxonC-HE, not a reimplementation that drifts after the next rule release.
19
+ - **Framework-free and side-effect-free.** Bytes go in and bytes come out, with no Django, no network and no
20
+ file system in the core.
21
+
22
+ ## What 0.1.0 supports
23
+
24
+ | Profile | Syntax | Generate | Parse | Validate |
25
+ |---|---|---|---|---|
26
+ | EN 16931 core | UBL 2.1, CII D16B | ✅ | ✅ | ✅ XSD + CEN |
27
+ | Peppol BIS Billing 3.0 | UBL 2.1, CII D16B | ✅ | ✅ | ✅ XSD + CEN + Peppol |
28
+ | XRechnung 3.0 (CIUS) | UBL 2.1, CII D16B | ✅ | ✅ | ✅ XSD + CEN + XRechnung |
29
+ | 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](https://github.com/letsrevel/euinvoice/issues/49)) |
30
+ | XRechnung 3.0 CVD | UBL 2.1, CII D16B | ❌ | ❌ conforming documents (BR-CL-13) | ⚠️ raw flags, see below |
31
+ | Factur-X 1.0 / ZUGFeRD 2.1+ EN 16931, XRECHNUNG | CII in PDF/A-3 | ✅ | ✅ | ⚠️ EN 16931 / XRechnung rules only |
32
+ | Factur-X 1.0 / ZUGFeRD 2.1+ BASIC, EXTENDED | CII in PDF/A-3 | ❌ | ✅ EXTENDED-only content listed as unmapped | ❌ |
33
+ | Factur-X 1.0 / ZUGFeRD 2.1+ MINIMUM, BASIC WL | CII in PDF/A-3 | ❌ | ❌ detected and extracted only | ❌ |
34
+ | 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](https://github.com/letsrevel/euinvoice/issues/69)); ❌ MINIMUM | ❌ |
35
+
36
+ - **Parse** means read into the EN 16931 model. Readers never drop input silently: every element or attribute
37
+ without a business term is listed in `parse_detailed(...).unmapped`.
38
+ - **Validate** reports the raw severities of the official rule sets. KoSIT's per-scenario severity overrides are
39
+ not applied ([#49](https://github.com/letsrevel/euinvoice/issues/49)): for example, 2 of the 6 official XRechnung Extension instances get fatal findings
40
+ that KoSIT downgrades.
41
+ - **XRechnung CVD:** BR-DE-CVD-03 (fatal, `XRechnung-UBL-validation.sch` lines 560-562) needs an item
42
+ classification with list id `CVD`, which BR-CL-13 (fatal, CEN `EN16931-UBL-codes.sch` lines 67-68) and
43
+ therefore the model refuse. No *conforming* CVD invoice can be built or read. `validate()` rejects every CVD
44
+ document: BR-CL-13 when it carries the `CVD` item classification, BR-DE-CVD-03 (fatal) when it does not
45
+ (KoSIT downgrades BR-CL-13, [#49](https://github.com/letsrevel/euinvoice/issues/49)).
46
+ - **Factur-X:** the rows above cover the Factur-X 1.0 / ZUGFeRD 2.1+ BT-24s. `facturx.embed` / `facturx.extract`
47
+ write and read the PDF container (the `[pdf]` extra). The
48
+ Factur-X XSD and Schematron are not pinned yet ([#42](https://github.com/letsrevel/euinvoice/issues/42)), so
49
+ `validate()` checks the embedded XML of the EN 16931 and XRECHNUNG levels against the EN 16931 / XRechnung
50
+ rules only, and raises `ArtifactsNotAvailableError` for MINIMUM, BASIC WL, BASIC and EXTENDED. MINIMUM and
51
+ BASIC WL carry no invoice lines, so they cannot become an `Invoice`: `parse()` raises `ParseError`
52
+ ([#69](https://github.com/letsrevel/euinvoice/issues/69)). The library does not check PDF/A-3 conformance
53
+ (the test suite runs veraPDF on `embed()` output). ZUGFeRD 2.0 PDFs are extract-only (not generated). Their
54
+ MINIMUM, BASIC and EXTENDED BT-24s (`urn:zugferd.de:2p0:*`) and the colon spellings of BASIC and EXTENDED
55
+ (`urn:cen.eu:en16931:2017:compliant:factur-x.eu:1p0:*`) are not registered as profiles, but `validate()` raises
56
+ `ArtifactsNotAvailableError` for them as for the levels above (CLI exit 2;
57
+ [#98](https://github.com/letsrevel/euinvoice/issues/98)). A ZUGFeRD 2.0 EN 16931 invoice carries the core BT-24
58
+ and is validated as EN 16931 core.
59
+ ZUGFeRD 1.0 PDFs are extract-only (`parse()` raises `UnsupportedDocumentError`).
60
+
61
+ Planned later: ebInterface, more national CIUSes (RO, HR, FR, DK, …), FatturaPA, KSeF, Facturae, and
62
+ clearance/transport integrations.
63
+
64
+ ## Install
65
+
66
+ ```bash
67
+ uv add euinvoice # model + UBL/CII serialize/parse
68
+ uv add 'euinvoice[validate]' # + official Schematron validation (SaxonC-HE)
69
+ uv add 'euinvoice[pdf]' # + Factur-X / ZUGFeRD PDF embedding
70
+ ```
71
+
72
+ Until 0.1.0 is on PyPI: `uv add 'euinvoice @ git+https://github.com/letsrevel/euinvoice'`.
73
+
74
+ The official validation artifacts are **downloaded, not bundled**, because of their licences. Fetch them
75
+ once (pinned and sha256-verified):
76
+
77
+ ```bash
78
+ python -m euinvoice artifacts fetch # cache: $EUINVOICE_ARTIFACTS_DIR or ~/.cache/euinvoice
79
+ ```
80
+
81
+ ## Usage
82
+
83
+ `draft` is an `euinvoice.InvoiceDraft`: the invoice without its derived totals. These examples run as tests
84
+ (`tests/_readme.py` builds the synthetic XRechnung draft they use); see the [quickstart](docs/quickstart.md) for
85
+ building a draft.
86
+
87
+ ```python
88
+ >>> from euinvoice import calc, detect, parse, parse_detailed, profiles, to_xml, validate
89
+ >>> invoice = calc.complete(draft) # derive line totals, document totals and the VAT breakdown (EN 16931)
90
+ >>> xml = to_xml(invoice, profile=profiles.XRECHNUNG, syntax="cii") # raises PreflightError on blocking findings
91
+ >>> parse(xml) == profiles.XRECHNUNG.prepare(invoice) # lossless; to_xml wrote the profile's BT-24 (prepare)
92
+ True
93
+ >>> parse_detailed(xml).unmapped # XPaths of input with no business term; parse() discards them
94
+ ()
95
+ >>> found = detect(xml) # syntax and profile from the root element and BT-24
96
+ >>> found.syntax, found.profile.id
97
+ (<Syntax.CII: 'cii'>, 'xrechnung')
98
+ ```
99
+
100
+ Validation needs the `[validate]` extra and the fetched artifacts. `validate()` takes XML; for a PDF, pass
101
+ `facturx.extract(pdf).xml` or use the CLI.
102
+
103
+ <!-- readme-doctest: needs-artifacts -->
104
+ ```python
105
+ >>> report = validate(xml) # profile auto-detected from BT-24
106
+ >>> report.ok
107
+ True
108
+ >>> for finding in report.findings:
109
+ ... print(finding.rule_id, finding.severity, finding.message)
110
+ ```
111
+
112
+ Factur-X / ZUGFeRD needs the `[pdf]` extra:
113
+
114
+ ```python
115
+ >>> from euinvoice import facturx
116
+ >>> # your renderer produces the human-readable PDF/A-3 (e.g. WeasyPrint pdf_variant="pdf/a-3b")
117
+ >>> hybrid_pdf = facturx.embed(rendered_pdf, invoice, profile=profiles.FACTURX_EN16931)
118
+ >>> found = facturx.extract(hybrid_pdf)
119
+ >>> found.filename, found.conformance_level, found.profile.id
120
+ ('factur-x.xml', 'EN 16931', 'facturx-en16931')
121
+ >>> parse(hybrid_pdf) == profiles.FACTURX_EN16931.prepare(invoice) # parse() reads PDFs too
122
+ True
123
+ ```
124
+
125
+ ## Command line
126
+
127
+ `python -m euinvoice` wraps the same functions. `FILE` is UBL, CII or a Factur-X / ZUGFeRD PDF (`-` reads
128
+ stdin), and `--profile` takes a profile id such as `xrechnung` or `peppol`.
129
+
130
+ ```bash
131
+ python -m euinvoice validate invoice.xml [--profile ID] [--json] # one line per finding, then the verdict
132
+ python -m euinvoice convert invoice.xml --to ubl|cii [--profile ID] [-o OUT]
133
+ python -m euinvoice info invoice.pdf [--json] # syntax, BT-24, profile, container, totals
134
+ python -m euinvoice artifacts fetch [--only NAME]
135
+ ```
136
+
137
+ Exit codes: `0` success (warnings allowed); `1` the document was rejected (a `fatal` or `error` finding, a
138
+ refused conversion, an unreadable invoice) or an artifact failed its integrity check; `2` no verdict (usage
139
+ error, unknown profile, unreadable file, failed download, missing artifacts or extras, a Factur-X level whose
140
+ rules are not pinned). `--help` on each subcommand lists them.
141
+
142
+ ## Documentation
143
+
144
+ The [`docs/`](docs/index.md) folder holds the guide: [quickstart](docs/quickstart.md),
145
+ [concepts](docs/concepts.md), [validation](docs/validation.md), [Factur-X](docs/facturx.md), a
146
+ [mapping guide](docs/mapping/index.md) with synthetic freelancer and ticketing examples, and the
147
+ [BT mapping](docs/reference/bt-mapping.md). It builds with `make docs` (mkdocs-material). The GitHub Pages site goes live
148
+ once the maintainer enables Pages for the repository and sets the repository variable `DOCS_DEPLOY=true`.
149
+
150
+ ## Known limitations
151
+
152
+ Open questions waiting for a maintainer decision (`needs-human`):
153
+
154
+ - [#42](https://github.com/letsrevel/euinvoice/issues/42): the Factur-X / ZUGFeRD XSD and Schematron have no
155
+ pinnable official download, so Factur-X levels are not validated against Factur-X rules (see above).
156
+ - [#69](https://github.com/letsrevel/euinvoice/issues/69): Factur-X MINIMUM and BASIC WL cannot be read into the
157
+ model.
158
+ - [#49](https://github.com/letsrevel/euinvoice/issues/49): KoSIT's XRechnung severity overrides are not applied,
159
+ so the verdict can differ from the KoSIT validator in both directions (e.g. BR-CL-21/23, XRechnung Extension
160
+ and CVD).
161
+ - [#67](https://github.com/letsrevel/euinvoice/issues/67): the XRechnung profiles set no default BT-23; the caller
162
+ must provide it (the pre-flight reports PEPPOL-EN16931-R001 otherwise).
163
+ - [#40](https://github.com/letsrevel/euinvoice/issues/40): the hardened parser rejects a single text node over
164
+ 10,000,000 bytes, i.e. a BT-125 attachment over about 7.5 MB.
165
+
166
+ ## Development
167
+
168
+ ```bash
169
+ make setup # uv sync --all-extras --group dev
170
+ make check # ruff + mypy --strict + file length
171
+ make test # unit tests, ≥95% branch coverage
172
+ make artifacts # fetch official artifacts + corpora
173
+ make conformance # run them
174
+ ```
175
+
176
+ See [`CONTRIBUTING.md`](CONTRIBUTING.md). Security reports: [`SECURITY.md`](SECURITY.md).
177
+
178
+ ## Licence
179
+
180
+ MIT. Official validation artifacts are third-party works under their own licences (EUPL-1.2,
181
+ Apache-2.0 and others). They are fetched at runtime and never redistributed by this package.
@@ -0,0 +1,138 @@
1
+ [project]
2
+ name = "euinvoice"
3
+ version = "0.1.0"
4
+ description = "EN 16931 e-invoicing for Python: build, serialize (UBL/CII), validate, parse and embed (Factur-X) European e-invoices."
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ authors = [
9
+ { name = "Biagio Distefano", email = "biagio@biagiodistefano.io" }
10
+ ]
11
+ keywords = ["e-invoicing", "en16931", "peppol", "xrechnung", "factur-x", "zugferd", "ubl", "cii", "einvoice"]
12
+ classifiers = [
13
+ "Development Status :: 2 - Pre-Alpha",
14
+ "Intended Audience :: Developers",
15
+ "Operating System :: OS Independent",
16
+ "Programming Language :: Python :: 3",
17
+ "Programming Language :: Python :: 3.12",
18
+ "Programming Language :: Python :: 3.13",
19
+ "Programming Language :: Python :: 3.14",
20
+ "Topic :: Office/Business :: Financial :: Accounting",
21
+ "Topic :: Text Processing :: Markup :: XML",
22
+ "Typing :: Typed",
23
+ ]
24
+ requires-python = ">=3.12"
25
+ dependencies = [
26
+ "lxml>=5.3",
27
+ "pydantic>=2.11",
28
+ ]
29
+
30
+ [project.optional-dependencies]
31
+ # Official Schematron validation (XSLT 3.0). SaxonC-HE is MPL-2.0.
32
+ validate = [
33
+ "saxonche>=12.9,!=13.0.0",
34
+ ]
35
+ # Factur-X / ZUGFeRD PDF/A-3 embedding and extraction.
36
+ pdf = [
37
+ "pypdf>=6.1",
38
+ ]
39
+
40
+ [project.urls]
41
+ Homepage = "https://github.com/letsrevel/euinvoice"
42
+ Issues = "https://github.com/letsrevel/euinvoice/issues"
43
+ Changelog = "https://github.com/letsrevel/euinvoice/blob/main/CHANGELOG.md"
44
+
45
+ [build-system]
46
+ requires = ["uv_build>=0.11.8,<0.13.0"]
47
+ build-backend = "uv_build"
48
+
49
+ [dependency-groups]
50
+ dev = [
51
+ "hypothesis>=6.168.4",
52
+ "licensecheck>=2026.0.8",
53
+ "lxml-stubs>=0.5.1",
54
+ "mkdocs-material>=9.7.7",
55
+ "mkdocstrings[python]>=1.0.6",
56
+ "mypy>=2.4.0",
57
+ "pip-audit>=2.10.1",
58
+ "pytest>=9.1.1",
59
+ "pytest-cov>=7.1.0",
60
+ "pytest-xdist>=3.8.0",
61
+ "ruff>=0.16.10",
62
+ ]
63
+
64
+ [tool.ruff]
65
+ line-length = 120
66
+ target-version = "py312"
67
+ src = ["src", "tests", "scripts"]
68
+
69
+ [tool.ruff.lint]
70
+ preview = true
71
+ # Compared to revel-backend: adds bugbear (B), comprehensions (C4), naming (N), pytest style (PT),
72
+ # pyupgrade (UP), simplify (SIM), ruff-specific (RUF), and flake8-bandit (S), which replaces the
73
+ # separate bandit run. Security rules that matter most here (hardened XML parsing) are enforced
74
+ # by tests, see CLAUDE.md.
75
+ select = ["B", "C4", "C90", "D", "E", "F", "I", "N", "import-private-name", "too-many-nested-blocks", "PT", "RUF", "S", "SIM", "T20", "UP", "W"]
76
+ ignore = ["undocumented-public-module", "undocumented-public-package", "undocumented-magic-method", "undocumented-public-init"]
77
+
78
+ [tool.ruff.lint.per-file-ignores]
79
+ "tests/**" = ["D", "assert", "import-private-name"]
80
+ "scripts/**" = ["print"]
81
+ # The Syntax enum lives in the package root (IMPLEMENTATION_PLAN.md §4: "Syntax enum {UBL, CII}").
82
+ "src/euinvoice/syntax/__init__.py" = ["non-empty-init-module"]
83
+ # The lazy ``facturx`` __getattr__ sits under ``if not TYPE_CHECKING`` so mypy users still get attribute errors (#27).
84
+ "src/euinvoice/__init__.py" = ["non-empty-init-module"]
85
+ # One row per business term (keyed by BT id for the coverage gate #32): long XPaths, kept one row per line.
86
+ "tests/syntax/test_*_write_bts.py" = ["line-too-long"]
87
+
88
+ [tool.ruff.lint.mccabe]
89
+ max-complexity = 10
90
+
91
+ [tool.ruff.lint.pydocstyle]
92
+ convention = "google"
93
+
94
+ [tool.ruff.lint.pylint]
95
+ max-nested-blocks = 4
96
+
97
+ [tool.mypy]
98
+ strict = true
99
+ extra_checks = true
100
+ warn_unreachable = true
101
+ warn_unused_ignores = true
102
+ plugins = ["pydantic.mypy"]
103
+ files = ["src", "tests", "scripts"]
104
+
105
+ [[tool.mypy.overrides]]
106
+ # saxonche ships no type information.
107
+ module = "saxonche.*"
108
+ ignore_missing_imports = true
109
+
110
+ [tool.pydantic-mypy]
111
+ init_typed = true
112
+ init_forbid_extra = true
113
+ warn_required_dynamic_aliases = true
114
+
115
+ [tool.pytest.ini_options]
116
+ testpaths = ["tests"]
117
+ # Dev scripts in scripts/ are importable from tests (tests/scripts/test_<name>.py).
118
+ pythonpath = ["scripts", "tests"]
119
+ addopts = "-ra --strict-markers --strict-config -m 'not conformance'"
120
+ xfail_strict = true
121
+ markers = [
122
+ "conformance: runs the official validation artifacts and upstream example corpora (needs `make artifacts`); run with `make conformance`",
123
+ ]
124
+ filterwarnings = ["error"]
125
+
126
+ [tool.coverage.run]
127
+ branch = true
128
+ source = ["euinvoice"]
129
+
130
+ [tool.coverage.report]
131
+ fail_under = 95
132
+ show_missing = true
133
+ skip_covered = true
134
+ exclude_also = [
135
+ "if t.TYPE_CHECKING:",
136
+ "raise NotImplementedError",
137
+ "@t.overload",
138
+ ]
@@ -0,0 +1,64 @@
1
+ """EN 16931 e-invoicing: build, serialize (UBL/CII), validate, parse and embed (Factur-X) European e-invoices.
2
+
3
+ The public API (plan §4): :func:`to_xml`, :func:`parse`, :func:`parse_detailed`, :func:`validate` and
4
+ :func:`detect`, the :class:`Invoice` / :class:`InvoiceDraft` models and the ``calc``, ``profiles`` and
5
+ ``facturx`` subpackages. ``facturx`` needs the ``[pdf]`` extra and is imported on first access
6
+ (``euinvoice.facturx`` or ``from euinvoice import facturx``); it is not in ``__all__``, so ``from euinvoice import *``
7
+ works without pypdf. ``import euinvoice`` loads neither pypdf nor saxonche (the ``[validate]`` extra, loaded when
8
+ validating).
9
+
10
+ The functions :func:`detect` and :func:`validate` live in the modules ``euinvoice.detection`` and
11
+ ``euinvoice.validation`` (renamed in #93 so the functions shadow no module).
12
+ """
13
+
14
+ import importlib
15
+ import typing as t
16
+ from importlib.metadata import version
17
+
18
+ from euinvoice import calc, profiles
19
+ from euinvoice._api import parse, parse_detailed, to_xml
20
+ from euinvoice.detection import detect
21
+ from euinvoice.model import Invoice, InvoiceDraft
22
+ from euinvoice.report import ValidationReport
23
+ from euinvoice.syntax import Syntax
24
+ from euinvoice.syntax.result import ParseResult
25
+ from euinvoice.validation import validate
26
+
27
+ if t.TYPE_CHECKING:
28
+ # The redundant alias marks an explicit re-export, so ruff does not add ``facturx`` to ``__all__`` (which would
29
+ # make ``from euinvoice import *`` need pypdf).
30
+ from euinvoice import facturx as facturx
31
+
32
+ __version__ = version("euinvoice")
33
+
34
+ __all__ = [
35
+ "Invoice",
36
+ "InvoiceDraft",
37
+ "ParseResult",
38
+ "Syntax",
39
+ "ValidationReport",
40
+ "__version__",
41
+ "calc",
42
+ "detect",
43
+ "parse",
44
+ "parse_detailed",
45
+ "profiles",
46
+ "to_xml",
47
+ "validate",
48
+ ]
49
+
50
+
51
+ # Hidden from type checkers so that a typo such as ``euinvoice.facturxx`` is a mypy error, not ``Any``; the
52
+ # ``TYPE_CHECKING`` import above gives them ``facturx``.
53
+ if not t.TYPE_CHECKING:
54
+
55
+ def __getattr__(name: str) -> t.Any:
56
+ """Import ``euinvoice.facturx`` on first access (it needs the ``[pdf]`` extra).
57
+
58
+ Raises:
59
+ AttributeError: ``name`` is not a lazily imported subpackage.
60
+ ImportError: ``name`` is ``"facturx"`` and pypdf is not installed; the message names the extra.
61
+ """
62
+ if name == "facturx":
63
+ return importlib.import_module("euinvoice.facturx")
64
+ raise AttributeError(f"module 'euinvoice' has no attribute {name!r}")