"""Shared base classes and aliases for registry schema models.
The ``SensitivityClassField`` alias coerces registry ``output_sensitivity``
tokens into :class:`SensitivityClass` members before strict schema validation.
``RevisionReviewStatusField`` does the same for the per-revision governance
stamp's ``review_status`` token, and ``RegistryAuthorityGradeField`` for the
per-revision ``authority_grade`` token.
"""
from __future__ import annotations
from collections.abc import Callable, Iterable
from dataclasses import dataclass
from decimal import Decimal
from enum import StrEnum
from types import UnionType
from typing import Annotated, Final, Literal, TypeGuard, Union, get_args, get_origin
from pydantic import BaseModel, BeforeValidator, Field, TypeAdapter, field_validator
from ....core.authority_grade import RegistryAuthorityGrade
from ....core.classification.policies import SensitivityClass
from ....core.errors.hierarchy import pydantic_validation_boundary
from ....core.models import STRICT_FROZEN_CONFIG
from ....core.period import Period, is_administrative_period_token, is_symbolic_event_selector
from ....core.revision_review import RevisionReviewStatus
from .errors import RegistryValidationError
from .ids import LegalRefId, SourceRefId
__all__ = [
"CHAIN_FAMILY",
"GOVERNANCE_STAMP",
"MANIFEST_ONLY",
"NUMERIC_CASILLA_DATA_TYPES",
"REGISTRY_SOURCE_GROUNDING_TIERS",
"SCHEMA_FAMILY",
"ZERO_PADDED_EXPORT_DATA_TYPES",
"CalculationClass",
"CalculationClassField",
"CasillaSignConstraint",
"CasillaSignConstraintField",
"CasillaSignConstraintValue",
"ChainFamilyMarker",
"DateAxis",
"DateAxisField",
"EvidenceTier",
"FormulaOperator",
"GovernanceStampMarker",
"LegalRefs",
"ManifestOnlyMarker",
"ModeloFilingCapabilities",
"ModeloFilingCapability",
"RegistryAuthorityGradeField",
"RegistryModel",
"RevisionReviewStatusField",
"SchemaFamilyMarker",
"SensitivityClassField",
"SourceCitation",
"SourceCitationText",
"SourceRefs",
"chain_family_fields",
"coerce_decimal_tuple",
"coerce_enum_member",
"coerce_enum_tuple",
"collection_shaped_fields",
"filing_period_from_scope",
"governance_stamp_fields",
"is_schema_model_element",
"manifest_only_fields",
"schema_family_fields",
"sorted_unique_capabilities",
]
@pydantic_validation_boundary
def _coerce_sensitivity_class(value: object) -> object:
if isinstance(value, SensitivityClass):
return value
if isinstance(value, str):
return SensitivityClass(value)
return value
SensitivityClassField = Annotated[SensitivityClass, BeforeValidator(_coerce_sensitivity_class)]
@pydantic_validation_boundary
def _coerce_revision_review_status(value: object) -> object:
if isinstance(value, RevisionReviewStatus):
return value
if isinstance(value, str):
return RevisionReviewStatus(value)
return value
RevisionReviewStatusField = Annotated[RevisionReviewStatus, BeforeValidator(_coerce_revision_review_status)]
"""Registry ``review_status`` token coerced into a :class:`RevisionReviewStatus` member.
Registry schema models validate under ``strict=True``, which refuses a bare TOML
string for an enum-typed field, so the governance stamp needs this coercion hop
exactly as ``output_sensitivity`` does. An unknown token raises out of the enum
constructor and surfaces as a registry load failure naming the offending value.
Shared with the legal catalogue rows, which reach the same vocabulary.
"""
@pydantic_validation_boundary
def _coerce_registry_authority_grade(value: object) -> object:
if isinstance(value, RegistryAuthorityGrade):
return value
if isinstance(value, str):
return RegistryAuthorityGrade(value)
return value
RegistryAuthorityGradeField = Annotated[RegistryAuthorityGrade, BeforeValidator(_coerce_registry_authority_grade)]
"""Registry ``authority_grade`` token coerced into a :class:`RegistryAuthorityGrade` member.
The same coercion hop :data:`RevisionReviewStatusField` needs, for the same
reason: registry schema models validate under ``strict=True``, which refuses a
bare TOML string for an enum-typed field. Registry TOML therefore stays
free-form and the loader boundary hydrates the typed member, so an unknown grade
surfaces as a registry load failure naming the offending value rather than
reaching a downstream branch on a string.
Distinct subject from :data:`RevisionReviewStatusField`: that vocabulary records
who signed a revision off, this one records how far the revision's authority
reaches.
"""
#: A parsed TOML tuple/list, typed at the boundary rather than left with the
#: ``Unknown`` element type an ``isinstance(value, (tuple, list))`` narrow of a
#: bare ``object`` carries -- both coercion hops below iterate a validated,
#: fully-typed ``list[object]`` through this one adapter.
_OBJECT_LIST_ADAPTER: TypeAdapter[list[object]] = TypeAdapter(list[object])
[docs]
def coerce_enum_member(enum_cls: type) -> Callable[[object], object]:
"""Return a scalar coercion hop for one enum-typed field.
The generated function is the identical shape as :func:`_coerce_sensitivity_class`
and its siblings above -- registry schema models validate under
``strict=True``, which refuses a bare TOML string for an enum-typed field, so
the loader boundary hydrates the typed member here. An unknown token still
raises out of the enum constructor and surfaces as a registry load failure
naming the offending value.
Factored out because the ledger binding-selector families (IVA, OSS/IOSS,
retenciones) need this same coercion for several enum types each; hand-
duplicating the four-line function per type is the drift risk this project's
architecture rules warn against, not the discipline they ask for.
"""
def _coerce(value: object) -> object:
if isinstance(value, enum_cls):
return value
if isinstance(value, str):
return enum_cls(value)
return value
return _coerce
[docs]
def coerce_enum_tuple(enum_cls: type) -> Callable[[object], object]:
"""Return a ``tuple[enum_cls, ...]`` coercion hop, element-wise.
Same rationale and mechanism as :func:`coerce_enum_member`, applied to each
element of an enum-typed tuple field rather than to a scalar field: strict
mode refuses a bare string for an enum-typed tuple ELEMENT exactly as it
refuses one for a scalar enum field, so each element is coerced
independently and an unknown token still raises out of the enum
constructor unchanged.
"""
def _coerce(value: object) -> object:
if not isinstance(value, (tuple, list)):
return value
items = _OBJECT_LIST_ADAPTER.validate_python(value)
return tuple(
item if isinstance(item, enum_cls) else enum_cls(item) if isinstance(item, str) else item for item in items
)
return _coerce
[docs]
def coerce_decimal_tuple(value: object) -> object:
"""Coerce a ``tuple[Decimal, ...]`` field's elements from TOML string/int tokens.
TOML has no native ``Decimal`` type; committed registry TOML stores a rate
tuple as quoted decimal strings (e.g. ``["0.21"]``) to avoid float
imprecision. Strict mode refuses that bare string for a ``Decimal``-typed
tuple element exactly as it refuses one for any other strict-typed element,
so this is the same coercion-hop mechanism as :func:`coerce_enum_tuple`,
targeting ``Decimal`` instead of an enum. An unparseable token still raises
out of the ``Decimal`` constructor and surfaces as a registry load failure.
"""
if not isinstance(value, (tuple, list)):
return value
items = _OBJECT_LIST_ADAPTER.validate_python(value)
return tuple(
item if isinstance(item, Decimal) else Decimal(item) if isinstance(item, (str, int)) else item for item in items
)
[docs]
@dataclass(frozen=True, slots=True)
class ManifestOnlyMarker:
"""``Annotated`` metadata pinning one field to the revision's own manifest.
A revision compiles from its ``revision.toml`` manifest plus up to several
hundred per-section fragment files, and the merge takes a scalar from
whichever file declares it. A field marked here is refused inside a section
fragment: it is a statement about the WHOLE revision, so it has to be
readable in the one place a reviewer opens, not merged in from a file
thousands deep in a fragment tree where the manifest still reads silent on
it.
Manifest-only-ness is not derivable from a field's annotation - the marked
fields are a free-text attribution, a closed enum, a date and two id tuples,
shapes a dozen unmarked fields on the same model share - so the enrolment
has to be declared. Declaring it *at the field* rather than in a hand-kept
list is the point: the list can be forgotten when a field is added, and it
is the sole input to the loader's placement refusal, so a forgotten entry
silently reopens the laundering route the refusal exists to close.
"""
MANIFEST_ONLY = ManifestOnlyMarker()
"""The singleton marker attached to every manifest-only field declaration."""
[docs]
@dataclass(frozen=True, slots=True)
class GovernanceStampMarker(ManifestOnlyMarker):
"""``Annotated`` metadata enrolling one field into the governance stamp.
Narrower than :class:`ManifestOnlyMarker`, and a subclass of it rather than
a sibling. Governance provenance and legal grounding are different subjects
- one is a claim about who built and signed off a revision, the other is the
law the revision stands on - and only the governance fields belong to the
stamp vocabulary the conformance tooling reads and writes. What they share
is the placement guarantee, and expressing that as a type relation rather
than a second marker on the same fields is what keeps it: a governance field
added tomorrow cannot be manifest-pinned by memory, because it already is
one by construction.
"""
GOVERNANCE_STAMP = GovernanceStampMarker()
"""The singleton marker attached to every governance-stamp field declaration."""
[docs]
@dataclass(frozen=True, slots=True)
class SchemaFamilyMarker:
"""``Annotated`` metadata enrolling one field as a revision schema family.
A family is one of the revision's declared content collections - its
casillas, its formulas, its bindings, its export layouts. Coverage asks one
question of each: is it populated, is it honestly not applicable, or is it
empty for a reason nobody has recorded. The marker is what makes a field
subject to that question.
Unrelated to :class:`ManifestOnlyMarker`, and deliberately not a subclass of
it. Placement asks WHERE a field may be written; enrolment asks WHETHER a
field's emptiness is a coverage claim. The two sets barely intersect: the
manifest-only fields are scalars, and the families are collections. A marker
hierarchy between them would assert a relationship that does not exist.
Family-ness IS derivable from the annotation - a family is exactly a
``tuple`` of a schema model - which is why the marker carries a completeness
check rather than standing alone: :func:`schema_family_fields` reads the
declarations, the check compares that against the shape-derived set, and a
collection added without the marker reds. Marking alone would catch a rename
and never an addition; deriving alone would enrol collections nobody meant
as families. Requiring both is what makes the enrolment exhaustive AND
deliberate.
"""
SCHEMA_FAMILY = SchemaFamilyMarker()
"""The singleton marker attached to every revision schema-family declaration."""
[docs]
class ChainFamilyMarker:
"""``Annotated`` metadata enrolling one field as a chain-statement family.
A chain family holds authored statements about an identifier-keyed family's
inheritance chain across editions - retirements and replacements - rather
than content of the revision itself. Its natural state is empty: an edition
that withdraws nothing declares nothing. Emptiness is therefore resolved by
construction and is never a coverage claim, which is why this marker is
distinct from :class:`SchemaFamilyMarker` and why coverage ignores it. The
loader still discovers the section by shape, and the conformance check
accepts either marker as deliberate enrolment.
"""
CHAIN_FAMILY = ChainFamilyMarker()
"""The singleton marker attached to every chain-statement family declaration."""
[docs]
def chain_family_fields(model: type[BaseModel]) -> frozenset[str]:
"""Return the names of ``model``'s fields marked :data:`CHAIN_FAMILY`."""
return frozenset[str](
name
for name, field in model.model_fields.items()
if any(isinstance(meta, ChainFamilyMarker) for meta in field.metadata)
)
[docs]
def manifest_only_fields(model: type[BaseModel]) -> frozenset[str]:
"""Return the names of ``model``'s fields marked :data:`MANIFEST_ONLY`.
Includes every field marked :data:`GOVERNANCE_STAMP`, since the governance
marker is a :class:`ManifestOnlyMarker`. Reads the marker back out of
pydantic's retained ``Annotated`` metadata, so a field carrying either
marker - including one added by a subclass - enrols itself without any
second list being edited.
"""
return frozenset[str](
name
for name, field in model.model_fields.items()
if any(isinstance(meta, ManifestOnlyMarker) for meta in field.metadata)
)
[docs]
def schema_family_fields(model: type[BaseModel]) -> frozenset[str]:
"""Return the names of ``model``'s fields marked :data:`SCHEMA_FAMILY`.
The declared enrolment. ``collection_shaped_fields`` computes the set
this one is meant to equal; the gate that compares them is what stops a new
collection shipping outside coverage.
"""
return frozenset[str](
name
for name, field in model.model_fields.items()
if any(isinstance(meta, SchemaFamilyMarker) for meta in field.metadata)
)
[docs]
def is_schema_model_element(element: object) -> bool:
"""Return whether a tuple element type is a registry model or a closed union of them.
A family may be a plain model class or an ``Annotated`` discriminated union of model
classes; both are collections of schema models for enrolment and loader purposes.
"""
if isinstance(element, type):
return issubclass(element, BaseModel)
origin = get_origin(element)
if origin is Annotated:
return is_schema_model_element(get_args(element)[0])
if origin in (Union, UnionType):
return all(is_schema_model_element(member) for member in get_args(element))
return False
[docs]
def collection_shaped_fields(model: type[BaseModel]) -> frozenset[str]:
"""Return the names of ``model``'s tuple fields whose element is a schema model."""
shaped: set[str] = set()
for name, field in model.model_fields.items():
if get_origin(field.annotation) is not tuple:
continue
args = get_args(field.annotation)
if args and is_schema_model_element(args[0]):
shaped.add(name)
return frozenset(shaped)
[docs]
def governance_stamp_fields(model: type[BaseModel]) -> frozenset[str]:
"""Return the names of ``model``'s fields marked :data:`GOVERNANCE_STAMP`.
The stamp vocabulary proper, narrower than :func:`manifest_only_fields`: a
field pinned to the manifest for legal-grounding reasons is not part of the
provenance claim and must not be written or read as one.
"""
return frozenset[str](
name
for name, field in model.model_fields.items()
if any(isinstance(meta, GovernanceStampMarker) for meta in field.metadata)
)
[docs]
class CalculationClass(StrEnum):
"""What kind of declaration a modelo is, as an enforcement posture."""
FILING = "filing"
"""A self-assessment the taxpayer settles."""
INFORMATIVE = "informative"
"""An informative declaration carrying no liquidacion."""
SUMMARY = "summary"
"""A summary rolling up period filings without settling on its own."""
CalculationClassField = Annotated[CalculationClass, BeforeValidator(coerce_enum_member(CalculationClass))]
"""Registry ``calculation_class`` token hydrated into a member.
Registry schema models validate strictly, which refuses a bare TOML string for an
enum-typed field, so the token is coerced at the boundary.
"""
ModeloFilingCapability = Literal["borrador", "renta_ledger_default"]
"""Discriminator for the calculation role of a ModeloDefinition.
- ``filing``: The modelo computes and submits filing-grade amounts.
Most modelos fall into this class.
- ``informative``: The modelo collects and reports data but does not
compute filing-grade amounts. Revisions must have empty ``formulas``
and empty ``relations``; every casilla must be ``manual`` or
``informational``. Modelo 232 is the canonical example.
- ``summary``: The modelo aggregates other modelos (e.g. 390 over 303)
and may declare cross-model relations but is not a filing modelo.
"""
def _is_object_iterable(value: object) -> TypeGuard[Iterable[object]]:
"""Narrow an arbitrary iterable to object-valued entries without changing its shape."""
return isinstance(value, Iterable)
[docs]
@pydantic_validation_boundary
def sorted_unique_capabilities(value: object) -> object:
"""Order declared modelo capabilities and refuse a repeated declaration.
A set-shaped container orders its members by hash, which varies between
processes, so the same declaration would serialise and digest differently
from one run to the next. Sorting on construction makes the declared order
irrelevant to the stored value; rejecting duplicates keeps the tuple's
length a truthful count of what was declared.
"""
if isinstance(value, str) or not _is_object_iterable(value):
return value
items = tuple(value)
if not all(isinstance(item, str) for item in items):
return items
if len(set(items)) != len(items):
raise RegistryValidationError("modelo capabilities must be declared at most once each")
string_items = tuple(item for item in items if isinstance(item, str))
return tuple(sorted(string_items))
ModeloFilingCapabilities = Annotated[
tuple[ModeloFilingCapability, ...],
BeforeValidator(sorted_unique_capabilities),
]
"""Declared modelo capabilities, stored sorted so serialisation is deterministic."""
DesignAuthority = Literal["authoritative", "provenance_only"]
"""Whether a bundled record design is a machine-readable authority or provenance.
``authoritative`` is the default and the case for every design a revision may
cite as an export-layout authority. ``provenance_only`` marks a design the
corpus keeps as evidence but which the registry must never treat as a layout
map: the raw BOE ordenes that carry a modelo's design as an annex and so refuse
to parse from wire position 1, and a superseded draft AEAT published alongside a
definitive edition. The distinction has to be declared rather than inferred --
parse failure alone cannot tell "this is provenance" from "this design is
broken", and neither can the absence of a record_design_epoch, which also covers
designs whose selection window is merely not assigned yet.
"""
[docs]
class DateAxis(StrEnum):
"""Which date a registry value or bracket table is keyed to."""
FILING_PERIOD = "filing_period"
"""The period the declaration covers, rather than any single date."""
DEVENGO_DATE = "devengo_date"
"""The accrual date the liability arises on."""
TRANSACTION_DATE = "transaction_date"
"""The date the underlying transaction occurred."""
INVOICE_DATE = "invoice_date"
"""The date the invoice was issued."""
SUBMISSION_DATE = "submission_date"
"""The date the declaration was submitted."""
[docs]
class ThresholdComparison(StrEnum):
"""How a dated value is compared when it is used as a threshold.
A provision can be redrafted so that only the COMPARISON changes: LIVA art.
103.Dos.2 read "exceda en un 20 por 100" until 2014 and "exceda en un 10 por
ciento o mas" from 2015, and the added "o mas" moves the boundary case from
excluded to included. A value alone cannot express that, so two redactions of
one provision could not be two dated values while the operator lived in code.
EXCLUSIVE is the default because it is the semantics every shipped value is
read with today, so declaring the field changes no existing meaning.
"""
EXCLUSIVE = "exclusive"
"""The threshold is met only when the compared amount is strictly beyond it."""
INCLUSIVE = "inclusive"
"""The threshold is met when the compared amount reaches it."""
DateAxisField = Annotated[DateAxis, BeforeValidator(coerce_enum_member(DateAxis))]
ThresholdComparisonField = Annotated[
ThresholdComparison,
BeforeValidator(coerce_enum_member(ThresholdComparison)),
]
"""Registry date-axis token hydrated into a member.
Registry schema models validate strictly, which refuses a bare TOML string for an
enum-typed field, so the token is coerced at the boundary.
"""
[docs]
class EvidenceTier(StrEnum):
"""What kind of authority grounds a registry entity."""
LEGAL_AUTHORITY = "legal_authority"
"""The law itself: a BOE provision or the orden that approves a design."""
OFFICIAL_SOURCE_GUIDANCE = "official_source_guidance"
"""AEAT-published guidance, instructions or a form specification."""
EXECUTABLE_PARITY_EVIDENCE = "executable_parity_evidence"
"""An artefact that can be executed and compared, such as a safe calculator."""
LAYOUT_AUTHORITY = "layout_authority"
"""A published record design fixing field offsets, widths and order."""
EvidenceTierField = Annotated[EvidenceTier, BeforeValidator(coerce_enum_member(EvidenceTier))]
"""Registry ``evidence_tier`` token hydrated into a member.
Registry schema models validate strictly, which refuses a bare TOML string for an
enum-typed field, so the token is coerced at the boundary.
"""
[docs]
class CasillaSignConstraint(StrEnum):
"""Which side of zero a casilla's computed value may fall on.
Declared by the registry and consumed unchanged by the spreadsheet surface that
renders the same restriction as a cell validation rule. The two stated it
separately before, so a member added here would have left the workbook accepting
a value the engine rejects.
"""
ANY = "any"
"""Unrestricted; the casilla admits either sign."""
NON_NEGATIVE = "non_negative"
"""Zero or positive. A negative computed value is a constraint violation."""
NON_POSITIVE = "non_positive"
"""Zero or negative, as for a casilla that only ever records a deduction."""
CasillaSignConstraintField = Annotated[
CasillaSignConstraint, BeforeValidator(coerce_enum_member(CasillaSignConstraint))
]
"""Registry ``sign`` token hydrated into a member.
Registry schema models validate strictly, which refuses a bare TOML string for an
enum-typed field, so the token is coerced at the boundary.
"""
CasillaSignConstraintValue = Literal[
CasillaSignConstraint.ANY,
CasillaSignConstraint.NON_NEGATIVE,
CasillaSignConstraint.NON_POSITIVE,
]
"""The same vocabulary where a coercing annotation may not be used.
A strict payload or operation surface refuses a bare enum for a raw token and forbids
a customised core schema, so those fields take this literal over the members above
rather than restating the tokens.
"""
#: The tiers that ground a registry entity on AEAT-published MATERIAL, as distinct
#: from the law itself and from executable parity artefacts. Three section validators
#: require exactly this pair through ``require_any_source_tier``. Derived from the
#: vocabulary above rather than restated, so a tier added there cannot leave this
#: narrowing silently measuring the old set.
REGISTRY_SOURCE_GROUNDING_TIERS: tuple[EvidenceTier, ...] = (
EvidenceTier.OFFICIAL_SOURCE_GUIDANCE,
EvidenceTier.LAYOUT_AUTHORITY,
)
[docs]
class CorpusTier(StrEnum):
"""How much of a legal instrument the bundled corpus artefact carries."""
FULL_CONSOLIDATED = "full_consolidated"
"""The whole consolidated instrument, not a provision-suffixed extract."""
PROVISION_EXCERPT = "provision_excerpt"
"""A single provision lifted from the instrument."""
CorpusTierField = Annotated[CorpusTier, BeforeValidator(coerce_enum_member(CorpusTier))]
"""Registry ``corpus_tier`` token hydrated into a member.
Registry schema models validate strictly, which refuses a bare TOML string for an
enum-typed field, so the token is coerced at the boundary.
"""
[docs]
class PublishingAuthority(StrEnum):
"""Who published the instrument or artefact a registry row cites."""
BOE = "boe"
"""The Boletin Oficial del Estado, where Spanish law is promulgated."""
AEAT = "aeat"
"""The Agencia Estatal de Administracion Tributaria."""
EU = "eu"
"""A European Union instrument."""
AUTONOMOUS_COMMUNITY = "autonomous_community"
"""An autonomous community exercising its own competence."""
OTHER = "other"
"""A publisher outside the four named above, recorded rather than assumed."""
PublishingAuthorityField = Annotated[PublishingAuthority, BeforeValidator(coerce_enum_member(PublishingAuthority))]
"""Registry ``authority`` token hydrated into a member.
Registry schema models validate strictly, which refuses a bare TOML string for an
enum-typed field, so the token is coerced at the boundary.
"""
[docs]
class SettlementDirection(StrEnum):
"""Which way a declaration's result settles between taxpayer and AEAT."""
INGRESAR = "ingresar"
"""The taxpayer owes: the result is paid in."""
DEVOLVER = "devolver"
"""The AEAT owes: the result is refunded."""
SettlementDirectionField = Annotated[SettlementDirection, BeforeValidator(coerce_enum_member(SettlementDirection))]
"""Registry settlement-direction token hydrated into a member.
Used as a mapping KEY as well as a field. Registry schema models validate strictly,
which refuses a bare TOML string, and a coercion hop on the key type hydrates each
key the same way it hydrates a scalar field.
"""
[docs]
class RegistrySourceKind(StrEnum):
"""What kind of official artefact a source reference cites."""
RECORD_DESIGN = "record_design"
"""A diseno de registro fixing field offsets, widths and order."""
MANUAL_PDF = "manual_pdf"
"""An AEAT manual published as PDF."""
INSTRUCTIONS = "instructions"
"""Filing instructions accompanying a modelo."""
FORM_SPEC = "form_spec"
"""A published specification of the form's own fields."""
DICTIONARY = "dictionary"
"""A data dictionary for an XML or structured submission."""
XSD = "xsd"
"""An XML schema definition."""
SUPPRESSION_NOTICE = "suppression_notice"
"""A notice withdrawing or superseding a previously published artefact."""
RegistrySourceKindField = Annotated[RegistrySourceKind, BeforeValidator(coerce_enum_member(RegistrySourceKind))]
"""Registry source ``kind`` token hydrated into a member.
Registry schema models validate strictly, which refuses a bare TOML string for an
enum-typed field, so the token is coerced at the boundary.
"""
[docs]
class CasillaDataType(StrEnum):
"""The scalar kind a casilla declares, and the root of every narrowing of it."""
DECIMAL = "decimal"
"""A decimal quantity that is not money."""
MONEY = "money"
"""A monetary amount, the default for a casilla."""
INTEGER = "integer"
"""A whole count."""
RATIO = "ratio"
"""A proportion or rate expressed as a fraction."""
TEXT = "text"
"""Free text the taxpayer or AEAT supplies."""
BOOLEAN = "boolean"
"""A declared yes or no."""
NIF = "nif"
"""A Spanish tax identity number."""
YEAR = "year"
"""A four-digit ejercicio."""
PERIOD_CODE = "period_code"
"""A filing-period token."""
COUNTRY_CODE = "country_code"
"""An ISO country code."""
IBAN = "iban"
"""An international bank account number."""
NAME = "name"
"""A party name."""
NIF_IVA = "nif_iva"
"""An intra-community NIF-IVA identifier."""
CCAA_CODE = "ccaa_code"
"""An autonomous-community code."""
PROVINCE_CODE = "province_code"
"""A Spanish province code."""
POSTAL_CODE = "postal_code"
"""A Spanish postal code."""
MUNICIPALITY_CODE = "municipality_code"
"""A municipality code."""
BIC = "bic"
"""A SWIFT business identifier code."""
DATE = "date"
"""A calendar date."""
CasillaDataTypeField = Annotated[CasillaDataType, BeforeValidator(coerce_enum_member(CasillaDataType))]
"""Registry casilla ``data_type`` token hydrated into a member.
Registry schema models validate strictly, which refuses a bare TOML string for an
enum-typed field, so the token is coerced at the boundary.
"""
NUMERIC_CASILLA_DATA_TYPES: Final[frozenset[CasillaDataType]] = frozenset(
{
CasillaDataType.DECIMAL,
CasillaDataType.INTEGER,
CasillaDataType.MONEY,
CasillaDataType.RATIO,
},
)
"""Data types a formula may consume as a scalar operand.
A narrowing of :class:`CasillaDataType`, held as members rather than as tokens so a
member added or renamed cannot leave this set validating the old spelling. A
``StrEnum`` member hashes as its own string, so this set accepts a raw token and a
member alike -- verified against the live enum, not assumed.
Distinct from :data:`ZERO_PADDED_EXPORT_DATA_TYPES`, which excludes ``RATIO``: a ratio
is a scalar a formula can read, and is not written as a zero-padded numeral.
"""
ZERO_PADDED_EXPORT_DATA_TYPES: Final[frozenset[CasillaDataType]] = frozenset(
{
CasillaDataType.DECIMAL,
CasillaDataType.INTEGER,
CasillaDataType.MONEY,
},
)
"""Data types written right-justified and zero-padded in a fixed-width export.
The narrower of the two numeric sets. Keeping both named, rather than one set with an
exception at the ratio, is what stops a later reader assuming they are the same
population.
"""
CasillaDataTypeValue = Literal[
CasillaDataType.DECIMAL,
CasillaDataType.MONEY,
CasillaDataType.INTEGER,
CasillaDataType.RATIO,
CasillaDataType.TEXT,
CasillaDataType.BOOLEAN,
CasillaDataType.NIF,
CasillaDataType.YEAR,
CasillaDataType.PERIOD_CODE,
CasillaDataType.COUNTRY_CODE,
CasillaDataType.IBAN,
CasillaDataType.NAME,
CasillaDataType.NIF_IVA,
CasillaDataType.CCAA_CODE,
CasillaDataType.PROVINCE_CODE,
CasillaDataType.POSTAL_CODE,
CasillaDataType.MUNICIPALITY_CODE,
CasillaDataType.BIC,
CasillaDataType.DATE,
]
"""The same vocabulary in the one form an operation public schema admits.
An operation model graph must not customise its Pydantic core schema, which the
coercing field alias does, and a bare enum under strict validation refuses the plain
token every caller sends. A literal over the enum's own members satisfies both: it
carries no core-schema hook and it still accepts the token. Rooted in the vocabulary
above rather than restated, so a member added there reaches this form too.
"""
LegalRefs = Annotated[tuple[LegalRefId, ...], Field(min_length=1)]
SourceRefs = Annotated[tuple[SourceRefId, ...], Field(min_length=1)]
SourceCitationText = Annotated[tuple[str, ...], Field(min_length=1)]
FormulaOperator = Literal[
"add",
"subtract",
"multiply",
"divide",
"percent",
"less_than",
"less_equal",
"greater_than",
"greater_equal",
"equal",
"sum",
"min",
"max",
"clamp",
"negate",
"copy",
"if_then_else",
"lookup_parameter",
"lookup_bracket",
"lookup_bracket_by_ccaa",
"m100_resolve_renta_inmobiliaria_imputada",
"irnr_resolve_tipo_gravamen",
"m210_resolve_base_imponible",
"lookup_parameter_by_entity_type",
"lookup_bracket_by_entity_type",
"previous_period_value",
"previous_period_sum",
"cross_model_sum",
"age_at_year_end",
"m131_resolve_modulos_previo",
"m131_resolve_modulos_minoracion_empleo",
"m131_resolve_modulos_indice_exceso",
"m131_resolve_modulos_indices_generales",
"m131_resolve_modulos_pequena_dimension_ignorado_flag",
"m131_resolve_modulos_temporada_inicio_conflicto_flag",
"m100_resolve_eo_agraria_indices_correctores",
]
[docs]
class RegistryModel(BaseModel):
"""Strict frozen base for registry schema objects."""
model_config = STRICT_FROZEN_CONFIG
[docs]
class SourceCitation(RegistryModel):
"""A source reference paired with the exact text required to ground it."""
source_ref: SourceRefId
required_text: SourceCitationText
@field_validator("required_text")
@classmethod
@pydantic_validation_boundary
def _required_text_non_empty(cls, value: tuple[str, ...]) -> tuple[str, ...]:
if any(not item.strip() for item in value):
raise RegistryValidationError("source citation required_text entries must be non-empty")
if len(set(value)) != len(value):
raise RegistryValidationError("source citation required_text entries must be unique")
return value
[docs]
def filing_period_from_scope(filing_year: int, period: str) -> Period | None:
"""Return a core :class:`Period` when the registry token is a real filing-period code.
An administrative registry token (Modelo 036 ``ALTA``, Modelo 145
``comunicacion``) addresses a revision rather than naming a period, and the
symbolic ``EVENT-N`` selector (Modelo 210) covers a set of event periods
without being one, so neither has a filing period and both yield ``None``. Every other token must build a
:class:`Period`; a malformed token or out-of-range year still raises.
Raises:
PeriodError: When a non-administrative token cannot build a period.
"""
if is_administrative_period_token(period) or is_symbolic_event_selector(period):
return None
return Period.from_year_and_code(filing_year, period)