Compare commits
6
Commits
3ec141c752
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9f2d96e64c | ||
|
|
22aec163d2 | ||
|
|
da23e99690 | ||
|
|
1356646abf | ||
|
|
cfb6e5ce52 | ||
|
|
fb54dbb281 |
No files matched your search
@@ -0,0 +1,9 @@
|
||||
validar-venv/
|
||||
__pycache__/
|
||||
logs/
|
||||
.git/
|
||||
.gitignore
|
||||
.env
|
||||
.env.*
|
||||
.DS_Store
|
||||
test/
|
||||
+2
-5
@@ -1,12 +1,9 @@
|
||||
__pycache__/
|
||||
.venv/
|
||||
venv/
|
||||
env/
|
||||
validar-venv/
|
||||
logs/
|
||||
.env.dev
|
||||
.env
|
||||
.DS
|
||||
.env
|
||||
.env.*
|
||||
.DS_Store
|
||||
.vscode/
|
||||
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
FROM python:3.12-slim
|
||||
|
||||
ENV PYTHONUNBUFFERED=1 \
|
||||
TZ=America/Lima
|
||||
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends tzdata \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY requirements.txt .
|
||||
RUN pip install --upgrade pip --no-cache-dir && pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
COPY . .
|
||||
|
||||
CMD ["python", "main.py"]
|
||||
@@ -0,0 +1,12 @@
|
||||
services:
|
||||
validador:
|
||||
build: .
|
||||
container_name: validador_documentos
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- TZ=America/Lima
|
||||
volumes:
|
||||
- ./.env:/app/.env:ro
|
||||
- ./scheduler.json:/app/scheduler.json:ro
|
||||
- ./empresas.json:/app/empresas.json:ro
|
||||
- ./logs:/app/logs
|
||||
+4
-4
@@ -19,8 +19,8 @@
|
||||
},
|
||||
{
|
||||
"CompanyDB": "SBO_CIRUGIA",
|
||||
"Client_ID": "21d41835-0289-4b3c-99c0-ff0591d2233d",
|
||||
"Client_Secret": "hzKoLbgjRqLUmNhsLagb7w==",
|
||||
"Client_ID": "258001f9-ed7b-4f93-84da-7b113a269401",
|
||||
"Client_Secret": "Y2YfcAB/wWcS4Cln/Rth5w==",
|
||||
"Ruc": "20521486857",
|
||||
"Locked": "N",
|
||||
"UserName": "manager",
|
||||
@@ -37,8 +37,8 @@
|
||||
},
|
||||
{
|
||||
"CompanyDB": "SBO_CALICAY",
|
||||
"Client_ID": "d7ceb759-b6dc-4d97-a0c7-42bfc5226c53",
|
||||
"Client_Secret": "MIjVHUtvqEIeE06M35nOuQ==",
|
||||
"Client_ID": "66419990-c40b-4ed4-b423-c857039c955c",
|
||||
"Client_Secret": "FWXTRHpUv+DeAif3hxxn+w==",
|
||||
"Ruc": "20611487143",
|
||||
"Locked": "N",
|
||||
"UserName": "manager",
|
||||
|
||||
+21
-22
@@ -8,13 +8,16 @@ from hdbcli import dbapi
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from constants import TABLAS, ESTADOS_COMPROBANTE, ESTADOS_CONTRIBUYENTE, CONDICIONES_DOMICILIADO
|
||||
from logger_manager import get_logger
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
# ============================================================================
|
||||
# CONFIGURACIÓN
|
||||
# ============================================================================
|
||||
|
||||
# Cargar variables de entorno desde .env
|
||||
load_dotenv(dotenv_path=".env.dev")
|
||||
load_dotenv(dotenv_path=".env")
|
||||
|
||||
# Configuración de la API
|
||||
HANA_HOST = os.getenv("HANA_HOST", "")
|
||||
@@ -34,11 +37,11 @@ def get_connection():
|
||||
)
|
||||
return conn
|
||||
except Exception as e:
|
||||
print(f" ❌ Error conectando a HANA: {e}")
|
||||
logger.error(f" ❌ Error conectando a HANA: {e}")
|
||||
return None
|
||||
|
||||
# ============================================================================
|
||||
def obtener_documentos_creados_actualizados_hace_60_minutos(schema, minutes=60):
|
||||
def obtener_documentos_aun_no_validados_ultimos_30_dias(schema):
|
||||
"""
|
||||
Se conecta a la base de datos SAP HANA, realiza una consulta a la tabla OPCH,
|
||||
y formatea el resultado para que coincida con el esquema de documentos.json.
|
||||
@@ -60,11 +63,7 @@ def obtener_documentos_creados_actualizados_hace_60_minutos(schema, minutes=60):
|
||||
FROM "{schema}".OPCH T0
|
||||
INNER JOIN "{schema}".OCRD T3 ON T3."CardCode" = T0."CardCode"
|
||||
INNER JOIN "{schema}".OUSR T1 ON T1."USERID" = T0."UserSign2"
|
||||
WHERE TO_TIMESTAMP(TO_VARCHAR(T0."UpdateDate", 'YYYY-MM-DD')
|
||||
|| ' ' || LPAD(TO_VARCHAR(T0."UpdateTS"), 6, '0'),
|
||||
'YYYY-MM-DD HH24MISS')
|
||||
>= ADD_SECONDS(CURRENT_TIMESTAMP, {-1 * minutes * 60})
|
||||
|
||||
WHERE T0."UpdateDate" >= ADD_DAYS(CURRENT_DATE, -30)
|
||||
AND T0."CANCELED" = 'N'
|
||||
AND IFNULL(T0."Indicator",'') IN ('01','02','03','08')
|
||||
AND LENGTH(IFNULL(T0."FolioPref",'')) > 0
|
||||
@@ -73,7 +72,7 @@ def obtener_documentos_creados_actualizados_hace_60_minutos(schema, minutes=60):
|
||||
AND (LENGTH(IFNULL(T0."U_PGX_STMENSAJE",'')) = 0 OR LOCATE('NO EXISTE', IFNULL(T0."U_PGX_STMENSAJE",'')) > 0)
|
||||
ORDER BY T0."CreateDate" DESC;
|
||||
"""
|
||||
print(f"🔌 Conectando al servidor SAP HANA ({HANA_HOST}:{HANA_PORT}) para schema {schema}...")
|
||||
logger.info(f"🔌 Conectando al servidor SAP HANA ({HANA_HOST}:{HANA_PORT}) para schema {schema}...")
|
||||
connection = get_connection()
|
||||
if not connection:
|
||||
raise ConnectionError("No se pudo establecer la conexión a SAP HANA.")
|
||||
@@ -94,14 +93,14 @@ def obtener_documentos_creados_actualizados_hace_60_minutos(schema, minutes=60):
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ Error en consulta HANA: {e}")
|
||||
logger.error(f" ❌ Error en consulta HANA: {e}")
|
||||
return []
|
||||
finally:
|
||||
if cursor:
|
||||
cursor.close()
|
||||
if connection:
|
||||
connection.close()
|
||||
print(f" 🔒 [{sys._getframe().f_code.co_name}] Conexión HANA cerrada.")
|
||||
logger.info(f" 🔒 [{sys._getframe().f_code.co_name}] Conexión HANA cerrada.")
|
||||
|
||||
# ============================================================================
|
||||
def obtener_documentos_fecha_emision_ultimos_8_dias(schema):
|
||||
@@ -129,7 +128,7 @@ def obtener_documentos_fecha_emision_ultimos_8_dias(schema):
|
||||
AND IFNULL(T3."U_EXX_TIPODOCU",'') IN ('6','4')
|
||||
ORDER BY T0."U_PGX_STMENSAJE", T0."CreateDate" DESC;
|
||||
"""
|
||||
print(f"🔌 Conectando al servidor SAP HANA ({HANA_HOST}:{HANA_PORT}) para schema {schema}...")
|
||||
logger.info(f"🔌 Conectando al servidor SAP HANA ({HANA_HOST}:{HANA_PORT}) para schema {schema}...")
|
||||
connection = get_connection()
|
||||
if not connection:
|
||||
raise ConnectionError("No se pudo establecer la conexión a SAP HANA.")
|
||||
@@ -148,14 +147,14 @@ def obtener_documentos_fecha_emision_ultimos_8_dias(schema):
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ Error en consulta HANA: {e}")
|
||||
logger.error(f" ❌ Error en consulta HANA: {e}")
|
||||
return []
|
||||
finally:
|
||||
if cursor:
|
||||
cursor.close()
|
||||
if connection:
|
||||
connection.close()
|
||||
print(f" 🔒 [{sys._getframe().f_code.co_name}] Conexión HANA cerrada.")
|
||||
logger.info(f" 🔒 [{sys._getframe().f_code.co_name}] Conexión HANA cerrada.")
|
||||
|
||||
# ============================================================================
|
||||
def obtener_documentos_creados_hace_x_dias(schema, dias=8):
|
||||
@@ -184,7 +183,7 @@ def obtener_documentos_creados_hace_x_dias(schema, dias=8):
|
||||
AND IFNULL(T3."U_EXX_TIPODOCU",'') IN ('6','4')
|
||||
ORDER BY T0."CreateDate" DESC;
|
||||
"""
|
||||
print(f"🔌 Conectando al servidor SAP HANA ({HANA_HOST}:{HANA_PORT}) para schema {schema}...")
|
||||
logger.info(f"🔌 Conectando al servidor SAP HANA ({HANA_HOST}:{HANA_PORT}) para schema {schema}...")
|
||||
connection = get_connection()
|
||||
if not connection:
|
||||
raise ConnectionError("No se pudo establecer la conexión a SAP HANA.")
|
||||
@@ -203,14 +202,14 @@ def obtener_documentos_creados_hace_x_dias(schema, dias=8):
|
||||
return results
|
||||
|
||||
except Exception as e:
|
||||
print(f" ❌ Error en consulta HANA: {e}")
|
||||
logger.error(f" ❌ Error en consulta HANA: {e}")
|
||||
return []
|
||||
finally:
|
||||
if cursor:
|
||||
cursor.close()
|
||||
if connection:
|
||||
connection.close()
|
||||
print(f" 🔒 [{sys._getframe().f_code.co_name}] Conexión HANA cerrada.")
|
||||
logger.info(f" 🔒 [{sys._getframe().f_code.co_name}] Conexión HANA cerrada.")
|
||||
|
||||
# ============================================================================
|
||||
def actualizar_documento_sap(schema, registro,resultado):
|
||||
@@ -263,10 +262,10 @@ def actualizar_documento_sap(schema, registro,resultado):
|
||||
"U_PGX_PETOTORHORA" = TO_VARCHAR(CURRENT_TIME,'HH24:MM:SS')
|
||||
WHERE "DocEntry" = {registro["fieldvalue"]};"""
|
||||
|
||||
print(f" ┌─ 🗄️ UPDATE HANA ──────────────────────")
|
||||
logger.info(f" ┌─ 🗄️ UPDATE HANA ──────────────────────")
|
||||
for line in query.splitlines():
|
||||
print(f" │ {line}")
|
||||
print(f" └─────────────────────────────────────────")
|
||||
logger.info(f" │ {line}")
|
||||
logger.info(f" └─────────────────────────────────────────")
|
||||
|
||||
try:
|
||||
cursor.execute(query)
|
||||
@@ -274,13 +273,13 @@ def actualizar_documento_sap(schema, registro,resultado):
|
||||
cursor.close()
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f" ❌ Error actualizando registro en HANA: {e}")
|
||||
logger.error(f" ❌ Error actualizando registro en HANA: {e}")
|
||||
return False
|
||||
finally:
|
||||
if cursor:
|
||||
cursor.close()
|
||||
if connection:
|
||||
connection.close()
|
||||
print(f" 🔒 [{sys._getframe().f_code.co_name}] Conexión HANA cerrada.")
|
||||
logger.info(f" 🔒 [{sys._getframe().f_code.co_name}] Conexión HANA cerrada.")
|
||||
|
||||
# ============================================================================
|
||||
@@ -0,0 +1,116 @@
|
||||
import logging
|
||||
import contextvars
|
||||
import os
|
||||
from contextlib import contextmanager
|
||||
from logging.handlers import TimedRotatingFileHandler
|
||||
from pathlib import Path
|
||||
|
||||
LOG_DIR = Path(__file__).parent / "logs"
|
||||
LOG_FILE = LOG_DIR / "validador-documentos.log"
|
||||
_LOG_CONFIGURED = False
|
||||
|
||||
_stack: contextvars.ContextVar[list] = contextvars.ContextVar("log_stack", default=[])
|
||||
# ============================================================================
|
||||
|
||||
class TreeFormatter(logging.Formatter):
|
||||
def format(self, record):
|
||||
return f"{self.formatTime(record)} ─── {record.getMessage()}"
|
||||
|
||||
def formatTime(self, record, datefmt=None):
|
||||
import datetime
|
||||
return datetime.datetime.fromtimestamp(record.created).strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# ============================================================================
|
||||
def _build_prefix(stack, is_last):
|
||||
if not stack:
|
||||
return ""
|
||||
parts = []
|
||||
for sib_is_last in stack[:-1]:
|
||||
parts.append(" " if sib_is_last else "│ ")
|
||||
parts.append("└── " if is_last else "├── ")
|
||||
return "".join(parts)
|
||||
|
||||
# ============================================================================
|
||||
class _TaskContext:
|
||||
def __init__(self, logger, msg, emoji, is_last):
|
||||
self._logger = logger
|
||||
self._msg = msg
|
||||
self._emoji = emoji
|
||||
self._is_last = is_last
|
||||
|
||||
def __enter__(self):
|
||||
stack = _stack.get()
|
||||
prefix = _build_prefix(stack, self._is_last)
|
||||
emoji_str = f"{self._emoji} " if self._emoji else ""
|
||||
self._logger._logger.info(f"{prefix}{emoji_str}{self._msg}")
|
||||
_stack.set(stack + [self._is_last])
|
||||
return TaskLogger(self._logger._logger)
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
_stack.set(_stack.get()[:-1])
|
||||
|
||||
# ============================================================================
|
||||
class TaskLogger:
|
||||
def __init__(self, logger):
|
||||
self._logger = logger
|
||||
|
||||
@contextmanager
|
||||
def task(self, msg, emoji=""):
|
||||
with _TaskContext(self, msg, emoji, is_last=False):
|
||||
yield
|
||||
|
||||
@contextmanager
|
||||
def last_task(self, msg, emoji=""):
|
||||
with _TaskContext(self, msg, emoji, is_last=True):
|
||||
yield
|
||||
|
||||
def info(self, msg, *args, _last=False, **kwargs):
|
||||
self._log(logging.INFO, msg, args, kwargs, _last)
|
||||
|
||||
def warning(self, msg, *args, _last=False, **kwargs):
|
||||
self._log(logging.WARNING, msg, args, kwargs, _last)
|
||||
|
||||
def error(self, msg, *args, _last=False, **kwargs):
|
||||
self._log(logging.ERROR, msg, args, kwargs, _last)
|
||||
|
||||
def _log(self, level, msg, args, kwargs, is_last):
|
||||
stack = _stack.get()
|
||||
prefix = _build_prefix(stack, is_last)
|
||||
self._logger.log(level, f"{prefix}{msg}", *args, **kwargs)
|
||||
|
||||
# ============================================================================
|
||||
def setup_logging(level=None, console=True):
|
||||
global _LOG_CONFIGURED
|
||||
if _LOG_CONFIGURED:
|
||||
return
|
||||
|
||||
if level is None:
|
||||
level_name = os.getenv("LOG_LEVEL", "INFO").upper()
|
||||
level = getattr(logging, level_name, logging.INFO)
|
||||
|
||||
LOG_DIR.mkdir(exist_ok=True)
|
||||
|
||||
formatter = TreeFormatter()
|
||||
|
||||
file_handler = TimedRotatingFileHandler(
|
||||
LOG_FILE,
|
||||
when="midnight",
|
||||
interval=1,
|
||||
backupCount=30,
|
||||
encoding="utf-8",
|
||||
)
|
||||
file_handler.setFormatter(formatter)
|
||||
|
||||
handlers = [file_handler]
|
||||
if console:
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setFormatter(formatter)
|
||||
handlers.append(console_handler)
|
||||
|
||||
logging.basicConfig(level=level, handlers=handlers, force=True)
|
||||
_LOG_CONFIGURED = True
|
||||
|
||||
# ============================================================================
|
||||
def get_logger(name="validador-documentos"):
|
||||
setup_logging()
|
||||
return TaskLogger(logging.getLogger(name))
|
||||
@@ -1,93 +0,0 @@
|
||||
import sys
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
_stdout_original = sys.stdout
|
||||
_stderr_original = sys.stderr
|
||||
_file_stream = None
|
||||
_log_dir = "logs"
|
||||
|
||||
# ============================================================================
|
||||
class MultiStream:
|
||||
def __init__(self, *streams):
|
||||
self.streams = list(streams)
|
||||
|
||||
def write(self, data):
|
||||
for stream in self.streams:
|
||||
stream.write(data)
|
||||
stream.flush()
|
||||
|
||||
def flush(self):
|
||||
for stream in self.streams:
|
||||
stream.flush()
|
||||
|
||||
def reemplazar_file(self, old_fh, new_fh):
|
||||
for i, s in enumerate(self.streams):
|
||||
if s is old_fh:
|
||||
self.streams[i] = new_fh
|
||||
return
|
||||
|
||||
# ============================================================================
|
||||
def _ruta_log(hoy=None):
|
||||
if hoy is None:
|
||||
hoy = datetime.now().strftime("%Y-%m-%d")
|
||||
if not os.path.exists(_log_dir):
|
||||
os.makedirs(_log_dir)
|
||||
return os.path.join(_log_dir, f"{hoy}.log")
|
||||
|
||||
# ============================================================================
|
||||
def setup_logger():
|
||||
global _file_stream
|
||||
log_path = _ruta_log()
|
||||
_file_stream = open(log_path, "a", encoding="utf-8")
|
||||
sys.stdout = MultiStream(_stdout_original, _file_stream)
|
||||
sys.stderr = MultiStream(_stderr_original, _file_stream)
|
||||
return log_path
|
||||
|
||||
# ============================================================================
|
||||
def rotar_log():
|
||||
global _file_stream
|
||||
hoy = datetime.now().strftime("%Y-%m-%d")
|
||||
log_path = _ruta_log(hoy)
|
||||
|
||||
if _file_stream and os.path.abspath(_file_stream.name) == os.path.abspath(log_path):
|
||||
return
|
||||
|
||||
nuevo_fh = open(log_path, "a", encoding="utf-8")
|
||||
if isinstance(sys.stdout, MultiStream):
|
||||
sys.stdout.reemplazar_file(_file_stream, nuevo_fh)
|
||||
if isinstance(sys.stderr, MultiStream):
|
||||
sys.stderr.reemplazar_file(_file_stream, nuevo_fh)
|
||||
try:
|
||||
_file_stream.close()
|
||||
except Exception:
|
||||
pass
|
||||
_file_stream = nuevo_fh
|
||||
print(f"📄 Log rotado a: {log_path}")
|
||||
|
||||
# ============================================================================
|
||||
def limpiar_logs_antiguos(dias=7):
|
||||
"""
|
||||
Elimina archivos de log con más de 'n' días de antigüedad.
|
||||
"""
|
||||
log_dir = "logs"
|
||||
if not os.path.exists(log_dir):
|
||||
return
|
||||
|
||||
limite = datetime.now() - timedelta(days=dias)
|
||||
count = 0
|
||||
|
||||
for archivo in os.listdir(log_dir):
|
||||
ruta_archivo = os.path.join(log_dir, archivo)
|
||||
if os.path.isfile(ruta_archivo) and archivo.endswith(".log"):
|
||||
fecha_archivo = datetime.fromtimestamp(os.path.getmtime(ruta_archivo))
|
||||
if fecha_archivo < limite:
|
||||
try:
|
||||
os.remove(ruta_archivo)
|
||||
count += 1
|
||||
except Exception as e:
|
||||
print(f"⚠️ Error al eliminar log {archivo}: {e}")
|
||||
|
||||
if count > 0:
|
||||
print(f"🧹 Se eliminaron {count} archivos de log antiguos (más de {dias} días).")
|
||||
# ============================================================================
|
||||
@@ -3,27 +3,43 @@ import time
|
||||
from datetime import datetime, timedelta
|
||||
from dotenv import load_dotenv
|
||||
|
||||
import logger_utilities
|
||||
from logger_manager import get_logger
|
||||
import sunat_manager
|
||||
import hana_manager
|
||||
import sapb1_manager
|
||||
from constants import ESTADOS_COMPROBANTE
|
||||
from hana_manager import actualizar_documento_sap
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
# ============================================================================
|
||||
|
||||
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
|
||||
]
|
||||
config = {}
|
||||
|
||||
|
||||
# ============================================================================
|
||||
def cargar_programacion() -> dict:
|
||||
global config
|
||||
with open("scheduler.json", "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
data["HORARIOS"] = [tuple(h) for h in data["HORARIOS"]]
|
||||
config = data
|
||||
return config
|
||||
|
||||
|
||||
def obtener_horarios() -> list:
|
||||
return config.get("HORARIOS", [])
|
||||
|
||||
|
||||
def obtener_dias_activos() -> list:
|
||||
return config.get("DIAS_ACTIVOS", [])
|
||||
|
||||
|
||||
# ============================================================================
|
||||
def obtener_intervalo(dt: datetime) -> tuple[int, str] | None:
|
||||
minutos = dt.hour * 60 + dt.minute
|
||||
|
||||
for hi, mi, hf, mf, intervalo, modo in HORARIOS:
|
||||
for hi, mi, hf, mf, intervalo, modo in obtener_horarios():
|
||||
inicio = hi * 60 + mi
|
||||
fin = hf * 60 + mf
|
||||
|
||||
@@ -34,13 +50,32 @@ def obtener_intervalo(dt: datetime) -> tuple[int, str] | None:
|
||||
|
||||
# ============================================================================
|
||||
def segundos_hasta_proximo_inicio(dt: datetime) -> int:
|
||||
inicio = dt.replace(hour=6, minute=30, second=0, microsecond=0)
|
||||
horarios = obtener_horarios()
|
||||
if horarios:
|
||||
hi, mi = horarios[0][0], horarios[0][1]
|
||||
else:
|
||||
hi, mi = 6, 30
|
||||
|
||||
inicio = dt.replace(hour=hi, minute=mi, second=0, microsecond=0)
|
||||
|
||||
if dt >= inicio:
|
||||
inicio += timedelta(days=1)
|
||||
|
||||
return int((inicio - dt).total_seconds())
|
||||
|
||||
# ============================================================================
|
||||
def segundos_hasta_proximo_dia_activo(dt: datetime) -> int:
|
||||
horarios = obtener_horarios()
|
||||
hi, mi = (horarios[0][0], horarios[0][1]) if horarios else (6, 30)
|
||||
dias_activos = obtener_dias_activos()
|
||||
|
||||
for dias in range(1, 8):
|
||||
prox = dt + timedelta(days=dias)
|
||||
if prox.weekday() in dias_activos:
|
||||
target = prox.replace(hour=hi, minute=mi, second=0, microsecond=0)
|
||||
return int((target - dt).total_seconds())
|
||||
return 86400
|
||||
|
||||
# ============================================================================
|
||||
def cargar_empresas():
|
||||
with open("empresas.json", "r", encoding="utf-8") as f:
|
||||
@@ -114,41 +149,39 @@ def enviar_mensaje_sl(usercode, doc, resultado, schema):
|
||||
|
||||
|
||||
|
||||
print(f" ┌─ 📩 Enviando Messages a {usercode} ───────────")
|
||||
# for line in json.dumps(payload, indent=2).splitlines():
|
||||
# print(f" │ {line}")
|
||||
logger.info(f" ┌─ 📩 Enviando Messages a {usercode} ───────────")
|
||||
sapb1_manager.send_message(payload)
|
||||
print(f" │ ✅ Mensaje enviado a {usercode}")
|
||||
print(f" └───────────────────────────────────────────────")
|
||||
logger.info(f" │ ✅ Mensaje enviado a {usercode}")
|
||||
logger.info(f" └───────────────────────────────────────────────")
|
||||
|
||||
# ============================================================================
|
||||
def procesar_empresa(schema, datos_empresa, modo):
|
||||
print(f"\n{'='*60}")
|
||||
print(f"🏢 Procesando empresa: {schema} [{modo}]")
|
||||
print(f"{'='*60}")
|
||||
logger.info(f"\n{'='*60}")
|
||||
logger.info(f"🏢 Procesando empresa: {schema} [{modo}]")
|
||||
logger.info(f"{'='*60}")
|
||||
|
||||
if modo == "rapido":
|
||||
documentos = hana_manager.obtener_documentos_creados_actualizados_hace_60_minutos(schema, 60)
|
||||
documentos = hana_manager.obtener_documentos_aun_no_validados_ultimos_30_dias(schema)
|
||||
else:
|
||||
documentos = hana_manager.obtener_documentos_fecha_emision_ultimos_8_dias(schema)
|
||||
if not documentos:
|
||||
print(f"⚠️ No se encontraron documentos para {schema}")
|
||||
logger.warning(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)}")
|
||||
logger.info(f"📄 Documentos encontrados: {len(documentos)}")
|
||||
|
||||
print(f"🔑 Solicitando token SUNAT...")
|
||||
logger.info(f"🔑 Solicitando token SUNAT...")
|
||||
token = sunat_manager.token(datos_empresa)
|
||||
if not token:
|
||||
print(f"❌ No se obtuvo token para {schema}")
|
||||
logger.error(f"❌ No se obtuvo token para {schema}")
|
||||
return 0, len(documentos)
|
||||
|
||||
print(f"✅ Token obtenido correctamente")
|
||||
logger.info(f"✅ Token obtenido correctamente")
|
||||
|
||||
sapb1_manager.login()
|
||||
print(f"✅ Sesión SL establecida para {schema}")
|
||||
logger.info(f"✅ Sesión SL establecida para {schema}")
|
||||
|
||||
ruc_empresa = datos_empresa["Ruc"]
|
||||
procesados = 0
|
||||
@@ -156,7 +189,7 @@ def procesar_empresa(schema, datos_empresa, modo):
|
||||
|
||||
try:
|
||||
for idx, doc in enumerate(documentos, start=1):
|
||||
print(f"\n--- [{idx}/{len(documentos)}] {doc['FolioPref']}-{doc['FolioNum']} ---")
|
||||
logger.info(f"\n--- [{idx}/{len(documentos)}] {doc['FolioPref']}-{doc['FolioNum']} ---")
|
||||
|
||||
try:
|
||||
payload = {
|
||||
@@ -170,15 +203,15 @@ def procesar_empresa(schema, datos_empresa, modo):
|
||||
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')}─────────────────────")
|
||||
logger.info(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" └────────────────────────────────────────────")
|
||||
logger.info(f" │ {line}")
|
||||
logger.info(f" └────────────────────────────────────────────")
|
||||
resultado = sunat_manager.validar_comprobante(token, ruc_empresa, payload)
|
||||
print(f" ┌─ 📥 Respuesta de SUNAT {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}───────────────────")
|
||||
logger.info(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" └────────────────────────────────────────────")
|
||||
logger.info(f" │ {line}")
|
||||
logger.info(f" └────────────────────────────────────────────")
|
||||
|
||||
registro = {
|
||||
"objtype": str(doc["ObjType"]),
|
||||
@@ -190,13 +223,13 @@ def procesar_empresa(schema, datos_empresa, modo):
|
||||
|
||||
if resultado.get("success"):
|
||||
estado_cp = resultado.get("data", {}).get("estadoCp", "")
|
||||
print(f"✅ OK - Estado CP: {estado_cp}")
|
||||
logger.info(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')}")
|
||||
logger.warning(f"⚠️ Falló - {resultado.get('message', 'Error desconocido')}")
|
||||
|
||||
procesados += 1
|
||||
|
||||
@@ -204,11 +237,11 @@ def procesar_empresa(schema, datos_empresa, modo):
|
||||
time.sleep(2)
|
||||
|
||||
except Exception as e:
|
||||
print(f"❌ Error procesando documento: {e}")
|
||||
logger.error(f"❌ Error procesando documento: {e}")
|
||||
errores += 1
|
||||
finally:
|
||||
sapb1_manager.logout()
|
||||
print(f"🔒 Sesión SL cerrada para {schema}")
|
||||
logger.info(f"🔒 Sesión SL cerrada para {schema}")
|
||||
|
||||
return procesados, errores
|
||||
|
||||
@@ -216,7 +249,7 @@ def procesar_empresa(schema, datos_empresa, modo):
|
||||
def procesar_todas_empresas(inicio_ciclo, modo):
|
||||
|
||||
empresas = cargar_empresas()
|
||||
print(f"📋 Empresas cargadas: {len(empresas)}")
|
||||
logger.info(f"📋 Empresas cargadas: {len(empresas)}")
|
||||
|
||||
total_procesados = 0
|
||||
total_errores = 0
|
||||
@@ -225,7 +258,7 @@ def procesar_todas_empresas(inicio_ciclo, modo):
|
||||
schema = empresa["CompanyDB"]
|
||||
|
||||
if empresa.get("Locked") == "Y":
|
||||
print(f"\n⏭️ Empresa {schema} está bloqueada (LOCKED=Y), saltando...")
|
||||
logger.warning(f"\n⏭️ Empresa {schema} está bloqueada (LOCKED=Y), saltando...")
|
||||
continue
|
||||
|
||||
sapb1_manager._sap_credentials = {
|
||||
@@ -239,39 +272,50 @@ def procesar_todas_empresas(inicio_ciclo, modo):
|
||||
total_procesados += p
|
||||
total_errores += e
|
||||
except Exception as ex:
|
||||
print(f"❌ Error procesando empresa {schema}: {ex}")
|
||||
logger.error(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")
|
||||
logger.info(f"\n📊 CICLO COMPLETADO en {str(duracion_ciclo).split('.')[0]}")
|
||||
logger.info(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")
|
||||
|
||||
load_dotenv(dotenv_path=".env.dev")
|
||||
cargar_programacion()
|
||||
|
||||
print("=" * 60)
|
||||
print("🚀 Demonio iniciado")
|
||||
print(f"📄 Log guardado en: {log_path}")
|
||||
print("Servicio iniciado")
|
||||
print("=" * 60)
|
||||
logger.info("=" * 60)
|
||||
logger.info("🚀 Demonio iniciado")
|
||||
logger.info("Servicio iniciado")
|
||||
logger.info("=" * 60)
|
||||
|
||||
while True:
|
||||
|
||||
inicio_ciclo = datetime.now()
|
||||
|
||||
cargar_programacion()
|
||||
|
||||
if inicio_ciclo.weekday() not in obtener_dias_activos():
|
||||
dormir = segundos_hasta_proximo_dia_activo(inicio_ciclo)
|
||||
logger.info(
|
||||
f"\n[{inicio_ciclo:%Y-%m-%d %H:%M:%S}] "
|
||||
f"Día no activo (weekday={inicio_ciclo.weekday()}).\n"
|
||||
f"Próxima ejecución: "
|
||||
f"{(inicio_ciclo + timedelta(seconds=dormir)):%Y-%m-%d %H:%M:%S}"
|
||||
)
|
||||
time.sleep(dormir)
|
||||
continue
|
||||
|
||||
resultado = obtener_intervalo(inicio_ciclo)
|
||||
|
||||
if resultado is not None:
|
||||
|
||||
intervalo, modo = resultado
|
||||
|
||||
print(
|
||||
logger.info(
|
||||
f"\n[{inicio_ciclo:%Y-%m-%d %H:%M:%S}] "
|
||||
f"Iniciando ciclo [{modo}] (cada {intervalo}s)"
|
||||
)
|
||||
@@ -281,14 +325,14 @@ def main():
|
||||
duracion = (datetime.now() - inicio_ciclo).total_seconds()
|
||||
|
||||
if duracion > intervalo:
|
||||
print(
|
||||
logger.warning(
|
||||
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...")
|
||||
logger.info(f"💤 Próximo ciclo [{modo}] en {dormir:.0f}s...")
|
||||
|
||||
else:
|
||||
|
||||
@@ -296,7 +340,7 @@ def main():
|
||||
|
||||
despertar = inicio_ciclo + timedelta(seconds=dormir)
|
||||
|
||||
print(
|
||||
logger.info(
|
||||
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}"
|
||||
@@ -309,4 +353,4 @@ if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except KeyboardInterrupt:
|
||||
print("\n\n🛑 Demonio detenido por el usuario.")
|
||||
logger.info("\n\n🛑 Demonio detenido por el usuario.")
|
||||
@@ -0,0 +1,14 @@
|
||||
[project]
|
||||
name = "validador_documentos"
|
||||
version = "1.1.0"
|
||||
description = "Validador de comprobantes electrónicos contra SUNAT"
|
||||
requires-python = ">=3.10"
|
||||
dependencies = [
|
||||
"certifi==2026.6.17",
|
||||
"charset-normalizer==3.4.9",
|
||||
"hdbcli==2.29.23",
|
||||
"idna==3.18",
|
||||
"python-dotenv==1.2.2",
|
||||
"requests==2.34.2",
|
||||
"urllib3==2.7.0",
|
||||
]
|
||||
+7
-8
@@ -1,8 +1,7 @@
|
||||
certifi==2026.6.17
|
||||
charset-normalizer==3.4.9
|
||||
dotenv==0.9.9
|
||||
hdbcli==2.29.23
|
||||
idna==3.18
|
||||
python-dotenv==1.2.2
|
||||
requests==2.34.2
|
||||
urllib3==2.7.0
|
||||
certifi
|
||||
charset-normalizer
|
||||
hdbcli
|
||||
idna
|
||||
python-dotenv
|
||||
requests
|
||||
urllib3
|
||||
+15
-16
@@ -7,15 +7,14 @@ import json
|
||||
from typing import Optional, Dict, Any
|
||||
from dotenv import load_dotenv
|
||||
|
||||
from logger_manager import get_logger
|
||||
|
||||
|
||||
# ============================================================================
|
||||
# CONFIGURACIÓN
|
||||
logger = get_logger()
|
||||
# ============================================================================
|
||||
|
||||
urllib3.disable_warnings()
|
||||
|
||||
load_dotenv(dotenv_path=".env.dev")
|
||||
load_dotenv(dotenv_path=".env")
|
||||
|
||||
SAPB1_URL = os.getenv("SAP_B1_URL", "")
|
||||
SAPB1_TIMEOUT = float(os.getenv("SAP_B1_TIMEOUT", 15.0))
|
||||
@@ -37,7 +36,7 @@ def login() -> dict:
|
||||
"UserName": _sap_credentials["username"],
|
||||
"Password": _sap_credentials["password"],
|
||||
}
|
||||
print("🔐 Iniciando sesión en SAP B1 Service Layer...")
|
||||
logger.info("🔐 Iniciando sesión en SAP B1 Service Layer...")
|
||||
resp = requests.post(
|
||||
f"{SAPB1_URL}/Login",
|
||||
json=payload,
|
||||
@@ -55,7 +54,7 @@ def login() -> dict:
|
||||
|
||||
headers = {"Cookie": f"B1SESSION={b1session}; ROUTEID={routeid}"}
|
||||
_sap_session.update({"headers": headers, "b1session": b1session, "routeid": routeid})
|
||||
print(f"✅ Sesión SAP B1 establecida (B1SESSION: {b1session[:10]}...)")
|
||||
logger.info(f"✅ Sesión SAP B1 establecida (B1SESSION: {b1session[:10]}...)")
|
||||
return headers
|
||||
|
||||
|
||||
@@ -64,9 +63,9 @@ def login() -> dict:
|
||||
# ============================================================================
|
||||
def logout():
|
||||
if not _sap_session["b1session"]:
|
||||
print("⚠️ No hay sesión activa para cerrar.")
|
||||
logger.warning("⚠️ No hay sesión activa para cerrar.")
|
||||
return
|
||||
print("🔓 Cerrando sesión SAP B1...")
|
||||
logger.info("🔓 Cerrando sesión SAP B1...")
|
||||
try:
|
||||
resp = requests.post(
|
||||
f"{SAPB1_URL}/Logout",
|
||||
@@ -75,11 +74,11 @@ def logout():
|
||||
verify=SAPB1_VERIFY_SSL,
|
||||
)
|
||||
if resp.status_code == 200:
|
||||
print("✅ Sesión SAP B1 cerrada correctamente.")
|
||||
logger.info("✅ Sesión SAP B1 cerrada correctamente.")
|
||||
else:
|
||||
print(f"⚠️ Logout respondió HTTP {resp.status_code}")
|
||||
logger.warning(f"⚠️ Logout respondió HTTP {resp.status_code}")
|
||||
except Exception as exc:
|
||||
print(f"❌ Error al cerrar sesión: {exc}")
|
||||
logger.error(f"❌ Error al cerrar sesión: {exc}")
|
||||
finally:
|
||||
_sap_session.update({"headers": None, "b1session": None, "routeid": None})
|
||||
_sap_credentials.clear()
|
||||
@@ -105,9 +104,9 @@ def _log_response(method: str, endpoint: str, response: requests.Response, paylo
|
||||
f" ({len(response.content)} bytes)"
|
||||
)
|
||||
if response.status_code >= 400:
|
||||
print(f" │ ⚠️ {log_line} - {response.text[:300]}")
|
||||
logger.warning(f" │ ⚠️ {log_line} - {response.text[:300]}")
|
||||
else:
|
||||
print(log_line)
|
||||
logger.info(log_line)
|
||||
|
||||
|
||||
# ============================================================================
|
||||
@@ -132,7 +131,7 @@ def forward_to_sapb1(
|
||||
**_sap_session["headers"],
|
||||
}
|
||||
|
||||
print(f" │ 🔄 [{method}] {endpoint_path} | {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
logger.info(f" │ 🔄 [{method}] {endpoint_path} | {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
|
||||
try:
|
||||
resp = requests.request(
|
||||
@@ -154,7 +153,7 @@ def forward_to_sapb1(
|
||||
if resp.status_code == 401:
|
||||
if not _sap_credentials:
|
||||
raise ConnectionError("Sesión expirada (401) y no hay credenciales para re-autenticar.")
|
||||
print(" │ ⚠️ Sesión expirada (401). Reautenticando e intentando una vez...")
|
||||
logger.warning(" │ ⚠️ Sesión expirada (401). Reautenticando e intentando una vez...")
|
||||
login()
|
||||
req_headers["Cookie"] = _sap_session["headers"]["Cookie"]
|
||||
resp = requests.request(
|
||||
@@ -181,5 +180,5 @@ def send_message(payload: dict) -> dict:
|
||||
f" │ Error al enviar Mensaje: HTTP {resp.status_code} - {resp.text}"
|
||||
)
|
||||
result = resp.json()
|
||||
print(f" │ ✅ SAP B1 Service Layer - {resp.status_code}")
|
||||
logger.info(f" │ ✅ SAP B1 Service Layer - {resp.status_code}")
|
||||
return result
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"HORARIOS": [
|
||||
[6, 0, 13, 15, 10, "rapido"],
|
||||
[13, 15, 14, 0, 1800, "full"],
|
||||
[14, 0, 17, 30, 10, "rapido"],
|
||||
[17, 30, 21, 30, 1800, "full"]
|
||||
],
|
||||
"DIAS_ACTIVOS": [0, 1, 2, 3, 4]
|
||||
}
|
||||
+9
-5
@@ -3,6 +3,10 @@ import json
|
||||
import urllib3
|
||||
import time
|
||||
|
||||
from logger_manager import get_logger
|
||||
|
||||
logger = get_logger()
|
||||
|
||||
# Desactivar advertencias de SSL no verificado
|
||||
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
||||
|
||||
@@ -28,8 +32,8 @@ def token(datos):
|
||||
|
||||
response = requests.request("POST", url, headers=headers, data=payload, verify=False, timeout=30)
|
||||
|
||||
print(f" 📤 Token request enviado")
|
||||
print(f" 📥 Token response: {response.status_code}")
|
||||
logger.info(f" 📤 Token request enviado")
|
||||
logger.info(f" 📥 Token response: {response.status_code}")
|
||||
|
||||
if response.status_code == 200:
|
||||
result = response.json()
|
||||
@@ -66,13 +70,13 @@ def validar_comprobante(token,RUC,payload:dict):
|
||||
for attempt in range(max_retries):
|
||||
try:
|
||||
response = requests.request("POST", url, headers=headers, json=payload, verify=False, timeout=30)
|
||||
print(f" 📥 Status code: {response.status_code}")
|
||||
logger.info(f" 📥 Status code: {response.status_code}")
|
||||
return response.json()
|
||||
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e:
|
||||
print(f" ⚠️ Error de conexión (intento {attempt + 1}/{max_retries}): {e}")
|
||||
logger.warning(f" ⚠️ Error de conexión (intento {attempt + 1}/{max_retries}): {e}")
|
||||
if attempt < max_retries - 1:
|
||||
time_to_wait = 2 ** (attempt + 1)
|
||||
print(f" 🔄 Reintentando en {time_to_wait} segundos...")
|
||||
logger.info(f" 🔄 Reintentando en {time_to_wait} segundos...")
|
||||
time.sleep(time_to_wait)
|
||||
else:
|
||||
raise e
|
||||
|
||||
Reference in new issue
Block a user