"""Typed, immutable filing producer inputs.
This module owns the filing-instance facts that export consumers need before
they translate them into a revision-specific registry vocabulary. It does not
own export keys, layout offsets, or rendered record fragments.
Core types:
:class:`~cadrumo.domain.deadlines.models.TaxpayerProfile`.
"""
from __future__ import annotations
from enum import StrEnum
from typing import Annotated, Final, Literal
from pydantic import BaseModel, StringConstraints, model_validator
from cadrumo.domain.calculations.registry.tax_id_format import SubjectTaxId
from ...core.errors.hierarchy import CadrumoError, pydantic_validation_boundary
from ...core.modelo import Modelo
from ...core.models import STRICT_FROZEN_CONFIG
from ...core.payment_election import PaymentElection
from ...core.period import Period, StandardPeriodCode
from ...core.prior_domiciliation_election import PriorDomiciliationElection
from ...core.refund_election import RefundElection
from ...core.result_disposition import ResultDisposition, result_disposition_is_refund
from ...domain.bienes_inversion.register import (
BienesInversionIvaRegister,
RegistroRegularizacionResult,
compute_registro_regularizacion,
)
from ...domain.bienes_inversion.regularizacion_parameters import (
BienesInversionRegularizacionParameters,
)
from ...domain.deadlines.models import ChargeAccount, ModeloIVAProfile, RefundAccount, TaxpayerProfile
from ...domain.modelos.calculation_revision_amendment import (
CalculationRevisionAmendmentKind,
M303RectificativaMotive,
m303_rectificativa_motive_is_applicable,
)
from ...domain.modelos.calculation_revision_m303_evidence import (
M303Exonerado390FilingEvidence,
M303InsolvencyFilingFact,
M303InsolvencyFilingSubtype,
M303RegimenSimplificadoCalculationResult,
)
from ...domain.modelos.calculation_revision_m303_handoff import (
FilingInstanceEvidence,
M303RegimenSimplificadoFilingEvidence,
)
from ...domain.prorrata_register.register import ProrrataRegister
from ..aggregation.iva_ledger import (
IvaDifferentiatedDeductionContribution,
)
from ..aggregation.m303_arrivals import (
M303ProrrataTransitionArrival,
M303SupplierRegimeArrival,
)
from ._producer_snapshot_m200 import Modelo200ProfileFacts as _Modelo200ProfileFacts
from ._producer_snapshot_m390 import M390FilingFacts as _M390FilingFacts
_NonBlankName = Annotated[str, StringConstraints(strip_whitespace=True, min_length=1, max_length=200)]
_AeatReceiptNumber = Annotated[str, StringConstraints(pattern=r"^\d{13}$")]
_CnaeCode = Annotated[str, StringConstraints(pattern=r"^\d{4}$")]
_M303_OFFICIAL_FILING_PERIODS: Final[frozenset[StandardPeriodCode]] = frozenset(
{
StandardPeriodCode.Q1,
StandardPeriodCode.Q2,
StandardPeriodCode.Q3,
StandardPeriodCode.Q4,
StandardPeriodCode.JAN,
StandardPeriodCode.FEB,
StandardPeriodCode.MAR,
StandardPeriodCode.APR,
StandardPeriodCode.MAY,
StandardPeriodCode.JUN,
StandardPeriodCode.JUL,
StandardPeriodCode.AUG,
StandardPeriodCode.SEP,
StandardPeriodCode.OCT,
StandardPeriodCode.NOV,
StandardPeriodCode.DEC,
},
)
[docs]
class FilingProducerSnapshotError(CadrumoError):
"""Raised when filing facts cannot form a complete producer snapshot."""
[docs]
def assert_m303_regularisation_result_matches_bienes_register(
*,
bienes_register: BienesInversionIvaRegister,
regularisation_result: RegistroRegularizacionResult,
parameters: BienesInversionRegularizacionParameters,
) -> None:
"""Refuse a result that is not the register's exact annual projection.
The result rows carry the definitive prorrata facts used to produce them,
so replay the canonical domain projection from those facts and compare the
complete immutable result. This admits no result-row omission, foreign
register, substituted contribution, or invented pending state.
The result also carries the registry declaration its figures came from, and
that is checked against ``parameters`` FIRST and by identity rather than by
replay. A result produced under a different revision would otherwise be
silently re-derived under this one and compared against itself, so the
provenance check is what stops the replay from confirming the wrong law.
"""
if regularisation_result.parameters_provenance != parameters.provenance:
raise ValueError(
"M303 regularisation result was produced from registry declaration "
f"{regularisation_result.parameters_provenance.modelo_id}/"
f"{regularisation_result.parameters_provenance.revision_id}"
f"@{regularisation_result.parameters_provenance.resolved_on.isoformat()}, "
"not from the supplied "
f"{parameters.provenance.modelo_id}/{parameters.provenance.revision_id}"
f"@{parameters.provenance.resolved_on.isoformat()}",
)
canonical = compute_registro_regularizacion(
bienes_register,
regularizacion_year=regularisation_result.regularizacion_year,
prorrata_definitiva_by_identifier={
row.identifier: row.prorrata_anio_pct
for row in regularisation_result.rows
if row.prorrata_anio_pct is not None
},
parameters=parameters,
)
if regularisation_result != canonical:
raise ValueError("M303 regularisation result must be the canonical projection of the supplied Bienes register")
def _require_m303_official_filing_period(period: Period) -> None:
"""Refuse non-303 periods before they can reach DP30301's producer fields."""
if period.standard_code not in _M303_OFFICIAL_FILING_PERIODS:
raise ValueError(
"Modelo 303 filing facts require an official quarterly or monthly period "
f"(1T-4T or 01-12), got {period.registry_token!r}",
)
[docs]
class PresenterIdentity(BaseModel):
"""Identity of the presenter for this filing instance.
Presenter identity is deliberately separate from taxpayer identity. A
caller must supply it; no taxpayer-to-presenter fallback exists here.
"""
model_config = STRICT_FROZEN_CONFIG
tax_id: SubjectTaxId
full_name: _NonBlankName
[docs]
class TaxpayerIdentityFacts(BaseModel):
"""Explicit taxpayer name facts for one filing instance.
The four fields model distinct official producer meanings. They are not
interchangeable aliases: a revision requiring one of them must receive
that exact fact, and an absent fact remains absent.
"""
model_config = STRICT_FROZEN_CONFIG
legal_name: _NonBlankName | None
given_name: _NonBlankName | None
surnames: _NonBlankName | None
full_name: _NonBlankName | None
@model_validator(mode="after")
@pydantic_validation_boundary
def _refuse_entity_and_person_names_together(self) -> TaxpayerIdentityFacts:
"""An entity carries a legal name; a natural person carries given name and surnames.
The two are exclusive by meaning, and the export reads the taxpayer's
legal form from which of them is present. Holding both would let a
requirement stated for natural persons be judged against an entity.
"""
if self.legal_name is not None and (self.given_name is not None or self.surnames is not None):
raise ValueError("taxpayer identity cannot carry both an entity legal name and personal names")
return self
[docs]
class Modelo111ProfileFacts(BaseModel):
"""Stable Modelo 111 profile facts consumed by a filing producer."""
model_config = STRICT_FROZEN_CONFIG
colegio_concertado: bool | None
_GrupoNumber = Annotated[str, StringConstraints(min_length=1, max_length=7)]
_ForalTerritory = Annotated[str, StringConstraints(min_length=1, max_length=2)]
[docs]
class Modelo222ProfileFacts(BaseModel):
"""Fiscal-group identity and régimen facts a Modelo 222 filing declares.
Modelo 222 is the pago fraccionado of a *grupo fiscal*, so the group's own identity is
not optional context -- it is what the return is about. AEAT's design prescribes a
format for the número de grupo (``Nota 8``: ``----/--`` estatal, ``---/--A`` foral),
which is a rule about content, not about an empty field.
Before this type existed the twenty-three ``m222.*`` producer keys were declared in the
vocabulary and resolved by nothing, so every one of them rendered blank on a
non-required field and the return emitted with its group number and its entidad
dominante empty.
Every field below is optional EXCEPT the group identity, because AEAT's own design
leaves the régimen marks blank when they do not apply, and a mark that does not apply
is genuinely absent rather than unknown. The group number and the dominante are not in
that category.
"""
model_config = STRICT_FROZEN_CONFIG
numero_grupo: _GrupoNumber
entidad_dominante_identificacion: str
entidad_dominante_razon_social: str
#: "1" representante (entidad no dominante), "2" dominante incluida en el grupo fiscal.
representante_o_dominante: str | None = None
normativa_territorio_foral: str | None = None
entidad_dominante_pais_territorio_foral: _ForalTerritory | None = None
fecha_inicio_periodo_impositivo: str | None = None
cnae_actividad_principal: _CnaeCode | None = None
regimen_entidades_navieras_tonelaje: str | None = None
regimen_reducida_dimension: str | None = None
cifra_negocios_grupo_doce_meses: str | None = None
cooperativa_fiscalmente_protegida: str | None = None
regimen_entidades_capital_riesgo: str | None = None
circunstancia_concurrente: str | None = None
cifra_negocios_periodo_anterior_tramo: str | None = None
multiples_tipos_impositivos: str | None = None
tipo_gravamen_impuesto_sociedades: str | None = None
importe_neto_cifra_negocios_tramo: str | None = None
modalidad_liquidacion: str | None = None
comunicacion_datos_adicionales: str | None = None
numero_referencia_sociedades: str | None = None
comunicacion_variacion_composicion_grupo: str | None = None
numero_referencia_sociedades_variacion: str | None = None
_M353GrupoNumber = Annotated[str, StringConstraints(min_length=1, max_length=10)]
_SiNoMark = Annotated[str, StringConstraints(pattern=r"^[12]$")]
_XOrBlankMark = Annotated[str, StringConstraints(pattern=r"^X$")]
[docs]
class Modelo353ProfileFacts(BaseModel):
"""Grupo de entidades IVA identity and régimen marks a Modelo 353 filing declares.
Modelo 353 is the *autoliquidación agregada* of the régimen especial del grupo de
entidades (LIVA art. 163 sexies), so the group's number is what the return is about
rather than optional colour.
Before this type existed the five ``m353.*`` producer keys were declared in the
vocabulary and resolved by nothing, so the número de grupo and the two marks rendered
from whatever the field's ``required`` flag allowed.
``numero_grupo`` is required here. The two régimen marks are required too, and that is
a departure from the Modelo 222 shape for a grounded reason: AEAT's design gives them
``1 -Sí, 2 -No`` and the published layout marks both ``required = true``, so there is
no blank state to represent -- a filer who is not inscrito declares ``"2"``, not
nothing. ``sin_actividad`` and ``grupo_normativa_foral`` are the genuinely optional
ones: the design reads ``X o blanco``.
"""
model_config = STRICT_FROZEN_CONFIG
#: Identificación. Nº Grupo -- design offset 109, length 10.
numero_grupo: _M353GrupoNumber
#: Tipo régimen especial aplicable, art. 163 sexies.cinco: "1" sí, "2" no.
regimen_especial_avanzado_elected: _SiNoMark
#: Inscrito en el Registro de devolución mensual (art. 30 RIVA): "1" sí, "2" no.
regimen_especial_inscrito_redeme: _SiNoMark
#: "X o blanco" in the design; absent means the group had activity.
sin_actividad: _XOrBlankMark | None = None
#: "X o blanco" in the design; absent means the group is not sometido a normativa foral.
grupo_normativa_foral: _XOrBlankMark | None = None
_M296SupportType = Annotated[str, StringConstraints(pattern=r"^[TC]$")]
_M296AmendmentMark = Annotated[str, StringConstraints(min_length=1, max_length=2)]
_FourDigitYear = Annotated[str, StringConstraints(pattern=r"^\d{4}$")]
_DigitString9 = Annotated[str, StringConstraints(pattern=r"^\d{1,9}$")]
[docs]
class Modelo296PerceptorRow(BaseModel):
"""One Modelo 296 perceptor: the payee, the renta and the retencion practicada.
The Tipo 2 record is emitted once per payee, so this row is the unit AEAT repeats.
Members are ``M296PerceptorField``'s values verbatim and are generated from that enum,
which keeps the projection reference and the row that answers it in lock-step: a field
the reference can name is a field this row has.
Every member is optional and rendered as text. The record design's own per-field width,
padding, justification and data type are what shape the bytes, and they live in the
published layout; duplicating them as constraints here would be a second copy of the
design that is free to disagree with it.
These rows are NOT yet fed from the withholding substrate. The values already exist in
the registry as ``Withholding296Observation`` -- perceptor tax id, legal name, naturaleza,
clave, subclave, base and retencion among them -- and the snapshot assembler should
project them from there rather than take them as operator entry. Until it does, a caller
that populates these by hand can state a figure the ledger disagrees with.
"""
model_config = STRICT_FROZEN_CONFIG
apellidos_y_nombre_razon_social_o_deno: str | None = None
base_retenciones_e_ingresos_a_cuenta: str | None = None
ciudad: str | None = None
clave: str | None = None
clave_de_mercado: str | None = None
codigo: str | None = None
codigo_bic_del_perceptor_mediador: str | None = None
codigo_cuenta_valores: str | None = None
codigo_emisor: str | None = None
codigo_lei_del_perceptor: str | None = None
codigo_pais: str | None = None
decimal: str | None = None
decimal_numerico_parte_decimal: str | None = None
decimal_numerico_parte_decimal_2: str | None = None
decimal_numerico_parte_decimal_3: str | None = None
declarante: str | None = None
direccion_del_perceptor: str | None = None
ejercicio: str | None = None
ejercicio_devengo: str | None = None
entero: str | None = None
entero_numerico_parte_entera: str | None = None
entero_numerico_parte_entera_2: str | None = None
entero_numerico_parte_entera_3: str | None = None
f_j: str | None = None
fecha_de_devengo: str | None = None
fecha_de_inicio_del_prestamo: str | None = None
fecha_de_nacimiento: str | None = None
fecha_de_vencimiento_del_prestamo: str | None = None
identificador_de_registro_o_numero_de: str | None = None
ingresos_a_cuenta_repercutidos: str | None = None
naturaleza: str | None = None
nif_del_declarante: str | None = None
nif_del_pagador_anterior: str | None = None
nif_del_perceptor: str | None = None
nif_del_representante_legal: str | None = None
nif_en_el_pais_de_residencia_fiscal: str | None = None
pais_o_territorio_de_residencia_fiscal: str | None = None
parte_decimal_del_importe_de_las_reten: str | None = None
parte_entera_del_importe_de_las_retenc: str | None = None
pendiente: str | None = None
perceptor_mediador: str | None = None
procedimiento_especial_de_retenciones: str | None = None
subclave: str | None = None
tipo_codigo: str | None = None
[docs]
class Modelo296PerceptorInteresesRow(BaseModel):
"""One Modelo 296 perceptor of the intereses hoja.
Members are ``M296PerceptorInteresesField``'s values verbatim and are generated from that
enum, so a field the projection reference can name is a field this row has. Every member
is optional and rendered as text: the per-field width, padding, justification and data
type belong to the published layout, and a second copy of them here would be free to
disagree with the design.
"""
model_config = STRICT_FROZEN_CONFIG
apellidos_y_nombre_razon_social_o_deno: str | None = None
ejercicio: str | None = None
f_j: str | None = None
identificador_de_registro_o_numero_de: str | None = None
nif_del_declarante: str | None = None
nif_del_perceptor: str | None = None
nif_del_representante_legal: str | None = None
retenciones_e_ingresos_a_cuenta_ingres: str | None = None
[docs]
class Modelo296AnexoPagoRow(BaseModel):
"""One Modelo 296 pago a contribuyente.
Members are ``M296AnexoPagoField``'s values verbatim and are generated from that
enum, so a field the projection reference can name is a field this row has. Every member
is optional and rendered as text: the per-field width, padding, justification and data
type belong to the published layout, and a second copy of them here would be free to
disagree with the design.
"""
model_config = STRICT_FROZEN_CONFIG
apellidos_y_nombre_razon_social_o_deno: str | None = None
apellidos_y_nombre_razon_social_o_deno_2: str | None = None
ciudad: str | None = None
clave_de_personalidad_del_contribuyent: str | None = None
codigo_isin: str | None = None
codigo_lei_del_contribuyente: str | None = None
codigo_pais: str | None = None
direccion_del_contribuyente: str | None = None
ejercicio: str | None = None
f_j: str | None = None
fecha_de_devengo: str | None = None
fecha_de_nacimiento_del_contribuyente: str | None = None
identificador_de_registro_o_numero_de: str | None = None
importe_del_pago_al_contribuyente: str | None = None
nif_del_contribuyente: str | None = None
nif_del_declarante: str | None = None
nif_del_perceptor: str | None = None
nif_del_representante_legal: str | None = None
nif_en_el_pais_de_residencia_fiscal_de: str | None = None
numero_de_justificante_del_modelo_210: str | None = None
pais_o_territorio_de_residencia_fiscal: str | None = None
porcentaje_de_retencion: str | None = None
retenciones: str | None = None
[docs]
class Modelo296AnexoCertificadoRow(BaseModel):
"""One Modelo 296 certificado de pago.
Members are ``M296AnexoCertificadoField``'s values verbatim and are generated from that
enum, so a field the projection reference can name is a field this row has. Every member
is optional and rendered as text: the per-field width, padding, justification and data
type belong to the published layout, and a second copy of them here would be free to
disagree with the design.
"""
model_config = STRICT_FROZEN_CONFIG
apellidos_y_nombre_razon_social_o_deno: str | None = None
apellidos_y_nombre_razon_social_o_deno_2: str | None = None
codigo_cuenta_valores_del_certificado: str | None = None
codigo_isin_del_certificado: str | None = None
codigo_lei_del_titular_registral: str | None = None
decimal: str | None = None
decimal_2: str | None = None
decimal_3: str | None = None
ejercicio: str | None = None
entero: str | None = None
entero_2: str | None = None
entero_3: str | None = None
f_j: str | None = None
fecha_de_pago: str | None = None
fecha_de_presentacion_del_modelo_210: str | None = None
identificador_de_registro_o_numero_de: str | None = None
nif_del_declarante: str | None = None
nif_del_perceptor: str | None = None
nif_del_representante_legal: str | None = None
numero_de_justificante_del_modelo_210: str | None = None
parte_decimal_del_numero_de_titulos: str | None = None
parte_decimal_del_numero_de_titulos_2: str | None = None
parte_entera_del_numero_de_titulos: str | None = None
parte_entera_del_numero_de_titulos_2: str | None = None
titular_registral_de_la_cuenta_de_valo: str | None = None
[docs]
class Modelo296ProfileFacts(BaseModel):
"""Declarant identity the Modelo 296 tipo-1 record declares.
Modelo 296 is the IRNR annual summary of retenciones e ingresos a cuenta on rentas
obtained by non-residents without permanent establishment (TRLIRNR art. 24, Orden
EHA/3290/2008 art. 6). Its first record identifies who is declaring, for which
ejercicio, and how many perceptores the file carries.
All twelve ``m296.dec.*`` keys were declared in the vocabulary and produced by nothing,
so every one of them rendered blank: the ejercicio at offset 5, the declarante NIF at
offset 9 and the razon social at offset 18 among them. A 296 emitted that way names
nobody.
Field names are the AEAT key tails verbatim, including two the source design truncated:
``apellidos_y_nombre`` is *persona con quien relacionarse* (design ordinal 7, the
49-byte contact block AEAT splits into a 9-byte telefono and a 40-byte name), and ``n``
is ``N.I.F. DEL REPRESENTANTE LEGAL.`` (design ordinal 16, offset 391) -- the slug
stopped at the first period in the label. Renaming either would change a published
layout, so the name stays and the meaning is recorded here.
``ejercicio`` restates a year the draft already knows. The structurally better home is
a ``draft`` field carrying ``ExportDraftAttribute.FILING_YEAR``, which cannot disagree
with the draft; that is a layout change and is not made here. Until it is, a snapshot
builder must populate this from the draft's own filing year rather than from an
independently entered value.
"""
model_config = STRICT_FROZEN_CONFIG
#: Ejercicio -- design offset 5, length 4, four-digit year.
ejercicio: _FourDigitYear
#: NIF del declarante -- design offset 9, length 9.
nif_del_declarante: _NonBlankName
#: Apellidos y nombre o razon social del declarante -- design offset 18, length 40.
apellidos_y_nombre_o_razon_social_del: _NonBlankName
#: Tipo de soporte -- design offset 58, length 1, alfabetico.
tipo_de_soporte: _M296SupportType | None = None
#: Persona con quien relacionarse, telefono -- design offset 59, length 9.
telefono: _DigitString9 | None = None
#: Persona con quien relacionarse, apellidos y nombre -- design offset 68, length 40.
apellidos_y_nombre: _NonBlankName | None = None
#: Numero identificativo de la declaracion -- design offset 108, length 13.
numero_identificativo_de_la_declaracio: _AeatReceiptNumber | None = None
#: Declaracion complementaria o sustitutiva -- design offset 121, length 2.
declaracion_complementaria_o_sustituti: _M296AmendmentMark | None = None
#: Numero identificativo de la declaracion anterior -- design offset 123, length 13.
numero_identificativo_de_la_declaracio_2: _AeatReceiptNumber | None = None
#: Numero total de perceptores -- design offset 136, length 9.
#:
#: An operator-supplied count that the perceptor rows themselves determine. It is
#: optional here deliberately: once the perceptor record repeats its rows, the count is
#: derived from them, and a value stated here that disagrees with the rows is a defect
#: rather than a fact.
numero_total_de_perceptores: _DigitString9 | None = None
#: N.I.F. del representante legal -- design offset 391, length 9. See the class note on
#: why the key tail is ``n``.
n: _NonBlankName | None = None
#: Sello electronico -- design offset 488, length 13.
sello_electronico: str | None = None
#: One entry per payee. Empty emits no perceptor record at all, which is what AEAT
#: expects of a declaration with nothing to report; whether that absence is admissible
#: is the record's own required flag, checked by the renderer.
perceptor_rows: tuple[Modelo296PerceptorRow, ...] = ()
#: One entry per perceptor of the intereses hoja. Empty emits no row of this record at all.
perceptor_intereses_rows: tuple[Modelo296PerceptorInteresesRow, ...] = ()
#: One entry per pago a contribuyente. Empty emits no row of this record at all.
anexo_pago_rows: tuple[Modelo296AnexoPagoRow, ...] = ()
#: One entry per certificado de pago. Empty emits no row of this record at all.
anexo_certificado_rows: tuple[Modelo296AnexoCertificadoRow, ...] = ()
[docs]
class Modelo210ContribuyenteFactSet(BaseModel):
"""Modelo 210 contribuyente facts, flat members named from the AEAT component vocabulary."""
model_config = STRICT_FROZEN_CONFIG
birth_city: str | None = None
birth_country_code: str | None = None
birth_date: str | None = None
foreign_address_city: str | None = None
foreign_address_complement: str | None = None
foreign_address_country_code: str | None = None
foreign_address_email: str | None = None
foreign_address_fax: str | None = None
foreign_address_mobile_phone: str | None = None
foreign_address_phone: str | None = None
foreign_address_postal_code: str | None = None
foreign_address_region: str | None = None
foreign_address_street: str | None = None
foreign_tax_id: str | None = None
full_name: str | None = None
person_type: str | None = None
tax_id: str | None = None
tax_residence_country_code: str | None = None
[docs]
class Modelo210DeclaracionFactSet(BaseModel):
"""Modelo 210 declaracion facts, flat members named from the AEAT component vocabulary."""
model_config = STRICT_FROZEN_CONFIG
tipo: str | None = None
[docs]
class Modelo210DeclaranteFactSet(BaseModel):
"""Modelo 210 declarante facts, flat members named from the AEAT component vocabulary."""
model_config = STRICT_FROZEN_CONFIG
capacity_contribuyente: str | None = None
capacity_depositario: str | None = None
capacity_gestor: str | None = None
capacity_pagador: str | None = None
capacity_representante: str | None = None
capacity_retenedor: str | None = None
full_name: str | None = None
tax_id: str | None = None
[docs]
class Modelo210DevengoFactSet(BaseModel):
"""Modelo 210 devengo facts, flat members named from the AEAT component vocabulary."""
model_config = STRICT_FROZEN_CONFIG
agrupacion: str | None = None
fecha_devengo: str | None = None
[docs]
class Modelo210DevolucionFactSet(BaseModel):
"""Modelo 210 devolucion facts, flat members named from the AEAT component vocabulary."""
model_config = STRICT_FROZEN_CONFIG
cuenta_resto_banco: str | None = None
cuenta_resto_ciudad: str | None = None
cuenta_resto_codigo_pais: str | None = None
cuenta_resto_direccion_banco: str | None = None
cuenta_resto_numero_cuenta: str | None = None
cuenta_resto_swift_bic: str | None = None
cuenta_sepa_iban: str | None = None
cuenta_sepa_swift_bic: str | None = None
cuenta_titular_full_name: str | None = None
cuenta_titular_tax_id: str | None = None
renuncia_a_favor_del_tesoro: str | None = None
[docs]
class Modelo210GananciaInmobiliariaFactSet(BaseModel):
"""Modelo 210 ganancia inmobiliaria facts, flat members named from the AEAT component vocabulary."""
model_config = STRICT_FROZEN_CONFIG
conyuge_full_name: str | None = None
conyuge_tax_id: str | None = None
cuota_participacion_contribuyente: str | None = None
cuota_participacion_conyuge: str | None = None
fecha_adquisicion: str | None = None
fecha_mejora: str | None = None
justificante_modelo_211: str | None = None
titularidad: str | None = None
[docs]
class Modelo210IngresoFactSet(BaseModel):
"""Modelo 210 ingreso facts, flat members named from the AEAT component vocabulary."""
model_config = STRICT_FROZEN_CONFIG
cuenta_resto_banco: str | None = None
cuenta_resto_ciudad: str | None = None
cuenta_resto_codigo_pais: str | None = None
cuenta_resto_direccion_banco: str | None = None
cuenta_resto_numero_cuenta: str | None = None
cuenta_resto_swift_bic: str | None = None
cuenta_sepa_iban: str | None = None
cuenta_sepa_swift_bic: str | None = None
cuenta_titular_full_name: str | None = None
cuenta_titular_tax_id: str | None = None
forma_pago: str | None = None
[docs]
class Modelo210InmuebleFactSet(BaseModel):
"""Modelo 210 inmueble facts, flat members named from the AEAT component vocabulary."""
model_config = STRICT_FROZEN_CONFIG
referencia_catastral: str | None = None
situacion_bloque: str | None = None
situacion_calificador_numero: str | None = None
situacion_codigo_ine_municipio: str | None = None
situacion_codigo_postal: str | None = None
situacion_codigo_provincia: str | None = None
situacion_datos_complementarios: str | None = None
situacion_escalera: str | None = None
situacion_localidad: str | None = None
situacion_nombre_via: str | None = None
situacion_numero_casa: str | None = None
situacion_planta: str | None = None
situacion_portal: str | None = None
situacion_puerta: str | None = None
situacion_tipo_numeracion: str | None = None
situacion_tipo_via: str | None = None
[docs]
class Modelo210PagadorFactSet(BaseModel):
"""Modelo 210 pagador facts, flat members named from the AEAT component vocabulary."""
model_config = STRICT_FROZEN_CONFIG
full_name: str | None = None
person_type: str | None = None
tax_id: str | None = None
[docs]
class Modelo210RentaFactSet(BaseModel):
"""Modelo 210 renta facts, flat members named from the AEAT component vocabulary."""
model_config = STRICT_FROZEN_CONFIG
clave_divisa: str | None = None
[docs]
class Modelo210RepresentanteFactSet(BaseModel):
"""Modelo 210 representante facts, flat members named from the AEAT component vocabulary."""
model_config = STRICT_FROZEN_CONFIG
appointment_kind: str | None = None
domicilio_bloque: str | None = None
domicilio_calificador_numero: str | None = None
domicilio_codigo_ine_municipio: str | None = None
domicilio_codigo_postal: str | None = None
domicilio_codigo_provincia: str | None = None
domicilio_datos_complementarios: str | None = None
domicilio_escalera: str | None = None
domicilio_localidad: str | None = None
domicilio_nombre_via: str | None = None
domicilio_numero_casa: str | None = None
domicilio_planta: str | None = None
domicilio_portal: str | None = None
domicilio_puerta: str | None = None
domicilio_tipo_numeracion: str | None = None
domicilio_tipo_via: str | None = None
fax: str | None = None
full_name: str | None = None
mobile_phone: str | None = None
person_type: str | None = None
phone: str | None = None
tax_id: str | None = None
[docs]
class Modelo210SinIngresoNiDevolucionFactSet(BaseModel):
"""Modelo 210 sin ingreso ni devolucion facts, flat members named from the AEAT component vocabulary."""
model_config = STRICT_FROZEN_CONFIG
cuota_cero: str | None = None
[docs]
class Modelo210ProfileFacts(BaseModel):
"""The party, property and settlement facts modelo 210's export layout cites.
Modelo 210 is the non-resident income tax return. Its layout cites 102 ``irnr.*``
producer keys across twelve scopes; every one of them resolved to nothing, so the
contribuyente, the representante, the inmueble and the refund account all rendered
blank on a filed return.
Each scope declares its own flat members rather than sharing one address or account
model. AEAT reuses one address grammar but not one address shape: modelo 210 identifies
the municipio by INE code where modelo 360 writes its name, so a shared type would assert
two shapes are interchangeable when they are not.
Every field is optional and absent stays absent: AEAT writes an alphanumeric header
field with no content to blancos, so an unsupplied scope is a legal filing rather than
a defect. Which scopes a given filing must carry is the caller's decision, not this
type's.
"""
model_config = STRICT_FROZEN_CONFIG
contribuyente: Modelo210ContribuyenteFactSet | None = None
declaracion: Modelo210DeclaracionFactSet | None = None
declarante: Modelo210DeclaranteFactSet | None = None
devengo: Modelo210DevengoFactSet | None = None
devolucion: Modelo210DevolucionFactSet | None = None
ganancia_inmobiliaria: Modelo210GananciaInmobiliariaFactSet | None = None
ingreso: Modelo210IngresoFactSet | None = None
inmueble: Modelo210InmuebleFactSet | None = None
pagador: Modelo210PagadorFactSet | None = None
renta: Modelo210RentaFactSet | None = None
representante: Modelo210RepresentanteFactSet | None = None
sin_ingreso_ni_devolucion: Modelo210SinIngresoNiDevolucionFactSet | None = None
[docs]
class GeneralFilingProfileFacts(BaseModel):
"""Explicit absence of modelo-specific producer facts for a layout."""
model_config = STRICT_FROZEN_CONFIG
[docs]
class Modelo202ActivityFacts(BaseModel):
"""One repeatable M202 activity fact, without claiming primacy."""
model_config = STRICT_FROZEN_CONFIG
cnae: _CnaeCode
[docs]
class M202UnsupportedProducerId(StrEnum):
"""M202 producer facts that are not yet admitted to the typed substrate."""
PRINCIPAL_CNAE = "m202.principal_cnae"
OFFICIAL_OFFSET_122 = "m202.official_offset_122"
OFFICIAL_OFFSET_123 = "m202.official_offset_123"
OFFICIAL_OFFSET_124 = "m202.official_offset_124"
OFFICIAL_OFFSET_125 = "m202.official_offset_125"
OFFICIAL_OFFSET_126 = "m202.official_offset_126"
OFFICIAL_OFFSET_127 = "m202.official_offset_127"
OFFICIAL_OFFSET_128 = "m202.official_offset_128"
OFFICIAL_OFFSET_129 = "m202.official_offset_129"
OFFICIAL_OFFSET_130 = "m202.official_offset_130"
OFFICIAL_OFFSET_131 = "m202.official_offset_131"
OFFICIAL_OFFSET_132 = "m202.official_offset_132"
OFFICIAL_OFFSET_147 = "m202.official_offset_147"
M202_UNSUPPORTED_PRODUCER_IDS: tuple[M202UnsupportedProducerId, ...] = tuple(M202UnsupportedProducerId)
[docs]
class Modelo202ProducerProfile(BaseModel):
"""M202 producer view referencing the canonical taxpayer profile owner.
The régimen marks and the principal CNAE below are what modelo 202's export layout
cites as header producers. Before they existed the eighteen ``m202.*`` keys were
declared in the vocabulary and resolved by nothing, so each one rendered blank on a
filed pago fraccionado.
``principal_cnae`` is DECLARED, never inferred from ``activities``:
:class:`Modelo202ActivityFacts` is documented as "one repeatable M202 activity fact,
without claiming primacy", so picking the first or the largest would invent a primacy
the substrate deliberately does not carry. AEAT asks which activity is principal, so
the operator answers it.
Every mark is optional and absent stays absent -- AEAT leaves a régimen mark blank when
the régimen does not apply, and a mark that does not apply is genuinely absent rather
than unknown.
"""
model_config = STRICT_FROZEN_CONFIG
taxpayer_profile: TaxpayerProfile
activities: tuple[Modelo202ActivityFacts, ...]
principal_cnae: _CnaeCode | None = None
regimen_ley_49_2002_sin_fines_lucrativos: str | None = None
regimen_ley_11_2009_socimi: str | None = None
regimen_entidades_navieras_tonelaje: str | None = None
regimen_articulo_101_lis_reducida_dimension: str | None = None
regimen_entidad_capital_riesgo: str | None = None
cifra_negocios_doce_meses_umbral: str | None = None
cifra_negocios_periodo_anterior_bajo_umbral: str | None = None
cooperativa_o_multiples_tipos: str | None = None
cooperativa_fiscalmente_protegida: str | None = None
multiples_tipos_impositivos: str | None = None
tipo_gravamen_impuesto_sociedades: str | None = None
importe_neto_cifra_negocios_tramo: str | None = None
marca_instrumental: str | None = None
discriminante_declaracion_negativa: str | None = None
normativa_territorio_foral: str | None = None
comunicacion_datos_adicionales: str | None = None
numero_referencia_sociedades: str | None = None
@property
def unsupported_producer_ids(self) -> tuple[M202UnsupportedProducerId, ...]:
"""Return the exact immutable M202 producer-gap inventory."""
return M202_UNSUPPORTED_PRODUCER_IDS
[docs]
class FilingElectionFacts(BaseModel):
"""Immutable operator elections and their resolved result disposition."""
model_config = STRICT_FROZEN_CONFIG
result_disposition: ResultDisposition
payment: PaymentElection
refund: RefundElection
prior_domiciliation: PriorDomiciliationElection
[docs]
class M303FilingFacts(BaseModel):
"""DP30301 facts owned by one immutable Modelo 303 filing instance."""
model_config = STRICT_FROZEN_CONFIG
joint_return_elected: bool
annual_volume_nonzero: bool
insolvency: M303InsolvencyFilingFact | None
exonerado_390: M303Exonerado390FilingEvidence
regimen_simplificado: M303RegimenSimplificadoFilingEvidence
regimen_simplificado_result: M303RegimenSimplificadoCalculationResult
period: Period
supplier_regime: M303SupplierRegimeArrival
prorrata_transition: M303ProrrataTransitionArrival
prorrata_register: ProrrataRegister
differentiated_contributions: tuple[IvaDifferentiatedDeductionContribution, ...]
bienes_register: BienesInversionIvaRegister
regularisation_result: RegistroRegularizacionResult
#: The registry-resolved figures the regularisation result was produced
#: under. Carried on the facts rather than resolved here because this
#: model holds no revision, and because the oracle's independence depends
#: on comparing the result's own provenance against a bundle supplied
#: from outside it.
bienes_parameters: BienesInversionRegularizacionParameters
@model_validator(mode="after")
@pydantic_validation_boundary
def _arrivals_share_one_filing_period(self) -> M303FilingFacts:
_validate_m303_filing_periods(self)
_validate_m303_calculation_results(self)
_validate_m303_register_evidence(self)
return self
def _validate_m303_filing_periods(facts: M303FilingFacts) -> None:
_require_m303_official_filing_period(facts.period)
if facts.period != facts.supplier_regime.period or facts.period != facts.prorrata_transition.period:
raise ValueError("M303 filing facts and arrivals must share one filing period")
if facts.regularisation_result.regularizacion_year != facts.period.filing_year:
raise ValueError("M303 regularisation result must use the filing year")
def _validate_m303_calculation_results(facts: M303FilingFacts) -> None:
if facts.regimen_simplificado_result != facts.regimen_simplificado.calculation_result:
raise ValueError("M303 filing facts must retain the exact simplified-regime calculation result")
if facts.regimen_simplificado_result.period != facts.period:
raise ValueError("M303 filing facts and simplified-regime result must share one filing period")
def _validate_m303_register_evidence(facts: M303FilingFacts) -> None:
assert_m303_regularisation_result_matches_bienes_register(
bienes_register=facts.bienes_register,
regularisation_result=facts.regularisation_result,
parameters=facts.bienes_parameters,
)
if facts.prorrata_transition.is_applicable and not facts.prorrata_register.has_complete_current_entry_coverage(
facts.period.filing_year
):
raise ValueError("M303 final-period filing facts require complete current-year prorrata register coverage")
for entry in facts.prorrata_transition.register_evidence:
if facts.prorrata_register.entry_for(entry.ejercicio, sector_id=entry.sector_id) != entry:
raise ValueError("M303 prorrata transition arrival evidence must belong to the supplied register")
[docs]
def resolve_m303_filing_facts(
*,
evidence: FilingInstanceEvidence,
supplier_regime: M303SupplierRegimeArrival,
prorrata_transition: M303ProrrataTransitionArrival,
prorrata_register: ProrrataRegister,
differentiated_contributions: tuple[IvaDifferentiatedDeductionContribution, ...],
bienes_register: BienesInversionIvaRegister,
regularisation_result: RegistroRegularizacionResult,
bienes_parameters: BienesInversionRegularizacionParameters,
) -> M303FilingFacts:
"""Project persisted M303 evidence together with canonical arrival facts."""
m303 = evidence.m303
_require_m303_official_filing_period(m303.period)
return M303FilingFacts(
joint_return_elected=m303.joint_return_elected,
annual_volume_nonzero=m303.annual_volume_nonzero,
insolvency=m303.insolvency,
exonerado_390=m303.exonerado_390,
regimen_simplificado=m303.regimen_simplificado,
regimen_simplificado_result=m303.regimen_simplificado.calculation_result,
period=m303.period,
supplier_regime=supplier_regime,
prorrata_transition=prorrata_transition,
prorrata_register=prorrata_register,
differentiated_contributions=differentiated_contributions,
bienes_register=bienes_register,
regularisation_result=regularisation_result,
bienes_parameters=bienes_parameters,
)
[docs]
class AmendmentEvidence(BaseModel):
"""Typed evidence for an amendment of an AEAT-accepted filing."""
model_config = STRICT_FROZEN_CONFIG
kind: CalculationRevisionAmendmentKind
m303_rectificativa_motive: M303RectificativaMotive | None
original_aeat_receipt: _AeatReceiptNumber
@model_validator(mode="after")
@pydantic_validation_boundary
def _motive_belongs_only_to_rectificativa(self) -> AmendmentEvidence:
if (
self.m303_rectificativa_motive is not None
and self.kind is not CalculationRevisionAmendmentKind.RECTIFICATIVA
):
raise ValueError("M303 rectificativa motive is valid only for rectificativa evidence")
return self
@property
def is_complementaria(self) -> bool:
"""True when this evidence records a complementaria amendment."""
return self.kind is CalculationRevisionAmendmentKind.COMPLEMENTARIA
@property
def is_sustitutiva(self) -> bool:
"""True when this evidence records a sustitutiva amendment."""
return self.kind is CalculationRevisionAmendmentKind.SUSTITUTIVA
@property
def is_rectificativa(self) -> bool:
"""True when this evidence records a rectificativa amendment."""
return self.kind is CalculationRevisionAmendmentKind.RECTIFICATIVA
[docs]
class RefundAccountSelection(BaseModel):
"""Secure account selected for a refund disposition."""
model_config = STRICT_FROZEN_CONFIG
role: Literal["refund"]
account: RefundAccount
[docs]
class ChargeAccountSelection(BaseModel):
"""Secure account selected for a direct-debit disposition."""
model_config = STRICT_FROZEN_CONFIG
role: Literal["charge"]
account: ChargeAccount
type SelectedFilingAccount = RefundAccountSelection | ChargeAccountSelection
type FilingModelProfileFacts = (
GeneralFilingProfileFacts
| Modelo111ProfileFacts
| Modelo202ProducerProfile
| _Modelo200ProfileFacts
| Modelo210ProfileFacts
| Modelo222ProfileFacts
| Modelo296ProfileFacts
| Modelo353ProfileFacts
| ModeloIVAProfile
)
[docs]
class FilingProducerSnapshot(BaseModel):
"""Complete immutable filing facts before registry-specific translation."""
model_config = STRICT_FROZEN_CONFIG
modelo: Modelo
taxpayer_tax_id: SubjectTaxId
taxpayer_identity: TaxpayerIdentityFacts
presenter: PresenterIdentity
#: Defaulted rather than required: every existing caller predates this fact
#: and none of them can supply it, so demanding it would refuse filings that
#: are legal without it. An absent contact renders as blancos, which is what
#: AEAT's own header rule prescribes.
declaration_contact: DeclarationContactFacts = DeclarationContactFacts()
model_profile: FilingModelProfileFacts
elections: FilingElectionFacts
amendment_evidence: AmendmentEvidence | None
selected_account: SelectedFilingAccount | None
m303_filing_facts: M303FilingFacts | None
m390_filing_facts: _M390FilingFacts | None = None
@model_validator(mode="after")
@pydantic_validation_boundary
def _validate_model_profile(self) -> FilingProducerSnapshot:
_validate_snapshot_model_profile(self)
_validate_snapshot_account_selection(self)
_validate_snapshot_profile_secrecy(self)
return self
def _validate_snapshot_model_profile(snapshot: FilingProducerSnapshot) -> None:
_validate_snapshot_cross_model_facts(snapshot)
_validate_snapshot_modelo_profile(snapshot)
def _validate_snapshot_cross_model_facts(snapshot: FilingProducerSnapshot) -> None:
if snapshot.modelo != Modelo("303") and snapshot.m303_filing_facts is not None:
raise ValueError("M303FilingFacts are valid only for modelo 303")
if snapshot.modelo != Modelo("390") and snapshot.m390_filing_facts is not None:
raise ValueError("M390FilingFacts are valid only for modelo 390")
if (
snapshot.modelo != Modelo("303")
and snapshot.amendment_evidence is not None
and snapshot.amendment_evidence.m303_rectificativa_motive is not None
):
raise ValueError("M303 rectificativa motive is valid only for modelo 303")
def _validate_snapshot_modelo_profile(snapshot: FilingProducerSnapshot) -> None:
if snapshot.modelo == Modelo("111"):
_validate_modelo_111_snapshot(snapshot)
return
if snapshot.modelo == Modelo("202"):
_validate_modelo_202_snapshot(snapshot)
return
if snapshot.modelo == Modelo("222"):
_validate_modelo_222_snapshot(snapshot)
return
if snapshot.modelo == Modelo("303"):
_validate_modelo_303_snapshot(snapshot)
return
if snapshot.modelo == Modelo("296"):
_validate_modelo_296_snapshot(snapshot)
return
if snapshot.modelo == Modelo("353"):
_validate_modelo_353_snapshot(snapshot)
return
_validate_general_modelo_snapshot(snapshot)
def _validate_modelo_296_snapshot(snapshot: FilingProducerSnapshot) -> None:
"""Modelo 296 identifies a declarante and an ejercicio; it cannot be filed without them."""
if not isinstance(snapshot.model_profile, Modelo296ProfileFacts):
raise ValueError("modelo 296 requires Modelo296ProfileFacts")
def _validate_modelo_353_snapshot(snapshot: FilingProducerSnapshot) -> None:
"""Modelo 353 is the grupo de entidades aggregate; it cannot be filed without it."""
if not isinstance(snapshot.model_profile, Modelo353ProfileFacts):
raise ValueError("modelo 353 requires Modelo353ProfileFacts")
def _validate_modelo_222_snapshot(snapshot: FilingProducerSnapshot) -> None:
"""Modelo 222 is a grupo fiscal return; it cannot be filed without the group."""
if not isinstance(snapshot.model_profile, Modelo222ProfileFacts):
raise ValueError("modelo 222 requires Modelo222ProfileFacts")
def _validate_modelo_111_snapshot(snapshot: FilingProducerSnapshot) -> None:
if not isinstance(snapshot.model_profile, Modelo111ProfileFacts):
raise ValueError("modelo 111 requires Modelo111ProfileFacts")
if snapshot.model_profile.colegio_concertado is None:
raise ValueError("Modelo 111 colegio_concertado must be explicitly declared")
def _validate_modelo_202_snapshot(snapshot: FilingProducerSnapshot) -> None:
if not isinstance(snapshot.model_profile, Modelo202ProducerProfile):
raise ValueError("modelo 202 requires Modelo202ProducerProfile")
unsupported = ", ".join(item.value for item in snapshot.model_profile.unsupported_producer_ids)
raise ValueError(f"Modelo 202 producer snapshot is incomplete: {unsupported}")
def _validate_modelo_303_snapshot(snapshot: FilingProducerSnapshot) -> None:
if not isinstance(snapshot.model_profile, ModeloIVAProfile):
raise ValueError("modelo 303 requires the canonical ModeloIVAProfile")
if snapshot.m303_filing_facts is None:
raise ValueError("modelo 303 requires complete M303FilingFacts")
amendment = snapshot.amendment_evidence
motive_applicable = m303_rectificativa_motive_is_applicable(
registry_revision_id=snapshot.m303_filing_facts.regimen_simplificado.regimen_snapshot.orden.registry_revision_id,
record_design=snapshot.m303_filing_facts.regimen_simplificado.regimen_snapshot.record_design,
)
has_rectificativa = amendment is not None and amendment.is_rectificativa
has_motive = amendment is not None and amendment.m303_rectificativa_motive is not None
if motive_applicable and has_rectificativa != has_motive:
raise ValueError("applicable M303 rectificativa evidence requires exactly one canonical motive")
if not motive_applicable and has_motive:
raise ValueError("M303 rectificativa motive is prohibited outside the admitted record-design sources")
def _validate_general_modelo_snapshot(snapshot: FilingProducerSnapshot) -> None:
if not isinstance(snapshot.model_profile, GeneralFilingProfileFacts):
raise ValueError(f"modelo {snapshot.modelo.value} requires GeneralFilingProfileFacts")
def _validate_snapshot_account_selection(snapshot: FilingProducerSnapshot) -> None:
disposition = snapshot.elections.result_disposition
if disposition is ResultDisposition.DOMICILIACION:
if snapshot.elections.payment is not PaymentElection.DOMICILIACION:
raise ValueError("domiciliacion disposition requires the matching payment election")
if not isinstance(snapshot.selected_account, ChargeAccountSelection):
raise ValueError("domiciliacion disposition requires a selected charge account")
elif snapshot.elections.payment is PaymentElection.DOMICILIACION:
raise ValueError("domiciliacion payment election requires the matching result disposition")
elif result_disposition_is_refund(disposition):
if not isinstance(snapshot.selected_account, RefundAccountSelection):
raise ValueError("refund disposition requires a selected refund account")
elif snapshot.selected_account is not None and not (
_m303_nota_three_shape(snapshot) and isinstance(snapshot.selected_account, RefundAccountSelection)
):
raise ValueError("a result disposition without an account must not retain one")
def _m303_nota_three_shape(snapshot: FilingProducerSnapshot) -> bool:
"""Whether Modelo 303 Nota 3 may carry the refund account on the account page."""
amendment = snapshot.amendment_evidence
return (
snapshot.modelo == Modelo("303")
and amendment is not None
and amendment.is_rectificativa
and snapshot.elections.prior_domiciliation is PriorDomiciliationElection.KEEP
)
def _validate_snapshot_profile_secrecy(snapshot: FilingProducerSnapshot) -> None:
profile_iva = _profile_iva(snapshot.model_profile)
if profile_iva is not None and (profile_iva.refund_account is not None or profile_iva.charge_account is not None):
raise ValueError("model profile must not retain accounts outside selected_account")
def _profile_iva(model_profile: FilingModelProfileFacts) -> ModeloIVAProfile | None:
if isinstance(model_profile, ModeloIVAProfile):
return model_profile
if isinstance(model_profile, Modelo202ProducerProfile):
return model_profile.taxpayer_profile.iva
return None
[docs]
def build_filing_producer_snapshot(
*,
modelo: Modelo,
taxpayer_tax_id: SubjectTaxId,
taxpayer_identity: TaxpayerIdentityFacts,
presenter: PresenterIdentity,
model_profile: FilingModelProfileFacts,
elections: FilingElectionFacts,
amendment_evidence: AmendmentEvidence | None,
refund_account: RefundAccount | None,
charge_account: ChargeAccount | None,
m303_filing_facts: M303FilingFacts | None,
declaration_contact: DeclarationContactFacts | None = None,
nota_three_refund_account: bool = False,
) -> FilingProducerSnapshot:
"""Build a snapshot retaining only the account selected by disposition.
``declaration_contact`` is optional so every caller that predates the
informativa contact fact keeps working unchanged; an absent contact renders
as blancos, which is what AEAT's own header rule prescribes.
"""
safe_model_profile = _without_embedded_accounts(model_profile)
selected_account: SelectedFilingAccount | None
if elections.result_disposition is ResultDisposition.DOMICILIACION:
if charge_account is None:
raise FilingProducerSnapshotError("domiciliacion requires a charge account")
selected_account = ChargeAccountSelection(role="charge", account=charge_account)
elif result_disposition_is_refund(elections.result_disposition):
if refund_account is None or refund_account.iban is None:
raise FilingProducerSnapshotError("refund disposition requires a refund account")
selected_account = RefundAccountSelection(role="refund", account=refund_account)
elif nota_three_refund_account:
# Modelo 303 Nota 3: the account page carries the refund account even
# though the disposition itself is not a refund.
if refund_account is None:
raise FilingProducerSnapshotError("Nota 3 account page requires a refund account")
selected_account = RefundAccountSelection(role="refund", account=refund_account)
else:
selected_account = None
try:
return FilingProducerSnapshot(
modelo=modelo,
taxpayer_tax_id=taxpayer_tax_id,
taxpayer_identity=taxpayer_identity,
presenter=presenter,
model_profile=safe_model_profile,
elections=elections,
amendment_evidence=amendment_evidence,
selected_account=selected_account,
m303_filing_facts=m303_filing_facts,
m390_filing_facts=None,
declaration_contact=declaration_contact or DeclarationContactFacts(),
)
except ValueError as exc:
raise FilingProducerSnapshotError(str(exc)) from exc
def _without_embedded_accounts(model_profile: FilingModelProfileFacts) -> FilingModelProfileFacts:
if isinstance(model_profile, ModeloIVAProfile):
return model_profile.model_copy(update={"refund_account": None, "charge_account": None})
if isinstance(model_profile, Modelo202ProducerProfile):
taxpayer_profile = model_profile.taxpayer_profile
if taxpayer_profile.iva is None:
return model_profile
safe_iva = taxpayer_profile.iva.model_copy(update={"refund_account": None, "charge_account": None})
safe_taxpayer = taxpayer_profile.model_copy(update={"iva": safe_iva})
return model_profile.model_copy(update={"taxpayer_profile": safe_taxpayer})
return model_profile
__all__ = [
"M202_UNSUPPORTED_PRODUCER_IDS",
"AmendmentEvidence",
"ChargeAccountSelection",
"FilingElectionFacts",
"FilingModelProfileFacts",
"FilingProducerSnapshot",
"FilingProducerSnapshotError",
"GeneralFilingProfileFacts",
"M202UnsupportedProducerId",
"M303FilingFacts",
"M303InsolvencyFilingFact",
"M303InsolvencyFilingSubtype",
"Modelo111ProfileFacts",
"Modelo202ActivityFacts",
"Modelo202ProducerProfile",
"PresenterIdentity",
"RefundAccountSelection",
"SelectedFilingAccount",
"TaxpayerIdentityFacts",
"assert_m303_regularisation_result_matches_bienes_register",
"build_filing_producer_snapshot",
"resolve_m303_filing_facts",
]