import json import time from datetime import datetime, timedelta from dotenv import load_dotenv import logger_utilities import sunat_manager import hana_manager import sapb1_manager from constants import ESTADOS_COMPROBANTE from hana_manager import actualizar_documento_sap # ============================================================================ HORARIOS = [ (6, 30, 13, 15, 10, "rapido"), # 06:30 - 13:15 -> cada 10 segundos (13, 15, 14, 0, 1800, "full"), # 13:15 - 14:00 -> cada 30 minutos (14, 0, 17, 30, 10, "rapido"), # 14:00 - 17:30 -> cada 10 segundos (17, 30, 21, 30, 1800, "full"), # 17:30 - 21:30 -> cada 30 minutos ] # ============================================================================ def obtener_intervalo(dt: datetime) -> tuple[int, str] | None: minutos = dt.hour * 60 + dt.minute for hi, mi, hf, mf, intervalo, modo in HORARIOS: inicio = hi * 60 + mi fin = hf * 60 + mf if inicio <= minutos < fin: return (intervalo, modo) return None # ============================================================================ def segundos_hasta_proximo_inicio(dt: datetime) -> int: inicio = dt.replace(hour=6, minute=30, second=0, microsecond=0) if dt >= inicio: inicio += timedelta(days=1) return int((inicio - dt).total_seconds()) # ============================================================================ def cargar_empresas(): with open("empresas.json", "r", encoding="utf-8") as f: return json.load(f) # ============================================================================ def enviar_mensaje_sl(usercode, doc, resultado, schema): estado_cp = resultado.get("data", {}).get("estadoCp", "") estado_texto = ESTADOS_COMPROBANTE.get(estado_cp, "DESCONOCIDO") subject = f"❌ SUNAT - {doc['Indicator']}-{doc['FolioPref']}-{doc['FolioNum']}" observaciones = resultado.get("data", {}).get("observaciones", [""])[0] or "" text = ( f"El documento {doc['Indicator']}-{doc['FolioPref']}-{doc['FolioNum']} " f"de {schema} entrego el siguiente estado por SUNAT.\n" f"Estado: {estado_texto} (código: {estado_cp})\n" f"RUC Proveedor: {doc.get('LicTradNum', 'N/A')}\n" f"Razón Social: {doc.get('CardName', 'N/A')}\n" f"Mensaje SUNAT: {resultado.get('message', 'N/A')}\n" ) if observaciones: text += f"Observaciones SUNAT: {observaciones}\n" payload = { "MessageDataColumns": [ { "ColumnName": "Numero", "Link": "tNO", "MessageDataLines": [ { "Object":"", "ObjectKey":"", "Value":f"{doc['Indicator']}-{doc['FolioPref']}-{doc['FolioNum']} " } ] }, { "ColumnName": "Documento", "Link": "tYES", "MessageDataLines": [ { "Object": str(doc.get("ObjType", "18")), "ObjectKey": str(doc["DocEntry"]), "Value": str(doc["DocNum"]) } ] }, { "ColumnName": "Razón Social / Nombre", "Link": "tNO", "MessageDataLines": [ { "Object": "", "ObjectKey": "", "Value": f"{doc.get('CardName', 'N/A')}" } ] } ], "RecipientCollection": [ { "SendInternal": "tYES", "UserCode": usercode } ], "Subject": subject, "Text": text } print(f" ┌─ 📩 Enviando Messages a {usercode} ───────────") # for line in json.dumps(payload, indent=2).splitlines(): # print(f" │ {line}") sapb1_manager.send_message(payload) print(f" │ ✅ Mensaje enviado a {usercode}") print(f" └───────────────────────────────────────────────") # ============================================================================ def procesar_empresa(schema, datos_empresa, modo): print(f"\n{'='*60}") print(f"🏢 Procesando empresa: {schema} [{modo}]") print(f"{'='*60}") if modo == "rapido": documentos = hana_manager.obtener_documentos_creados_actualizados_hace_60_minutos(schema, 60) else: documentos = hana_manager.obtener_documentos_fecha_emision_ultimos_8_dias(schema) if not documentos: print(f"⚠️ No se encontraron documentos para {schema}") return 0, 0 ## documentos = hana_manager.obtener_documentos_creados_hace_x_dias(schema, 8) print(f"📄 Documentos encontrados: {len(documentos)}") print(f"🔑 Solicitando token SUNAT...") token = sunat_manager.token(datos_empresa) if not token: print(f"❌ No se obtuvo token para {schema}") return 0, len(documentos) print(f"✅ Token obtenido correctamente") sapb1_manager.login() print(f"✅ Sesión SL establecida para {schema}") ruc_empresa = datos_empresa["Ruc"] procesados = 0 errores = 0 try: for idx, doc in enumerate(documentos, start=1): print(f"\n--- [{idx}/{len(documentos)}] {doc['FolioPref']}-{doc['FolioNum']} ---") try: payload = { "numRuc": doc["LicTradNum"], "codComp": "R1" if doc["Indicator"] == "02" else doc["Indicator"], "numeroSerie": doc["FolioPref"], "numero": doc["FolioNum"], "fechaEmision": doc["TaxDate"], } if doc["FolioPref"] and doc["FolioPref"][0].isalpha(): payload["monto"] = str(doc["DocTotal"]) print(f" ┌─ 📤 Enviando a SUNAT {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}─────────────────────") for line in json.dumps(payload, indent=2).splitlines(): print(f" │ {line}") print(f" └────────────────────────────────────────────") resultado = sunat_manager.validar_comprobante(token, ruc_empresa, payload) print(f" ┌─ 📥 Respuesta de SUNAT {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}───────────────────") for line in json.dumps(resultado, indent=2).splitlines(): print(f" │ {line}") print(f" └────────────────────────────────────────────") registro = { "objtype": str(doc["ObjType"]), "companydb": schema, "fieldvalue": doc["DocEntry"], } actualizar_documento_sap(schema, registro, resultado) if resultado.get("success"): estado_cp = resultado.get("data", {}).get("estadoCp", "") print(f"✅ OK - Estado CP: {estado_cp}") if estado_cp not in ["1","3"] and doc.get("UserCode"): enviar_mensaje_sl(doc["UserCode"], doc, resultado, schema) else: print(f"⚠️ Falló - {resultado.get('message', 'Error desconocido')}") procesados += 1 if idx < len(documentos): time.sleep(2) except Exception as e: print(f"❌ Error procesando documento: {e}") errores += 1 finally: sapb1_manager.logout() print(f"🔒 Sesión SL cerrada para {schema}") return procesados, errores # ============================================================================ def procesar_todas_empresas(inicio_ciclo, modo): empresas = cargar_empresas() print(f"📋 Empresas cargadas: {len(empresas)}") total_procesados = 0 total_errores = 0 for empresa in empresas: schema = empresa["CompanyDB"] if empresa.get("Locked") == "Y": print(f"\n⏭️ Empresa {schema} está bloqueada (LOCKED=Y), saltando...") continue sapb1_manager._sap_credentials = { "company": empresa["CompanyDB"], "username": empresa["UserName"], "password": empresa["Password"], } try: p, e = procesar_empresa(schema, empresa, modo) total_procesados += p total_errores += e except Exception as ex: print(f"❌ Error procesando empresa {schema}: {ex}") total_errores += 1 duracion_ciclo = datetime.now() - inicio_ciclo print(f"\n📊 CICLO COMPLETADO en {str(duracion_ciclo).split('.')[0]}") print(f" ✅ {total_procesados} procesados | ❌ {total_errores} errores") return total_procesados, total_errores # ============================================================================ def main(): log_path = logger_utilities.setup_logger() logger_utilities.limpiar_logs_antiguos(dias=7) load_dotenv(dotenv_path=".env.dev") print("=" * 60) print("🚀 Demonio iniciado") print(f"📄 Log guardado en: {log_path}") print("Servicio iniciado") print("=" * 60) while True: inicio_ciclo = datetime.now() resultado = obtener_intervalo(inicio_ciclo) if resultado is not None: intervalo, modo = resultado print( f"\n[{inicio_ciclo:%Y-%m-%d %H:%M:%S}] " f"Iniciando ciclo [{modo}] (cada {intervalo}s)" ) procesar_todas_empresas(inicio_ciclo, modo) duracion = (datetime.now() - inicio_ciclo).total_seconds() if duracion > intervalo: print( f"⚠️ El ciclo demoró {duracion:.1f}s " f"(intervalo configurado {intervalo}s)" ) dormir = max(0, intervalo - duracion) print(f"💤 Próximo ciclo [{modo}] en {dormir:.0f}s...") else: dormir = segundos_hasta_proximo_inicio(inicio_ciclo) despertar = inicio_ciclo + timedelta(seconds=dormir) print( f"\n[{inicio_ciclo:%Y-%m-%d %H:%M:%S}] " f"Fuera del horario.\n" f"Próxima ejecución: {despertar:%Y-%m-%d %H:%M:%S}" ) time.sleep(dormir) # ============================================================================ if __name__ == "__main__": try: main() except KeyboardInterrupt: print("\n\n🛑 Demonio detenido por el usuario.")