Source code for cadrumo.domain.justificante.schema
"""Strict pydantic v2 schema for parsed AEAT justificantes.The :class:`Justificante` record is the boundary-crossing type consumedby downstream subpackages (the submission engine and the statusreader). It is *frozen* and *strict* so callers can rely ondeterministic field types, and so mutating it after parse requires anexplicit :meth:`pydantic.BaseModel.model_copy`."""from__future__importannotationsfromdatetimeimportdatetimefromdecimalimportDecimalfromenumimportStrEnumfrompathlibimportPathfrompydanticimportAnyHttpUrl,BaseModel,Field,ValidationInfo,field_validatorfrom...core.errors.hierarchyimportpydantic_validation_boundaryfrom...core.identity.aeat_csvimportAeatCsvfrom...core.identity.aeat_presentationimportAeatPresentationIdfrom...core.identity.digestimportContentDigestfrom...core.identity.tax_idimportsame_tax_identifierfrom...core.modelsimportSTRICT_FROZEN_CONFIGfrom...core.periodimportPeriod,PeriodErrorfrom...core.time.utcimportUtcInstantfrom..calculations.registry.tax_id_formatimportSubjectTaxId
[docs]classJustificanteParserBackend(StrEnum):"""Closed set of supported parser backends. Attributes: PDFPLUMBER: Fidelity-first default backend. """PDFPLUMBER="PDFPLUMBER"
[docs]classJustificante(BaseModel):"""Parsed AEAT *justificante de presentación* receipt. A ``Justificante`` represents a single successful filing receipt produced by AEAT after a modelo has been submitted. Every field is either pulled verbatim from the PDF body or derived deterministically from the source file (``source_pdf_sha256``, ``parsed_at``). Attributes: csv: Código Seguro de Verificación — the short AEAT-assigned hash used to verify the document on the Sede electrónica. modelo: String ID of the modelo the receipt belongs to. References the modelo catalogue in :mod:`domain.modelos`. period: Typed filing period resolved from the AEAT period token printed on the receipt and ``ejercicio``. ejercicio: Four-digit tax year as printed on the receipt, when present. ``None`` for receipts that omit the label. presentation_id: AEAT's internal ``Número de justificante`` if present on the receipt; ``None`` when the modelo does not print a separate presentation ID. presented_at: Timestamp AEAT stamped on the receipt at submission. tax_id: NIF/NIE of the taxpayer who filed (the *autónomo* owner). total_a_ingresar: Amount to be paid in, if the receipt includes one. total_a_devolver: Amount to be refunded, if the receipt includes one. verification_url: AEAT URL printed on the receipt where the CSV can be re-verified against the Sede electrónica. source_pdf_path: Privacy-preserving source reference derived from the source PDF digest. source_pdf_sha256: Lowercase hex sha-256 of the source PDF bytes. parsed_at: UTC wall-clock time the parse finished. """model_config=STRICT_FROZEN_CONFIGcsv:AeatCsvmodelo:str=Field(...,min_length=1,max_length=16)ejercicio:str|None=Field(default=None,max_length=8)period:Periodpresentation_id:AeatPresentationId|None=Nonepresented_at:datetime"""Naive by design: the receipt prints a Europe/Madrid wall-clock time. AEAT stamps the local time on the justificante without an offset, and the extractor reads it with ``strptime``, so no instant is available to validate. Typing this as a UTC instant refuses every real receipt -- which is what it did until this line was corrected -- and inventing an offset here would be guessing at a timezone the document never states. """tax_id:SubjectTaxIdtotal_a_ingresar:Decimal|None=Nonetotal_a_devolver:Decimal|None=Noneverification_url:AnyHttpUrlsource_pdf_path:Pathsource_pdf_sha256:ContentDigestparsed_at:UtcInstant@field_validator("period",mode="before")@classmethod@pydantic_validation_boundarydef_coerce_printed_period(cls,raw_period:object,info:ValidationInfo)->object:ejercicio=info.data.get("ejercicio")ifnotisinstance(raw_period,str):returnraw_periodifnotisinstance(ejercicio,str)ornotejercicio.isdigit():returnraw_periodtry:returnPeriod.from_year_and_code(int(ejercicio),raw_period)exceptPeriodError:returnraw_period
[docs]defmatches_filing_target(self,*,modelo:str,filing_year:int,period:Period,tax_id:str|None=None,)->bool:"""Return whether this receipt belongs to one filing target. ``tax_id`` is an optional refinement for receipt sources that do not expose that axis. Verifying that the receipt is the one AEAT issued for a filing is the caller's job, and it is a :attr:`csv` comparison against a csv the caller obtained from somewhere other than this PDF. This predicate deliberately offers no parameter for that axis: the only receipt identifier printed beside the csv is :attr:`presentation_id`, AEAT's *Número de justificante*, and the values callers actually hold are register-issued expediente ids from *Consultar declaraciones presentadas* — a different AEAT namespace that never appears on a receipt body, so no caller could populate such a parameter correctly. """return(self.modelo.strip()==modelo.strip()andstr(self.ejercicioor"").strip()==str(filing_year)andself.period==periodand(tax_idisNoneorsame_tax_identifier(self.tax_id,tax_id)))