"""Typed CLI row models for multi-row informational modelos.
Provides strictly-validated pydantic row shapes for operator-supplied
detail rows on modelos whose filing content is a list of repeating
records rather than a set of scalar casilla values.
Supported row types:
* ``Modelo184MemberRow`` — atribución member for modelo 184
(``--row miembro nif=X share=Y importe=Z``)
* ``Modelo232VinculadaRow`` — operación vinculada for modelo 232
(``--row vinculada nif=X tipo_vinculacion=Y importe=Z metodo=M pais=P``)
* ``Modelo349OperadorRow`` — operador intracomunitario for modelo 349
(``--row operador codigo_pais=DE nif_comunitario=DE123456789 razon_social=X clave_operacion=E importe=Y``)
Used when no collectible-invoice ledger exists; maps directly to the
Tipo-2 operador record layout (Orden HAC/174/2020 Anexo II).
* ``Modelo349RectificacionRow`` — rectificación intracomunitaria for modelo 349
(``--row rectificacion codigo_pais=DE nif_comunitario=DE123456789 razon_social=X``
``clave_operacion=E ejercicio=2025 periodo=2T base_rectificada=Y base_anterior=Z``)
Used when the operator declares Tipo-2 rectification records directly.
* ``Modelo347ContraparteRow`` — contraparte declarada for modelo 347
(``--row contraparte nif=X nombre=Y importe_Q1=Z clave_operacion=A``)
One row per counterparty. Annual importe threshold check (> €3,005.06)
is performed by the CLI validator, not the model, so partial row sets
accumulate correctly before final validation.
These models are the CLI boundary layer. They validate operator input
before being carried into ``detail_rows`` on the ``CalculationRevision``.
"""
from __future__ import annotations
import re
from collections.abc import Sequence
from decimal import Decimal
from typing import Annotated, Literal
from pydantic import BaseModel, Field, StringConstraints, field_validator
from ...core import STRICT_FROZEN_CONFIG
from ...core.errors import AeatError
from ...core.external_constants import M347_THRESHOLD_EUR as M347_THRESHOLD_EUR # re-export
from ...core.identity import nif_iva_format_for_country
# ---------------------------------------------------------------------------
# Shared type aliases
# ---------------------------------------------------------------------------
_NifStr = Annotated[str, StringConstraints(strip_whitespace=True, min_length=1, max_length=20)]
_NameStr = Annotated[str, StringConstraints(strip_whitespace=True, max_length=200)]
_RequiredNameStr = Annotated[str, StringConstraints(strip_whitespace=True, min_length=1, max_length=200)]
_IsoCountryCode = Annotated[str, StringConstraints(strip_whitespace=True, min_length=2, max_length=2)]
# ---------------------------------------------------------------------------
# Modelo 184 - atribucion de rentas member row
#
# Legal authority: Orden HAP/2250/2015 art. 3; Ley 35/2006 arts. 86-89.
# One row per miembro (socio / comunero / partícipe) of the entity.
# ---------------------------------------------------------------------------
[docs]
class Modelo184MemberRow(BaseModel):
"""One atribución member row for Modelo 184.
Fields mirror the per-record Tipo-2 layout declared in the M184
``bindings/0001-bindings.toml`` atribucion_member source block.
Parity assertions:
* ``nif`` → ``member_tax_id`` (binding: modelo-184-member-row-nif)
* ``nombre`` → ``member_legal_name`` (binding: modelo-184-member-row-name)
* ``porcentaje`` → ``share_percentage`` (binding: modelo-184-member-row-share)
* ``importe`` → ``base_imponible_assigned`` (binding: modelo-184-member-row-base-assigned)
* ``pais`` → ``country_code`` (ES default)
"""
model_config = STRICT_FROZEN_CONFIG
row_type: Literal["miembro"] = "miembro"
nif: _NifStr
nombre: _NameStr = Field(default="")
pais: _IsoCountryCode = Field(default="ES")
porcentaje: Decimal = Field(description="Share percentage in the entity [0, 100]")
importe: Decimal = Field(description="Attributed income/base imponible in EUR")
@field_validator("pais")
@classmethod
def _pais_uppercase_alpha(cls, value: str) -> str:
if value != value.upper() or not value.replace(" ", "").isalpha():
raise ValueError("pais must be an uppercase two-letter ISO 3166-1 country code (e.g. ES, DE, FR)")
return value
@field_validator("porcentaje")
@classmethod
def _porcentaje_within_bounds(cls, value: Decimal) -> Decimal:
if value < Decimal("0") or value > Decimal("100"):
raise ValueError(f"porcentaje must be within [0, 100]; got {value}")
return value
@field_validator("nif")
@classmethod
def _nif_not_blank(cls, value: str) -> str:
if not value.strip():
raise ValueError("nif cannot be blank")
return value.upper()
# ---------------------------------------------------------------------------
# Modelo 232 - operacion vinculada row
#
# Legal authority: Orden HFP/816/2017 art. 3; Ley 27/2014 art. 18;
# RD 634/2015 art. 13 (LIS transfer pricing).
# One row per related-party transaction group.
# ---------------------------------------------------------------------------
# Closed catalogue of válid tipo_vinculacion codes per M232 form.
_M232_TIPO_VINCULACION = Literal[
"1",
"2",
"3",
"4",
"5",
"6",
"7",
"8",
"9",
"10",
"11",
"12",
"13",
"14",
"15",
"16",
]
# Closed catalogue of valid tipo_operacion codes per M232 form.
_M232_TIPO_OPERACION = Literal[
"01",
"02",
"03",
"04",
"05",
"06",
"07",
"08",
"09",
"10",
"11",
"12",
"13",
"14",
"15",
"16",
"17",
"18",
"19",
"20",
]
# Closed catalogue of transfer-pricing method codes per M232 / LIS art. 18.
_M232_METODO = Literal["CUP", "RPM", "CPM", "PS", "TNMM", ""]
[docs]
class Modelo232VinculadaRow(BaseModel):
"""One operación vinculada row for Modelo 232.
Fields mirror the related_party_operation binding source declared in
``232/revisions/2018-y-siguientes/bindings/0218…0223-*.toml``.
Parity assertions:
* ``nif`` → ``counterparty_tax_id`` (binding: modelo-232-related-party-row-nif)
* ``nombre`` → ``counterparty_legal_name`` (binding: modelo-232-related-party-row-name)
* ``pais`` → ``country_code`` (binding: modelo-232-related-party-row-country)
* ``tipo_operacion`` → ``operation_kind_code`` (binding: modelo-232-related-party-row-operation-kind)
* ``metodo`` → ``transfer_pricing_method_code`` (binding: modelo-232-related-party-row-tpr-method)
* ``importe`` → ``amount`` (binding: modelo-232-related-party-row-amount)
"""
model_config = STRICT_FROZEN_CONFIG
row_type: Literal["vinculada"] = "vinculada"
nif: _NifStr
nombre: _NameStr = Field(default="")
pais: _IsoCountryCode = Field(default="ES")
tipo_vinculacion: str = Field(default="1", min_length=1, max_length=2)
tipo_operacion: str = Field(default="01", min_length=1, max_length=2)
metodo: str = Field(default="", max_length=6)
importe: Decimal
@field_validator("pais")
@classmethod
def _pais_uppercase_alpha(cls, value: str) -> str:
if value != value.upper() or not value.replace(" ", "").isalpha():
raise ValueError("pais must be an uppercase two-letter ISO 3166-1 country code (e.g. ES, DE, FR)")
return value
@field_validator("nif")
@classmethod
def _nif_not_blank(cls, value: str) -> str:
if not value.strip():
raise ValueError("nif cannot be blank")
return value.upper()
@field_validator("metodo")
@classmethod
def _metodo_uppercase(cls, value: str) -> str:
return value.upper()
# ---------------------------------------------------------------------------
# Modelo 349 - operador intracomunitario row (manual-entry path)
#
# Legal authority: Orden HAC/174/2020 (Anexo II — Tipo 2 operador record);
# Orden EHA/769/2010 art. 3; Ley 58/2003 art. 93; Ley 37/1992 arts. 66-70
# (operaciones intracomunitarias).
# One row per counterparty + clave_operacion combination.
# NIF-IVA format validation enforces country-specific patterns from
# Council Directive 2006/112/EC Annex XI (the VIES registry format rules).
# ---------------------------------------------------------------------------
# Country-specific NIF-IVA format patterns for Modelo 349. Every current EU
# Member State (plus post-Brexit Northern Ireland ``XI``) routes through the
# canonical :data:`aeat.core.identity.NIF_IVA_FORMATS` authority so the
# structural pattern lives in exactly one place (per the
# binding-source-kind-single-taxonomy discipline: a per-family collection is
# derived from the core table, never hand-maintained as a parallel literal
# set). ``GB`` is the sole deliberate exception: post-Brexit UK is not an EU
# Member State, so the general IVA/invoice counterparty boundary
# (:mod:`aeat.domain.invoices`) correctly carries no GB structural pattern and
# falls back to its generic non-EU shape check. Modelo 349's Brexit-transition
# filing rules (:func:`validate_m349_country_prefix_context`) still permit a
# historical ``GB`` prefix for pre-2021 rectifications and the 2021 1M/1T
# transition period, so the exact GB VAT structural shape (9 or 12 digits, or
# the ``GD``/``HA`` government/health-authority forms) is retained here only,
# scoped to Modelo 349's own transition-period need.
_M349_GB_NIF_PATTERN: re.Pattern[str] = re.compile(r"^GB(\d{9}|\d{12}|GD\d{3}|HA\d{3})$")
# Valid clave de operación codes per Orden HAC/174/2020 Anexo II.
_M349_CLAVE_OPERACION = Literal["E", "M", "H", "A", "T", "S", "I", "R", "D", "C"]
_M349_RECTIFICACION_PERIODO = Literal[
"01",
"02",
"03",
"04",
"05",
"06",
"07",
"08",
"09",
"10",
"11",
"12",
"1M",
"1T",
"2T",
"3T",
"4T",
]
_M349_NI_PREFIX = "XI"
_M349_GB_PREFIX = "GB"
_M349_SERVICE_CLAVES = frozenset({"S", "I"})
_M349_2021_FIRST_PERIODS = frozenset({"01", "1M", "1T"})
[docs]
class Modelo349CountryPrefixContextError(AeatError, ValueError):
"""A Modelo 349 country prefix is invalid for the filing context."""
def __init__(
self,
*,
country_code: str,
clave_operacion: str,
filing_year: int,
period: str,
reason: str,
) -> None:
self.country_code = country_code
self.clave_operacion = clave_operacion
self.filing_year = filing_year
self.period = period
self.reason = reason
super().__init__(
translated_message="errors.refused.modelo_349_country_prefix_context",
context={
"country_code": country_code,
"clave_operacion": clave_operacion,
"filing_year": filing_year,
"period": period,
"reason": reason,
},
)
[docs]
class Modelo349OperadorRow(BaseModel):
"""One operador intracomunitario row for Modelo 349 (manual-entry path).
Fields mirror the Tipo-2 operador record layout declared in
``349/revisions/2020-y-siguientes/bindings/0007-bindings.toml``.
This row is used when the collectible-invoice ledger is absent and
the operator declares intracom counterparties directly via the CLI.
Parity assertions:
* ``codigo_pais`` -> ``op.codigo-pais`` (record positions 76-77)
* ``nif_comunitario`` -> ``op.nif-comunitario`` (record positions 78-92)
* ``razon_social`` -> ``op.apellidos-razon-social`` (record positions 93-132)
* ``clave_operacion`` -> ``op.clave-operacion`` (record position 133)
* ``importe`` -> ``op.base-imponible`` (record positions 134-146)
"""
model_config = STRICT_FROZEN_CONFIG
row_type: Literal["operador"] = "operador"
codigo_pais: _IsoCountryCode
nif_comunitario: _NifStr
razon_social: _RequiredNameStr
clave_operacion: _M349_CLAVE_OPERACION
importe: Decimal = Field(description="Base imponible o importe de la operacion en EUR")
@field_validator("codigo_pais")
@classmethod
def _codigo_pais_uppercase_alpha(cls, value: str) -> str:
if value != value.upper() or not value.replace(" ", "").isalpha():
raise ValueError("codigo_pais must be an uppercase two-letter ISO 3166-1 country code (e.g. DE, FR, IT)")
return value
@field_validator("nif_comunitario")
@classmethod
def _nif_comunitario_not_blank(cls, value: str) -> str:
if not value.strip():
raise ValueError("nif_comunitario cannot be blank")
return value.upper()
@field_validator("importe")
@classmethod
def _importe_non_negative(cls, value: Decimal) -> Decimal:
if value < Decimal("0"):
raise ValueError(f"importe must be non-negative per Orden HAC/174/2020 Anexo II constraint; got {value}")
return value
[docs]
class Modelo349RectificacionRow(BaseModel):
"""One rectificación row for Modelo 349 (manual-entry path).
Fields mirror the Tipo-2 rectificación record layout declared in
``349/revisions/2020-y-siguientes/bindings/0007-bindings.toml``.
Parity assertions:
* ``codigo_pais`` -> ``op.codigo-pais`` (record positions 76-77)
* ``nif_comunitario`` -> ``op.nif-comunitario`` (record positions 78-92)
* ``razon_social`` -> ``op.apellidos-razon-social`` (record positions 93-132)
* ``clave_operacion`` -> ``op.clave-operacion`` (record position 133)
* ``ejercicio`` -> ``rect.ejercicio-rectificado`` (record positions 147-150)
* ``periodo`` -> ``rect.periodo-rectificado`` (record positions 151-152)
* ``base_rectificada`` -> ``rect.base-rectificada`` (record positions 153-165)
* ``base_anterior`` -> ``rect.base-anterior`` (record positions 166-178)
"""
model_config = STRICT_FROZEN_CONFIG
row_type: Literal["rectificacion"] = "rectificacion"
codigo_pais: _IsoCountryCode
nif_comunitario: _NifStr
razon_social: _RequiredNameStr
clave_operacion: _M349_CLAVE_OPERACION
ejercicio: Annotated[str, StringConstraints(strip_whitespace=True, min_length=4, max_length=4)]
periodo: _M349_RECTIFICACION_PERIODO
base_rectificada: Decimal = Field(description="Base imponible o importe rectificado en EUR")
base_anterior: Decimal = Field(description="Base imponible declarada anteriormente en EUR")
@field_validator("codigo_pais")
@classmethod
def _codigo_pais_uppercase_alpha(cls, value: str) -> str:
if value != value.upper() or not value.replace(" ", "").isalpha():
raise ValueError("codigo_pais must be an uppercase two-letter ISO 3166-1 country code (e.g. DE, FR, IT)")
return value
@field_validator("nif_comunitario")
@classmethod
def _nif_comunitario_not_blank(cls, value: str) -> str:
if not value.strip():
raise ValueError("nif_comunitario cannot be blank")
return value.upper()
@field_validator("periodo", mode="before")
@classmethod
def _periodo_uppercase(cls, value: object) -> object:
if isinstance(value, str):
return value.strip().upper()
return value
@field_validator("ejercicio")
@classmethod
def _ejercicio_four_digit_year(cls, value: str) -> str:
if not value.isdigit():
raise ValueError("ejercicio must be a four-digit year")
return value
@field_validator("base_rectificada", "base_anterior")
@classmethod
def _bases_non_negative(cls, value: Decimal) -> Decimal:
if value < Decimal("0"):
raise ValueError("rectification bases must be non-negative per Orden HAC/174/2020 Anexo II constraint")
return value
[docs]
def validate_m349_country_prefix_context(
*,
country_code: str,
clave_operacion: str,
filing_year: int,
period: str,
is_rectification: bool = False,
rectified_year: int | None = None,
rectified_period: str | None = None,
) -> None:
"""Validate post-Brexit ``GB`` / ``XI`` rules for Modelo 349.
AEAT's Brexit IVA instructions keep ``XI`` for Northern Ireland goods
operations after 2021 and exclude ``S`` / ``I`` service keys from ``XI``.
Ordinary ``GB`` rows are not valid for post-transition periods, except for
the limited 2021 first-period and pre-2021 rectification cases named by
the official instructions.
"""
country = country_code.strip().upper()
clave = clave_operacion.strip().upper()
period_code = _normalise_m349_period(period)
rectified_period_code = _normalise_m349_period(rectified_period) if rectified_period is not None else None
if country == _M349_NI_PREFIX:
if clave in _M349_SERVICE_CLAVES:
_raise_m349_country_context_error(
country_code=country,
clave_operacion=clave,
filing_year=filing_year,
period=period_code,
reason="Northern Ireland prefix XI is not accepted for service keys S or I",
)
if is_rectification and rectified_year is not None and rectified_year < 2021:
_raise_m349_country_context_error(
country_code=country,
clave_operacion=clave,
filing_year=filing_year,
period=period_code,
reason="pre-2021 rectifications use GB, not XI",
)
if not is_rectification and filing_year < 2021:
_raise_m349_country_context_error(
country_code=country,
clave_operacion=clave,
filing_year=filing_year,
period=period_code,
reason="XI applies only from 2021 onward",
)
return
if country != _M349_GB_PREFIX:
return
if is_rectification:
if rectified_year is not None and rectified_year < 2021:
return
if (
rectified_year == 2021
and rectified_period_code in _M349_2021_FIRST_PERIODS
and clave not in _M349_SERVICE_CLAVES
):
return
_raise_m349_country_context_error(
country_code=country,
clave_operacion=clave,
filing_year=filing_year,
period=period_code,
reason="GB is limited to pre-2021 rectifications and the 2021 1M/1T transition case",
)
if filing_year < 2021:
return
if filing_year == 2021 and period_code in _M349_2021_FIRST_PERIODS and clave not in _M349_SERVICE_CLAVES:
return
_raise_m349_country_context_error(
country_code=country,
clave_operacion=clave,
filing_year=filing_year,
period=period_code,
reason="ordinary post-transition Modelo 349 rows use XI for Northern Ireland goods and exclude GB",
)
def _normalise_m349_period(period: str | None) -> str:
if period is None:
return ""
token = str(period).strip().upper()
if len(token) == 1 and token.isdigit():
return f"0{token}"
return token
def _raise_m349_country_context_error(
*,
country_code: str,
clave_operacion: str,
filing_year: int,
period: str,
reason: str,
) -> None:
raise Modelo349CountryPrefixContextError(
country_code=country_code,
clave_operacion=clave_operacion,
filing_year=filing_year,
period=period,
reason=reason,
)
[docs]
def m349_nif_number_for_export(nif: str, pais: str) -> str:
"""Return the BOE NIF subfield without the separate country-code prefix.
Modelo 349 operator records split the VAT identifier into ``codigo_pais``
and ``nif_comunitario`` fields. The CLI accepts and validates the full
prefixed VAT identifier for operator ergonomics, but the fixed-width export
must write only the number part into positions 78-92.
"""
normalized_pais = pais.upper()
normalized_nif = nif.upper()
if not validate_m349_nif_format(normalized_nif, normalized_pais):
raise ValueError(
f"nif_comunitario {nif} does not match the expected NIF-IVA format for country {pais}",
)
return normalized_nif[len(normalized_pais) :]
# ---------------------------------------------------------------------------
# Modelo 347 - contraparte declarada row
#
# Legal authority: Orden EHA/3012/2008 art. 1; RD 1065/2007 arts. 31-35
# (reglamento de gestión e inspección tributaria, obligación de informar
# sobre operaciones con terceros); Ley 58/2003 art. 93.
# Threshold: total annual importe > €3,005.06 per counterparty (RD
# 1065/2007 art. 33.1). The threshold check is performed at the CLI
# validator level, not here, so that partial row accumulation works.
# ---------------------------------------------------------------------------
# Valid clave de operacion codes per M347 form / Orden EHA/3012/2008.
_M347_CLAVE_OPERACION = Literal["A", "B", "C", "D", "E", "F", "G", "H", "I"]
[docs]
class Modelo347ContraparteRow(BaseModel):
"""One contraparte declarada row for Modelo 347.
Fields mirror the per-counterparty Tipo-2 record layout declared in
``347/revisions/2008-y-siguientes``.
One row per counterparty. The annual total importe (sum of Q1-Q4)
must exceed €3,005.06 per RD 1065/2007 art. 33.1.
Parity assertions:
* ``nif`` → ``contraparte.nif`` (counterparty tax id)
* ``nombre`` → ``contraparte.nombre`` (legal name)
* ``importe_Q1/Q2/Q3/Q4`` → quarterly importe slots
* ``clave_operacion`` → operation type code
* ``pais_codigo`` → ``contraparte.pais`` (ISO 3166-1; None = domestic)
"""
model_config = STRICT_FROZEN_CONFIG
row_type: Literal["contraparte"] = "contraparte"
nif: _NifStr
nombre: _NameStr = Field(default="")
importe_Q1: Decimal = Field(default=Decimal("0"))
importe_Q2: Decimal = Field(default=Decimal("0"))
importe_Q3: Decimal = Field(default=Decimal("0"))
importe_Q4: Decimal = Field(default=Decimal("0"))
clave_operacion: _M347_CLAVE_OPERACION = "A"
pais_codigo: _IsoCountryCode | None = None
@field_validator("nif")
@classmethod
def _nif_not_blank(cls, value: str) -> str:
if not value.strip():
raise ValueError("nif cannot be blank")
return value.upper()
@field_validator("pais_codigo")
@classmethod
def _pais_codigo_uppercase_alpha(cls, value: str | None) -> str | None:
if value is None:
return value
v = value.strip().upper()
if not v.isalpha() or len(v) != 2:
raise ValueError("pais_codigo must be an uppercase two-letter ISO 3166-1 country code or None for domestic")
return v
@property
def importe_total(self) -> Decimal:
"""Sum of quarterly importes — used for M347 threshold check."""
return self.importe_Q1 + self.importe_Q2 + self.importe_Q3 + self.importe_Q4
# ---------------------------------------------------------------------------
# Discriminated union — single type accepted by the CLI --row argument
# ---------------------------------------------------------------------------
ModeloDetailRow = (
Modelo184MemberRow
| Modelo232VinculadaRow
| Modelo349OperadorRow
| Modelo349RectificacionRow
| Modelo347ContraparteRow
)
# ---------------------------------------------------------------------------
# Statutory cross-row / threshold validations (domain-owned)
# ---------------------------------------------------------------------------
[docs]
class Modelo347ThresholdError(AeatError, ValueError):
"""A Modelo 347 contraparte row falls at or below the declarability threshold."""
def __init__(self, *, nif: str, total: Decimal) -> None:
self.nif = nif
self.total = total
super().__init__(
f"M347 contraparte (nif={nif!r}): importe total {total} does not exceed the "
f"{M347_THRESHOLD_EUR} threshold required by RD 1065/2007 art. 33.1",
)
[docs]
class Modelo184ShareSumError(AeatError, ValueError):
"""Modelo 184 member share percentages do not sum to exactly 100%."""
def __init__(self, *, total: Decimal, count: int) -> None:
self.total = total
self.count = count
super().__init__(
f"M184 miembro rows: share percentages must sum to exactly 100%; got {total} across {count} rows",
)
[docs]
def validate_m347_threshold(rows: Sequence[Modelo347ContraparteRow]) -> None:
"""Enforce the Modelo 347 per-counterparty declarability threshold.
RD 1065/2007 art. 33.1: only counterparties whose annual operations exceed
EUR 3,005.06 are declarable. The threshold applies to the SUM of every
operation with the same person (same NIF), aggregated across all contraparte
rows — not to each row in isolation. A counterparty's operations may be split
across several rows (e.g. entregas and adquisiciones), so a per-row check would
wrongly reject a counterparty whose individual rows are each at/below the
threshold while their annual aggregate exceeds it (a missed declaration), and
would never apply the "same person" threshold the regulation defines.
Raises:
Modelo347ThresholdError: for the first counterparty (in NIF first-appearance
order) whose AGGREGATED annual total is at or below the threshold.
"""
totals_by_nif: dict[str, Decimal] = {}
for row in rows:
totals_by_nif[row.nif] = totals_by_nif.get(row.nif, Decimal("0")) + row.importe_total
for nif, total in totals_by_nif.items():
if total <= M347_THRESHOLD_EUR:
raise Modelo347ThresholdError(nif=nif, total=total)
[docs]
def validate_m184_member_share_sum(rows: Sequence[Modelo184MemberRow]) -> None:
"""Enforce that Modelo 184 member share percentages sum to exactly 100%.
Only checked when at least one miembro row is present (partial sets are skipped).
Raises:
Modelo184ShareSumError: when the share percentages do not total exactly 100.
"""
if not rows:
return
total = sum((row.porcentaje for row in rows), Decimal("0"))
if total != Decimal("100"):
raise Modelo184ShareSumError(total=total, count=len(rows))
__all__ = [
"M347_THRESHOLD_EUR",
"Modelo184MemberRow",
"Modelo184ShareSumError",
"Modelo232VinculadaRow",
"Modelo347ContraparteRow",
"Modelo347ThresholdError",
"Modelo349CountryPrefixContextError",
"Modelo349OperadorRow",
"Modelo349RectificacionRow",
"ModeloDetailRow",
"m349_nif_number_for_export",
"validate_m184_member_share_sum",
"validate_m347_threshold",
"validate_m349_country_prefix_context",
"validate_m349_nif_format",
]