Files
descarga_factura_transporte/persistencia.py
T
GeorgeandClaude Sonnet 5.5 7d2f3212c0 se divide pipeline.py en módulos (adjuntos, documentos, persistencia, draft_sap, notificaciones)
Las funciones anidadas de _procesar_un_correo pasan a funciones de módulo
con parámetros explícitos; pipeline.py queda solo como orquestador.
copiar_a_red lee las variables de entorno al ejecutarse. Se actualizan docs.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-20 22:03:38 -05:00

164 lines
6.1 KiB
Python

"""Persistencia a disco (JSON) de resultados y drafts."""
import os
import re
import json
from datetime import datetime
import sunat_schema
import pdf_utilities
from pipeline_utils import log
# ============================================================================
def guardar_resultado(documentos: list[dict], datos_pdf: list[dict], output_folder: str) -> list[str]:
"""
Guarda los resultados XML y PDF en archivos JSON separados.
El nombre del archivo sigue el patrón
``{tipo}_resultado_{serie}-{numero}_{timestamp}.json`` y se escribe en
``output_folder``.
Args:
documentos: Lista de diccionarios con datos extraídos de XML
(producida por ``procesar_xmls``).
datos_pdf: Lista de diccionarios con datos extraídos de PDF
(producida por ``procesar_pdfs``).
Example:
Si ``documentos[0]`` tiene ``serie="F002", numero="486"``::
guardar_resultado(documentos, pdf_datos, output_folder)
# → Crea xml_resultado_F002-486_2026-07-08_12-00-00.json
# → Crea pdf_resultado_F002-486_2026-07-08_12-00-00.json
"""
if not documentos:
return []
if not os.path.exists(output_folder):
os.makedirs(output_folder, exist_ok=True)
ts = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
serie = documentos[0].get("serie", "unknown")
numero = documentos[0].get("numero", "unknown")
base = f"{serie}-{numero}_{ts}"
saved: list[str] = []
# XML result
xml_name = f"xml_resultado_{base}.json"
xml_path = os.path.join(output_folder, xml_name)
with open(xml_path, "w", encoding="utf-8") as f:
json.dump(documentos, f, ensure_ascii=False, indent=4)
log(f"💾 XML guardado: {xml_name}")
saved.append(xml_path)
# PDF result (normalizado al mismo schema que XML)
pdf_docs = []
for raw in datos_pdf:
if raw.get("error"):
continue
td = (raw.get("tipo_documento") or "").upper()
tipo = pdf_utilities.TIPO_MAP.get(td)
if not tipo:
continue
sn = raw.get("serie_numero") or ""
serie_p, num_p = sunat_schema.split_serie_numero(sn)
if not serie_p:
continue
doc = {
"ruc_emisor": raw.get("ruc_emisor"),
"razon_social_emisor": None, # el PDF no extrae la razón social del emisor
"fecha_emision": raw.get("fecha_emision"),
"tipo": tipo,
"serie": serie_p,
"numero": num_p,
"ruc_receptor": raw.get("ruc_receptor"),
"razon_social_receptor": raw.get("nombre_receptor"),
"total_factura": raw.get("monto_total"),
"documentos_referencia": [],
"tiene_detraccion": False,
"codigo_detraccion": None,
"porcentaje_detraccion": None,
"monto_detraccion": None,
"lineas": [],
"ruta_pdf": raw.get("archivo"),
}
det = raw.get("detraccion") or {}
if det.get("tiene"):
doc["tiene_detraccion"] = True
doc["porcentaje_detraccion"] = det.get("porcentaje")
doc["monto_detraccion"] = det.get("monto")
for item in raw.get("items") or []:
linea = {
"descripcion": item.get("descripcion", ""),
"cantidad": item.get("cantidad"),
"precio_unitario": item.get("precio_unitario"),
"total_linea": item.get("total"),
"guias_remision": [],
"guia_transportista": None,
}
for g in item.get("guias_remision") or []:
if isinstance(g, str):
m = re.search(r"\b([TVE][A-Z0-9]{3,4})\s*-?\s*(\d+)", g)
if m:
linea["guias_remision"].append({
"tipo": "09",
"serie": m.group(1).upper(),
"numero": m.group(2).lstrip("0") or "0",
})
elif isinstance(g, dict):
linea["guias_remision"].append(dict(g))
gt = item.get("guia_transportista")
if gt:
if isinstance(gt, str):
m = re.search(r"\b([TVE][A-Z0-9]{3,4})\s*-?\s*(\d+)", gt)
if m:
linea["guia_transportista"] = {
"tipo": "31",
"serie": m.group(1).upper(),
"numero": m.group(2).lstrip("0") or "0",
}
elif isinstance(gt, dict):
linea["guia_transportista"] = dict(gt)
doc["lineas"].append(linea)
pdf_docs.append(doc)
if pdf_docs:
pdf_name = f"pdf_resultado_{base}.json"
pdf_path = os.path.join(output_folder, pdf_name)
with open(pdf_path, "w", encoding="utf-8") as f:
json.dump(pdf_docs, f, ensure_ascii=False, indent=4)
log(f"💾 PDF guardado: {pdf_name}")
saved.append(pdf_path)
return saved
# ============================================================================
def guardar_draft_json(draft: dict, serie: str, numero: str, output_folder: str) -> None:
"""
Guarda el payload del draft SAP B1 en un archivo JSON para
depuración.
El nombre sigue el patrón
``draft_sapb1_{serie}-{numero}_{timestamp}.json``.
Args:
draft: Payload del draft SAP B1 (producido por
``build_sap_draft``).
serie: Serie de la factura (ej. ``"F002"``).
numero: Número de la factura (ej. ``"486"``).
Example:
Draft de factura F002-486::
guardar_draft_json(draft, "F002", "486", output_folder)
# → Crea draft_sapb1_F002-486_2026-07-08_12-00-00.json
"""
if not os.path.exists(output_folder):
os.makedirs(output_folder, exist_ok=True)
ts = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
filename = f"draft_sapb1_{serie}-{numero}_{ts}.json"
with open(os.path.join(output_folder, filename), "w", encoding="utf-8") as f:
json.dump(draft, f, ensure_ascii=False, indent=4)
log(f"💾 Draft guardado: {filename}")