cadrumo.domain.invoices.decomposition module¶
The invoice decomposition contract: usable, or excluded-but-visible.
An invoice record either decomposes into the components a calculation needs, or it does not. Before this module the second case had no name: a partial record either travelled onward carrying nulls that downstream code read as legally-absent components, or it was filtered out at some consumer’s discretion and stopped existing. Both are silent. What an operator needs to see is the third outcome – source data exists which the calculation could not ground – and that is the outcome this module makes representable.
The decomposition is therefore a classifier, never a refusal. A partial
invoice is a real document the taxpayer holds, so Invoice construction
must keep accepting it; refusing at construction would delete the very record
the operator has to be told about. The contract runs afterwards, over an
already-valid record, and its verdict is data.
What “grounded” means is decided by the Axis-A component-expectation table
(category_components()), not by inspecting which
fields happen to be populated. That is the whole point: field nullness cannot
distinguish “this component does not exist in law” from “nobody recorded it”,
and conflating the two is what let an IVA-exempt professional service and an
untagged bank credit look identical. The declared IvaCategory is the
axis that separates them, so a record with no declared category is ungrounded
by construction however complete its amounts look.
The canonical identity the grounded case yields is:
total (contraprestación) = taxable_base + cuota + recargo + suplido
cash = total - retención
Three optional terms ride this identity and only one of them sits opposite
the other two. Retención is a settlement-side deduction, not a price
component: it reduces what the payer transfers, never what the operation
cost. Recargo de equivalencia and the suplido both join total instead –
LIVA art. 161 has the supplier repercutir the recargo on the entrega
alongside the cuota, so the comerciante minorista owes it and it belongs to
what the operation cost; a suplido (LIVA art. 78.Tres.3.º) is a sum the
issuer paid in the client’s name and on their behalf, excluded from the base
imponible but still something the client owes – a third position on the
identity, joining neither taxable_base nor cuota.
See also
cadrumo.domain.iva.componentsAxis A – which components an operation in each category has.
cadrumo.domain.transactions.retencion_factsThe RIRPF art. 95 rates bounding the row-level retención inference.
- class InvoiceDecompositionDefect(*values)[source]¶
Bases:
StrEnumWhy one invoice record could not be decomposed.
Every member EXCLUDES the record from enrichment and aggregation and is surfaced to the operator. None of them is a construction refusal: the record stays in the catalogue, keeps its bytes, and remains editable into a grounded state.
- IVA_TREATMENT_UNDECLARED¶
No usable
IvaCategoryis declared.Either the field is absent, or the declared category is one whose Axis-A row states its own components are unknown. Without it an exempt supply and an untagged operation are indistinguishable, so every downstream component question is unanswerable.
- TAXABLE_BASE_ABSENT¶
The category requires a taxable base and the record carries none.
Cuota-less is not substrate-less: an entrega intracomunitaria exenta and an IVA-exempt professional service both require a real base even though neither bears a cuota.
- CATEGORY_IMPOSSIBLE_ON_THIS_KIND¶
The declared category is one-directional and this invoice is the other side.
Kept separate from
IVA_TREATMENT_UNDECLAREDbecause the operator fix differs. An absent category is a data-entry gap: supply one. A category on its impossible side is a CONTRADICTION between two declarations that were both made — an entrega intracomunitaria recorded as received, say — and the contract cannot tell which of the two is the mistake. Telling that operator to “declare the IVA treatment” would send them to fix a field they already filled in.
- CUOTA_CONTRADICTS_CATEGORY¶
The record carries a cuota the declared category makes zero by law.
One of the two declarations is wrong and the contract cannot tell which, so it grounds neither.
- FX_UNRESOLVED¶
A non-euro invoice carries no resolved conversion rate.
The euro components are genuinely unknown, and approximating them is refused rather than guessed.
- class InvoiceComponents(**data)[source]¶
Bases:
BaseModelThe decomposed euro components of one grounded invoice.
Every field is required and non-optional. That is deliberate: this type exists only for a record whose components are all known, so there is no null here for a reader to misinterpret. A record with an unknown component produces defects instead, never an
InvoiceComponentswith a hole in it.- Variables:
taxable_base – The base imponible in euro.
cuota – The IVA cuota in euro; zero for a category whose cuota is zero by law.
retencion – The declared IRPF retención in euro; zero when none was declared.
recargo – The declared recargo de equivalencia in euro; zero when none was declared.
suplido – The declared suplido (LIVA art. 78.Tres.3.º) in euro; zero when none was declared.
total – The contraprestación –
taxable_base + cuota + recargo + suplido.cash – What the payer transfers –
total - retencion.
- Parameters:
data (Any)
- taxable_base: Decimal¶
- cuota: Decimal¶
- retencion: Decimal¶
- recargo: Decimal¶
- suplido: Decimal¶
- total: Decimal¶
- cash: Decimal¶
- class InvoiceDecomposition(**data)[source]¶
Bases:
BaseModelThe verdict for one invoice: its components, or why it has none.
componentsanddefectsare mutually exclusive and jointly exhaustive, enforced below. A caller therefore cannot read this record ambiguously: populated components mean grounded, a non-empty defect tuple means excluded-and-reportable, and neither state can be mistaken for the other by looking at a null.- Variables:
invoice_id – The record this verdict is about.
category – The declared IVA treatment, or
Nonewhen undeclared.components – The decomposed euro components, present iff grounded.
defects – Why the record was excluded, non-empty iff ungrounded.
- Parameters:
data (Any)
- invoice_id: InvoiceId¶
- category: IvaCategory | None¶
- components: InvoiceComponents | None¶
- defects: tuple[InvoiceDecompositionDefect, ...]¶
- decompose_invoice(invoice)[source]¶
Return the decomposition verdict for one invoice record.
Never raises for a partial record: an invoice the contract cannot ground comes back carrying defects, which is the outcome the caller surfaces.
- Parameters:
invoice (
Invoice) – An already-valid invoice record.- Return type:
- Returns:
The verdict, carrying either euro components or the defects that excluded the record.