Spain's official e-invoice format, generated from Python with zero dependencies.
🇬🇧 English · 🇪🇸 Versión en español
Royal Decree 238/2026, of 25 March (BOE-A-2026-7295), develops mandatory e-invoicing between Spanish businesses and professionals. The deadlines are not counted from that decree: its fourth final provision ties them to the entry into force of the ministerial order developing the public invoicing solution — twelve months afterwards for businesses whose turnover exceeded 8 million euros the previous year, twenty-four for everyone else. Whenever that clock starts, a lot of people will need to emit Facturae XML. This library emits it.
from decimal import Decimal
from facturae_es import Direccion, Emisor, Factura, Linea, Receptor, generar
factura = Factura(
numero="001",
emisor=Emisor(
nif="B12345674",
nombre="Talleres Ejemplo SL",
direccion=Direccion("Calle Mayor 1", "08001", "Barcelona", "Barcelona"),
),
receptor=Receptor(
nif="A58818501",
nombre="Cliente Ejemplo SA",
direccion=Direccion("Gran Via 100", "28013", "Madrid", "Madrid"),
),
lineas=[Linea("Consultoría", cantidad=Decimal("10"), precio_unitario=Decimal("100"))],
)
factura.total # Decimal('1210.00')
print(generar(factura))pip install git+https://github.com/mindset-code/facturae-esPython 3.9+. Zero dependencies — xml.etree.ElementTree for the document,
decimal for the arithmetic. Check the install with:
facturae-es autocomprobarThere is no total= parameter. The total comes from the lines, the lines from
quantity times price, the tax from the base:
total = Σ lines + Σ taxes charged − Σ withholdings
An invoice whose stated total disagrees with its detail is not a rounding problem to fix later; it is a document that gets rejected. Making it underivable removes the failure mode entirely.
Everything is Decimal, rounded at each amount rather than once at the end, so
Σ lines == total_bruto holds exactly.
$ facturae-es plantilla > factura.json # a filled-in starting point
$ facturae-es validar factura.json
Factura A001 de 2026-01-15: válida.
Base 1250.00
+ 01 al 21% sobre 1250.00: 262.50
- 04 al 15% sobre 1000.00: 150.00
TOTAL 1362.50 EUR
$ facturae-es generar factura.json -o factura.xmlReads - from standard input, so it composes:
mi-erp exportar | facturae-es generar - > factura.xml.
Full reference: docs/cli.md.
Most integrations do not have model objects; they have a row or a payload:
from facturae_es import desde_dict, desde_json, a_dict
factura = desde_json(texto)
datos = a_dict(factura) # round-trips to the identical XMLA misspelled key raises instead of being ignored. A precio_unitraio that
gets quietly dropped produces an invoice for zero euros and says nothing; this
is the single most valuable thing the loader does. See docs/json.md.
Each of these is enforced at construction, with a message that says what the schema expects:
- A Spanish postcode of four digits.
"8001"is what a spreadsheet does to Barcelona's"08001", and it is the most common data error of all. - A country in alpha-2.
ESP, notES— the schema wants ISO 3166-1 alpha-3. - A line with no tax block. If the operation is exempt, that is
Impuesto(IVA, 0)explicitly: the schema needs the block, and 0 % is a different statement from silence. - The same tax code twice on a line. That is not two rates, it is a double-count.
- A natural person with no surname. Facturae keeps
NameandFirstSurnamein separate elements; there is no full-name field.
The XML this produces is valid and unsigned. Facturae requires an XAdES signature before submission to FACe, and signing needs a certificate and a crypto library — neither of which belongs in a zero-dependency formatter. docs/signing.md explains the division of labour and what not to do to the bytes in between.
| Page | What is in it |
|---|---|
| Getting started | install, first invoice, why totals are derived |
| The invoice model | parties, lines, taxes, every validation and its reason |
| JSON format | every field, floats, the round trip |
| Command line | each subcommand with real output |
| Signing and FACe | what is missing to submit, and why |
| FAQ | rounding, NIF check digits, the legal calendar |
A runnable end-to-end example is in
examples/facturacion_completa.py: build
an invoice, check the totals, emit the XML, read it back to verify what was
written, round-trip through JSON, and walk through five rejected inputs. The
test suite runs it, so it cannot rot.
It generates the document. It does not sign it, does not submit it, and does not validate against the official XSD at runtime — that would mean vendoring or fetching the schema. For the chained hash VERI*FACTU requires of billing systems, a separate obligation, see pyverifactu-huella.
This is not legal or tax advice. Check the official sources before resting a real obligation on it.
- Facturae — official format, schemas and documentation
- Real Decreto 238/2026, de 25 de marzo — mandatory B2B e-invoicing
- Ley 18/2022 (Crea y Crece), which the decree develops
See CONTRIBUTING.md.
git clone https://github.com/mindset-code/facturae-es
cd facturae-es
pip install -e ".[dev]"
pytest -vMIT — see LICENSE.
Maintained by Mindset & Code. If you need e-invoicing wired into a system that is already running, write to contacto@mindset-code.com.
