cadrumo.application.aggregation.invoice_retencion module¶
Route a received invoice’s retención into the one per-perceptor store.
Retención arithmetic is identical on both kinds of invoice and means opposite things. On an ISSUED invoice the payer withholds from what they owe the taxpayer and remits it on the taxpayer’s account: the taxpayer is the retenido and the amount is a CREDIT against the pago fraccionado (RIRPF art. 110.3.a). On a RECEIVED invoice from a resident professional the taxpayer is the obligated retenedor and the amount is a LIABILITY settled to AEAT through Modelo 111 and its annual summary.
Those two destinations already exist and neither is here. The credit reaches
the M130/M100 retenciones casilla through the renta income ledger. The
liability’s canonical home is the per-perceptor retención store behind the
retenciones_aggregation binding family, whose committed M111 bindings
already read it. This module is the projection into that store and nothing
else: it builds the same RetencionObservation the
operator-declared path builds, so a received invoice becomes one more
observation in the one store rather than a second retención path with its own
totals to reconcile.
The scheme is not inferred. Which registry-owned retención scheme a payment falls under is a legal fact about the perceptor’s activity, not a property of the invoice. An invoice record carries no field that settles it. So the caller declares it, and this module refuses to guess: choosing a scheme here would file a figure under a clave the taxpayer never asserted.
The production caller is the modelo aggregate CLI: an operator (or the LLM
operator on their behalf) declares (invoice, scheme) pairs via
--received-invoice-retencion, and merge_manual_and_routed_retencion_observations()
unions the resulting observations with any hand-typed
--retencion-observation rows into the ONE set the CLI passes to
persist_retencion_observations – never two separate persist calls for one
window, since that write is set-replace.
See also
retencionesThe observation type, the aggregators, and the source-kind taxonomy.
retencion_observations_repositoryThe encrypted per-perceptor store this projection feeds through
persist_retencion_observations, the one shared write path every producer calls.IvaRetencionRoleThe declared per-(category, kind) role this module routes on, rather than re-deriving the direction from the invoice kind.
- class InvoiceRetencionProjectionDefect(*values)[source]¶
Bases:
StrEnumWhy one invoice’s retención did not become a store observation.
Every member EXCLUDES the invoice from the retenciones store and is reported rather than dropped. None of them is a refusal to record the invoice itself – the record stays in the catalogue and stays editable.
- NOT_A_RETENEDOR_LIABILITY¶
The Axis-A role for this invoice is not a liability the taxpayer owes.
Most often the invoice is issued, so its retención is the taxpayer’s CREDIT: routing it here would declare them retenedor for money withheld FROM them, inverting the direction of a tax liability. It is not a missing figure; it belongs to the income side and is already carried there. The same member covers a (category, kind) pair whose role is
NONEorUNKNOWN– neither is a liability this store may assert.
- IVA_TREATMENT_UNDECLARED¶
No IVA category is declared, so the retención role cannot be read.
The role is per
(category, kind)pair, so without a category there is no row to consult and the direction of the amount is unknown. Guessing it from the kind alone is exactly the re-derivation this module refuses.
- NO_RETENCION_DECLARED¶
The invoice declares no withheld amount, or declares it as zero.
Nothing to route. Most received invoices are in this state legitimately.
- NON_RESIDENT_SUPPLIER¶
The supplier is not Spanish-resident, so Modelo 111 is the wrong home.
The retenedor obligation on payments to non-residents runs through the IRNR surface, not the IRPF per-perceptor family this store feeds. Excluding is the conservative direction: it surfaces the invoice for the operator rather than filing a figure under a modelo that does not govern it.
- FX_UNRESOLVED¶
A non-euro invoice carries no resolved conversion rate.
The euro base and retención are unknown, and the store holds euro figures.
- MISSING_COUNTERPARTY_TAX_ID¶
A factura simplificada declared a retención but names no perceptor.
Modelo 111 requires the perceptor’s NIF; an invoice may legitimately carry no
counterparty_tax_id(RD 1619/2012 art. 6.1.d), but a supplier withholding tax is one of the cases that article makes the tax id mandatory for regardless, so this exclusion should be rare in practice.
- class InvoiceRetencionProjection(**data)[source]¶
Bases:
BaseModelThe verdict for one invoice: a store observation, or why there is none.
observationanddefectsare mutually exclusive and jointly exhaustive, enforced below, so a reader never has to interpret a null.- Variables:
invoice_id – The record this verdict is about.
observation – The routed observation, present iff the invoice routes.
defects – Why it did not route, non-empty iff it did not.
- Parameters:
data (Any)
- invoice_id: InvoiceId¶
- observation: RetencionObservation | None¶
- defects: tuple[InvoiceRetencionProjectionDefect, ...]¶
- class InvoiceRetencionRouting(**data)[source]¶
Bases:
BaseModelBoth outcome classes for a set of invoices, in one object.
The excluded half travels with the routed half so a caller cannot persist what routed without holding what did not – an excluded retención is a liability the taxpayer may still owe, and losing it silently is the failure this whole surface exists to prevent.
- Variables:
observations – The observations to persist into the per-perceptor store.
excluded – Verdicts that did not route and must be surfaced.
- Parameters:
data (Any)
- observations: tuple[RetencionObservation, ...]¶
- excluded: tuple[InvoiceRetencionProjection, ...]¶
- class InvoiceRetencionRouteRequest(**data)[source]¶
Bases:
BaseModelOne operator-declared
(invoice, scheme)pair to route at aggregation time.The wire shape the
modelo aggregateCLI parses--received-invoice-retencionJSON into. The scheme is supplied here rather than read off the invoice for the same reasonproject_received_invoice_retencion()never infers it: it is a legal fact about the perceptor’s activity the invoice record does not carry.- Parameters:
data (Any)
- invoice_id: InvoiceId¶
- scheme: RetencionScheme¶
- project_received_invoice_retencion(invoice, *, scheme)[source]¶
Project one received invoice’s declared retención into a store observation.
Never raises for an unroutable invoice: it comes back carrying defects, which is the outcome the caller surfaces.
- Parameters:
invoice (
Invoice) – An already-valid invoice record.scheme (
RetencionScheme) – The declared retención scheme. A legal fact about the perceptor’s activity that the invoice does not carry, so it is supplied rather than inferred.
- Return type:
- Returns:
The verdict, carrying either the routed observation or the defects that excluded it.
- route_invoice_retenciones(entries)[source]¶
Route many invoices, keeping the excluded ones alongside the routed ones.
- Parameters:
entries (
Iterable[tuple[Invoice,RetencionScheme]]) – Pairs of an invoice and its declared retención scheme.- Return type:
- Returns:
The observations to persist and the verdicts that did not route.
- merge_manual_and_routed_retencion_observations(manual_observations, routed_observations)[source]¶
Union hand-typed and invoice-routed observations for one persist call.
persist_retencion_observationsis a SET-REPLACE write: whatever this returns becomes the entire per-perceptor window for one(modelo, filing_year, period), so a caller must union every source before persisting rather than call it once per source. Rather than pick a winner when the operator has also hand-typed an observation this module would independently route from the same invoice, the union refuses – a bound value has one writer, and a silent pick would either double-count the invoice’s retención in the per-perceptor rollup or silently drop whichever side lost.- Parameters:
manual_observations (
Sequence[RetencionObservation]) – Observations the operator declared directly (a ledger-sourced retención, or a hand-typed invoice observation).routed_observations (
Sequence[RetencionObservation]) – Observationsroute_invoice_retenciones()produced for this same persist call.
- Return type:
- Returns:
The union of both sequences, manual observations first.
- Raises:
AggregationValidationError – A manual observation shares its
(source_kind, source_object_id)identity with a routed one – the same invoice was both hand-typed and auto-routed in one call.