-
Notifications
You must be signed in to change notification settings - Fork 0
The invoice model
🇬🇧 English first · 🇪🇸 Español más abajo
Facturae is a large schema. This library does not try to mirror all of it — it models the part you actually fill in for a normal invoice, and gives you the element tree if you need the rest.
| Class | What it is |
|---|---|
Factura |
The invoice. Holds issuer, recipient and lines |
Emisor / Receptor
|
The two parties, each with a Direccion
|
Direccion |
Postal address |
Linea |
One invoice line: description, quantity, unit price |
Impuesto |
One tax applied to a line: a code and a rate |
Supporting enumerations: TipoPersona (natural or legal), TipoResidencia,
ClaseFactura, TipoDocumento.
These are Facturae's own codes, not names we invented. Getting one wrong produces a document that parses and is rejected downstream, which is why they are exported as constants rather than left as string literals in your code.
| Constant | Code | Tax |
|---|---|---|
IVA |
01 |
Value added tax |
IPSI |
02 |
Ceuta and Melilla production, services and imports tax |
IGIC |
03 |
Canary Islands general indirect tax |
IRPF |
04 |
Personal income tax withholding |
IRPF is the one that behaves differently: it is withheld, not charged, so
it subtracts from the total instead of adding to it.
Other constants worth knowing:
| Constant | Value | Meaning |
|---|---|---|
ClaseFactura.ORIGINAL |
OO |
An original invoice, not a correcting one |
TipoDocumento.COMPLETA |
FC |
A complete invoice, not simplified |
VERSION_ESQUEMA |
3.2.2 |
The schema version generated |
There is no total field to fill in. Factura exposes four properties:
| Property | What it is |
|---|---|
total_bruto |
Sum of the lines, before tax |
total_repercutido |
Taxes charged (VAT, IGIC, IPSI) |
total_retenido |
Taxes withheld (IRPF) |
total |
total_bruto + total_repercutido − total_retenido |
Why this matters. In a model where totals are fields, nothing stops the header from saying 1 000 while the lines add up to 1 100 — and that is one of the most common reasons an invoice is rejected. Here the totals are derived from the lines, so they cannot disagree with the detail.
A distinction worth internalising, because it bites once:
- The four scalars above are properties —
factura.total, no parentheses. - The breakdowns are methods —
factura.impuestos_repercutidos()andfactura.impuestos_retenidos(), with parentheses.
The scalars are cheap reads; the breakdowns build and return a list, and are
methods to make that visible at the call site. Calling a property with () or
iterating a method without them fails with a TypeError that does not obviously
point at the cause.
The invoice used by autocomprobar, which the library recomputes at runtime:
total_bruto |
1250.00 |
total_repercutido |
262.50 |
total_retenido |
150.00 |
total |
1362.50 |
Amounts go through a_decimal(), and a float is converted via its repr()
first.
That last detail is the whole reason the function exists:
Decimal(0.1) → 0.1000000000000000055511151231257827
Decimal(str(0.1)) → 0.1
An invoice built from floats is arithmetically wrong in a way that shows up as a
cent of difference between the lines and the total — after which the document is
rejected and the cause is genuinely hard to find. redondear() and formatear()
handle rounding and the string form the schema expects.
FacturaInvalida (a ValueError subclass) is raised for a document that cannot
be built. Notably, an unknown key in the input dictionary is an error, not
something quietly ignored — see Design decisions.
| Function | Direction |
|---|---|
desde_dict / desde_json
|
Data → Factura
|
a_dict |
Factura → data |
generar |
Factura → XML string |
generar_arbol |
Factura → element tree, for post-processing |
The JSON round trip is what lets a system written in something other than Python use this: build the JSON there, pipe it into the CLI, get the XML back.
Facturae es un esquema grande. Esta biblioteca no intenta reflejarlo entero: modela la parte que de verdad rellenas en una factura normal, y te da el árbol de elementos si necesitas el resto.
| Clase | Qué es |
|---|---|
Factura |
La factura. Contiene emisor, receptor y líneas |
Emisor / Receptor
|
Las dos partes, cada una con su Direccion
|
Direccion |
Dirección postal |
Linea |
Una línea de factura: descripción, cantidad, precio unitario |
Impuesto |
Un impuesto aplicado a una línea: un código y un tipo |
Enumeraciones de apoyo: TipoPersona (física o jurídica), TipoResidencia,
ClaseFactura, TipoDocumento.
Son los códigos de Facturae, no nombres que nos hayamos inventado. Equivocarse en uno produce un documento que se parsea y luego se rechaza, y por eso se exportan como constantes en vez de quedarse como literales sueltos en tu código.
| Constante | Código | Impuesto |
|---|---|---|
IVA |
01 |
Impuesto sobre el valor añadido |
IPSI |
02 |
Impuesto sobre la producción, los servicios y la importación (Ceuta y Melilla) |
IGIC |
03 |
Impuesto general indirecto canario |
IRPF |
04 |
Retención del impuesto sobre la renta |
El IRPF es el que se comporta distinto: se retiene, no se repercute, así que
resta del total en vez de sumar.
Otras constantes que conviene conocer:
| Constante | Valor | Significado |
|---|---|---|
ClaseFactura.ORIGINAL |
OO |
Factura original, no rectificativa |
TipoDocumento.COMPLETA |
FC |
Factura completa, no simplificada |
VERSION_ESQUEMA |
3.2.2 |
Versión del esquema que se genera |
No hay un campo total que rellenar. Factura expone cuatro propiedades:
| Propiedad | Qué es |
|---|---|
total_bruto |
Suma de las líneas, antes de impuestos |
total_repercutido |
Impuestos repercutidos (IVA, IGIC, IPSI) |
total_retenido |
Impuestos retenidos (IRPF) |
total |
total_bruto + total_repercutido − total_retenido |
Por qué importa. En un modelo donde los totales son campos, nada impide que la cabecera diga 1.000 mientras las líneas suman 1.100 — y ese es uno de los motivos más frecuentes de rechazo de una factura. Aquí los totales se derivan de las líneas, así que no pueden discrepar del detalle.
Una distinción que conviene interiorizar, porque muerde una vez:
- Los cuatro escalares de arriba son propiedades:
factura.total, sin paréntesis. - Los desgloses son métodos:
factura.impuestos_repercutidos()efactura.impuestos_retenidos(), con paréntesis.
Los escalares son lecturas baratas; los desgloses construyen y devuelven una
lista, y son métodos para que eso se vea en el punto de llamada. Llamar a una
propiedad con () o iterar un método sin ellos falla con un TypeError que no
apunta de forma evidente a la causa.
La factura que usa autocomprobar, que la biblioteca recalcula en tiempo de
ejecución:
total_bruto |
1250.00 |
total_repercutido |
262.50 |
total_retenido |
150.00 |
total |
1362.50 |
Los importes pasan por a_decimal(), y un float se convierte antes a través de
su repr().
Ese último detalle es toda la razón de ser de la función:
Decimal(0.1) → 0.1000000000000000055511151231257827
Decimal(str(0.1)) → 0.1
Una factura construida con float está aritméticamente mal de una forma que
aparece como un céntimo de diferencia entre las líneas y el total, tras lo cual
el documento se rechaza y la causa es genuinamente difícil de encontrar.
redondear() y formatear() se ocupan del redondeo y de la forma textual que
espera el esquema.
FacturaInvalida (subclase de ValueError) se lanza para un documento que no se
puede construir. En particular, una clave desconocida en el diccionario de
entrada es un error, no algo que se ignore en silencio — ver
Design decisions.
| Función | Sentido |
|---|---|
desde_dict / desde_json
|
Datos → Factura
|
a_dict |
Factura → datos |
generar |
Factura → cadena XML |
generar_arbol |
Factura → árbol de elementos, para post-procesar |
El viaje de ida y vuelta por JSON es lo que permite usar esto desde un sistema escrito en algo que no sea Python: construyes el JSON allí, lo canalizas a la CLI y recuperas el XML.