Source code for aeat.domain.bienes_inversion

"""Capital-goods IVA deduction-regularización register and annual/disposal computes.

Models the LIVA arts. 107-110 regularización de deducciones por bienes de
inversión: a durable, cross-year :class:`BienesInversionIvaRegister`, one
:class:`BienInversionIvaRecord` per capital good, the pure art-109
:class:`RegularizacionAnualResult` computed for each supplied definitive prorrata
percentage, and the pure art-110 :class:`RegularizacionTransmisionResult` computed
for a good disposed of during its regularisation window.

The register is a taxpayer-fact store (owned goods, acquisition year, cuota
soportada, initial definitive prorrata percentage), sibling to
:mod:`domain.iva_compensation`; the regulatory constants it consumes (the
4/9-year windows, the over-10-point gate, and the /5, /10 divisors) live in the
central authoring surface :mod:`core.external_constants`, grounded verbatim
in the bundled consolidated LIVA corpus.

Register-wide projection returns :class:`RegistroRegularizacionResult` for the
ordinary annual art-109 path: each art-108-eligible in-window good (not yet
disposed of) is either computed into the proposed Modelo 303 casilla 43 / Modelo
390 regularización value when the current-year definitive prorrata fact is
available, or reported as pending that separate input. A good recorded as
disposed of in the projected year routes instead through
:func:`compute_registro_transmisiones`, which folds the art-110 single ("única")
regularización for every remaining window year into the same casilla-43 total;
art-110 carries no pending state — the disposal regime and acquisition-year facts
are already on the record, so every disposed good is always computed. This domain
module does not read the secure-object store or derive prorrata; application and
persistence layers supply those facts.

See Also:
    :mod:`application.bienes_inversion`
        Profile-scoped service that declares and lists the persisted register.
    :mod:`adapters.persistence.profile.bienes_inversion`
        FINANCIAL secure-object repository that stores the register singleton.
    :mod:`application.calculations`
        Source resolver and advisory projection surfaces for the
        ``bienes_inversion_regularizacion`` calculation source.
    :mod:`domain.iva`
        Legal prorrata substrate that supplies the separate definitive
        percentage input; usage ratios are not a substitute.
"""

from __future__ import annotations

from collections.abc import Mapping
from decimal import Decimal
from enum import StrEnum

from pydantic import BaseModel, Field, field_validator, model_validator

from ...core import STRICT_FROZEN_CONFIG as _STRICT_FROZEN_CONFIG
from ...core.errors import AeatError as _AeatError
from ...core.external_constants import (
    IVA_BIEN_INVERSION_INMUEBLE_DIVISOR as _IVA_BIEN_INVERSION_INMUEBLE_DIVISOR,
)
from ...core.external_constants import (
    IVA_BIEN_INVERSION_INMUEBLE_VENTANA_ANOS as _IVA_BIEN_INVERSION_INMUEBLE_VENTANA_ANOS,
)
from ...core.external_constants import (
    IVA_BIEN_INVERSION_MUEBLE_DIVISOR as _IVA_BIEN_INVERSION_MUEBLE_DIVISOR,
)
from ...core.external_constants import (
    IVA_BIEN_INVERSION_MUEBLE_VENTANA_ANOS as _IVA_BIEN_INVERSION_MUEBLE_VENTANA_ANOS,
)
from ...core.external_constants import (
    IVA_BIEN_INVERSION_REGULARIZACION_UMBRAL_PUNTOS as _IVA_BIEN_INVERSION_REGULARIZACION_UMBRAL_PUNTOS,
)
from ...core.money import round_to_cents as _quantize


[docs] class BienInversionRecordError(_AeatError): """Raised when a bien-de-inversión register record is structurally invalid."""
[docs] class BienInversionValidationError(BienInversionRecordError, ValueError): """Raised when a bien-de-inversión record fails Pydantic validation."""
BIENES_INVERSION_SCHEMA_VERSION = "1" """Forward-compatible schema version stamped onto every record in this module.""" _HUNDRED = Decimal("100") #: Lowest calendar year the register accepts; LIVA art. 107 predates it, but a #: pre-2000 acquisition can never be in-window for any modelled filing year. _MIN_ACQUISITION_YEAR = 2000
[docs] class BienInversionKind(StrEnum): """LIVA art. 107 regularisation-window taxonomy for a capital good. Distinct from the LIS art. 12 :class:`domain.contribuyente.assets.AssetClass` amortization taxonomy: this axis is the mueble-4yr / inmueble-9yr LIVA regularisation window (art. 107.Uno vs art. 107.Tres), not a depreciation coefficient family. """ MUEBLE = "mueble" INMUEBLE = "inmueble" @property def ventana_anos(self) -> int: """Count of following calendar years in the art-107 regularisation window.""" if self is BienInversionKind.INMUEBLE: return _IVA_BIEN_INVERSION_INMUEBLE_VENTANA_ANOS return _IVA_BIEN_INVERSION_MUEBLE_VENTANA_ANOS @property def divisor(self) -> Decimal: """Art-109 per-year regularisation divisor (5 mueble / 10 inmueble).""" if self is BienInversionKind.INMUEBLE: return _IVA_BIEN_INVERSION_INMUEBLE_DIVISOR return _IVA_BIEN_INVERSION_MUEBLE_DIVISOR
[docs] class BienInversionDisposalRegime(StrEnum): """LIVA art. 110 disposal (transmisión) regime. ``SUJETA_NO_EXENTA`` imputes the remaining window years at a 100% deduction percentage (capped at the amount originally deducted); ``EXENTA_O_NO_SUJETA`` imputes them at 0%. """ SUJETA_NO_EXENTA = "sujeta_no_exenta" EXENTA_O_NO_SUJETA = "exenta_o_no_sujeta"
[docs] class BienInversionDisposal(BaseModel): """Optional art-110 disposal event carried on a register record. Attributes: year: Calendar year the good was transmitted, within the window. regime: :class:`BienInversionDisposalRegime` of the transmission. """ model_config = _STRICT_FROZEN_CONFIG year: int = Field(ge=_MIN_ACQUISITION_YEAR, le=2099) regime: BienInversionDisposalRegime
[docs] class BienInversionIvaRecord(BaseModel): """One capital good tracked for LIVA arts. 107-110 IVA regularización. Strict, frozen, no extra fields. Carries the taxpayer facts the art-109 annual compute needs (acquisition year, cuota soportada, initial-year definitive prorrata percentage, mueble/inmueble window) plus an optional cross-reference to an :class:`domain.contribuyente.assets.AssetRecord` to avoid double data-entry, and the art-108 concept-eligibility flag. Attributes: identifier: Stable natural key chosen by the operator. description: Free-text human description. acquisition_year: Calendar year the good was acquired / put into use. cuota_soportada: Total input IVA (cuota repercutida) borne on the acquisition. Strictly positive. prorrata_inicial_pct: Definitive deduction percentage (0-100) that prevailed in the acquisition year — the baseline art-109 compares each later year's definitive percentage against. kind: :class:`BienInversionKind` — the mueble/inmueble regularisation window. art108_elegible: Whether the good qualifies as a bien de inversión under LIVA art. 108 (value at/above the escaso-valor threshold, normally used over a year as an instrument of work). ``False`` marks a good the operator recorded but which is excluded from regularisation. asset_record_ref: Optional identifier of the sibling :class:`domain.contribuyente.assets.AssetRecord`. Cross-reference only; this register — not the assets ledger — is the LIVA authority. disposal: Optional :class:`BienInversionDisposal` (art-110). When present, the good is routed through the art-110 single ("única") regularización (:func:`compute_registro_transmisiones`) in its disposal year instead of the ordinary annual art-109 comparison. schema_version: Forward-compatible schema version. ``"1"``. """ model_config = _STRICT_FROZEN_CONFIG identifier: str = Field(min_length=1) description: str = Field(min_length=1) acquisition_year: int = Field(ge=_MIN_ACQUISITION_YEAR, le=2099) cuota_soportada: Decimal = Field(gt=Decimal("0")) prorrata_inicial_pct: Decimal = Field(ge=Decimal("0"), le=Decimal("100")) kind: BienInversionKind art108_elegible: bool = True asset_record_ref: str | None = Field(default=None, min_length=1) disposal: BienInversionDisposal | None = None schema_version: str = BIENES_INVERSION_SCHEMA_VERSION @field_validator("schema_version") @classmethod def _schema_version_supported(cls, value: str) -> str: """Reject any schema_version other than :data:`BIENES_INVERSION_SCHEMA_VERSION`.""" if value != BIENES_INVERSION_SCHEMA_VERSION: raise BienInversionValidationError(f"unsupported BienInversionIvaRecord schema_version {value!r}") return value @model_validator(mode="after") def _validate_disposal_window(self) -> BienInversionIvaRecord: """A disposal year cannot precede acquisition.""" if self.disposal is not None and self.disposal.year < self.acquisition_year: raise BienInversionValidationError("disposal year cannot precede acquisition year") return self @property def deduccion_efectuada(self) -> Decimal: """Deduction actually made in the acquisition year (cuota × prorrata inicial).""" return _quantize(self.cuota_soportada * self.prorrata_inicial_pct / _HUNDRED)
[docs] def is_within_regularization_window(self, regularization_year: int) -> bool: """Whether ``regularization_year`` is one of the art-107 following window years. The window is the ``ventana_anos`` calendar years *following* acquisition (art. 107.Uno "los cuatro años naturales siguientes" / art. 107.Tres "los nueve años naturales siguientes"). The acquisition year itself is excluded: that is the year the original deduction was made, not a regularisation year. """ last_year = self.acquisition_year + self.kind.ventana_anos return self.acquisition_year < regularization_year <= last_year
[docs] def remaining_regularization_years(self, disposal_year: int) -> int: """Count of art-110 "años que resten" from ``disposal_year`` to window end. Art. 110.Uno: "se efectuará una regularización única por el tiempo de dicho período que quede por transcurrir", counting the disposal year itself and every later year through the last window year (inclusive). A disposal in the acquisition year itself counts the full window (the deduction was never regularised, so every following window year remains to transcur). """ last_year = self.acquisition_year + self.kind.ventana_anos first_pending_year = max(disposal_year, self.acquisition_year + 1) return max(0, last_year - first_pending_year + 1)
[docs] class RegularizacionDireccion(StrEnum): """Direction of an art-109 annual regularisation quotient. ``INGRESO`` — the acquisition-year deduction exceeded what the current year's percentage would allow: an ingreso complementario (repay). ``DEDUCCION`` — the current year allows more: a deducción complementaria (claim more). ``NINGUNA`` — the over-10-point gate did not fire, or the good is out of window. """ INGRESO = "ingreso" DEDUCCION = "deduccion" NINGUNA = "ninguna"
[docs] class RegularizacionAnualResult(BaseModel): """Outcome of the art-109 single-good annual regularización compute. Attributes: aplica: Whether the art-107 over-10-point gate fired (a regularisation is due). diferencia_puntos: Absolute percentage-point difference between the current year's definitive percentage and the acquisition-year one. divisor: Art-109 divisor applied (5 mueble / 10 inmueble). importe: The regularisation quotient (deducción efectuada − deducción que procedería) ÷ divisor, rounded to cents. Positive = ingreso complementario, negative = deducción complementaria; ``0.00`` when the gate did not fire. direccion: :class:`RegularizacionDireccion` describing ``importe``'s sign. """ model_config = _STRICT_FROZEN_CONFIG aplica: bool diferencia_puntos: Decimal divisor: Decimal importe: Decimal direccion: RegularizacionDireccion
[docs] def compute_regularizacion_anual( *, cuota_soportada: Decimal, prorrata_inicial_pct: Decimal, prorrata_anio_pct: Decimal, kind: BienInversionKind, ) -> RegularizacionAnualResult: """Compute the LIVA art-109 annual regularización for one capital good. Implements the art-109 procedure verbatim: 1.º the deduction that *would apply* if the cuota were borne in the year considered — ``cuota_soportada × prorrata_anio_pct``; 2.º subtract it from the deduction actually made in the acquisition year — ``cuota_soportada × prorrata_inicial_pct``; 3.º divide the (positive or negative) difference by 5, or by 10 for land and buildings; the quotient is the ingreso / deducción complementaria. The art-107.Uno gate applies: the regularisation is practised only when the absolute difference between the two definitive percentages is *strictly greater than* :data:`core.external_constants.IVA_BIEN_INVERSION_REGULARIZACION_UMBRAL_PUNTOS` (10 points). When the gate does not fire, ``importe`` is ``0.00`` and ``direccion`` is :attr:`RegularizacionDireccion.NINGUNA`. Both percentages are supplied as inputs; deriving the current-year definitive percentage (LIVA arts. 102-106) is a separate registry/materialisation concern and this function stays independent of it. Args: cuota_soportada: Total input IVA borne on acquisition (strictly positive). prorrata_inicial_pct: Definitive deduction percentage of the acquisition year (0-100). prorrata_anio_pct: Definitive deduction percentage of the regularisation year (0-100). kind: :class:`BienInversionKind` selecting the divisor. Returns: A :class:`RegularizacionAnualResult`. Raises: BienInversionValidationError: On a non-positive cuota or an out-of-range percentage. """ if cuota_soportada <= Decimal("0"): raise BienInversionValidationError("cuota_soportada must be strictly positive") for label, pct in (("prorrata_inicial_pct", prorrata_inicial_pct), ("prorrata_anio_pct", prorrata_anio_pct)): if pct < Decimal("0") or pct > _HUNDRED: raise BienInversionValidationError(f"{label} must be between 0 and 100") diferencia_puntos = abs(prorrata_anio_pct - prorrata_inicial_pct) divisor = kind.divisor if diferencia_puntos <= _IVA_BIEN_INVERSION_REGULARIZACION_UMBRAL_PUNTOS: return RegularizacionAnualResult( aplica=False, diferencia_puntos=diferencia_puntos, divisor=divisor, importe=Decimal("0.00"), direccion=RegularizacionDireccion.NINGUNA, ) deduccion_efectuada = cuota_soportada * prorrata_inicial_pct / _HUNDRED deduccion_procedente = cuota_soportada * prorrata_anio_pct / _HUNDRED importe = _quantize((deduccion_efectuada - deduccion_procedente) / divisor) if importe > Decimal("0"): direccion = RegularizacionDireccion.INGRESO elif importe < Decimal("0"): direccion = RegularizacionDireccion.DEDUCCION else: direccion = RegularizacionDireccion.NINGUNA return RegularizacionAnualResult( aplica=True, diferencia_puntos=diferencia_puntos, divisor=divisor, importe=importe, direccion=direccion, )
[docs] class RegularizacionTransmisionResult(BaseModel): """Outcome of the art-110 single-final ("única") disposal regularización. Attributes: regime: :class:`BienInversionDisposalRegime` applied. anos_restantes: Count of window years — the disposal year plus every later year through window expiry — the single regularización covers (art. 110.Uno "el tiempo de dicho período que quede por transcurrir"). divisor: Art-109 divisor applied (5 mueble / 10 inmueble), carried into the art-110 single computation per art. 110.Uno's cross-reference to the art-109 procedure. importe_sin_limite: The signed quotient before the regla-1ª cap, i.e. ``(deducción efectuada − deducción imputada) × años_restantes ÷ divisor``. importe: ``importe_sin_limite`` after applying the regla-1ª cap — a negative (DEDUCCION / additional-deduction) result never exceeds ``-cuota_devengada_entrega`` in magnitude, when supplied; equals ``importe_sin_limite`` unqualified for regla 2ª (no cap applies there) and for a non-negative regla-1ª result. direccion: :class:`RegularizacionDireccion` describing ``importe``'s sign. capped: Whether the regla-1ª cap reduced ``importe_sin_limite``'s magnitude. """ model_config = _STRICT_FROZEN_CONFIG regime: BienInversionDisposalRegime anos_restantes: int divisor: Decimal importe_sin_limite: Decimal importe: Decimal direccion: RegularizacionDireccion capped: bool
[docs] def compute_regularizacion_transmision( *, cuota_soportada: Decimal, prorrata_inicial_pct: Decimal, anos_restantes: int, kind: BienInversionKind, regime: BienInversionDisposalRegime, cuota_devengada_entrega: Decimal | None = None, ) -> RegularizacionTransmisionResult: """Compute the LIVA art-110 single ("única") disposal regularización. Art. 110.Uno: on a disposal (entrega) during the regularisation window, a SINGLE regularización is practised for the window time remaining (the disposal year plus every later window year), applying the art-109 procedure once over that whole remaining span rather than year by year: Regla 1.ª (entrega sujeta y no exenta — or an exempt/non-subject entrega that itself originates a deduction right, e.g. exports / intra-EU supplies, per the art. 110.Uno final paragraph): the good is deemed used 100% in deduction-generating operations for every remaining year, which typically yields a negative (additional-deduction) quotient since the imputed 100% usually exceeds the acquisition-year percentage. Art. 110.Uno caps the MAGNITUDE of that additional deduction at the cuota devengada on the disposal itself ("no será deducible la diferencia ... y el importe de la cuota devengada por la entrega del bien") — applied via ``cuota_devengada_entrega`` when supplied. Regla 2.ª (entrega exenta o no sujeta, without its own deduction right — the ordinary case): the good is deemed used 0% for every remaining year. No cap applies (the result is a repayment of previously-taken deduction, never an additional one). Both reglas apply the SAME art-109 quotient — (deducción efectuada − deducción imputada) ÷ divisor — but multiply the per-year difference by ``anos_restantes`` before dividing, since art. 110.Uno folds every remaining window year into one regularización rather than repeating art-109 per year. Unlike :func:`compute_regularizacion_anual`, art. 110 carries no diferencia-de-puntos gate: a disposal always triggers the single regularización regardless of how close the imputed percentage is to the acquisition-year one (the disposal itself, not a percentage drift, is what obliges it). Args: cuota_soportada: Total input IVA borne on acquisition (strictly positive). prorrata_inicial_pct: Definitive deduction percentage of the acquisition year (0-100). anos_restantes: Count of remaining window years the single regularización covers; see :meth:`BienInversionIvaRecord.remaining_regularization_years`. Must be strictly positive (a disposal outside the window has nothing left to regularise and is a caller-level concern, not this function's). kind: :class:`BienInversionKind` selecting the divisor. regime: :class:`BienInversionDisposalRegime` selecting regla 1ª (100% imputation, capped) or regla 2ª (0% imputation, uncapped). cuota_devengada_entrega: The cuota devengada on the disposal itself, applied as the regla-1ª cap. ``None`` leaves regla 1ª uncapped (the caller has not supplied the disposal's own cuota devengada yet). Returns: A :class:`RegularizacionTransmisionResult`. Raises: BienInversionValidationError: On a non-positive cuota, an out-of-range percentage, a non-positive ``anos_restantes``, or a negative ``cuota_devengada_entrega``. """ if cuota_soportada <= Decimal("0"): raise BienInversionValidationError("cuota_soportada must be strictly positive") if prorrata_inicial_pct < Decimal("0") or prorrata_inicial_pct > _HUNDRED: raise BienInversionValidationError("prorrata_inicial_pct must be between 0 and 100") if anos_restantes <= 0: raise BienInversionValidationError("anos_restantes must be strictly positive") if cuota_devengada_entrega is not None and cuota_devengada_entrega < Decimal("0"): raise BienInversionValidationError("cuota_devengada_entrega must not be negative") prorrata_imputada_pct = _HUNDRED if regime is BienInversionDisposalRegime.SUJETA_NO_EXENTA else Decimal("0") divisor = kind.divisor deduccion_efectuada = cuota_soportada * prorrata_inicial_pct / _HUNDRED deduccion_imputada = cuota_soportada * prorrata_imputada_pct / _HUNDRED importe_sin_limite = _quantize((deduccion_efectuada - deduccion_imputada) * anos_restantes / divisor) # Regla 1.ª (sujeta y no exenta) imputes 100% usage, so `importe_sin_limite` # is typically negative (deducción complementaria — additional deduction # claimed). Art. 110.Uno caps that ADDITIONAL DEDUCTION at the cuota devengada # on the disposal itself ("no será deducible la diferencia entre la cantidad # que resulte ... y el importe de la cuota devengada por la entrega del bien"). # The cap therefore bounds the MAGNITUDE of a negative (DEDUCCION) result; # regla 2.ª and a non-negative regla-1.ª result are never capped. importe = importe_sin_limite capped = False if ( regime is BienInversionDisposalRegime.SUJETA_NO_EXENTA and cuota_devengada_entrega is not None and importe_sin_limite < Decimal("0") and -importe_sin_limite > cuota_devengada_entrega ): importe = -cuota_devengada_entrega capped = True if importe > Decimal("0"): direccion = RegularizacionDireccion.INGRESO elif importe < Decimal("0"): direccion = RegularizacionDireccion.DEDUCCION else: direccion = RegularizacionDireccion.NINGUNA return RegularizacionTransmisionResult( regime=regime, anos_restantes=anos_restantes, divisor=divisor, importe_sin_limite=importe_sin_limite, importe=importe, direccion=direccion, capped=capped, )
[docs] class BienesInversionIvaRegister(BaseModel): """Encrypted JSON document holding the per-good IVA regularización register. Attributes: schema_version: Forward-compatible schema version. ``"1"``. records: Tuple of :class:`BienInversionIvaRecord` rows. """ model_config = _STRICT_FROZEN_CONFIG schema_version: str = BIENES_INVERSION_SCHEMA_VERSION records: tuple[BienInversionIvaRecord, ...] = () @field_validator("schema_version") @classmethod def _schema_version_supported(cls, value: str) -> str: """Reject any schema_version other than :data:`BIENES_INVERSION_SCHEMA_VERSION`.""" if value != BIENES_INVERSION_SCHEMA_VERSION: raise BienInversionValidationError(f"unsupported BienesInversionIvaRegister schema_version {value!r}") return value @model_validator(mode="after") def _identifiers_unique(self) -> BienesInversionIvaRegister: """Reject a register that carries two records with the same identifier.""" seen = [record.identifier for record in self.records] if len(seen) != len(set(seen)): raise BienInversionValidationError("register carries duplicate record identifiers") return self
[docs] def in_window_records(self, regularization_year: int) -> tuple[BienInversionIvaRecord, ...]: """Return each art-108-eligible :class:`BienInversionIvaRecord` in-window for the year. A good disposed of AT OR BEFORE ``regularization_year`` is excluded: art. 110.Uno's single ("única") regularización supersedes the ordinary annual art-109 comparison from the disposal year onward — see :meth:`disposed_records` and :func:`compute_registro_transmisiones` for the disposal path. """ return tuple( record for record in self.records if record.art108_elegible and record.is_within_regularization_window(regularization_year) and (record.disposal is None or record.disposal.year > regularization_year) )
[docs] def disposed_records(self, disposal_year: int) -> tuple[BienInversionIvaRecord, ...]: """Return each art-108-eligible good whose art-110 disposal falls in ``disposal_year``. Only a disposal that still leaves window time to regularise is included (:meth:`BienInversionIvaRecord.remaining_regularization_years` strictly positive); a disposal recorded outside the window has nothing left to regularise under art. 110. """ return tuple( record for record in self.records if record.art108_elegible and record.disposal is not None and record.disposal.year == disposal_year and record.remaining_regularization_years(disposal_year) > 0 )
[docs] class RegistroRegularizacionRow(BaseModel): """One good's contribution to the annual register-wide regularización. Attributes: identifier: The record identifier. kind: :class:`BienInversionKind` of the good. prorrata_anio_pct: The definitive percentage supplied for the year, or ``None`` when the caller could not supply it (the good is reported but not yet computed because the prorrata-definitiva input is absent). result: The :class:`RegularizacionAnualResult`, or ``None`` when ``prorrata_anio_pct`` was absent. """ model_config = _STRICT_FROZEN_CONFIG identifier: str kind: BienInversionKind prorrata_anio_pct: Decimal | None result: RegularizacionAnualResult | None
[docs] class RegistroRegularizacionResult(BaseModel): """Register-wide art-109 regularización projection for one year. Attributes: regularizacion_year: The year the projection covers. rows: Per-good :class:`RegistroRegularizacionRow` entries for every in-window, art-108-eligible good. proposed_casilla_43: The signed sum of every computed ``importe`` (art-109 step 3), the value proposed for Modelo 303 casilla 43 / the Modelo 390 regularización field. Positive = net ingreso, negative = net deducción complementaria. Rows without a supplied percentage contribute nothing. computed_count: Number of goods whose regularización was actually computed (a percentage was supplied and the gate fired). pending_percentage_count: Number of in-window goods for which no current-year definitive percentage was supplied. """ model_config = _STRICT_FROZEN_CONFIG regularizacion_year: int rows: tuple[RegistroRegularizacionRow, ...] proposed_casilla_43: Decimal computed_count: int pending_percentage_count: int
[docs] def compute_registro_regularizacion( register: BienesInversionIvaRegister, *, regularizacion_year: int, prorrata_definitiva_by_identifier: Mapping[str, Decimal], ) -> RegistroRegularizacionResult: """Project the register onto its annual art-109 regularización for a year. Iterates every art-108-eligible, in-window good; for each good whose current-year definitive percentage is supplied in ``prorrata_definitiva_by_identifier``, runs :func:`compute_regularizacion_anual` and folds the signed importe into the proposed casilla-43 total. Goods without a supplied percentage are reported as pending rather than silently dropped. Args: register: The persisted :class:`BienesInversionIvaRegister`. regularizacion_year: The year to regularise. prorrata_definitiva_by_identifier: Current-year definitive deduction percentage (0-100) keyed by record identifier. Absent keys mark a good whose percentage is not yet known. Returns: A :class:`RegistroRegularizacionResult`. """ rows: list[RegistroRegularizacionRow] = [] proposed = Decimal("0.00") computed_count = 0 pending = 0 for record in register.in_window_records(regularizacion_year): pct = prorrata_definitiva_by_identifier.get(record.identifier) if pct is None: pending += 1 rows.append( RegistroRegularizacionRow( identifier=record.identifier, kind=record.kind, prorrata_anio_pct=None, result=None, ) ) continue result = compute_regularizacion_anual( cuota_soportada=record.cuota_soportada, prorrata_inicial_pct=record.prorrata_inicial_pct, prorrata_anio_pct=pct, kind=record.kind, ) if result.aplica: computed_count += 1 proposed += result.importe rows.append( RegistroRegularizacionRow( identifier=record.identifier, kind=record.kind, prorrata_anio_pct=pct, result=result, ) ) return RegistroRegularizacionResult( regularizacion_year=regularizacion_year, rows=tuple(rows), proposed_casilla_43=proposed, computed_count=computed_count, pending_percentage_count=pending, )
[docs] class RegistroTransmisionRow(BaseModel): """One disposed good's contribution to the art-110 single regularización. Attributes: identifier: The record identifier. kind: :class:`BienInversionKind` of the good. disposal_year: The recorded art-110 disposal year. result: The :class:`RegularizacionTransmisionResult`. """ model_config = _STRICT_FROZEN_CONFIG identifier: str kind: BienInversionKind disposal_year: int result: RegularizacionTransmisionResult
[docs] class RegistroTransmisionesResult(BaseModel): """Register-wide art-110 single-regularización projection for one disposal year. Attributes: disposal_year: The year every included disposal occurred in. rows: Per-good :class:`RegistroTransmisionRow` entries for every art-108-eligible good disposed of in ``disposal_year`` with window time remaining. proposed_casilla_43: The signed sum of every row's ``importe`` — the value proposed for Modelo 303 casilla 43 / the Modelo 390 regularización field for the disposals in this year. Positive = net ingreso, negative = net deducción complementaria. computed_count: Number of disposed goods included in the projection. """ model_config = _STRICT_FROZEN_CONFIG disposal_year: int rows: tuple[RegistroTransmisionRow, ...] proposed_casilla_43: Decimal computed_count: int
[docs] def compute_registro_transmisiones( register: BienesInversionIvaRegister, *, disposal_year: int, cuota_devengada_entrega_by_identifier: Mapping[str, Decimal] | None = None, ) -> RegistroTransmisionesResult: """Project the register onto its art-110 single ("única") regularización for a year. Iterates every art-108-eligible good recorded as disposed of in ``disposal_year`` with window time remaining (:meth:`BienesInversionIvaRegister.disposed_records`); for each, computes :func:`compute_regularizacion_transmision` over the remaining window years and folds the signed importe into the proposed casilla-43 total. Unlike :func:`compute_registro_regularizacion`, a disposal has no pending state analogous to a missing current-year prorrata-definitiva input: every disposal fact the register carries (acquisition-year percentage, cuota soportada, disposal regime) is already on the record, so every disposed good is always computed. Args: register: The persisted :class:`BienesInversionIvaRegister`. disposal_year: The year to project disposals for. cuota_devengada_entrega_by_identifier: Optional per-good cuota devengada on the disposal itself, applied as the regla-1ª cap (:func:`compute_regularizacion_transmision`). Absent keys leave regla 1ª uncapped for that good. Returns: A :class:`RegistroTransmisionesResult`. """ cap_by_identifier = cuota_devengada_entrega_by_identifier or {} rows: list[RegistroTransmisionRow] = [] proposed = Decimal("0.00") for record in register.disposed_records(disposal_year): assert record.disposal is not None # guaranteed by disposed_records's filter result = compute_regularizacion_transmision( cuota_soportada=record.cuota_soportada, prorrata_inicial_pct=record.prorrata_inicial_pct, anos_restantes=record.remaining_regularization_years(disposal_year), kind=record.kind, regime=record.disposal.regime, cuota_devengada_entrega=cap_by_identifier.get(record.identifier), ) proposed += result.importe rows.append( RegistroTransmisionRow( identifier=record.identifier, kind=record.kind, disposal_year=disposal_year, result=result, ) ) return RegistroTransmisionesResult( disposal_year=disposal_year, rows=tuple(rows), proposed_casilla_43=proposed, computed_count=len(rows), )
__all__ = [ "BIENES_INVERSION_SCHEMA_VERSION", "BienInversionDisposal", "BienInversionDisposalRegime", "BienInversionIvaRecord", "BienInversionKind", "BienInversionRecordError", "BienInversionValidationError", "BienesInversionIvaRegister", "RegistroRegularizacionResult", "RegistroRegularizacionRow", "RegistroTransmisionRow", "RegistroTransmisionesResult", "RegularizacionAnualResult", "RegularizacionDireccion", "RegularizacionTransmisionResult", "compute_registro_regularizacion", "compute_registro_transmisiones", "compute_regularizacion_anual", "compute_regularizacion_transmision", ]