import requests import urllib3 import os import json from typing import Optional, Dict, Any from dotenv import load_dotenv from logger_manager import get_logger # ============================================================================ urllib3.disable_warnings() load_dotenv(dotenv_path=".env") SAPB1_URL = os.getenv("SAP_B1_URL", "") SAPB1_USER = os.getenv("SAP_B1_USER", "") SAPB1_PASSWORD = os.getenv("SAP_B1_PASSWORD", "") SAPB1_COMPANY = os.getenv("SAP_B1_COMPANY", "") SAPB1_TIMEOUT = float(os.getenv("SAP_B1_TIMEOUT", 15.0)) SAPB1_VERIFY_SSL = os.getenv("SAP_B1_VERIFY_SSL", "False").lower() == "true" _sap_session: dict = {"headers": None, "b1session": None, "routeid": None} # ============================================================================ log = get_logger() # ============================================================================ def login() -> dict: payload = { "CompanyDB": SAPB1_COMPANY, "UserName": SAPB1_USER, "Password": SAPB1_PASSWORD, } log.info("🔐 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}) log.info("✅ Sesión SAP B1 establecida (B1SESSION: {}...)".format(b1session[:10])) return headers # ============================================================================ def logout() -> None: if not _sap_session["b1session"]: log.warning("⚠️ No hay sesión activa para cerrar.") return log.info("🔓 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: log.info("✅ Sesión SAP B1 cerrada correctamente.") else: log.warning("⚠️ Logout respondió HTTP {}".format(resp.status_code)) except Exception as exc: log.error("❌ Error al cerrar sesión: {}".format(exc)) finally: _sap_session.update({"headers": None, "b1session": None, "routeid": None}) # ============================================================================ def _ensure_session() -> dict: if _sap_session["headers"]: return _sap_session["headers"] return login() # ============================================================================ def _log_response(method: str, endpoint: str, response: requests.Response, payload: Any = None) -> None: log_line = "[{}] {} → {} ({} bytes)".format( method, endpoint, response.status_code, len(response.content) ) if response.status_code >= 400: log.warning("⚠️ {} - {}".format(log_line, response.text[:300])) else: log.info("🔄 " + log_line) # ============================================================================ def forward_to_sapb1( method: str, endpoint_path: str, json_data: Optional[Dict[str, Any]] = None, params: Optional[Dict[str, Any]] = None, extra_headers: Optional[Dict[str, str]] = None, raise_on_error: bool = True, ) -> 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"], } if extra_headers: req_headers.update(extra_headers) 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: log.warning("⚠️ Sesión expirada (401). Reautenticando e intentando una vez...") req_headers["Cookie"] = login()["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) if raise_on_error and resp.status_code >= 400: try: error_body = resp.json() sap_message = ( error_body.get("error", {}) .get("message", {}) .get("value", resp.text) ) except (json.JSONDecodeError, AttributeError): sap_message = resp.text raise ConnectionError( f"Error en SAP {method} {endpoint_path}: " f"HTTP {resp.status_code} - {sap_message}" ) return resp # ============================================================================ def create_drafts(payload: dict) -> dict: resp = forward_to_sapb1("POST", "Drafts", json_data=payload, raise_on_error=False) if resp.status_code == 201: result = resp.json() log.info("✅ Draft creado DocEntry={}".format(result.get("DocEntry"))) return result try: error_body = resp.json() sap_message = ( error_body.get("error", {}) .get("message", {}) .get("value", resp.text) ) except (json.JSONDecodeError, AttributeError): sap_message = resp.text raise ConnectionError( f"Error creando Draft: HTTP {resp.status_code} - {sap_message}" ) # ============================================================================ def create_orders(payload: dict) -> dict: resp = forward_to_sapb1("POST", "Orders", json_data=payload, raise_on_error=False) if resp.status_code == 201: result = resp.json() log.info("✅ Orden creada DocEntry={}".format(result.get("DocEntry"))) return result try: error_body = resp.json() sap_message = ( error_body.get("error", {}) .get("message", {}) .get("value", resp.text) ) except (json.JSONDecodeError, AttributeError): sap_message = resp.text raise ConnectionError( f"Error creando Orden venta: HTTP {resp.status_code} - {sap_message}" ) # ============================================================================