cadrumo.application.aggregation.iva_ledger module

Repository-backed IVA observation projection from ledger catalogues.

This module classifies bucket-local TransactionCatalogue rows into typed IvaLedgerObservation records and binding-ready totals. The source-mesh resolver in modelo_bindings then applies the target ModeloRevision, resolves ledger_iva_aggregation bindings, and surfaces source diagnostics for ledger rows that no declared binding consumes.

When the bucket’s cross-period prorrata register resolves an active general provisional percentage for the filing year, the aggregation result carries IvaLedgerProrrataApportionment. The binding resolver applies that percentage only to deducible IVA cuota bindings; bases and output IVA cuotas stay unapportioned.

The repository-backed entry point requires the application-owned transaction catalogue capability for the active bucket.

See also

prorrata_register

Per-ejercicio carry home for the provisional percentage consumed by the IVA ledger apportionment.

LedgerIvaAggregationSourceResolver

Source-mesh adapter that calls this projection and records prorrata apportionment provenance.

test_iva_ledger_prorrata_apportionment

Regression coverage proving the active provisional percentage reduces deducible cuotas without reducing bases.

renta_ledger, renta_income_ledger, renta_gasto_ledger

Sibling Renta ledger projections.

class IvaLedgerAggregationIssueReason(*values)[source]

Bases: StrEnum

Machine-readable reasons why a ledger row did not produce IVA observations.

The first five values are shared with RentaLedgerAggregationIssueReason through _shared_issue_reasons so cross-ledger telemetry can group upstream filter rejections under one key. The remaining values are IVA-specific.

UNSUPPORTED_DIRECTION
UNSUPPORTED_CURRENCY
UNCLASSIFIED_BUSINESS_STATE
PERSONAL_TRANSACTION
OUTSIDE_PERIOD
MISSING_TAXABLE_BASE
MISSING_IVA_AMOUNT
MISSING_IVA_RATE
UNSUPPORTED_IVA_RATE
IVA_RATE_DATE_OUTSIDE_TABLE_COVERAGE
CUOTA_ON_ZERO_RATED_ROW
NON_ZERO_RATE_ON_ZERO_CUOTA_CATEGORY
NON_ARISING_CATEGORY_FOR_INVOICE_SIDE
MISSING_EUR_TAX_SUBSTRATE
INVALID_PRORRATA_REFERENCE
UNSUPPORTED_IVA_CATEGORY
MISSING_COUNTERPARTY_IDENTIFICATION_STATE
DOMESTIC_IDENTIFICATION_ON_INTRA_COMMUNITY_TRANSACTION
EU_MEMBER_STATE_ON_EXPORT_TRANSACTION
MISSING_COUNTERPARTY_ESTABLISHMENT_ON_EXPORT
CASH_ACCOUNTING_EXCLUDED_CATEGORY
MISSING_DEDUCTION_CLASSIFICATION
class IvaLedgerAggregationIssue(**data)[source]

Bases: BaseModel

Traceable exclusion emitted while projecting IVA ledger observations.

Parameters:

data (Any)

transaction_id: str
reason: IvaLedgerAggregationIssueReason
detail: IssueDetail
class ProrrataLedgerReference(**data)[source]

Bases: BaseModel

Bucket-local ledger row pointer to a legal IVA prorrata reference.

Parameters:

data (Any)

transaction_id: TransactionId
transaction_date: date
reference: ProrrataReference
base_amount: Decimal
input_iva_amount: Decimal
class IvaLedgerSectorApportionment(**data)[source]

Bases: BaseModel

Per-sector prorrata apportionment for a sectores-diferenciados bucket.

Under LIVA arts. 9.1.c / 101 a taxpayer with differentiated sectors applies the deduction regime separately per sector. Each declared sector carries its own provisional percentage and its own regime (a sector may run general while another runs especial); the sector-aware binding resolver applies THIS sector’s apportionment to every deducible cuota whose observation carries the matching sector_id.

See also

SectorDefinition

Operator-declared sector this apportionment resolves for.

Parameters:

data (Any)

sector_id: str
percentage: Decimal
regime: ProrrataRegisterRegime
class IvaLedgerProrrataApportionment(**data)[source]

Bases: BaseModel

Prorrata percentage applied to deducible ledger IVA cuotas.

Under regime == GENERAL (LIVA art. 104) the single percentage is applied to every deducible cuota binding. Under regime == ESPECIAL (LIVA art. 106) percentage is the general percentage that applies only to the COMMON-use inputs; exclusively-deductible inputs deduct in full and exclusively-non-deductible inputs deduct nothing, routed per the observation’s input_classification.

When sector_apportionments is non-empty (LIVA arts. 9.1.c / 101), every sector-owned input carries one exact declared sector. Cross-sector common use is represented only by the registry-declared mixed-use token; a bare missing sector never defaults to common. Empty sector_apportionments is the whole-entity register.

See also

ProrrataProvisionalProvenance

Regulated source of the provisional percentage carried on this apportionment.

resolve_iva_ledger_binding_values()

Applies the percentage after registry selector resolution.

Parameters:

data (Any)

percentage: Decimal
provenance: ProrrataProvisionalProvenance
regime: ProrrataRegisterRegime
source_observation_ref: str | None
authorisation_reference: str | None
sector_apportionments: tuple[IvaLedgerSectorApportionment, ...]
class IvaDifferentiatedDeductionContribution(**data)[source]

Bases: BaseModel

Immutable canonical apportioned contribution for one sector/source family.

Parameters:

data (Any)

sector_id: str
deduction_fact_kind: IvaDeductionFactKind
source_ledger_ids: tuple[str, ...]
base_amount: Decimal
deducible_iva_amount: Decimal
class AnnualDeducibleTotalsByRegime(**data)[source]

Bases: BaseModel

The ejercicio’s whole-year deducible IVA cuota under both prorrata regimes.

The settlement input to the LIVA art. 103.Dos.2.º mandatory-especial check (build_prorrata_especial_mandatory_advisory): art. 103.Dos.2.º makes prorrata especial obligatory when the deducción under the general regime exceeds the deducción under the especial regime by ten percent or more. deduction_under_general is mechanically derivable for any bucket (art. 104 applies one whole-entity percentage), so it is always honest; the especial total (art. 106 per-input classification) is honest only when the register regime is ESPECIAL, or when every deducible soportado row of the ejercicio carries a declared input_classification — unclassified_deducible_count records how many deducible soportado observations are still unclassified, so the caller can decide whether the especial total is honestly computable or the filer must first classify.

See also

compute_annual_deducible_totals_by_regime()

Builds this record from one annual observation aggregation and two apportionment passes.

Parameters:

data (Any)

deduction_under_general: Decimal
deduction_under_especial: Decimal
unclassified_deducible_count: int
regime: ProrrataRegisterRegime
class IvaLedgerAggregation(**data)[source]

Bases: BaseModel

IVA observations produced from one bucket-local transaction catalogue.

out_of_window_summary is only populated by repository-backed date partitions. Full-catalogue aggregation continues to emit row-level issues because every transaction is already loaded for classification.

Parameters:

data (Any)

period: Period
observations: Sequence[IvaLedgerObservation]
prorrata_references: Sequence[ProrrataLedgerReference]
prorrata_apportionment: IvaLedgerProrrataApportionment | None
issues: Sequence[IvaLedgerAggregationIssue]
out_of_window_summary: OutOfWindowTransactionSummary | None
art_104_tres_excluded_ledger_ids: tuple[str, ...]
aggregate_iva_ledger_observations_from_repositories(*, bucket_id, period, prorrata_register_repository, transaction_repository, investment_asset_register=None, investment_asset_profile_id=None, operation)[source]

Load the bucket-local transaction catalogue and project IVA observations.

Returns an IvaLedgerAggregation.

Parameters:
  • bucket_id (str) – The profile bucket every supplied repository must be scoped to; a divergent repository bucket is refused, not silently used.

  • period (Period) – The filing period whose transactions are projected.

  • prorrata_register_repository (ProrrataRegisterRepositoryProtocol) – The canonical register repository read for the active general-prorrata provisional percentage. Required, so no caller can fall back to an implicitly constructed register whose bucket nobody checked.

  • transaction_repository (TransactionCatalogueRepositoryProtocol) – Required bucket-bound catalogue capability.

  • investment_asset_register (BienesInversionIvaRegister | None) – Bienes-inversion authority, mandatory whenever transaction_repository is injected.

  • investment_asset_profile_id (str | None) – Profile owning that register.

  • operation (PinnedAuthorityOperation) – Existing generation-pinned authority operation owned by the enclosing workflow.

Return type:

IvaLedgerAggregation

aggregate_iva_ledger_observations(transactions, *, period, ledger_profile_id, investment_asset_register=None, investment_asset_profile_id=None, prorrata_apportionment=None, operation)[source]

Project classified ledger transaction tax facts into an IvaLedgerAggregation.

Parameters:
Return type:

IvaLedgerAggregation

resolve_iva_ledger_binding_values(revision, observations, *, prorrata_apportionment=None, operation)[source]

Resolve IVA ledger bindings, applying general-prorrata to deducible cuotas only.

Parameters:
Return type:

dict[TypeAliasType, Decimal]

Under regime == GENERAL the single provisional percentage multiplies every deducible cuota binding (LIVA art. 104). Under regime == ESPECIAL the deducible cuota is routed per the observation’s input_classification (LIVA art. 106.Uno: exclusively-deductible 100%, exclusively-non-deductible 0%, common at the general percentage) by _apply_especial_apportionment(); the general-regime code path is unchanged.

See also

resolve_ledger_iva_aggregation_binding_values()

Registry selector resolver that produces the unapportioned binding values before this wrapper applies prorrata.

ProrrataRegisterEntry

Source record for the active provisional percentage represented by IvaLedgerProrrataApportionment.

resolve_iva_differentiated_deduction_contributions(revision, observations, *, apportionment, operation)[source]

Expose the canonical sector apportionment as immutable per-kind outputs.

Core types: ModeloRevision.

Return type:

tuple[IvaDifferentiatedDeductionContribution, ...]

Parameters:
compute_annual_deducible_totals_by_regime(*, bucket_id, ejercicio, revision, prorrata_register_repository, transaction_repository, operation)[source]

Compute the ejercicio’s deducible IVA cuota under both prorrata regimes.

The plumbing for the LIVA art. 103.Dos.2.º mandatory-especial settlement check. Aggregates the ejercicio’s annual IVA observations ONCE (aggregate_iva_ledger_observations_from_repositories() over the canonical 0A annual Period), then resolves resolve_iva_ledger_binding_values() TWICE over the same observations — once with a GENERAL-stamped and once with an ESPECIAL-stamped IvaLedgerProrrataApportionment at the register’s resolved percentage — and sums the deducible-cuota binding ids under each. One aggregation, two apportionment passes through the one canonical resolver (aeat-calculation-aggregation); no second aggregation implementation is introduced.

Returns None when no register apportionment resolves for the ejercicio (prorrata inapplicable), when the register is sectorized (LIVA arts. 9.1.c / 101 — the art-103.Dos.2.º comparison composes per sector, a named v1 deferral), or when the revision declares no deducible cuota bindings. A negative deducible total (an adjustment-heavy degenerate case the art-103.Dos.2 comparison is undefined over) also returns None so the check stays silent rather than crashing on a non-comparable input.

Parameters:
  • bucket_id (str) – Active bucket whose annual ledger and prorrata register are read.

  • ejercicio (int) – The filing year whose annual deducible totals are computed.

  • revision (ModeloRevision) – The target ModeloRevision whose deducible cuota bindings are summed.

  • transaction_repository (TransactionCatalogueRepositoryProtocol) – Required bucket-bound catalogue capability.

  • prorrata_register_repository (ProrrataRegisterRepositoryProtocol) – Canonical register repository for the same bucket as the transaction catalogue.

  • operation (PinnedAuthorityOperation) – Caller-owned pinned authority operation retained through the annual aggregation.

Return type:

AnnualDeducibleTotalsByRegime | None

See also

AnnualDeducibleTotalsByRegime

The frozen record returned.

build_prorrata_especial_mandatory_advisory()

Consumes the two totals to build the mandatory-especial advisory.

validate_intracom_export_counterparty(*, transaction_id, category, counterparty_country, eu_member_state, identification_state, effective_date=None, operation)[source]

Return a gate issue when the counterparty/category coupling is violated.

Rules:

  • INTRA_COMMUNITY_SUPPLY requires a non-ES identification_state.

  • Export and export-assimilated categories require the counterparty to be POSITIVELY established in a third country, and must carry no EUMemberState.

The two rules read DIFFERENT facts, deliberately. Ley 37/1992 art. 25 exempts on the acquirer holding an IVA identification assigned by another Member State and says nothing about where it has its sede, so the intra-community rule reads identification and establishment does not enter it at all. The export rule is the one genuinely about place – an export leaves the Union – so it keeps reading establishment.

Absent identification refuses rather than falling back to eu_member_state. That fallback is precisely the defect this shape replaced: it read an address fact as a registration fact, over-declaring a Spanish-established acquirer holding a German IVA number and silently under-declaring a German-established acquirer purchasing under a Spanish NIF-IVA.

Return type:

IvaLedgerAggregationIssue | None

Parameters:
validate_iva_ledger_counterparty_category(transaction, *, operation)[source]

Return the D5 counterparty/category gate IvaLedgerAggregationIssue for a ledger transaction.

Return type:

IvaLedgerAggregationIssue | None

Parameters:
business_proportionality_for(transaction)[source]
Return type:

Decimal | None

Parameters:

transaction (Transaction)

has_converted_non_eur_amount(transaction)[source]
Return type:

bool

Parameters:

transaction (Transaction)

missing_tax_fact_reason(transaction)[source]
Return type:

IvaLedgerAggregationIssueReason | None

Parameters:

transaction (Transaction)

IVA_LEDGER_MISSING_FACT_REASONS: Final[frozenset[IvaLedgerAggregationIssueReason]]

Every reason iva_ledger_missing_fact_reasons() can emit, derived.

IVA_LEDGER_COUNTERPARTY_GATE_REASONS: Final[frozenset[IvaLedgerAggregationIssueReason]]

Every reason validate_iva_ledger_counterparty_category() can emit.

Declared rather than derived because the three branches read three different counterparty facts and collapsing them into one table would erase the legal distinction between identification and establishment the gate turns on. A behavioural gate exercises the real screen across the category matrix and asserts the observed emissions equal this set, so the declaration is pinned to behaviour rather than trusted.

iva_ledger_missing_fact_reasons(transaction)[source]

Return missing IVA fact reasons for a transaction without projecting it.

Each element is an IvaLedgerAggregationIssueReason describing one absent required tax fact.

Return type:

tuple[IvaLedgerAggregationIssueReason, ...]

Parameters:

transaction (Transaction)

missing_tax_fact_detail(reason)[source]
Return type:

str

Parameters:

reason (IvaLedgerAggregationIssueReason)

prorrata_reference_for(reference_id, *, transaction_id)[source]
Return type:

ProrrataReference | IvaLedgerAggregationIssue | None

Parameters:
  • reference_id (str | None)

  • transaction_id (str)

iva_rate_kind_for(rate, *, on_date, operation)[source]

Return the tier a declared rate belongs to, or None if it is not one.

Delegates to rate_kinds_for_declared_rate(), the registry’s own value-to-tier direction. This previously iterated the tiers and called the tier-to-value lookup once each, comparing percentages – a simulation of the inverse that holds only while the mapping is one-to-one per date. Spain’s 2024 temporary food rates broke that: 2 % and 4 % were both correct super-reducido rates, and the simulation found neither for a 2 % row.

When a rate matches more than one tier the FIRST registered match wins, and the ambiguity is real rather than a defect – the tiers genuinely share that rate on that date, and no bundled AEAT surface carries the goods axis that would separate them. Callers that must report the rate itself carry it separately on the observation.

Return type:

IvaRateKind | None

Parameters: