-
Notifications
You must be signed in to change notification settings - Fork 0
Design decisions
🇬🇧 English first · 🇪🇸 Español más abajo
An invoice that is subtly wrong is not rejected by your code — it is rejected weeks later by FACe, with a message that does not say which field. Every decision here trades convenience for finding out early.
Facturae requires an XAdES signature before submission. This library generates a valid, unsigned document and stops.
Why. Signing needs three things that do not belong here:
- A private key and a certificate. Where they live, how they are unlocked, and who may use them are decisions for your application's security model, not for a formatting library.
- A cryptography dependency. Zero dependencies is what lets this drop into an existing system without an argument about versions. A signature library brings OpenSSL bindings with it.
- XAdES specifically — an enveloped signature with a signing certificate, signing time and a signature policy — not plain XML-DSig. Getting it subtly wrong produces a document that parses and is rejected.
A library that generated almost a signature would be worse than one that is
clear about stopping at the document.
docs/signing.md
says what to reach for instead.
There is no total to fill in. See The invoice model.
Why. Because a header total that disagrees with the lines is one of the most common rejection causes, and it is entirely preventable by removing the opportunity. The cost is that you cannot express an invoice whose stated total differs from its detail — which is not a thing you should be able to express.
desde_dict raises FacturaInvalida on a key it does not recognise, rather than
ignoring it.
Why. The alternative fails in the worst possible way: you write "importe"
where the schema wants "importe_total", the library shrugs, and you get a
document that is silently missing a value. The typo is invisible precisely
because nothing complained. An error at parse time points at the exact key.
The cost. You cannot pass a dictionary with extra fields of your own through this function. Strip them first — that is a one-line dictionary comprehension, and it is better than a silently wrong invoice.
Decimal(0.1) → 0.1000000000000000055511151231257827
Decimal(str(0.1)) → 0.1
The first is what you get from the obvious implementation. The second is what
anybody actually means. Accepting a float and converting it through its string
form is the compromise between "never use floats for money" (correct, and
ignored) and accepting whatever the caller has.
factura.total has no parentheses; factura.impuestos_repercutidos() does.
This is a genuine inconsistency if you look at it as a naming scheme, and a deliberate signal if you look at it as cost: properties are cheap reads, methods build a list. Making them look identical would hide that.
It bites exactly once, with a TypeError about a method object not being
iterable. It is documented here rather than smoothed over, because smoothing it
over would mean changing the API to be less informative.
FacturaInvalida is a ValueError. An existing except ValueError around your
invoice-building code keeps catching what it should, and callers who want the
detail can still catch the specific class.
plantilla, validar, totales and generar all accept -.
Why. The JSON round trip only earns its keep if the tool composes. Reading stdin means the library is usable from Node, from PHP, from a shell script — build the JSON there, pipe it in, take the XML out — without anybody having to write Python.
The CLI recalculates a hand-written invoice (1 250,00 / 262,50 / 150,00 / 1 362,50) and fails if the numbers do not come out.
Why. Tests prove the code was correct on the machine that ran CI. They prove
nothing about the copy installed on yours. CI also installs the built wheel and
runs it from another directory with the source not alongside — the only job
that catches a missing py.typed or a broken console-script entry point.
The standard library's XML support is enough. Nothing else is imported, so the library imposes no version constraints on the accounting system it lands in.
Una factura sutilmente mal no la rechaza tu código: la rechaza FACe semanas después, con un mensaje que no dice en qué campo. Todas las decisiones de aquí cambian comodidad por enterarse pronto.
Facturae exige una firma XAdES antes de presentarse. Esta biblioteca genera un documento válido sin firmar, y para.
Por qué. Firmar necesita tres cosas que no pintan nada aquí:
- Una clave privada y un certificado. Dónde viven, cómo se desbloquean y quién puede usarlos son decisiones del modelo de seguridad de tu aplicación, no de una biblioteca de formato.
- Una dependencia de criptografía. Cero dependencias es lo que permite que esto entre en un sistema existente sin discusión de versiones. Una biblioteca de firma se trae los enlaces de OpenSSL con ella.
- XAdES en concreto —firma envolvente con certificado de firma, hora de firma y política de firma—, no XML-DSig a secas. Equivocarse sutilmente produce un documento que se parsea y se rechaza.
Una biblioteca que generase casi una firma sería peor que una que deja claro
que se detiene en el documento.
docs/signing.md
cuenta a qué recurrir en su lugar.
No hay un total que rellenar. Ver The invoice model.
Por qué. Porque un total de cabecera que discrepa de las líneas es una de las causas de rechazo más frecuentes, y se evita del todo quitando la oportunidad. El coste es que no puedes expresar una factura cuyo total declarado difiera de su detalle — que no es algo que debieras poder expresar.
desde_dict lanza FacturaInvalida ante una clave que no reconoce, en vez de
ignorarla.
Por qué. La alternativa falla de la peor manera posible: escribes "importe"
donde el esquema quiere "importe_total", la biblioteca se encoge de hombros y
te quedas con un documento al que le falta un valor en silencio. La errata es
invisible precisamente porque nadie protestó. Un error al interpretar la entrada
señala la clave exacta.
El coste. No puedes pasar por esta función un diccionario con campos tuyos de más. Quítalos antes: es una comprensión de diccionario de una línea, y es mejor que una factura silenciosamente equivocada.
Decimal(0.1) → 0.1000000000000000055511151231257827
Decimal(str(0.1)) → 0.1
Lo primero es lo que sale de la implementación evidente. Lo segundo es lo que
cualquiera quiere decir en realidad. Aceptar un float y convertirlo a través de
su forma textual es el compromiso entre «nunca uses float para dinero»
—correcto, y desatendido— y aceptar lo que traiga quien llama.
factura.total no lleva paréntesis; factura.impuestos_repercutidos() sí.
Es una inconsistencia real si lo miras como criterio de nombres, y una señal deliberada si lo miras como coste: las propiedades son lecturas baratas y los métodos construyen una lista. Hacer que se vieran iguales lo escondería.
Muerde exactamente una vez, con un TypeError que dice que un objeto de tipo
método no es iterable. Está documentado aquí en vez de disimulado, porque
disimularlo significaría cambiar la API para que informe menos.
FacturaInvalida es un ValueError. Un except ValueError que ya rodeara tu
código de construcción de facturas sigue capturando lo que debe, y quien quiera
el detalle puede capturar la clase concreta.
plantilla, validar, totales y generar aceptan -.
Por qué. El viaje de ida y vuelta por JSON solo sale a cuenta si la herramienta se combina con otras. Leer de la entrada estándar hace que la biblioteca se pueda usar desde Node, desde PHP o desde un script de shell —construyes el JSON allí, lo canalizas y sacas el XML— sin que nadie tenga que escribir Python.
La CLI recalcula una factura escrita a mano (1.250,00 / 262,50 / 150,00 / 1.362,50) y falla si los números no salen.
Por qué. Las pruebas demuestran que el código estaba bien en la máquina que
ejecutó la CI. No demuestran nada sobre la copia instalada en la tuya. La CI
además instala la rueda construida y la ejecuta desde otro directorio, sin el
código fuente al lado: es el único trabajo que detecta un py.typed que falta
o un punto de entrada roto.
El soporte de XML de la biblioteca estándar es suficiente. No se importa nada más, así que la biblioteca no impone restricciones de versión al sistema contable donde aterrice.