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.components

Axis A – which components an operation in each category has.

cadrumo.domain.transactions.retencion_facts

The RIRPF art. 95 rates bounding the row-level retención inference.

class InvoiceDecompositionDefect(*values)[source]

Bases: StrEnum

Why 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 IvaCategory is 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_UNDECLARED because 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: BaseModel

The 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 InvoiceComponents with 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: BaseModel

The verdict for one invoice: its components, or why it has none.

components and defects are 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 None when 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, ...]
property is_grounded: bool

Whether the record may feed enrichment and aggregation.

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:

InvoiceDecomposition

Returns:

The verdict, carrying either euro components or the defects that excluded the record.