207 lines
7.3 KiB
Python
Executable File
207 lines
7.3 KiB
Python
Executable File
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}"
|
|
)
|
|
# ============================================================================ |