Source code for aeat.domain.contribuyente

"""The operator's tax-residence profile.

This package is intentionally separate from financial usage-ratio
profiles, browser profiles, and spending-category profiles. It owns
personal local state needed to parameterize RENTA verification.

:class:`TaxResidenceProfile` and :class:`ResidenceChange` carry the
:class:`CCAA` residence axis; :class:`RentaFamilyProfile` and
:class:`DescendantInfo` carry the Modelo 100 personal/family facts, and
:class:`ProfileKey` exposes the wizard-registered editable profile schema.
"""

from __future__ import annotations

from datetime import date
from typing import TYPE_CHECKING
from unicodedata import category, normalize

from pydantic import BaseModel, ConfigDict, Field, field_validator

from ...core.parsing import parse_iso8601_date as _parse_iso8601_date
from ._ccaa import CCAA
from ._constants import ProfileName
from ._deduccion_maternidad import compute_deduccion_maternidad_0611
from ._descendant_facts import descendant_facts_from_list, descendant_list_from_facts, parse_descendiente_flag
from ._errors import ForalRegimeError, ProfileNotConfiguredError, ProfileValidationError, TaxResidenceProfileError
from ._keys import (
    ProfileKey,
    ProfileKeyRequirement,
    get_profile_key,
    optional_profile_keys,
    profile_keys,
    register_profile_keys,
    required_profile_keys,
)
from ._marriage_facts import (
    marriage_date_from_facts,
    marriage_derived_facts,
    marriage_full_year,
    marriage_month_start,
    parse_marriage_date_flag,
)
from ._normalise import normalise_key
from ._renta_codes import (
    UE_EEA_COUNTRY_CODES,
    FiscalResidency,
    RentaDeclaracionType,
    RentaDisabilityGrade,
    RentaMaritalStatus,
    RentaSexCode,
    SituacionFamiliar,
    SituacionFamiliarM145,
    modelo100_ecivil_export_code,
)
from .family import (
    DescendantInfo,
    RentaAscendantProfile,
    RentaDescendantProfile,
    RentaFamilyProfile,
    within_multi_year_applicability_window,
)

if TYPE_CHECKING:
    # ``PROFILE_KEYS`` is defined lazily via ``__getattr__`` below so the
    # wizard catalogue (the source of truth) can import the leaf modules
    # under ``aeat.domain.contribuyente`` without triggering the catalogue-driven
    # build. Type checkers see the same tuple-of-``ProfileKey`` contract as
    # an eager export would expose.
    from ._keys import PROFILE_KEYS as PROFILE_KEYS


def __getattr__(name: str) -> tuple[ProfileKey, ...]:
    """Lazily resolve ``PROFILE_KEYS`` so the wizard catalogue can import first."""
    if name == "PROFILE_KEYS":
        from ._keys import PROFILE_KEYS

        return PROFILE_KEYS
    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")


_FORAL_ALIASES = frozenset(
    {
        "pais-vasco",
        "país-vasco",
        "pais_vasco",
        "país_vasco",
        "euskadi",
        "navarra",
    },
)


[docs] class ResidenceChange(BaseModel, frozen=True, strict=True): """One historic tax-residence transition.""" model_config = ConfigDict(extra="forbid", validate_assignment=True) from_ccaa: CCAA | None to_ccaa: CCAA effective_from: date reason: str | None = None @field_validator("from_ccaa", "to_ccaa", mode="before") @classmethod def _parse_ccaa(cls, value: object) -> object: if isinstance(value, str): return parse_tax_region(value) return value @field_validator("effective_from", mode="before") @classmethod def _parse_effective_from(cls, value: object) -> object: if isinstance(value, str): return _parse_iso8601_date(value) return value
[docs] class TaxResidenceProfile(BaseModel, frozen=True, strict=True): """The operator's current ordinary CCAA tax residence for RENTA.""" model_config = ConfigDict(extra="forbid", validate_assignment=True) schema_version: str = Field(default="1") ccaa: CCAA tax_residence_since: date | None = None tax_residence_change_history: tuple[ResidenceChange, ...] = () @field_validator("schema_version") @classmethod def _schema_version_is_supported(cls, value: str) -> str: if value != "1": raise ProfileValidationError("schema_version must be '1'") return value @field_validator("ccaa", mode="before") @classmethod def _parse_ccaa(cls, value: object) -> object: if isinstance(value, str): return parse_tax_region(value) return value @field_validator("tax_residence_change_history", mode="before") @classmethod def _parse_change_history(cls, value: object) -> object: if isinstance(value, list): return tuple(value) return value @field_validator("tax_residence_since", mode="before") @classmethod def _parse_since(cls, value: object) -> object: if isinstance(value, str): return _parse_iso8601_date(value) return value
[docs] def parse_tax_region(raw: str) -> CCAA: """Parse a CLI/user tax-region token into the closed :class:`CCAA` enum.""" normalized = _normalize_region_token(raw) if normalized in _FORAL_ALIASES: raise ForalRegimeError(raw) try: return CCAA(normalized) except ValueError as exc: valid = ", ".join(sorted(ccaa.value for ccaa in CCAA)) raise TaxResidenceProfileError( f"unknown tax-region {raw!r}; valid CCAA values: {valid}", context={"tax_region": raw}, suggestion="aeat config profile edit NAME --tax-residence-ccaa <ccaa>", ) from exc
def _normalize_region_token(raw: str) -> str: stripped = raw.strip().lower().replace(" ", "_").replace("-", "_") decomposed = normalize("NFD", stripped) return "".join(char for char in decomposed if category(char) != "Mn") __all__ = [ "CCAA", "PROFILE_KEYS", "UE_EEA_COUNTRY_CODES", "DescendantInfo", "FiscalResidency", "ForalRegimeError", "ProfileKey", "ProfileKeyRequirement", "ProfileName", "ProfileNotConfiguredError", "ProfileValidationError", "RentaAscendantProfile", "RentaDeclaracionType", "RentaDescendantProfile", "RentaDisabilityGrade", "RentaFamilyProfile", "RentaMaritalStatus", "RentaSexCode", "ResidenceChange", "SituacionFamiliar", "SituacionFamiliarM145", "TaxResidenceProfile", "TaxResidenceProfileError", "compute_deduccion_maternidad_0611", "descendant_facts_from_list", "descendant_list_from_facts", "get_profile_key", "marriage_date_from_facts", "marriage_derived_facts", "marriage_full_year", "marriage_month_start", "modelo100_ecivil_export_code", "normalise_key", "optional_profile_keys", "parse_descendiente_flag", "parse_marriage_date_flag", "parse_tax_region", "profile_keys", "register_profile_keys", "required_profile_keys", "within_multi_year_applicability_window", ]