Files
validador_documentos/sapb1_manager.py
T
2026-07-09 09:29:54 -05:00

186 lines
6.4 KiB
Python

from datetime import datetime
import requests
import urllib3
import os
import json
from typing import Optional, Dict, Any
from dotenv import load_dotenv
# ============================================================================
# CONFIGURACIÓN
# ============================================================================
urllib3.disable_warnings()
load_dotenv(dotenv_path=".env.dev")
SAPB1_URL = os.getenv("SAP_B1_URL", "")
SAPB1_TIMEOUT = float(os.getenv("SAP_B1_TIMEOUT", 15.0))
SAPB1_VERIFY_SSL = os.getenv("SAP_B1_VERIFY_SSL", "False").lower() == "true"
# Almacenamiento global de sesión
_sap_session: dict = {"headers": None, "b1session": None, "routeid": None}
_sap_credentials: dict = {}
# ============================================================================
# LOGIN
# ============================================================================
def login() -> dict:
if not _sap_credentials:
raise RuntimeError("No hay credenciales. Asigna sapb1_manager._sap_credentials antes de llamar login().")
payload = {
"CompanyDB": _sap_credentials["company"],
"UserName": _sap_credentials["username"],
"Password": _sap_credentials["password"],
}
print("🔐 Iniciando sesión en SAP B1 Service Layer...")
resp = requests.post(
f"{SAPB1_URL}/Login",
json=payload,
timeout=SAPB1_TIMEOUT,
verify=SAPB1_VERIFY_SSL,
)
if resp.status_code != 200:
raise ConnectionError(
f"Login fallido: HTTP {resp.status_code} - {resp.text}"
)
data = resp.json()
b1session = data["SessionId"]
routeid = resp.cookies.get("ROUTEID", "")
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]}...)")
return headers
# ============================================================================
# LOGOUT
# ============================================================================
def logout():
if not _sap_session["b1session"]:
print("⚠️ No hay sesión activa para cerrar.")
return
print("🔓 Cerrando sesión SAP B1...")
try:
resp = requests.post(
f"{SAPB1_URL}/Logout",
headers=_sap_session["headers"],
timeout=SAPB1_TIMEOUT,
verify=SAPB1_VERIFY_SSL,
)
if resp.status_code == 200:
print("✅ Sesión SAP B1 cerrada correctamente.")
else:
print(f"⚠️ Logout respondió HTTP {resp.status_code}")
except Exception as exc:
print(f"❌ Error al cerrar sesión: {exc}")
finally:
_sap_session.update({"headers": None, "b1session": None, "routeid": None})
_sap_credentials.clear()
# ============================================================================
# SESIÓN (interna - lazy login)
# ============================================================================
def _ensure_session() -> dict:
if _sap_session["headers"]:
return _sap_session["headers"]
if _sap_credentials:
return login()
raise RuntimeError("No hay sesión ni credenciales para autenticarse en SAP B1.")
# ============================================================================
# LOGGING DE RESPUESTA
# ============================================================================
def _log_response(method: str, endpoint: str, response: requests.Response, payload: Any = None):
log_line = (
f" │ [{method}] {endpoint} → {response.status_code}"
f" ({len(response.content)} bytes)"
)
if response.status_code >= 400:
print(f" │ ⚠️ {log_line} - {response.text[:300]}")
else:
print(log_line)
# ============================================================================
# PROXY CENTRALIZADO
# ============================================================================
def forward_to_sapb1(
method: str,
endpoint_path: str,
json_data: Optional[Dict[str, Any]] = None,
params: Optional[Dict[str, Any]] = None,
) -> requests.Response:
if not method or not endpoint_path:
raise ValueError(" │ method y endpoint_path son requeridos")
method = method.upper()
headers = _ensure_session()
url = f"{SAPB1_URL}/{endpoint_path}"
req_headers = {
"Content-Type": "application/json",
"Accept": "application/json",
**_sap_session["headers"],
}
print(f" │ 🔄 [{method}] {endpoint_path} | {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
try:
resp = requests.request(
method=method,
url=url,
json=json_data,
params=params,
headers=req_headers,
timeout=SAPB1_TIMEOUT,
verify=SAPB1_VERIFY_SSL,
)
except requests.exceptions.Timeout:
raise ConnectionError(f" │ Timeout ({SAPB1_TIMEOUT}s) en {endpoint_path}")
except requests.exceptions.ConnectionError as exc:
raise ConnectionError(f" │ No se pudo conectar con SAP B1: {exc}")
except requests.RequestException as exc:
raise ConnectionError(f" │ Error en solicitud HTTP: {exc}")
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...")
login()
req_headers["Cookie"] = _sap_session["headers"]["Cookie"]
resp = requests.request(
method=method,
url=url,
json=json_data,
params=params,
headers=req_headers,
timeout=SAPB1_TIMEOUT,
verify=SAPB1_VERIFY_SSL,
)
_log_response(method, endpoint_path, resp, json_data)
return resp
# ============================================================================
# SEND MESSAGE (POST /Messages)
# ============================================================================
def send_message(payload: dict) -> dict:
resp = forward_to_sapb1("POST", "Messages", json_data=payload)
if resp.status_code not in (200, 201):
raise ConnectionError(
f" │ Error al enviar Mensaje: HTTP {resp.status_code} - {resp.text}"
)
result = resp.json()
print(f" │ ✅ SAP B1 Service Layer - {resp.status_code}")
return result