getafix
Build, parse and validate
ZUGFeRD 2.x / Factur-X
Cross-Industry-Invoice (CII) XML in Python. getafix provides a fully typed dataclass model for creating and parsed invoices - every field traceable to the EN 16931 business terms (BT-…) and groups (BG-…). Validation follows a broad, but selective set of business rules (BR-…). An accompanied cli tool getafix renders and validaes invoices in the terminal.
Why "getafix"?
"get a fix" — get a Factur-X
The project's working title was carthorse - in the spirit of drafthorse, pycheval, mustang, ... - but when I learned, that is already taken, I wanted to break free from the horse naming.
Getafix is the village druid in Asterix — the one that brews the magic potion. I only knew the german name Miraculix and was hooked. The Gaulish -ix ending echoes the -X in Factur-X, and Factur-X is the French half of the standard: so the French druid it is!
Status: pre-1.0. Solid for the fields that are modelled across MINIMUM / BASIC_WL / BASIC / COMFORT (EN 16931); broad EXTENDED coverage. PDF/A-3 conformance for the host PDF is out of scope —
getafixattaches the embeddedfactur-x.xmlbut does not upgrade the surrounding PDF to PDF/A-3.
License and attribution
getafix is distributed under the Apache License 2.0 — see LICENSE.
This project is an application of the ZUGFeRD / Factur-X publication issued by the Forum elektronische Rechnung Deutschland (FeRD) at AWV e.V. The format incorporates EN 16931, reproduced by FeRD with the permission of CEN and DIN. ZUGFeRD and Factur-X are trademarks of FeRD / AWV e.V., used here only to identify the standard this library implements.
The vendored test fixtures under tests/schemas/ and tests/samples/ come from third parties and are redistributed under the Apache License 2.0:
- FeRD / AWV e.V. — XML schemas, Schematron, and the
*_zf24_*example invoices from the official ZUGFeRD 2.4 / Factur-X 1.08 distribution. - ZUGFeRD/mustangproject — additional reference CII invoices from the Java implementation's test resources.
- ZUGFeRD/corpus —
community-curated
XML-Rechnung/CII/reference invoices.
Original copyright remains with the respective upstream holders; per-file provenance is tracked in tests/samples/SOURCES.md.
Important: It is the user's responsibility to ensure that invoices generated or parsed with
getafixmeet all legal and regulatory requirements applicable in their jurisdiction.getafixdoes not guarantee compliance with any specific national or sector-specific e-invoicing mandate.
Installation
Requires Python 3.12+. We recommend uv for dependency management.
pip install getafix
The base install lets you build / serialise / validate documents with the Python stdlib XML parser. The optional extras unlock more:
| Extra | Pulls in | Enables |
|---|---|---|
getafix[lxml] |
lxml |
Round-tripping XML produced by other tools (the stdlib parser is fine for most documents; lxml is faster and more tolerant of large / namespaced inputs). |
getafix[pdf] |
pypdf |
Embedding / extracting factur-x.xml in a PDF (getafix.pdf.attach_xml and extract_xml). |
getafix[rich] |
rich |
Pretty-print an invoice using the rich library. |
getafix[cli] |
lxml, rich, pypdf |
The getafix console script — pretty-print an invoice and run the BR-* validators against it. |
Install several at once:
pip install 'getafix[lxml,pdf]'
Quickstart — build an invoice
from datetime import date
from decimal import Decimal
from getafix.schema.document import (
Context, Document, GuidelineDocument, Header
)
from getafix.schema.accounting import MonetarySummation, TaxTotal
from getafix.schema.agreement import TradeAgreement
from getafix.schema.delivery import TradeDelivery
from getafix.schema.party import (
BuyerTradeParty, PostalTradeAddressExtended, SellerTradeParty,
SpecifiedTaxRegistration, TaxSchemeId,
)
from getafix.schema.settlement import TradeSettlement
from getafix.schema.trade import Trade
from getafix.schema.types import Country, Currency, Profile, TypeCode
doc = Document(
context=Context(guideline=GuidelineDocument(id=Profile.MINIMUM)),
header=Header(
id="INV-2025-0001",
type_code=TypeCode.T_CommercialInvoice, # 380
issue_date=date(2025, 11, 16),
),
trade=Trade(
agreement=TradeAgreement(
seller=SellerTradeParty(
name="Acme GmbH",
address=PostalTradeAddressExtended(country_id=Country.DE),
tax_registrations=[
SpecifiedTaxRegistration(
id=TaxSchemeId(id="DE123456789", scheme_id="VA"),
),
],
),
buyer=BuyerTradeParty(
name="Beta AG",
address=PostalTradeAddressExtended(country_id=Country.DE),
),
),
delivery=TradeDelivery(),
settlement=TradeSettlement(
currency_code=Currency.EUR,
monetary_summation=MonetarySummation(
tax_basis_total=Decimal("100.00"),
tax_total=[TaxTotal(amount=Decimal("19.00"), currency_id=Currency.EUR)],
grand_total=Decimal("119.00"),
due_amount=Decimal("119.00"),
),
),
),
)
xml = doc.to_xml().render(indent=True) # str — ready to write to factur-x.xml
doc.validate() # raises ValidationErrors on BR-* failures
doc.to_xml() picks the right profile from Context.guideline.id. Setting a field that requires a higher profile than the document raises ProfileMismatch at render time.
Quickstart — parse an invoice
From XML bytes or a file:
import xml.etree.ElementTree as ET
from getafix.schema.document import Document
tree = ET.parse("factur-x.xml")
doc = Document.from_xml(tree.getroot())
print(doc.header.id, doc.header.type_code, doc.header.issue_date)
print(doc.trade.settlement.monetary_summation.grand_total)
doc.validate() # raises ValidationErrors with every violation
lxml.etree works the same way — pass the root element to Document.from_xml.
From a Factur-X / ZUGFeRD PDF (getafix[pdf] extra):
import xml.etree.ElementTree as ET
from getafix.pdf import extract_xml
from getafix.schema.document import Document
payload = extract_xml(Path("invoice.pdf")) # bytes or None
if payload is None:
raise SystemExit("No factur-x.xml found in the PDF")
doc = Document.from_xml(ET.fromstring(payload))
To embed an XML into an existing PDF:
from pathlib import Path
from getafix.pdf import attach_xml
attach_xml(Path("invoice.pdf"), Path("factur-x.xml")) # in-place
attach_xml(Path("invoice.pdf"), Path("factur-x.xml"),
pdf_out=Path("invoice-with-xml.pdf"))
attach_xml produces a valid PDF with a generic embedded file; it does not upgrade the host PDF to PDF/A-3, which is the formal Factur-X compliance requirement. Pair with a dedicated PDF/A-3 converter for full conformance.
Validation
from getafix.schema.element import ValidationErrors
try:
doc.validate()
except ValidationErrors as exc:
for err in exc.errors:
print(f"{err.code}: {err.message}")
Document.validate() walks the document tree once and collects every business-rule violation, raising a single ValidationErrors aggregate. Each ValidationError carries the rule's code (e.g. BR-CO-15) and a human-readable message.
Plausibility checks are reported as ValidationWarning (a ValidationError subclass). They never raise: validate() returns them when the document is otherwise valid, and the CLI prints them without failing. Rule codes starting with GETAFIX- are getafix's own checks, not part of Factur-X — e.g. GETAFIX-ADV-PREPAID (advance payments must not exceed BT-113) the GETAFIX-ADV-VAT-PLAUSIBLE warning (an advance payment's VAT must fit its amount and rate), and the GETAFIX-LINE-* warnings (a line's net amount, allowance / charge totals, VAT and gross total must follow from its quantity, price, allowances, charges and VAT rate).
Every rule getafix enforces lives in getafix.rules — one module per schema topic (accounting, line, party, settlement, trade, extended), each wired onto the relevant element's _validators.
Command-line tool
The getafix[cli] extra ships a console script that pretty-prints an invoice and runs the validators:
> getafix path/to/factur-x.xml
> getafix path/to/invoice.pdf # reads the embedded XML
> getafix --no-validate path/to/file.xml # skip BR-* checks
Exit codes:
0— parsed cleanly and passed every validator.1— parsed but at least one validation rule fired (or the document could not be parsed as a CII invoice, or no Factur-X XML was found in the supplied PDF).2— usage / IO / missing dependency error.
Profiles
ZUGFeRD / Factur-X defines five conformance profiles, ordered by completeness:
| Profile | URN | Carries line items |
|---|---|---|
MINIMUM |
urn:factur-x.eu:1p0:minimum |
✗ (header totals only). |
BASIC_WL |
urn:factur-x.eu:1p0:basicwl |
✗ (basic, without lines) |
BASIC |
urn:cen.eu:en16931:2017#compliant#urn:factur-x.eu:1p0:basic |
✓ |
COMFORT |
urn:cen.eu:en16931:2017 (a.k.a. EN 16931) |
✓ |
EXTENDED |
urn:cen.eu:en16931:2017#conformant#urn:factur-x.eu:1p0:extended |
✓ + sub-lines |
The profile is set on the document via Context(guideline=GuidelineDocument(id=Profile.X)). Getafix enforces it at render time: setting a field that only exists at a higher profile raises ProfileMismatch.
Status and known gaps
Getafix models every field that the MINIMUM, BASIC_WL, BASIC and EN 16931 (COMFORT) profiles permit. EXTENDED coverage is broad — sub-line hierarchy (BT-X-7 / BT-X-8 / BT-X-304), bundle composition (BG-X-1 IncludedReferencedProduct), per-instance batch / serial details (BG-X-84), logistics service charges (BG-X-42), advance payments (BG-X-45 with BG-X-46 / BG-X-85), the EXTENDED-only deviating parties (sales agent, buyer agent, buyer / item-level seller / tax representative, invoicer, invoicee, payer, product end-user), the penalty / discount payment-term schedules (BG-X-43 / BG-X-44), tax-currency exchange (BG-X-41), line monetary totals (BT-X-327…BT-X-330, BT-X-590), delivery terms (BG-X-22), quotation / ultimate-customer-order references, and the PEPPOL-flavoured rule overlay (BR-FXEXT-*) are all modelled.
Every shipped sample re-renders 1:1 with its source XML (tests/test_roundtrip_fidelity.py) — getafix never silently drops a field it claims to model.
The remaining EXTENDED gaps are optional leaf attributes and line-level twins the shipped samples don't exercise. Each one should be easy to add: declare the field() gated at Profile.EXTENDED, ensure XSD-sequence order - test will fail otherwise; getafix will accept a PR, or pick one up when there is a need for it:
- Party extras —
RoleCode(BT-X-483…BT-X-575) on every trade party; additional legal info (Description) on the Buyer (BT-X-334) and the line-level item seller (BT-X-571); contactTypeCode(BT-X-315…BT-X-575) on everyDefinedTradeContact(BG-6 / BG-9). - Line-level twins of header references — on
LineTradeAgreement: delivery terms (BG-X-87), contract (BG-X-2), seller order (BG-X-81), ultimate-customer order (BG-X-5); onLineTradeDelivery: the actual delivery event / date (BT-X-85-000), despatch (BG-X-13) and receiving (BG-X-82) advice; onLineTradeSettlement: the preceding-invoice reference (BG-X-48); line-note codes (BT-X-9/BT-X-10). (The line-level delivery-noteBG-X-83and additional-documentBG-X-3twins are modelled.) - Referenced-document leaves —
FormattedIssueDateTimeon the header additional / contract references (BT-X-33-00/BT-X-148-00/BT-X-149-00); preceding-invoiceTypeCode(BT-X-555); accounting referenceTypeCode(BT-X-99/BT-X-290). - Other shared leaves — item characteristic
TypeCode/ValueMeasure(BT-X-11/BT-X-12), invoicing-periodDescription(BT-X-264), allowance/chargeSequenceNumeric/BasisQuantity(BT-X-265/BT-X-266), per-line product localID(BT-X-305), net-priceIncludedTradeTax(BG-X-4, B2C VAT in the unit price).
PDF/A-3 packaging is out of scope; use factur-x, Mustangproject or a dedicated converter for full Factur-X PDF conformance.
Contributing
See CONTRIBUTING.md for the developer guide — module layout, how the dataclass model works, how to add a new BT field or BR validator, and the test / lint workflow.
References
Specification and validators:
Metadata
Release files for getafix 0.4.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 | Provenance | Actions |
|---|---|---|---|---|
| getafix-0.4.0.tar.gz | 120.0 kB | Without provenance | Details for getafix-0.4.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| getafix-0.4.0-py3-none-any.whl | Python 3 | none | any | Details for getafix-0.4.0-py3-none-any.whl |
Total release size: 256.0 kB
Release files / getafix-0.4.0.tar.gz
| Download URL | getafix-0.4.0.tar.gz |
|---|---|
| Size | 120.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
1803c6ad11bcddbd554f364d370900c6e01692bf2eed4543abcc5e657248ffb3
|
|
BLAKE2b-256 checksum How to use checksums |
f4283ba900e9f4dad46d1706bc4120de01d826f098b834c96f8ba00e74b91043
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.20 {"installer":{"name":"uv","version":"0.12.20","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 / getafix-0.4.0-py3-none-any.whl
| Download URL | getafix-0.4.0-py3-none-any.whl |
|---|---|
| Size | 136.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
eba84797789145a154b09898e57ce0077224dade69b8ea264269e25f9feba871
|
|
BLAKE2b-256 checksum How to use checksums |
39238cf368a46f002607c16e3b72be1c89c1e7bbe0511bece16a62e7421d2308
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.12.20 {"installer":{"name":"uv","version":"0.12.20","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}
|
Security for getafix 0.4.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.