aeat.domain.iva_compensation._carry_forward module

Pure regulatory IVA-compensation carry-forward logic for Modelo 303.

This module owns the typed period-state and carry-forward-lot records, the FIFO projection that turns filed-period states into source-period lots, and the four-year-window expiry policy. All logic here is pure: it depends only on decimal, datetime, pydantic, and STRICT_FROZEN_CONFIG from aeat.core. Repositories, port adapters, and orchestration that wire these pure pieces to persistence live in the application layer.

class IvaCompensationExpiryReviewState(*values)[source]

Bases: StrEnum

Review state for an IVA compensation carry-forward lot.

ACTIVE
EXPIRY_REVIEW_DUE
EXPIRED_REVIEW_REQUIRED
class IvaCompensationPeriodState(**data)[source]

Bases: BaseModel

Latest known Modelo 303 compensation state for one filed period.

Parameters:
  • taxpayer_nif (str)

  • filing_year (int)

  • period (Period)

  • expediente_id (str)

  • status (str)

  • presented_at (datetime)

  • prior_pending_amount (Decimal | None)

  • applied_amount (Decimal | None)

  • pending_for_later_amount (Decimal | None)

  • period_result_amount (Decimal | None)

  • final_result_amount (Decimal | None)

  • generated_amount (Decimal)

  • available_end_amount (Decimal)

  • source_observation_key (str)

  • source_artefact_sha256 (str | None)

taxpayer_nif: str
filing_year: int
period: Period
expediente_id: str
status: str
presented_at: datetime
prior_pending_amount: Decimal | None
applied_amount: Decimal | None
pending_for_later_amount: Decimal | None
period_result_amount: Decimal | None
final_result_amount: Decimal | None
generated_amount: Decimal
available_end_amount: Decimal
source_observation_key: str
source_artefact_sha256: str | None
class IvaCompensationCarryForwardLot(**data)[source]

Bases: BaseModel

One generated IVA compensation balance tracked from its source period.

Parameters:
taxpayer_nif: str
source_filing_year: int
source_period: Period
generated_amount: Decimal
applied_amount: Decimal
remaining_amount: Decimal
age_years: int
expiry_review_state: IvaCompensationExpiryReviewState
source_observation_key: str
class IvaCompensationCarryForwardReport(**data)[source]

Bases: BaseModel

Carry-forward lot projection from filed Modelo 303 compensation history.

Parameters:
as_of_year: int
lots: tuple[IvaCompensationCarryForwardLot, ...]
unallocated_applied_amount: Decimal
derive_303_compensation_available(*, posterior, resultado, refunded=False)[source]

Modelo 303 end-of-period available compensation carry-forward.

available equals iva.compensacion-pendiente-periodos-posteriores (AEAT box 87) plus generated, where generated is max(0, -iva.resultado) (AEAT box 69). A negative result (a quota a compensar) generates new carry-forward; a positive result generates none.

When refunded is True the period’s negative result is requested as devolución (fichero “Tipo de declaración” D) rather than carried forward: the generated credit is excluded from compensación carry, so the GENERATED component is zero and available = posterior only (for a full monthly/annual devolución request the posterior is also zero, so the period carries nothing). The default False keeps the standard compensación (C) behaviour, where the negative result generates the carry. Legal basis: RD 1624/1992 art. 30 / Ley 37/1992 art. 116 — a devolución-requested credit is not carried.

Return type:

Decimal

Parameters:
build_iva_compensation_carry_forward_report(states, *, as_of_year)[source]

Project filed-period compensation states into source-period lots.

Applications of prior compensation are allocated FIFO across earlier generated lots. Current-period generation is added after any application recorded for that same period, matching Modelo 303’s prior-balance-before-new-generation shape.

Returns an IvaCompensationCarryForwardReport.

Return type:

IvaCompensationCarryForwardReport

Parameters:
class IvaCompensationYearEndCarryPartition(**data)[source]

Bases: BaseModel

Modelo 390 year-end carry fields as one FIFO partition.

The two AEAT annual carry-forward boxes partition the year’s pending compensation credit governed by FIFO application netting across the whole ejercicio; they are NOT two independent per-period sums:

  • last_period_amount (iva.anual.compensacion-ultimo-periodo-97, AEAT box 97, “Resultado de la última autoliquidación. A compensar”) is the saldo the LAST filed period carries forward — the year-generated remaining credit still pending at the end of the last period’s autoliquidación (its disponible, net of any prior-year credit still pending).

  • generated_not_in_last_amount (iva.anual.compensacion-generada-ejercicio-no-97, AEAT box 662, “Cuotas a compensar generadas en el ejercicio, distintas a las incluidas en [97]”) is the remaining of credits GENERATED in the ejercicio that did NOT carry into the last period’s autoliquidación (e.g. a credit refunded in an intervening period, so it left the carry chain) — explicitly the year’s pending credit not included in AEAT box 97.

Applied credits appear in NEITHER box. The AEAT annual identity [86] = [95] [97] [98] [662] binds the pair: box 97 + box 662 must equal the year’s total pending with no double-count and no drop, so total_year_remaining_amount == last_period_amount + generated_not_in_last_amount always holds.

Parameters:
  • filing_year (int)

  • last_period_amount (Decimal)

  • generated_not_in_last_amount (Decimal)

  • total_year_remaining_amount (Decimal)

filing_year: int
last_period_amount: Decimal
generated_not_in_last_amount: Decimal
total_year_remaining_amount: Decimal
derive_iva_compensation_year_end_carry_partition(report, period_states, *, filing_year)[source]

Partition the year’s pending compensation credit into the Modelo 390 annual carry ids.

Drives BOTH year-end carry boxes from the single FIFO projection (report) plus the year’s filed period states, so they partition the year’s pending credit with no double-count and no drop (the AEAT identity).

The discriminator between iva.anual.compensacion-ultimo-periodo-97 (AEAT box 97, carried into the last period) and iva.anual.compensacion-generada-ejercicio-no-97 (AEAT box 662, generated-but-not-carried) is the last filed period’s available carry-forward saldo (available_end_amount = iva.compensacion-pendiente-periodos-posteriores + generated):

  • iva.anual.compensacion-ultimo-periodo-97 = the year-generated credit carried into the last period — the last filed period’s disponible (available_end_amount) capped at the year’s total remaining (the disponible may also carry prior-YEAR credit, which AEAT box 97 must not double-count, hence the cap).

  • iva.anual.compensacion-generada-ejercicio-no-97 = the rest of the year’s remaining credit (generated in the ejercicio but not carried into the last period’s autoliquidación).

In the common always-carry case every pending credit flows forward into the last period, so iva.anual.compensacion-ultimo-periodo-97 collapses to the whole year’s remaining and iva.anual.compensacion-generada-ejercicio-no-97 is zero. When a period’s credit does NOT carry into the last period (it left the chain), that remaining lands in iva.anual.compensacion-generada-ejercicio-no-97.

Returns an IvaCompensationYearEndCarryPartition.

Return type:

IvaCompensationYearEndCarryPartition

Parameters:
enforce_iva_compensation_four_year_window(report)[source]

Refuse remaining IVA compensation lots beyond the four-year window.

Returns the IvaCompensationCarryForwardReport unchanged when all lots are within the window.

Return type:

IvaCompensationCarryForwardReport

Parameters:

report (IvaCompensationCarryForwardReport)

iva_compensation_period_sort_key(period)[source]
Return type:

tuple[int, str]

Parameters:

period (Period)