2026-06-03 18:32:18 -05:00
|
|
|
|
import os
|
2026-06-01 16:31:24 -05:00
|
|
|
|
import sys
|
|
|
|
|
|
import json
|
|
|
|
|
|
import email_utilities
|
|
|
|
|
|
import outlook_utilities
|
|
|
|
|
|
import database_utilities
|
|
|
|
|
|
import logger_utilities
|
2026-06-03 18:32:18 -05:00
|
|
|
|
import sunat_schema
|
2026-06-01 16:31:24 -05:00
|
|
|
|
from datetime import datetime, timedelta
|
|
|
|
|
|
from config_loader import CONFIG
|
|
|
|
|
|
|
|
|
|
|
|
########################################################
|
|
|
|
|
|
def obtener_token():
|
|
|
|
|
|
try:
|
|
|
|
|
|
print("Intentando obtener token de acceso...")
|
|
|
|
|
|
outlook_token = outlook_utilities.get_access_token()
|
|
|
|
|
|
print("✅ Token obtenido exitosamente.")
|
|
|
|
|
|
print(f"Token prefix: {outlook_token[:25]}...")
|
|
|
|
|
|
return outlook_token
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"❌ Error: {e}")
|
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
|
|
########################################################
|
|
|
|
|
|
def descargar_correos(outlook_token, fecha_inicio, fecha_fin):
|
|
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
|
print("\n" + "="*60)
|
|
|
|
|
|
print(f"[MAIL] {CONFIG.client.email}")
|
|
|
|
|
|
|
|
|
|
|
|
emails = outlook_utilities.get_emails(CONFIG.client.email, outlook_token, fecha_inicio, fecha_fin)
|
|
|
|
|
|
print(f"Se encontraron {len(emails)} correos.")
|
|
|
|
|
|
|
|
|
|
|
|
return emails
|
|
|
|
|
|
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
print(f"❌ Error: {e}")
|
|
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
########################################################
|
|
|
|
|
|
def descargar_adjuntos_de_correos(outlook_token, emails, ruta_descarga):
|
|
|
|
|
|
try:
|
2026-06-01 18:05:44 -05:00
|
|
|
|
emails_con_adjuntos = []
|
2026-06-01 16:31:24 -05:00
|
|
|
|
for email in emails:
|
|
|
|
|
|
# Guardar lista original para el log
|
|
|
|
|
|
adjuntos = outlook_utilities.download_attachments(
|
|
|
|
|
|
CONFIG.client.email,
|
|
|
|
|
|
email['id'],
|
|
|
|
|
|
outlook_token,
|
|
|
|
|
|
ruta_descarga)
|
|
|
|
|
|
if adjuntos:
|
2026-06-01 18:05:44 -05:00
|
|
|
|
email['adjuntos'] = adjuntos
|
|
|
|
|
|
emails_con_adjuntos.append(email)
|
2026-06-01 16:31:24 -05:00
|
|
|
|
|
2026-06-01 18:05:44 -05:00
|
|
|
|
return emails_con_adjuntos
|
2026-06-01 16:31:24 -05:00
|
|
|
|
|
|
|
|
|
|
except Exception as ex:
|
|
|
|
|
|
print(f" ❌ Error procesando {email['id'][:10]}: {ex}")
|
|
|
|
|
|
|
|
|
|
|
|
return []
|
|
|
|
|
|
|
2026-06-01 18:05:44 -05:00
|
|
|
|
########################################################
|
|
|
|
|
|
def dejar_mail_si_tiene_adjunto_pdf_o_xml(emails):
|
|
|
|
|
|
"""
|
|
|
|
|
|
Recorre los adjuntos de cada email y elimina los que no tengan extensión pdf o xml
|
|
|
|
|
|
actualiza la lista de adjuntos del correo
|
|
|
|
|
|
si el mail no tiene pdf ni xml también se elimina
|
|
|
|
|
|
"""
|
|
|
|
|
|
emails_con_pdf_o_xml = []
|
|
|
|
|
|
for email in emails:
|
|
|
|
|
|
adjuntos_validos = [adjunto for adjunto in email['adjuntos'] if adjunto.lower().endswith(('.pdf', '.xml'))]
|
|
|
|
|
|
if adjuntos_validos:
|
|
|
|
|
|
email['adjuntos'] = adjuntos_validos
|
|
|
|
|
|
emails_con_pdf_o_xml.append(email)
|
|
|
|
|
|
return emails_con_pdf_o_xml
|
|
|
|
|
|
|
2026-06-01 16:31:24 -05:00
|
|
|
|
########################################################
|
2026-06-03 18:32:18 -05:00
|
|
|
|
def leer_xml_obtener_datos_validos(emails):
|
|
|
|
|
|
json_final = []
|
|
|
|
|
|
for e in emails:
|
|
|
|
|
|
print(f"\n📬 [{e['fecha']}] {e['subject'][:80]}...")
|
|
|
|
|
|
#adjuntos originales
|
|
|
|
|
|
print(f" ├── 📂 Adjuntos originales:")
|
|
|
|
|
|
|
|
|
|
|
|
for idx, a in enumerate(e['adjuntos']):
|
|
|
|
|
|
prefijo = " │ └── " if idx == len(e['adjuntos'])-1 else " │ ├── "
|
|
|
|
|
|
print(f"{prefijo}{a}")
|
|
|
|
|
|
if a.lower().endswith(".xml"):
|
|
|
|
|
|
# procesar archivo xml de SUNAT
|
|
|
|
|
|
carpeta_adjuntos = os.path.join(os.path.dirname(__file__), CONFIG.client.ruta_descarga)
|
|
|
|
|
|
archivo_xml = os.path.join(carpeta_adjuntos, a)
|
|
|
|
|
|
print(f" │ 📄 Procesando XML: {a}")
|
|
|
|
|
|
data = sunat_schema.extraer_datos_xml(archivo_xml)
|
|
|
|
|
|
if data:
|
|
|
|
|
|
print(f" │ ✅ Factura extraída: {data.get('serie')}-{data.get('numero')}")
|
|
|
|
|
|
json_final.append(data)
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(" │ ℹ️ XML sin datos (no es factura tipo 01)")
|
|
|
|
|
|
|
|
|
|
|
|
# opcional: guardar JSON junto al XML
|
|
|
|
|
|
archivo_json = f"ejecucion_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.json"
|
|
|
|
|
|
with open(archivo_json, "w", encoding="utf-8") as f:
|
|
|
|
|
|
json.dump(json_final, f, ensure_ascii=False, indent=4)
|
|
|
|
|
|
|
|
|
|
|
|
return json_final
|
|
|
|
|
|
|
|
|
|
|
|
########################################################
|
|
|
|
|
|
def anidar_guias_en_facturas(json_final):
|
|
|
|
|
|
# obtener solo facturas
|
|
|
|
|
|
facturas = [item for item in json_final if item.get("tipo") == "01"]
|
|
|
|
|
|
|
|
|
|
|
|
# obtener solo guias
|
|
|
|
|
|
guias = [item for item in json_final if item.get("tipo") == "31"]
|
|
|
|
|
|
|
|
|
|
|
|
# recorrer cada factura y buscar guias relacionadas de campo "documento_relacionado" que tenga el mismo numero de serie y numero
|
|
|
|
|
|
for factura in facturas:
|
|
|
|
|
|
# obtener la guia de tipo ='31' de los documemtos_relacionados
|
|
|
|
|
|
guias_transporte = [item for item in factura.get("documentos_referencia", []) if item.get("tipo") == "31"]
|
|
|
|
|
|
|
|
|
|
|
|
# buscar en la lista de guias si existe una guia que tenga el mismo numero de serie y numero que la guia de transporte
|
|
|
|
|
|
for guia in guias_transporte:
|
|
|
|
|
|
guia_encontrada = next((g for g in guias if g.get("serie")== guia.get("serie")
|
|
|
|
|
|
and g.get("numero")== guia.get("numero")), None)
|
|
|
|
|
|
if guia_encontrada:
|
|
|
|
|
|
# si se encuentra, anidar la guia dentro de la factura en un nuevo campo "guia_trasporte"
|
|
|
|
|
|
factura["guia_transporte"] = guia_encontrada
|
|
|
|
|
|
print(f" ├── 📦 Guía {guia_encontrada.get('serie')}-{guia_encontrada.get('numero')} anidada en factura {factura.get('serie')}-{factura.get('numero')}")
|
|
|
|
|
|
else:
|
|
|
|
|
|
print(f" ├── ⚠️ Guía {guia.get('serie')}-{guia.get('numero')} no encontrada para factura {factura.get('serie')}-{factura.get('numero')}")
|
|
|
|
|
|
|
|
|
|
|
|
# opcional: guardar JSON junto al XML
|
|
|
|
|
|
archivo_json = f"anidado_{datetime.now().strftime('%Y-%m-%d_%H-%M-%S')}.json"
|
|
|
|
|
|
with open(archivo_json, "w", encoding="utf-8") as f:
|
|
|
|
|
|
json.dump(facturas, f, ensure_ascii=False, indent=4)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return facturas
|
|
|
|
|
|
########################################################
|
2026-06-01 16:31:24 -05:00
|
|
|
|
def main():
|
|
|
|
|
|
|
|
|
|
|
|
# Inicializar Base de Datos
|
|
|
|
|
|
database_utilities.init_db()
|
|
|
|
|
|
|
|
|
|
|
|
# Calcular fechas
|
|
|
|
|
|
hoy = datetime.now()
|
2026-06-03 18:32:18 -05:00
|
|
|
|
ayer = hoy - timedelta(days=30)
|
2026-06-01 16:31:24 -05:00
|
|
|
|
|
|
|
|
|
|
fecha_inicio = ayer.strftime("%Y-%m-%d")
|
|
|
|
|
|
fecha_fin = hoy.strftime("%Y-%m-%d")
|
|
|
|
|
|
|
|
|
|
|
|
print(f"Rango de búsqueda: {fecha_inicio} al {fecha_fin}")
|
|
|
|
|
|
|
|
|
|
|
|
outlook_token = obtener_token()
|
|
|
|
|
|
# se descarga todos los correos en el rango de fecha ingresado
|
|
|
|
|
|
# tengan o no adjuntos
|
|
|
|
|
|
emails = descargar_correos(outlook_token, fecha_inicio, fecha_fin)
|
|
|
|
|
|
|
|
|
|
|
|
# se verifica si estos correos ya estan en la base de datos
|
|
|
|
|
|
# si ya esta registrado, se quita del listado
|
|
|
|
|
|
# si no esta se inserta en la base de datos
|
|
|
|
|
|
emails = email_utilities.quitar_correos_ya_registrados_en_base_de_datos(emails)
|
|
|
|
|
|
|
2026-06-01 18:05:44 -05:00
|
|
|
|
# descargar adjuntos de cada correo y agregar
|
|
|
|
|
|
emails = descargar_adjuntos_de_correos(outlook_token, emails, CONFIG.client.ruta_descarga)
|
2026-06-01 16:31:24 -05:00
|
|
|
|
|
2026-06-01 18:05:44 -05:00
|
|
|
|
# with open("correos_con_adjuntos.json", "w", encoding="utf-8") as f:
|
|
|
|
|
|
# json.dump(emails, f, ensure_ascii=False, indent=4)
|
2026-06-01 16:31:24 -05:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# si el correo tiene archivos zip, se descomprimen y se agregan a
|
|
|
|
|
|
# la lista de archivos adjuntos
|
2026-06-01 18:05:44 -05:00
|
|
|
|
emails = email_utilities.descomprimir_adjuntos(emails, CONFIG.client.ruta_descarga)
|
|
|
|
|
|
|
|
|
|
|
|
# se eliminan mails que no tengan adjunto pdf o xml
|
|
|
|
|
|
# también se eliminar los adjuntos no que sean xml y pdf
|
|
|
|
|
|
emails = dejar_mail_si_tiene_adjunto_pdf_o_xml(emails)
|
2026-06-01 16:31:24 -05:00
|
|
|
|
|
2026-06-03 18:32:18 -05:00
|
|
|
|
# obtener datos de los archivos xml y generar JSON final
|
|
|
|
|
|
json_final = leer_xml_obtener_datos_validos(emails)
|
|
|
|
|
|
|
|
|
|
|
|
anidar_guias_en_facturas(json_final)
|
2026-06-01 16:31:24 -05:00
|
|
|
|
|
2026-06-01 18:05:44 -05:00
|
|
|
|
|
|
|
|
|
|
|
2026-06-01 16:31:24 -05:00
|
|
|
|
########################################################
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
|
|
|
|
|
|
|
# Configurar Logger (Redirige stdout a archivo y consola)
|
|
|
|
|
|
log_path = logger_utilities.setup_logger()
|
|
|
|
|
|
logger_utilities.limpiar_logs_antiguos(dias=30)
|
|
|
|
|
|
|
|
|
|
|
|
inicio_ejecucion = datetime.now()
|
|
|
|
|
|
print("="*60)
|
|
|
|
|
|
print(f"🚀 INICIO DE EJECUCIÓN: {inicio_ejecucion.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
|
|
|
|
print(f"📄 Log guardado en: {log_path}")
|
|
|
|
|
|
print("="*60)
|
|
|
|
|
|
|
|
|
|
|
|
main()
|
|
|
|
|
|
|
|
|
|
|
|
fin_ejecucion = datetime.now()
|
|
|
|
|
|
duracion = fin_ejecucion - inicio_ejecucion
|
|
|
|
|
|
print("\n" + "="*60)
|
|
|
|
|
|
print(f"🏁 FIN DE EJECUCIÓN: {fin_ejecucion.strftime('%Y-%m-%d %H:%M:%S')}")
|
|
|
|
|
|
print(f"⏱️ Duración total: {str(duracion).split('.')[0]}") # Sin microsegundos
|
|
|
|
|
|
print("="*60)
|