Source code for aeat.core._amendment_kind_regime

"""Per-modelo, period-aware autoliquidación amendment-kind regime.

AEAT's amendment mechanism for a self-assessment (autoliquidación) changed
over time. Before the reform, a correction that raised the tax due filed as
a ``complementaria`` (LGT art. 122.2, ``ley-58-2003:art-122``) while a
correction that lowered it required a separate ``solicitud de rectificación``
procedure (LGT art. 120.3, ``ley-58-2003:art-120``). RD-ley 13/2023 plus each
tax's implementing orden unified both into a single ``autoliquidación
rectificativa`` (LGT art. 120.4) that may raise OR lower the resultado — but
only from the period each modelo's own orden establishes; earlier periods are
still governed by the dual complementaria/rectificación regime.

This module codifies, per modelo, the filing period from which the
rectificativa mechanism applies, grounded in the bundled AEAT Diseño de
Registros and manual corpus:

- **M303** (IVA autoliquidación): the rectificativa casillas (fichero-BOE
  position 392 ``autoliq_rectificativa`` onward) appear starting with the
  diseño "ejercicio 2024 a partir de periodos 09 y 3T y siguientes"; the
  prior diseño "ejercicio 2024 hasta periodos 08 y 2T" carries no rectificativa
  fields. The boundary is therefore filing_year 2024, period 09 (monthly) or
  3T (quarterly) onward — every subsequent period and year uses rectificativa.
- **M100** (IRPF Renta): the Manual Práctico de Renta states the rectificativa
  became the general correction mechanism "para los períodos impositivos 2024
  y siguientes", with effective application from Orden HAC/242/2025 (14 March
  2025); the registry casilla ``0669`` (2024 revision, "discrepancia de
  criterio administrativo... autoliquidación rectificativa") corroborates M100
  carries rectificativa support from filing_year 2024.
- **M200** (Impuesto sobre Sociedades): the 2024-y-siguientes registry
  revision carries rectificativa casillas (``0497``/``0499``/``0508``/``0509``/
  ``0513``/``0519``/etc.), so M200 supports rectificativa from filing_year
  2024 onward.
- **M130/M131** (IRPF pagos fraccionados): the bundled diseño de registros
  for M130 stops at "ejercicios 2019 y siguientes" with no rectificativa
  fields at all, and the registry carries only that single revision. No
  bundled AEAT source grounds a specific rectificativa-adoption period for
  M130/M131, so this module conservatively reports them as NOT YET
  supporting rectificativa — every period continues to route through the
  dual complementaria/solicitud-de-rectificación regime — until a bundled
  AEAT diseño or orden grounds an effective period
  (`legal-grounding-verifies-bundled-authoritative-corpus`).

A modelo absent from :data:`_RECTIFICATIVA_EFFECTIVE_FROM` has no codified
regime at all: :func:`permitted_amendment_kind_values` falls back to the
complementaria/sustitutiva pair for every period, never asserting rectificativa
support that no bundled source confirms.

See Also:
    :func:`~application.modelo._amendment_kind_resolution.assert_amendment_kind_permitted`:
        Application-layer guard that binds this table to
        :class:`~domain.modelos.CalculationRevisionAmendmentKind` and
        refuses an operator-requested kind the period does not permit.
    :class:`~domain.modelos.CalculationRevisionAmendmentKind`:
        The domain enum this module's string values back.
"""

from __future__ import annotations

from dataclasses import dataclass
from datetime import date
from enum import StrEnum
from typing import TYPE_CHECKING

from ._modelo import Modelo

if TYPE_CHECKING:
    # Deferred: ``._period`` transitively imports ``.errors``, which imports
    # ``core.classification``, which imports ``STRICT_FROZEN_CONFIG`` back from
    # the ``core`` package facade (``._models``). Importing ``Period`` eagerly
    # here would run that chain before ``core/__init__.py`` has bound
    # ``STRICT_FROZEN_CONFIG`` (this module sorts alphabetically before
    # ``._models`` in the facade's import block), reproducing the partial-
    # initialization ``ImportError`` the facade's ordering otherwise avoids.
    # ``Period`` is used only as a type annotation here, so the deferred import
    # is sufficient and the module needs no runtime binding of the name.
    from _typeshed import SupportsAllComparisons

    from ._period import Period


[docs] class AmendmentLiabilityDirection(StrEnum): """Direction of an amendment's effect on the taxpayer's declared liability. Under the pre-rectificativa dual regime, the direction determines which of the two legally-distinct procedures applies: an ``INCREASE`` (the corrected resultado raises the tax due, or lowers a requested devolución) is a ``complementaria`` (LGT art. 122.2); a ``DECREASE`` (the corrected resultado lowers the tax due, or raises a requested devolución) requires the ``solicitud de rectificación`` procedure (LGT art. 120.3) rather than a self-filed complementaria. Post-rectificativa, the direction no longer selects a different legal mechanism (both raise and lower route through the unified rectificativa), so this classification is only load-bearing for pre-rectificativa periods. """ INCREASE = "increase" DECREASE = "decrease" UNCHANGED = "unchanged"
[docs] @dataclass(frozen=True) class AmendmentKindRegime: """The codified amendment-kind regime resolved for one (modelo, period). Attributes: permitted_kinds: The closed set of :class:`~domain.modelos.CalculationRevisionAmendmentKind` string values legally available for this modelo and period. ``"sustitutiva"`` is always permitted (a material restatement is never barred by the rectificativa timeline); ``"rectificativa"`` is permitted only once the modelo's own effective period is reached; ``"complementaria"`` is permitted only before that point (once rectificativa applies, it *replaces* complementaria for the modelo's ordinary corrective filings — see ``CalculationRevisionAmendmentKind.RECTIFICATIVA``'s docstring). rectificativa_effective: Whether the resolved period is within the modelo's rectificativa-supported window (``True``) or the pre-rectificativa dual regime still applies (``False``). """ permitted_kinds: frozenset[str] rectificativa_effective: bool
_COMPLEMENTARIA: str = "complementaria" _SUSTITUTIVA: str = "sustitutiva" _RECTIFICATIVA: str = "rectificativa" #: Pre-rectificativa regime: only the dual complementaria/sustitutiva pair. _PRE_RECTIFICATIVA_KINDS: frozenset[str] = frozenset({_COMPLEMENTARIA, _SUSTITUTIVA}) #: Post-rectificativa regime: rectificativa replaces complementaria as the #: modelo's unified ordinary-correction mechanism; sustitutiva remains for #: material restatements. _POST_RECTIFICATIVA_KINDS: frozenset[str] = frozenset({_RECTIFICATIVA, _SUSTITUTIVA}) #: Per-modelo rectificativa effective-period boundary, grounded in the #: bundled AEAT Diseño de Registros / manual corpus (see module docstring). #: The boundary is expressed as the earliest *period end date* (inclusive) #: from which the modelo's rectificativa mechanism applies; every period #: whose ``Period.end_date`` falls on or after this date is post-rectificativa. #: A modelo absent from this table has no codified rectificativa-adoption #: date and is therefore never treated as rectificativa-effective by #: :func:`permitted_amendment_kind_values`. _RECTIFICATIVA_EFFECTIVE_FROM: dict[str, date] = { # M303: diseño "ejercicio 2024 a partir de periodos 09 y 3T y siguientes" # introduces the rectificativa fields; period 09 and 3T both end on # 2024-09-30 (monthly September / calendar Q3), the shared boundary date. Modelo.M303: date(2024, 9, 30), # M100: Manual Práctico de Renta 2025 — rectificativa is the general IRPF # correction mechanism "para los períodos impositivos 2024 y siguientes" # (effective application from Orden HAC/242/2025, 14 March 2025); the # registry's 2024 revision carries the rectificativa discrepancia-de- # criterio casilla (0669). M100's revisions are single filing years, so # the boundary is simply the close of filing_year 2024 (annual, 0A). Modelo.M100: date(2024, 12, 31), # M200 (Impuesto sobre Sociedades): the 2024-y-siguientes registry # revision carries rectificativa casillas throughout its "liquidacion_iv" # / "rectificativa" sections. M200's revision windows are annual. Modelo.M200: date(2024, 12, 31), }
[docs] def modelo_has_codified_amendment_regime(modelo: str) -> bool: """Return whether ``modelo`` has a bundled-source-grounded rectificativa boundary. A modelo without a codified boundary (e.g. M130/M131, whose bundled diseño de registros carries no rectificativa fields) is never reported as rectificativa-effective; :func:`permitted_amendment_kind_values` returns the pre-rectificativa complementaria/sustitutiva pair for every period. """ return modelo in _RECTIFICATIVA_EFFECTIVE_FROM
[docs] def resolve_amendment_kind_regime(modelo: str, period: Period) -> AmendmentKindRegime: """Resolve the codified amendment-kind regime for ``modelo`` at ``period``. Returns an :class:`AmendmentKindRegime` naming the legally-permitted :class:`~domain.modelos.CalculationRevisionAmendmentKind` string values for this ``(modelo, period)`` pair. A modelo with no codified boundary (:func:`modelo_has_codified_amendment_regime` is ``False``) always resolves to the pre-rectificativa pair, never asserting rectificativa support no bundled source confirms. For a period with no calendar date span (an instalment clave or an extended/ad-hoc form), the resolution falls back to ``filing_year >= <the modelo's boundary year>`` using the boundary date's year, since no calendar comparison is possible for those forms. """ boundary = _RECTIFICATIVA_EFFECTIVE_FROM.get(modelo) if boundary is None: return AmendmentKindRegime(permitted_kinds=_PRE_RECTIFICATIVA_KINDS, rectificativa_effective=False) if period.has_date_span(): rectificativa_effective = period.end_date >= boundary else: rectificativa_effective = period.filing_year >= boundary.year if rectificativa_effective: return AmendmentKindRegime(permitted_kinds=_POST_RECTIFICATIVA_KINDS, rectificativa_effective=True) return AmendmentKindRegime(permitted_kinds=_PRE_RECTIFICATIVA_KINDS, rectificativa_effective=False)
[docs] def permitted_amendment_kind_values(modelo: str, period: Period) -> frozenset[str]: """Return the closed set of legally-permitted amendment-kind string values. Thin convenience wrapper over :func:`resolve_amendment_kind_regime` for callers that only need the permitted set (e.g. rendering an accepted-value list in a refusal message). """ return resolve_amendment_kind_regime(modelo, period).permitted_kinds
[docs] def classify_amendment_liability_direction( *, baseline_result: SupportsAllComparisons, corrected_result: SupportsAllComparisons, ) -> str: """Classify whether a correction increases, decreases, or leaves liability unchanged. ``baseline_result`` and ``corrected_result`` are the modelo's signed final result casilla value (a positive value means "a ingresar" tax due; a negative value means a credit or refund position, modelo-dependent) before and after the operator's overrides. An increase in the signed result (a higher amount to pay, or a lower credit/refund) is :attr:`~core.AmendmentLiabilityDirection.INCREASE` (LGT art. 122.2, complementaria territory); a decrease is :attr:`~core.AmendmentLiabilityDirection.DECREASE` (LGT art. 120.3, solicitud de rectificación territory pre-unification). Equal values are :attr:`~core.AmendmentLiabilityDirection.UNCHANGED`. Accepts any ``Decimal``-comparable numeric type so callers do not need to import :mod:`decimal` solely to call this classifier. """ if corrected_result > baseline_result: return AmendmentLiabilityDirection.INCREASE if corrected_result < baseline_result: return AmendmentLiabilityDirection.DECREASE return AmendmentLiabilityDirection.UNCHANGED
__all__ = [ "AmendmentKindRegime", "AmendmentLiabilityDirection", "classify_amendment_liability_direction", "modelo_has_codified_amendment_regime", "permitted_amendment_kind_values", "resolve_amendment_kind_regime", ]