- HORARIOS cubre las 24 horas: se agregan 21:30-24:00 (cada 60 min) y 05:00-06:30 (cada 30 min); antes el demonio dormía hasta las 06:30 en esos tramos. Se corrigen los comentarios, que no coincidían con los valores. tests/test_horarios.py garantiza que no haya huecos ni solapes. - La carpeta de resultados se resuelve junto al código, no relativa al cwd (un servicio de Windows puede arrancar desde cualquier carpeta). - copiar_a_red no crea carpetas locales cuando RUTA_RED_FACTURACION no es una ruta absoluta en el sistema operativo (una UNC fuera de Windows); antes el draft podía referenciar archivos que no estaban en el recurso compartido. - El log muestra la versión del proyecto (pyproject.toml) al arrancar. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
166 lines
6.3 KiB
Python
166 lines
6.3 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, ruta_carpeta
|
|
|
|
|
|
# ============================================================================
|
|
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 []
|
|
output_folder = ruta_carpeta(output_folder) # relativa -> junto al código; absoluta -> tal cual
|
|
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
|
|
"""
|
|
output_folder = ruta_carpeta(output_folder)
|
|
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}")
|