"""Closed AEAT Renta profile-code vocabularies.
These enums model the small code sets consumed by the Modelo 100
profile bindings. They are intentionally domain-owned so CLI and wizard
surfaces can expose accepted values without hard-coding tax vocabulary
in the presentation layer.
:class:`RentaDeclaracionType`, :class:`RentaSexCode`,
:class:`RentaMaritalStatus`, and :class:`RentaDisabilityGrade` back Modelo 100
profile bindings; :class:`SituacionFamiliar` and
:class:`SituacionFamiliarM145` keep the Art. 82 LIRPF joint-taxation axis
separate from Modelo 145 withholding categories.
"""
from __future__ import annotations
from enum import StrEnum
[docs]
class RentaDeclaracionType(StrEnum):
"""Modelo 100 ``TIPOTRIBUTACION`` values."""
INDIVIDUAL = "1"
JOINT = "2"
[docs]
class RentaSexCode(StrEnum):
"""Modelo 100 ``tipo_Sexo`` values."""
HOMBRE = "H"
MUJER = "M"
[docs]
class RentaMaritalStatus(StrEnum):
"""Renta taxpayer marital / registered-partnership profile values."""
SOLTERO = "1"
CASADO = "2"
VIUDO = "3"
SEPARADO_DIVORCIADO = "4"
PAREJA_HECHO = "5"
RENTA_MODELO100_ECIVIL_EXPORT_CODES: frozenset[str] = frozenset(
{
RentaMaritalStatus.SOLTERO.value,
RentaMaritalStatus.CASADO.value,
RentaMaritalStatus.VIUDO.value,
RentaMaritalStatus.SEPARADO_DIVORCIADO.value,
}
)
"""Official Modelo 100 ECIVIL export codes accepted by the bundled XSD."""
[docs]
def modelo100_ecivil_export_code(value: object) -> str:
"""Return a validated official Modelo 100 ECIVIL export code.
``RentaMaritalStatus.PAREJA_HECHO`` is intentionally profile-only. The
official Modelo 100 ECIVIL field is restricted to Estado Civil codes 1-4,
so callers must supply the taxpayer's true official civil-status code
instead of exporting the registered-partnership profile marker.
"""
code = str(value).strip()
if code in RENTA_MODELO100_ECIVIL_EXPORT_CODES:
return code
if code == RentaMaritalStatus.PAREJA_HECHO.value:
raise ValueError(
"profile-only pareja de hecho marital status code '5' is not a valid Modelo 100 ECIVIL export code; "
"supply the official Estado Civil code 1-4"
)
raise ValueError(f"Modelo 100 ECIVIL export code must be one of 1, 2, 3, or 4; got {code!r}")
[docs]
class RentaDisabilityGrade(StrEnum):
"""Modelo 100 ``tipo_GradoDiscapacidad`` values."""
GE_33_LT_65 = "1"
GE_65 = "2"
JUDICIAL_INCAPACITY = "3"
ASSISTANCE_OR_REDUCED_MOBILITY = "4"
# ISO 3166-1 alpha-2 codes for EU member states and EEA members.
# Post-Brexit: GB is excluded (left EU 2020-12-31, left EEA 2020-12-31).
# Source: https://ec.europa.eu/eurostat/statistics-explained/index.php/Glossary:European_Economic_Area_(EEA)
# EEA = EU27 + IS, LI, NO. CH has bilateral agreements but is not EEA.
UE_EEA_COUNTRY_CODES: frozenset[str] = frozenset(
{
# EU 27
"AT",
"BE",
"BG",
"CY",
"CZ",
"DE",
"DK",
"EE",
"GR",
"ES",
"FI",
"FR",
"HR",
"HU",
"IE",
"IT",
"LT",
"LU",
"LV",
"MT",
"NL",
"PL",
"PT",
"RO",
"SE",
"SI",
"SK",
# EEA non-EU
"IS",
"LI",
"NO",
},
)
"""Closed set of EU + EEA ISO-3166-1 alpha-2 country codes (post-Brexit)."""
[docs]
class FiscalResidency(StrEnum):
"""Fiscal residency category governing the applicable tax regime.
Determines whether the taxpayer files under IRPF (Spanish resident)
or IRNR (non-resident), following TRLIRNR RDLeg 5/2004 Art. 2:
- ``RESIDENT_IRPF``: habitual residence in Spain; subject to IRPF
(Ley 35/2006 LIRPF). Files Modelo 100 (or Modelo 151 for impatriados).
- ``NON_RESIDENT_IRNR``: no habitual residence in Spain; subject to
IRNR (RDLeg 5/2004 TRLIRNR). Files Modelo 210 (general),
Modelo 216 (retenciones), or Modelo 247 (pensiones).
Post-Brexit note (from 1 January 2021): GB is no longer an EU/EEA
member; ``ue_eee_status`` returns ``False`` for GB residents regardless
of prior residence history.
"""
RESIDENT_IRPF = "resident_irpf"
NON_RESIDENT_IRNR = "non_resident_irnr"
[docs]
class SituacionFamiliar(StrEnum):
"""Legal family situation for the Art. 82 LIRPF unidad-familiar eligibility test.
Determines whether conjunta (joint) taxation is available and which
unidad familiar variant applies:
- ``casado``: married; conjunta available (Art. 82.1.1°).
- ``pareja_hecho_registrada``: registered civil partnership in an
autonomic registry; conjunta available (Art. 82.1.2°).
- ``pareja_hecho_no_registrada``: de-facto couple, not registered;
conjunta NOT available.
- ``soltero``: single; conjunta only available as monoparental
(Art. 82.1.2° second indent) when hijos a cargo present.
- ``separado_divorciado``: legally separated or divorced; conjunta
only available as monoparental when hijos a cargo present.
"""
CASADO = "casado"
PAREJA_HECHO_REGISTRADA = "pareja_hecho_registrada"
PAREJA_HECHO_NO_REGISTRADA = "pareja_hecho_no_registrada"
SOLTERO = "soltero"
SEPARADO_DIVORCIADO = "separado_divorciado"
[docs]
def conjunta_eligible(self) -> bool:
"""True when this situation permits conjunta taxation."""
return self in (
SituacionFamiliar.CASADO,
SituacionFamiliar.PAREJA_HECHO_REGISTRADA,
SituacionFamiliar.SOLTERO,
SituacionFamiliar.SEPARADO_DIVORCIADO,
)
[docs]
def requires_spouse_or_partner(self) -> bool:
"""True when a spouse / registered partner NIF is required for conjunta."""
return self in (
SituacionFamiliar.CASADO,
SituacionFamiliar.PAREJA_HECHO_REGISTRADA,
)
[docs]
def monoparental_required(self) -> bool:
"""True for the single-parent situations a monoparental unit requires.
Per Art. 82.1.2 LIRPF the monoparental unidad familiar applies to a
non-partnered parent, i.e. soltero or separado/divorciado.
"""
return self in (
SituacionFamiliar.SOLTERO,
SituacionFamiliar.SEPARADO_DIVORCIADO,
)
[docs]
class SituacionFamiliarM145(StrEnum):
"""Trinary "Situación familiar" axis declared on Modelo 145 (box 1).
The Modelo 145 form (Comunicación de datos al pagador, BOE-A-2011-208,
art. 88 RIRPF) collects the recipient's family-situation trinary that the
pagador uses to apply Art. 81 RIRPF withholding adjustments. It is a
distinct axis from :class:`SituacionFamiliar`, which encodes the Art. 82
LIRPF unidad-familiar conjunta-eligibility test — Art. 81 retención
arithmetic and Art. 82 conjunta arithmetic do not share categories.
Form-numbered values (mirroring the three numbered boxes on the
physical mod145 form):
- ``familia_1``: viudo/a o casado/a separado/a legalmente con
descendientes que dan derecho a la totalidad del mínimo por
descendientes. Eligible for the supplementary withholding reduction
under RIRPF art. 81.1.1°.
- ``familia_2``: casado/a y no separado/a legalmente cuyo cónyuge no
obtiene rentas anuales > €1,500 (excluidas las exentas). Eligible
for the supplementary withholding reduction under RIRPF art. 81.1.2°.
- ``familia_3``: situación familiar distinta de las anteriores. The
default; no supplementary withholding reduction.
"""
FAMILIA_1 = "familia_1"
FAMILIA_2 = "familia_2"
FAMILIA_3 = "familia_3"
[docs]
def is_eligible_for_supplementary_reduction(self) -> bool:
"""True when the situation grants the RIRPF art. 81.1.1°/2° reduction."""
return self in (
SituacionFamiliarM145.FAMILIA_1,
SituacionFamiliarM145.FAMILIA_2,
)
__all__ = [
"RENTA_MODELO100_ECIVIL_EXPORT_CODES",
"UE_EEA_COUNTRY_CODES",
"FiscalResidency",
"RentaDeclaracionType",
"RentaDisabilityGrade",
"RentaMaritalStatus",
"RentaSexCode",
"SituacionFamiliar",
"SituacionFamiliarM145",
"modelo100_ecivil_export_code",
]