Spain IVA pack (khata-es)¶
KhataOS can keep books for a Spanish business. It: - checks the NIF in all its forms; - charges IVA at 21%, 10% or 4%; - handles inversión del sujeto pasivo; - checks invoice numbers and their series; - works out the modelo 303 from the books; - generates VeriFactu billing records: the SHA-256 huella chain, the record XML and the QR code URL.
Nothing is sent to the AEAT. Nothing here is tax advice: check the figures with your asesor before you file.
Code:
- crates/khata-es holds the rules: nif.rs, comunidades.rs, iva.rs,
returns.rs, verifactu.rs.
- apps/desktop/src-tauri/src/vat_es.rs turns vouchers into 303 documents
and VeriFactu records. It provides the commands modelo_303_es(from, to),
verifactu_record(voucher_id), verifactu_system() and
set_verifactu_system(settings). It reads vouchers with the shared
reader in vat_fr.rs.
- apps/desktop/src/screens/vat_es.ts provides Modelo303Report (Go To
→ Modelo 303 (Spain VAT return)) and exportVerifactu (Ctrl+E on a
saved Sales invoice or Credit Note → VeriFactu record).
Sources and open questions are in
docs/research/spain-compliance.md.
Tags:
- [PRIMARY]: read in the BOE or an AEAT document.
- [SECONDARY]: from python-stdnum, Wikipedia or other non-official
sources.
- [UNCERTAIN]: not confirmed.
Setting up a Spanish company¶
- Pick Spain as the country. The books use EUR and calendar years. The Impuesto sobre Sociedades period is the company's own year, at most 12 months (LIS art. 27); IVA periods are calendar quarters or months either way.
- In Company → Company details (Alt+K), enter the NIF and pick the
comunidad. The codes are ISO 3166-2:ES without "ES-":
AN,AR,AS,CB,CL,CM,CN,CT,EX,GA,IB,MC,MD,NC,PV,RI,VC,CE,ML. - Canarias (IGIC), Ceuta and Melilla (IPSI) are outside the IVA territory (LIVA art. 3). A company there gets an error. A sale to a party there is 0% (an export).
- País Vasco and Navarra apply the common rates, but their haciendas forales collect the IVA. Bizkaia, Gipuzkoa and Álava use TicketBAI / Batuz, not VeriFactu; this is not modelled.
- Add the rate list (also in
khata_es::iva::default_rates):
| Class | Rate | From | Notes |
|---|---|---|---|
STANDARD |
21% | 1 Sep 2012 | 18% from 1 Jul 2010 in the list [PRIMARY] |
REDUCED |
10% | 1 Sep 2012 | 8% before |
SUPERREDUCED |
4% | bread, milk, books, medicines; olive oil from 2025 | |
ZERO |
0% | the temporary food measure (until 30 Sep 2024) | |
EXEMPT |
0% | art. 20 exemptions | |
REVERSE |
0% | inversión del sujeto pasivo (art. 84.Uno.2º) |
The temporary 5% (food and energy, to 30 Sep 2024), and the 2% and 7.5% of Oct–Dec 2024, are classes you add if old periods are booked.
The furnished short lets of RDL 29/2026 (10% from 1 Dec 2026) await convalidation [PRIMARY, research §2].
Tax identifiers¶
All NIFs are nine characters [PRIMARY for the DNI rule; SECONDARY for the rest, python-stdnum]:
| Kind | Form | Check character |
|---|---|---|
| DNI | eight digits and a letter | TRWAGMYFPDXBNJZSQVHLCKE[n mod 23] |
| NIE | X, Y or Z, seven digits, a letter | X/Y/Z read as 0/1/2, then the DNI rule |
| K, L, M | the letter, seven digits, a letter | the DNI letter of the seven digits |
| Entity (former CIF) | a letter for the legal form (Orden EHA/451/2008), seven digits, a control | over the seven digits: even positions added to the digit sums of twice the odd positions; control = (10 − sum mod 10) mod 10, or JABCDEFGHI[control] |
- Entity controls: which legal forms use a letter or a digit is [UNCERTAIN] (sources conflict), so both are accepted, as python-stdnum does.
- ES prefix: an
ESprefix (the NIF-IVA) is accepted and dropped. - Test vectors: 54362315K, X2482300W, B58378431, A58818501, Q2826000H, 89890001K (the AEAT's VeriFactu test NIF).
- Other EU VAT identifiers are accepted on their shape only.
Invoices¶
- Numbers and series (RD 1619/2012 art. 6.1.a): "Número y, en su caso, serie. La numeración de las facturas dentro de cada serie será correlativa" [PRIMARY].
- Separate series are mandatory for rectificativas, for invoices issued by the recipient or a third party, and for simplified and full invoices in the same year.
- Validation: the pack requires a number to be unique in its series
across all years, and at most 60 characters (VeriFactu's
NumSerieFactura) of
[A-Za-z0-9/-], inside VeriFactu's printable ASCII. - Correlative check:
iva::correlative_gapwarns when a hand-typed number does not follow its series, or an earlier number is dated after it.verifactu_recordreports it. - Recommended series: KhataOS numbers each voucher type in its own
gap-free series. Give Credit Notes their own prefix (
R-…) so rectificativas are a separate series. - Printed invoices must carry:
- "inversión del sujeto pasivo" on a REVERSE line;
- from the VeriFactu dates, the QR code with "QR tributario:" above it, and "VERI*FACTU" or "Factura verificable en la sede electrónica de la AEAT" below it (art. 6.5, added by RD 1007/2023).
Modelo 303¶
The desktop's modelo_303_es(from, to) reads Sales and Credit Notes as
outward and Purchases and Debit Notes as inward. Tax ledgers of type "IVA"
(or "VAT") give the IVA. Credit notes are facturas rectificativas.
khata_es::returns::modelo_303 turns them into casillas
[PRIMARY: AEAT Instrucciones 2026, diseño DR303e26v101]:
| Casillas | What goes in them |
|---|---|
| 01–03 / 04–06 / 07–09 | 4% / 10% / 21%: base and cuota (base × tipo, with a warning when the IVA booked differs) |
| 150–152, 153–155 | 0% and 5% (temporary measures; the 2026 instructions give 153–155 no rate [UNCERTAIN]) |
| 10/11 | Adquisiciones intracomunitarias de bienes y servicios (self-assessed) |
| 12/13 | Otras operaciones con inversión del sujeto pasivo: REVERSE purchases from Spain, services from outside the EU |
| 14/15 | Modificación de bases y cuotas: the company's rectificativas |
| 27 | Total cuota devengada |
| 28/29 | Operaciones interiores corrientes, including the self-assessed IVA of 12/13 and of EU services |
| 36/37 | Adquisiciones intracomunitarias de bienes corrientes |
| 40/41 | Rectificación de deducciones: suppliers' rectificativas |
| 45 / 46 | Total a deducir; resultado régimen general (27 − 45) |
| 59 / 60 | Entregas intracomunitarias de bienes y servicios; exportaciones |
| 120 / 122 | No sujetas por reglas de localización; sujetas con inversión del sujeto pasivo (sales) |
| 64, 65, 66, 69, 71 | Results: 65 is 100% (no foral share); no amounts carried from earlier periods |
Not worked out (left at zero): - the recargo de equivalencia (16–26, 156–158, 168–170); - bienes de inversión (30/31, 38/39) and importaciones (32–35, 77); - the regularizaciones (43, 44, 68), cuotas a compensar (110, 78, 87), casilla 112; - the régimen simplificado, the criterio de caja and the prorrata.
Filing is quarterly (by the 20th of April, July and October, and the 30th of January), or monthly for REDEME, large companies and the SII.
SII. Companies in the SII (turnover over €6 M, REDEME, VAT groups) report invoices within 4 days and are outside VeriFactu. The SII is not modelled.
VeriFactu¶
Real Decreto 1007/2023 sets the requirements for billing software: integridad, conservación, accesibilidad, legibilidad, trazabilidad and inalterabilidad. Orden HAC/1177/2024 fixes the record layout.
Current dates [PRIMARY: RDL 15/2025, BOE 3 Dec 2025]: - before 1 Jan 2027 for Impuesto sobre Sociedades taxpayers; - before 1 Jul 2027 for everyone else.
Earlier dates were 1 Jul 2025 (RD 1007/2023) and 1 Jan / 1 Jul 2026 (RD 254/2025).
khata_es::verifactu generates the records:
- Registro de alta for an invoice. It follows the element order of the
AEAT's
SuministroInformacion.xsdv1.0, in namespacesum1: IDFactura,NombreRazonEmisor,TipoFactura;- for a rectificativa,
TipoRectificativa"I" andFacturasRectificadas; DescripcionOperacion,Destinatarios;Desglose, at most 12 lines: S1, S2, E1–E6, N2;CuotaTotal,ImporteTotal,Encadenamiento,SistemaInformatico;FechaHoraHusoGenRegistro,TipoHuella01,Huella.- Registro de anulación for a cancelled invoice.
- The huella is SHA-256 over the UTF-8 string
IDEmisorFactura=…&NumSerieFactura=…&FechaExpedicionFactura=dd-mm-yyyy&TipoFactura=…&CuotaTotal=…&ImporteTotal=…&Huella=<previous>&FechaHoraHusoGenRegistro=yyyy-mm-ddThh:mm:ss+hh:mm. - The anulación string is
IDEmisorFacturaAnulada=…&NumSerieFacturaAnulada=…&FechaExpedicionFacturaAnulada=…&Huella=…&FechaHoraHusoGenRegistro=…. - The result is written as 64 upper-case hex characters.
- Values are trimmed. The first record has
Huella=empty. - Amounts are written with two decimals, in the hash and in the XML alike.
- The golden examples: the AEAT's three worked examples (a first alta,
a chained second alta, an anulación) are tests, and all three
reproduce. The first is
3C464DAF61ACB827C65FDA19F352A4E3BDC2C640E9E9FC4CC058073F38F12F60[PRIMARY: Detalle de las especificaciones técnicas para generación de la huella o hash]. - The QR URL is
https://www2.agenciatributaria.gob.es/wlpl/TIKE-CONT/ValidarQR?nif=…&numserie=…&fecha=dd-mm-yyyy&importe=…. - The test host is
prewww2.aeat.es. Without VeriFactu mode the path isValidarQRNoVerifactu. - Values are encoded as Java's
URLEncoderdoes, which the AEAT's QR specification names. - The QR image itself (ISO/IEC 18004, level M, 30–40 mm) is left to the print code.
In the desktop¶
verifactu_record(voucher_id) keeps the chain per installation, as the
ZATCA device chain does. The settings live in the company file, outside
the audit trail, and are never synced:
| Setting | What it holds |
|---|---|
verifactu.chain |
Record count, the last record's id and huella, and its time |
verifactu.doc.<voucher id> |
The voucher's alta, its XML and QR URL. Asking again returns the same record; if the voucher changed, a warning says to issue a rectificativa |
verifactu.cancel.<voucher id> |
The anulación of a cancelled voucher that has an alta |
verifactu.system |
The SistemaInformatico (producer name and NIF, system name "KhataOS", id "KO", version, a generated installation number) and the test-QR flag |
How a record is made:
- The chain is written before the record, so a crash never reuses a
huella.
- TipoFactura: Sales → F1, or F2 when the buyer has no Spanish NIF;
Credit Note → R1, or R5 for a simplified one.
- Warnings:
- the producer fields are blank;
- the invoice is dated before 28-10-2024;
- the previous record is more than a minute later (Orden art. 7.i);
- the number breaks its series;
- foreign buyers (IDOtro) are not written yet.
- Foreign currency: a voucher with lines in a foreign currency gets no
record yet ("A VeriFactu record for an invoice in USD is not supported
yet…", forex::refuse_foreign_einvoice). The record holds the books'
EUR figures, which would not match an invoice printed in the foreign
currency, so it stays refused until the two are made to agree.
Future work:
- Submission: the VERI*FACTU SOAP service at
www1.agenciatributaria.gob.es/wlpl/TIKE-CONT/ws/SistemaFacturacion/VerifactuSOAP
needs a qualified certificate. Records are sent in batches of up to
1,000, with the AEAT's wait time, and retried.
- **The "no VERI*FACTU" mode**: XAdES-signed records and the event log
with its own chain.
- The rest: Subsanacion, IDOtro for foreign buyers, the QR image
on the printed invoice, the producer's declaración responsable, and
validation against the XSD.
Where it is in the app¶
- Modelo 303: Go To (Alt+G) → Modelo 303 (Spain VAT return), shown only for Spanish companies.
- VeriFactu: on a saved Sales invoice or Credit Note, Ctrl+E (Export) → VeriFactu record saves the record XML and shows its huella and QR address. The same menu offers Factur-X / ZUGFeRD PDF.
- Still open: no record is made when a voucher is saved (calling
esApi.verifactuRecordat save time is the next step), the QR is not on the printed invoice, and there is no settings screen foresApi.verifactuSystem/setVerifactuSystem.
Next steps¶
- Record at save time, the QR on the printed invoice, AEAT submission.
- Modelo 349 and 390; the recargo de equivalencia; the SII.
- B2B e-invoicing: RD 238/2026 makes UBL the reference syntax, and Orden
HAC/1028/2026 starts the clock (about Oct 2027 for companies over €8 M
and about Oct 2028 for others). These belong to the shared
khata-en16931crate.