cadrumo.domain.calculations.registry.ledger_iva_bindings module

Canonical IVA ledger aggregation binding family.

class IvaLedgerObservation(**data)[source]

Bases: BaseModel

One factual ledger line tagged with the IVA classification triple.

Modelo 303 / 322 / 353 / 309 / 390 binding selectors filter these observations by category, rate kind, and flow direction; the runtime aggregates the matched lines through the binding’s aggregation operator.

Variables:
  • ledger_id – Stable id of the source ledger line.

  • transaction_date – When the supply takes place.

  • category – Substrate IvaCategory resolved by the classifier.

  • rate_kind – Substrate IvaRateKind rate tier.

  • flow_direction – Substrate IvaFlowDirection (output / input / self-assessed reverse charge).

  • base_amount – Taxable base in EUR.

  • iva_amount – IVA amount in EUR (cuota repercutida or soportada, depending on flow direction).

Parameters:

data (Any)

ledger_id: str
transaction_date: date
category: IvaCategory
exemption_article: IvaExemptionArticle | None
rate_kind: IvaRateKind
flow_direction: IvaFlowDirection
base_amount: Decimal
iva_amount: Decimal
applied_rate: UnitProportion | None

The numeric IVA rate this line was charged at, as a fraction, when known.

Carried ALONGSIDE rate_kind rather than instead of it, because the two answer different questions and the annual return asks both. rate_kind is the semantic tier (general / reducido / super-reducido / zero); this is the rate that tier resolved to on this line’s date. One tier can produce several rates within a single filing year: the anti-inflation food measures stepped the super-reducido tier 4 % to 2 % to 0 % and the reducido tier 10 % to 5 % to 7.5 % across successive extensions, which is why Modelo 390 carries one box per rate per window where Modelo 303 carries one per tier. Resolving to the tier and discarding the value made those boxes unpopulatable, because two lines at 4 % and 2 % arrived downstream indistinguishable.

None where the rate is genuinely unknown rather than zero — a pre-classified candidate supplied without one, or an exempt line that carries no rate at all. A caller MUST NOT read None as 0 %: zero-rated and rate-less are different declarations, and IvaRateKind already distinguishes ZERO from EXEMPT for exactly that reason.

recargo_amount: Decimal

Recargo de equivalencia cuota the supplier charged on a repercutido sale to a recargo-regime retailer, in EUR. Decimal("0") on every line that carries no recargo. Modelo 303 recargo cuota casillas select these via the recargo_amount_sum fact, routed by the line’s IVA rate tier.

prorrata_reference_id: str | None

Stable id of the linked ProrrataLedgerReference row, when set.

Populated by the aggregator only on SOPORTADO (input IVA) flows that carry a validated prorrata reference. Downstream Modelo 303 / 390 binding selectors filter prorrata-linked observations without a manual join against the parallel prorrata_references tuple.

cash_accounting_treatment: IvaCashAccountingTreatment

Independent criterio-de-caja affiliation for this projection.

observation_role: IvaLedgerObservationRole

Whether this is a monetary settlement or an art. 75 information projection.

Cash-accounting operation and payment observations retain the same typed treatment affiliation. Registry bindings select this orthogonal role so informational boxes cannot consume monetary settlements, and ordinary IVA boxes cannot consume the operation-date information projection.

input_classification: InputClassification | None

Operator-declared LIVA art. 106 prorrata-especial per-input use class.

Carried from the source ledger transaction’s input_classification. Meaningful only for SOPORTADO (input IVA) rows in a bucket under prorrata especial: the regime-aware ledger IVA apportionment routes the deducible cuota by this classification (the art. 106.Uno reglas 100%/0%/general). None for every row not under especial or carrying no per-input use declaration; the general-regime apportionment ignores it.

prorrata_sector_id: str | None

Operator-declared LIVA arts. 9.1.c / 101 differentiated sector.

Carried from the source ledger transaction’s prorrata_sector_id. Meaningful only for SOPORTADO (input IVA) rows in a sectorized bucket: the sector-aware ledger IVA apportionment applies THAT sector’s provisional percentage to the deducible cuota. None is a common-use input in a sectorized bucket (apportioned by the art. 104.Dos common percentage) and the whole-entity default otherwise; the non-sectorized apportionment ignores it.

deduction_fact_kind: IvaDeductionFactKind | None

Exact, evidence-grounded differentiated-sector deduction family.

deduction_provenance: IvaDeductionClassificationProvenance | None
investment_asset_id: str | None
rectifies_ledger_id: str | None
iva_ledger_selector(binding)[source]

Validate and parse a binding selector into a typed IVA selector.

Return type:

LedgerIvaProvider

Parameters:

binding (BindingDefinition)

class IvaLedgerScreenBinding(binding_id, selector)[source]

Bases: NamedTuple

One selected-revision binding used by the invoice-versus-ledger screen.

The screen is deliberately selected from the typed IVA selector rather than from binding IDs authored in application code. Keeping the parsed selector alongside its ID lets callers retain the category/rate/flow provenance when producing diagnostics without introducing a second selector representation.

Parameters:
binding_id: TypeAliasType

Alias for field number 0

selector: LedgerIvaProvider

Alias for field number 1

invoice_ledger_screen_bindings(revision, *, modelo)[source]

Resolve the invoice screen’s seven typed bindings from a revision.

The invoice-versus-ledger refusal applies only to the M303 and M390 revisions. For every other modelo the empty result preserves the previous non-applicability contract. For an applicable modelo, all seven shapes must be present exactly once; an incomplete, duplicate, cross-model, or unknown screen-shaped binding raises a registry validation error rather than silently weakening the screen.

Binding IDs are returned in a stable conceptual order (three IVA output tiers, domestic input IVA, then three recargo tiers). The IDs are merely selected outputs; the selector is the authority used to identify them.

Core types: ModeloRevision.

Return type:

tuple[IvaLedgerScreenBinding, ...]

Parameters:
invoice_ledger_screen_binding_ids(revision, *, modelo)[source]

Return the selected revision’s stable invoice-screen binding IDs.

Core types: ModeloRevision.

Return type:

tuple[TypeAliasType, ...]

Parameters:
validate_ledger_iva_aggregation_binding_definition(binding)[source]

Validate a ledger_iva_aggregation binding’s selector and aggregation.

Parameters:

binding (BindingDefinition) – The BindingDefinition to validate. Must have source == "ledger_iva_aggregation".

Raises:

RegistryValidationError – If the selector is malformed (unknown category / rate kind / flow direction / fact, empty tuple), if the aggregation operator is not “sum”, or if the binding source is not “ledger_iva_aggregation”.

Return type:

None

class IvaSelectorAxesProtocol(*args, **kwargs)[source]

Bases: Protocol

The six axes the IVA selector matcher reads off an observation.

Declared so the matcher can state the shape it actually needs instead of naming the full IvaLedgerObservation record. Both that record and the reachability probe below satisfy it structurally, which is what makes the probe a legitimate stand-in rather than an unchecked substitution – previously the probe was passed where the concrete record was declared and only the absence of a checked seam kept that quiet.

Same shape as RentaIncomeObservationProtocol serves for the renta side of this module.

property category: IvaCategory

Return the IVA category assigned to the observation.

property rate_kind: IvaRateKind

Return the IVA rate tier assigned to the observation.

property flow_direction: IvaFlowDirection

Return the observation’s IVA flow direction.

property cash_accounting_treatment: str

Return the observation’s cash-accounting treatment.

property observation_role: IvaLedgerObservationRole

Return the ledger observation role used by the selector.

property exemption_article: IvaExemptionArticle | None

Return the exemption article when the observation is exempt.

property applied_rate: Decimal | None

Return the applied IVA rate as a fraction, when known.

resolve_ledger_iva_aggregation_binding_values(revision, observations)[source]

Resolve every ledger_iva_aggregation binding on revision.

Filters observations by the three classification axes (category in selector.categories, rate_kind in selector.rate_kinds, flow_direction matches selector.flow_direction) and aggregates the matched lines’ iva_amount, recargo_amount, or base_amount per the declared fact. Delegates the filter/aggregate skeleton to resolve_ledger_family_binding_values(), shared by every ledger family resolver; the per-selector categories / rate_kinds sets are built once when the matcher closure is constructed for a binding, not once per observation.

Parameters:
Return type:

dict[TypeAliasType, Decimal]

Returns:

Mapping of binding id to the aggregated Decimal value. Empty match sets resolve to Decimal("0").

unsupported_ledger_iva_observations(revision, observations)[source]

Return IVA observations no binding on revision can consume.

Delegates the screen to unsupported_ledger_family_observations() — see that function for the shared fail-closed contract (why an unmatched observation is a modelling gap, not a legitimate zero). This family carries the campaign’s two documented, deliberate asymmetries against its six siblings — both load-bearing, neither a gap to “fix”:

  1. ``extra_exclusion`` (no sibling has one). Categories that bear no Modelo 303 cuota by law (the published 0084 cuota-less projection — exempt, zero-rated, not-subject, exempt intra-community supplies/exports, triangulation, régimen simplificado) are excluded before the binding check: they correctly match no cuota binding, so flagging them would be a false positive. After the M303 routing tail (domestic / intra-community reverse-charge bindings, the import deducible binding) landed, every cuota-bearing declarable category has a consuming binding, so the residual unsupported set is empty for the known declarable categories; the function still fail-closes on any new declarable triple that no binding selects.

  2. ``is_declarable`` is ``lambda observation: True`` — this family has NO zero-amount false-fire guard, unlike every other ledger family. This is intentional, not an oversight: adding one would SUPPRESS real findings the current code fires on (no-silent-under-declaration). Do not add a zero-amount / zero-cuota guard here to “match” the other six families.

Parameters:
Return type:

tuple[IvaLedgerObservation, ...]

Returns:

Tuple of IvaLedgerObservation instances not matched by any binding.

unrouted_ledger_iva_quantities(revision, observations)[source]

Return IVA quantities the rows carry that no binding on revision draws.

The quantity-keyed sibling of unsupported_ledger_iva_observations(), and it watches the axis that screen cannot. That one asks whether a ROW is selected by some binding; every IVA row carries three independent quantities, so a row selected for its cuota reads as consumed while its base imponible reaches nothing at all.

The gap is live, not hypothetical, and it survives a revision declaring the fact. Both Modelo 303 and Modelo 390 declare base_amount_sum bindings covering the domestic tiers, so “is base imponible drawn” answers yes on each. Their base bindings nonetheless reach no import or reverse-charge row: import_third_country, intra_community_acquisition_reverse_charge and intra_community_service_acquisition_reverse_charge have their CUOTA drawn and their base drawn by nothing, on both modelos. The rows are consumed for that cuota, so the row screen is silent by construction, and a coverage test keyed on the fact alone would be silent too.

That is why coverage is asked per row and per fact. The earlier worked example here — Modelo 390 declaring no base binding at all — was closed by the annual-form campaign, and closing it is precisely what would have blinded a fact-keyed screen to the three categories above. A screen’s value is the mechanism, never the instance: this one keeps reporting whatever the declared bindings fail to reach, and correctly falls silent on the four domestic categories both modelos now cover.

This function reports; it does not close. Routing an import or reverse-charge base imponible is registry work with its own casillas and grounding.

Parameters:
Return type:

tuple[UnroutedLedgerQuantity[IvaLedgerObservation], ...]

Returns:

One UnroutedLedgerQuantity per fact with uncovered non-zero rows, ordered by fact name, each carrying only the rows that fact’s bindings fail to reach.

structurally_unroutable_iva_base_categories(revision, *, out_of_scope=None)[source]

Return IvaCategory members no base_amount_sum binding on revision could ever reach.

A fourth axis beside unsupported_ledger_iva_observations() (is this ROW selected) and unrouted_ledger_iva_quantities() (is this row’s FACT drawn). Both siblings are OBSERVATION-DEPENDENT: they cannot fire until a taxpayer’s ledger actually holds a row of the affected category, and unrouted_ledger_iva_quantities() in particular reports only “uncovered non-zero rows” – its own docstring states it “must not manufacture a finding from” a zero total, which is exactly why it needs no category exclusion set: a cuota-less category’s cuota is 0.00 BY LAW, so it is filtered by the non-zero guard before any exclusion set could matter.

This screen has no rows to filter on, so it needs the opposite of that guard – an explicit declaration of what is out of scope, because “unmentioned” would otherwise mean “unrouted.” It answers a question neither sibling can: “could this revision’s bindings EVER route this category’s base, independent of whether any taxpayer has fallen into the hole yet?” True or false from the registry alone, before a single ledger row exists. For a filing-grade tool that distinction matters: the first taxpayer with an unrouted reverse-charge base should not be the detector.

Three states result from combining this screen with its siblings, and a caller must not collapse two of them into one “no finding”:

  • unroutable: this screen fires – no binding on the revision could ever draw this category’s base, for any observation.

  • unrouted: this screen is silent (some binding COULD draw it) but unrouted_ledger_iva_quantities() fires for a real row – the binding exists but a particular row still reached nothing (e.g. a binding scoped to one flow direction leaves a row of the same category under a different flow direction uncovered).

  • routed: both are silent.

out_of_scope is deliberately NOT the published 0084 cuota-less projection. That set answers “does this category produce a cuota?”; this screen asks “does this category’s BASE reach some casilla?”, and several by-law cuota-less categories DO carry a real base by law – the registry-declared zero-rated category is the proof: zero cuota by definition, base-bearing by law. Reusing CUOTA_LESS here would suppress exactly the population this screen exists to catch. The default is M303’s own published 0084 out-of-scope declaration; no generic per-modelo scope mechanism exists in the registry today (a registry-expressiveness gap in its own right), so a caller working a different modelo must supply its own set rather than default into M303’s.

Uses the real production selector parser (iva_ledger_selector()) and matcher (_iva_build_matcher()) – never a re-implementation of the match rule. For each base_amount_sum binding whose declared categories include the candidate, a probe observation is assembled from values the selector’s OWN declared axes already admit (one representative rate kind, the declared flow direction, one representative cash-accounting treatment, and one representative applied rate / exemption article where the selector restricts them) and run through the real matcher. This never guesses a value the selector did not itself declare, so it cannot invent a false match.

Parameters:
  • revision (ModeloRevision) – The ModeloRevision whose ledger_iva_aggregation

  • drawn. (bindings decide which categories' base is)

  • out_of_scope (frozenset[IvaCategory] | None) – Categories this screen must not evaluate at all, because “unroutable base” is not a meaningful question for them on this modelo. When omitted, the published 0084 M303 projection is resolved fail-closed.

Return type:

tuple[IvaCategory, ...]

Returns:

Every IvaCategory member not in out_of_scope for which no base_amount_sum binding on revision could ever match an observation of that category, in enum declaration order.

validate_ledger_iva_aggregation_binding(binding)[source]

Validate a ledger_iva_aggregation binding at registry-build time.

Accumulating list[str] validator over LedgerIvaProvider; runs the fact/aggregation-op invariant at build time through invariant_diagnostics(), whose raise-style body is validate_ledger_iva_aggregation_binding_definition().

Return type:

list[str]

Parameters:

binding (BindingDefinition)

class LedgerIvaProvider(**data)[source]

Bases: BaseModel

Validated form of a ledger_iva_aggregation binding selector.

Parameters:

data (Any)

kind: Literal[BindingSourceKind.LEDGER_IVA_AGGREGATION]
categories: Annotated[tuple[IvaCategory, ...], BeforeValidator(coerce_enum_tuple(IvaCategory))]
exemption_articles: Annotated[tuple[IvaExemptionArticle, ...], BeforeValidator(coerce_enum_tuple(IvaExemptionArticle))] | None
rate_kinds: Annotated[tuple[IvaRateKind, ...], BeforeValidator(coerce_enum_tuple(IvaRateKind))]
flow_direction: Annotated[IvaFlowDirection, BeforeValidator(pydantic_validation_boundary(require_registry_declared_iva_flow_direction))]
observation_roles: Annotated[tuple[IvaLedgerObservationRole, ...], BeforeValidator(coerce_enum_tuple(IvaLedgerObservationRole))]
cash_accounting_treatments: Annotated[tuple[IvaCashAccountingTreatmentCode, ...], BeforeValidator(_coerce_cash_accounting_treatment_codes)]
applied_rates: Annotated[tuple[Decimal, ...], BeforeValidator(coerce_decimal_tuple)] | None

Numeric rates this binding accepts, when the box is rate-specific.

None – the default and the shape every quarterly binding uses – means the binding does not discriminate on the value and matches whatever rates its rate_kinds tiers admit. Set it only where the FORM asks per rate rather than per tier: Modelo 390 carries one box per rate per window because a tier’s rate can change inside a filing year, so its 2 % box must reject a 4 % line that shares the super-reducido tier with it.

An observation whose applied_rate is None (rate genuinely unknown – an invoice-sourced line carries a rate slot, not a number) matches NO rate-specific binding. That is deliberate: admitting it would put an unmeasured line in a box that asserts a specific rate, and the annual return is where that assertion is read.

fact: LedgerIvaFactValue