primer commit

This commit is contained in:
GEORGE committed 2026-05-28 15:08:25 -05:00
commit d858235656
14 files changed
+21213

No files matched your search

+10
View File
@@ -0,0 +1,10 @@
__pycache__/
*.pyc
.env
venv/
.DS_Store
secrets.yaml
.vscode/
.idea/
logs/
+16
View File
@@ -0,0 +1,16 @@
{
"hana": {
"company": "SBO_TAGUMEDICA_21",
"server": "192.168.1.175",
"port": 30015,
"user": "SYSTEM",
"password": "SAPB1Admin.25"
},
"service_layer": {
"url": "https://192.168.1.175:50000/b1s/v1/",
"username": "manager",
"password": "m1r1",
"maxpagesize": 50,
"b1s_replacecollectionsonpatch": true
}
}
+16
View File
@@ -0,0 +1,16 @@
from config_model import Config
import os
# Definir la ruta del archivo de configuración
CONFIG_PATH = os.path.join(os.path.dirname(__file__), "config.json")
def load_config():
"""Carga la configuración desde el archivo JSON."""
try:
return Config.load(CONFIG_PATH)
except Exception as e:
print(f"ERROR: Al cargar la configuracion: {e}")
raise e
# Variable global CONFIG disponible para otros módulos
CONFIG = load_config()
+53
View File
@@ -0,0 +1,53 @@
from dataclasses import dataclass
from typing import Optional
import json
import os
@dataclass
class HanaConfig:
company: str
server: str
port: int
user: str
password: str
@dataclass
class ServiceLayerConfig:
url: str
username: str
password: str
maxpagesize: int
b1s_replacecollectionsonpatch: bool
@dataclass
class Config:
hana: HanaConfig
service_layer: ServiceLayerConfig
@staticmethod
def load(path: str = "config.json") -> 'Config':
if not os.path.exists(path):
raise FileNotFoundError(f"No se encontró el archivo de configuración: {path}")
with open(path, 'r', encoding='utf-8') as f:
data = json.load(f)
hana_data = data.get('hana', {})
service_layer_data = data.get('service_layer', {})
# Validar campos requeridos para HANA
required_hana = ['company', 'server', 'port', 'user', 'password']
for field in required_hana:
if field not in hana_data:
raise KeyError(f"Falta el campo requerido '{field}' en la sección 'hana' de {path}")
# Validar campos requeridos para Service Layer
required_service_layer = ['url', 'username', 'password', 'maxpagesize', 'b1s_replacecollectionsonpatch']
for field in required_service_layer:
if field not in service_layer_data:
raise KeyError(f"Falta el campo requerido '{field}' en la sección 'service_layer' de {path}")
hana = HanaConfig(**hana_data)
service_layer = ServiceLayerConfig(**service_layer_data)
return Config(hana=hana, service_layer=service_layer)
+356
View File
@@ -0,0 +1,356 @@
import math
from decimal import Decimal
from itertools import combinations
from math import floor
import sap_services
import hana_client
###################################################
reglas = {
"100": lambda m: m > 0,
"101": lambda m: m < 12,
"102": lambda m: m >= 12,
"103": lambda m: m >= 18,
"104": lambda m: m >= 24,
"105": lambda m: m >= 30,
"106": lambda m: m >= 36,
"107": lambda m: m >= 42,
"108": lambda m: m >= 48,
"109": lambda m: m >= 54,
"110": lambda m: m >= 60
}
vigencias = {
"100": "meses > 0",
"101": "meses < 12",
"102": "meses >= 12",
"103": "meses >= 18",
"104": "meses >= 24",
"105": "meses >= 30",
"106": "meses >= 36",
"107": "meses >= 42",
"108": "meses >= 48",
"109": "meses >= 54",
"110": "meses >= 60",
}
presentacion = {
"100": 1,
"101": 1,
"102": 2,
"103": 4,
"104": 6,
"105": 8,
"106": 10,
"107": 12,
"108": 24,
"109": 36,
"110": 12,
"111": 12,
"112": 24,
"113": 3
}
# si es venta consignación, se vende por unidad
# el multiplo = 1
# vigencia en meses > 0
ventas_tipo_consignacion = {
"110": "venta consignacion"
}
###################################################
###################################################
def obtener_documento_formato_json(session,registro):
objkey = registro['DocEntry']
endpoint = f"Orders({objkey})"
return sap_services.get_by_key(session,endpoint)
###################################################
def actualizar_detalle_documento(session,documento,body):
objkey = documento['DocEntry']
endpoint = f"Orders({objkey})"
return sap_services.update_document(session,endpoint,body)
###################################################
def seleccionar_mejor_lotes(line,lotes,vigencia,unidades_por_caja,tipo_venta,max_lotes=3):
'''
Si el tipo_venta = consignacion
unidades_por_caja = 1
'''
# cantidad total de unidades requeridas
total_objetivo = line["Quantity"]
# si el tipo de venta no es consignacion
# se debe ver si las cantidades requeridas
# son multiplos o no de las unidades_por_caja
if not tipo_venta in ventas_tipo_consignacion:
# validar si la cantidad solicitada es múltiplo de unidades_por_caja
es_multiplo = True if total_objetivo % unidades_por_caja == 0 else False
# si no es multiplo, entonces obtengo el maximo comun divisor
if not es_multiplo:
maximo_comun_divisor = math.gcd(int(total_objetivo), int(unidades_por_caja))
print(f" ⚠️Se utilizara el MCD que es {maximo_comun_divisor}")
unidades_por_caja = maximo_comun_divisor
# quitar los lotes que no cumplan con los meses minimos requeridos de vencimiento de lotes
filtrados = [l for l in lotes if reglas[vigencia](l["Months"])]
# ordenar los lotes por fecha de creacion, el mas antiguo primero
filtrados = sorted(filtrados, key=lambda x: x["InDate"])
# agregar indice para priorizar antiguedad
for i, lote in enumerate(filtrados):
lote["indice"] = i
mejor_exacto = None
mejor_parcial = None
for r in range(1, max_lotes + 1):
for combo in combinations(filtrados, r):
total = 0
detalle = []
# clave: lista ordenada de indices
prioridad_fifo = sorted(l["indice"] for l in combo)
for lote in combo:
restante = total_objetivo - total
if restante <= 0:
break
disponible = lote["AvailableQty"]
tomar = min(disponible, restante)
tomar = floor(tomar / unidades_por_caja) * unidades_por_caja
if tomar > 0:
detalle.append({
"DistNumber": lote["DistNumber"],
"InDate": lote["InDate"],
"Quantity": lote["Quantity"],
"AvailableQty": lote["AvailableQty"],
"TakeQuantity": tomar,
"ExpDate": lote["ExpDate"],
"Months": lote["Months"]
})
total += tomar
# ignorar combos que no aportan nada
if total == 0:
continue
# solucion exacta -> priorizar absoluta
if total == total_objetivo:
if not mejor_exacto or prioridad_fifo < mejor_exacto[2]:
mejor_exacto = (detalle, total, prioridad_fifo)
# mejor solucion parcial -> mayor total posible
elif total < total_objetivo:
if not mejor_parcial or (
total > mejor_parcial[1] or
(total == mejor_parcial[1] and prioridad_fifo < mejor_parcial[2])
):
mejor_parcial = (detalle, total, prioridad_fifo)
if mejor_exacto:
return mejor_exacto[0], mejor_exacto[1]
elif mejor_parcial:
return mejor_parcial[0], mejor_parcial[1]
return [], 0
###################################################
def se_le_debe_asignar_lote_a_linea(line):
cantidad = line["Quantity"]
lotes = [] if "BatchNumbers" not in line else line["BatchNumbers"]
linestatus = line["LineStatus"] ## bost_Open
if linestatus == "bost_Close":
return False
# no tiene asignado lotes
if len(lotes) == 0:
# se deba asignar lotes
return True
else:
# sumar cantidades de lotes
total_lotes = sum(lote["Quantity"] for lote in lotes)
if total_lotes == cantidad:
# no se deba asignar lotes
return False
else:
# se deba asignar lotes
return True
###################################################
def agregar_columna_available_qty(lotes):
for lote in lotes:
lote["AvailableQty"] = (lote["Quantity"] or Decimal(0)) - (lote["CommitQty"] or Decimal(0))
return lotes
###################################################
def documento_requiere_desasignar_lotes(documento):
necesita_actualizar = False
nuevas_lineas = []
for line in documento.get("DocumentLines",[]):
nueva_linea = {
"LineNum": line.get("LineNum"),
"ItemCode": line.get("ItemCode"),
"Quantity": line.get("Quantity"),
"WarehouseCode": line.get("WarehouseCode"),
"BatchNumbers": []
}
batches = line.get("BatchNumbers",[])
if batches:
total_line = line.get("Quantity",0)
total_batches = sum(b.get("Quantity",0) for b in batches)
if total_batches != total_line:
necesita_actualizar = True
for b in batches:
nueva_linea["BatchNumbers"].append({
"BaseLineNumber": line.get("LineNum"),
"BatchNumber": b.get("BatchNumber"),
"Quantity": 0.00
})
else:
# copiar tal cual
for b in batches:
nueva_linea["BatchNumbers"].append(b.copy())
else:
# sin lotes -> se deja vacio
nueva_linea["BatchNumbers"] = []
nuevas_lineas.append(nueva_linea)
nuevo_json = {
"DocumentLines": nuevas_lineas
}
return necesita_actualizar, nuevo_json
###################################################
def procesar_registro(registro,session):
documento = obtener_documento_formato_json(session,registro)
necesita_actualizar, nuevo_json = documento_requiere_desasignar_lotes(documento)
if necesita_actualizar:
print(f" 🧹 Desasignando lotes previos del documento...")
status_code = actualizar_detalle_documento(session,documento, nuevo_json)
print(f" ✅ Documento actualizado (Desasignación).")
documento = obtener_documento_formato_json(session,registro)
else:
# continuar con el documento
pass
maxima_cantidad_lotes = 3
vigencia = "100"
tipo_venta = documento["U_SIS_TIPVEN"]
if tipo_venta in ventas_tipo_consignacion:
print(f" 🚨 Tipo de venta: {tipo_venta} -> {ventas_tipo_consignacion[tipo_venta]}")
print(f" 🚨 Presentacion: sera de 1 Unidad para todos los articulos")
print(f" 🚨 Meses de vigencia > 0 meses")
else:
# de lo contrario se toma la vigencia seleccionada en el documento
vigencia = documento["U_SIS_VIGREQ"]
print(f" 📅 Vigencia requerida(meses de vencimiento): {vigencia} -> {vigencias[vigencia]}")
new_lines = []
# Reservas en memoria: {(itemcode, whscode, dist_number): cantidad_ya_reservada}
reservas = {}
for index, line in enumerate(documento.get("DocumentLines",[]),start=1):
treetype = line["TreeType"]
new_line = {
"LineNum": line.get("LineNum"),
"ItemCode": line.get("ItemCode"),
"Quantity": line.get("Quantity"),
"WarehouseCode": line.get("WarehouseCode"),
"BatchNumbers": []
}
if treetype == "iNotATree":
if se_le_debe_asignar_lote_a_linea(line):
codigo_presentacion = str(hana_client.obtener_presentacion_articulo(line["ItemCode"]))
# Presentacion del articulo o Unidaddes por Caja
unidades_por_caja = presentacion.get(codigo_presentacion, 1)
lotes_disponibles = hana_client.obtener_lotes_disponibles(line["ItemCode"],line["WarehouseCode"])
lotes_disponibles = agregar_columna_available_qty(lotes_disponibles)
# Descontar reservas ya asignadas a líneas anteriores del mismo documento
for lote in lotes_disponibles:
clave = (line["ItemCode"], line["WarehouseCode"], lote["DistNumber"])
ya_reservado = reservas.get(clave, 0)
if ya_reservado > 0:
lote["AvailableQty"] = max(lote["AvailableQty"] - ya_reservado, 0)
# core del proceso de asignacion
lotes_seleccionados, total_seleccionado = seleccionar_mejor_lotes(line,lotes_disponibles,vigencia
,1 if tipo_venta in ventas_tipo_consignacion else unidades_por_caja
,tipo_venta,maxima_cantidad_lotes)
### impresion de lotes disponibles
print(f" ➖ Línea {index} | Artículo: {line['ItemCode']} | Cant. req: {line['Quantity']} | Múltiplo de emp.: {unidades_por_caja} | Almacen: {line['WarehouseCode']}")
print(f" 📦 Se encontraron {len(lotes_disponibles)} lotes disponibles.")
print(" 🔍 Detalle lotes disponibles:")
for a in sorted(lotes_disponibles, key=lambda x: x["InDate"]):
print(f" - Lote: {a['DistNumber']:10} | F. Fab: {a['InDate']:10} | Disp: {float(a['AvailableQty']):10.2f} | Meses: {a['Months']:4}")
### impresion de lotes seleccionados
print(" 🎯 Lotes seleccionados:")
for a in lotes_seleccionados:
print(f" - Lote: {a['DistNumber']:10} | F. Fab: {a['InDate']:10} | A tomar: {float(a['TakeQuantity']):10.2f} | Meses: {a['Months']:4}")
for lote in lotes_seleccionados:
new_line["BatchNumbers"].append({
"BaseLineNumber": line.get("LineNum"),
"BatchNumber": lote.get("DistNumber"),
"Quantity": lote.get("TakeQuantity")
})
# Registrar la reserva en memoria
clave = (line["ItemCode"], line["WarehouseCode"], lote["DistNumber"])
reservas[clave] = reservas.get(clave, 0) + lote["TakeQuantity"]
else:
# el TreeType de esta Linea es diferente a iNotATree
pass
new_lines.append(new_line)
# Obtener líneas que tienen lotes asignados
lineas_con_lotes = [line for line in new_lines if len(line["BatchNumbers"]) > 0]
# Debo enviar todas las lineas del documento, tengan o no lotes asignados
body = {
"DocumentLines": new_lines
}
hizo_asignacion = len(lineas_con_lotes) > 0
# si no se realizo asignacion de lotes, no tiene sentido actualizar el documento
if hizo_asignacion:
print(f" 🔄 Asignando lotes en SAP ({len(lineas_con_lotes)} líneas con lotes)...")
status_code = actualizar_detalle_documento(session, documento, body)
print(f" ✅ Documento actualizado con lotes asignados.")
else:
print(f" ✅ No es necesario actualizar el documento.")
###################################################
+49
View File
@@ -0,0 +1,49 @@
import json
import core
import sap_services
import logger_utilities
# Configurar Logger (Redirige stdout a archivo y consola)
log_path = logger_utilities.setup_logger()
session = sap_services.login()
registro = {
'DocEntry' : 111239
}
documento = core.obtener_documento_formato_json(session,registro)
new_lines = []
for index, line in enumerate(documento.get("DocumentLines",[]),start=1):
batches = line.get("BatchNumbers",[])
new_batches = []
if batches:
for b in batches:
new_batches.append({
"BaseLineNumber": line.get("LineNum"),
"BatchNumber": b.get("BatchNumber"),
"Quantity": 0.00
})
new_lines.append({
"LineNum": line.get("LineNum"),
"ItemCode": line.get("ItemCode"),
"Quantity": line.get("Quantity"),
"WarehouseCode": line.get("WarehouseCode"),
"BatchNumbers": new_batches
})
body = {
"DocumentLines": new_lines
}
print (json.dumps(body, indent=2))
status_code = core.actualizar_detalle_documento(session, documento, body)
print(status_code)
+155
View File
@@ -0,0 +1,155 @@
from hdbcli import dbapi
from config_loader import CONFIG
###################################################
def get_connection(schema):
"""Crea y retorna una conexión a SAP HANA."""
try:
conn = dbapi.connect(
address=CONFIG.hana.server,
port=CONFIG.hana.port,
user=CONFIG.hana.user,
password=CONFIG.hana.password
)
return conn
except Exception as e:
print(f"❌ Error conectando a HANA para {schema}: {e}")
return None
###################################################
def obtener_registros():
"""
Obtiene los documentos de los ultimos 'days' dias
Args:
company: Objeto con la configuracion de la base de datos
days: Numero de dias a consultar, entero positivo
Returns:
Lista de diccionarios con los documentos encontrados
Raises:
Exception: Si hay un error conectando a la base de datos
Example:
where DocDate >= ADD_DAYS(CURRENT_DATE, -{days})
"""
hana_config = CONFIG.hana
db_name = hana_config.company
conn = get_connection(db_name)
if not conn:
return []
query = f"""
SELECT T0."DocEntry", T0."DocNum", T0."ObjType", T2."USER_CODE" "UserCode"
FROM "{db_name}".ORDR T0
INNER JOIN "{db_name}".OUSR T2 ON T2."USERID" = T0."UserSign"
WHERE T0."CANCELED" = 'N' AND T0."DocStatus" = 'O'
AND T0."DocType" = 'I'
AND IFNULL("U_SIS_IMPAUT",'N') = 'Y'
AND IFNULL("U_SIS_VIGREQ",'') <> ''
AND TO_VARCHAR(T0."CreateDate",'YYYYMMDD') = '20260528'
"""
try:
cursor = conn.cursor()
cursor.execute(query)
# Obtener nombres de columnas
columns = [column[0] for column in cursor.description]
# Convertir resultados a lista de diccionarios
results = []
for row in cursor.fetchall():
results.append(dict(zip(columns, row)))
cursor.close()
conn.close()
print(f"📄 Se encontraron {len(results)} documentos para procesar.")
return results
except Exception as e:
print(f"❌ Error ejecutando la consulta en HANA: {e}")
if 'conn' in locals():
conn.close()
return []
###################################################
def obtener_lotes_disponibles(itemcode:str,whscode:str):
hana_config = CONFIG.hana
db_name = hana_config.company
conn = get_connection(db_name)
if not conn:
return []
query = f"""
SELECT T0."DistNumber", TO_VARCHAR(T0."InDate",'YYYY-MM-DD') "InDate",
TO_VARCHAR(T0."ExpDate",'YYYY-MM-DD') "ExpDate",
T0."AbsEntry", T1."WhsCode", T1."Quantity", T1."CommitQty",
FLOOR(MONTHS_BETWEEN(CURRENT_DATE, T0."ExpDate")) "Months"
FROM "{db_name}".OBTN T0
INNER JOIN "{db_name}".OBTQ T1 ON T1."MdAbsEntry" = T0."AbsEntry"
WHERE T0."ItemCode" = '{itemcode}'
AND T1."WhsCode" = '{whscode}'
AND T1."Quantity" > 0.00
"""
try:
cursor = conn.cursor()
cursor.execute(query)
# Obtener nombres de columnas
columns = [column[0] for column in cursor.description]
# Convertir resultados a lista de diccionarios
results = []
for row in cursor.fetchall():
results.append(dict(zip(columns, row)))
cursor.close()
conn.close()
return results
except Exception as e:
print(f"❌ Error ejecutando la consulta en HANA: {e}")
if 'conn' in locals():
conn.close()
return []
###################################################
def obtener_presentacion_articulo(itemcode):
hana_config = CONFIG.hana
db_name = hana_config.company
conn = get_connection(db_name)
if not conn:
return []
query = f"""
SELECT IFNULL(T0."U_SIS_PRECAJ",'100') "Presentacion"
FROM "{db_name}".OITM T0
WHERE T0."ItemCode" = '{itemcode}'
"""
try:
cursor = conn.cursor()
cursor.execute(query)
presentacion = cursor.fetchone()[0]
cursor.close()
conn.close()
return presentacion
except Exception as e:
print(f"❌ Error ejecutando la consulta en HANA: {e}")
if 'conn' in locals():
conn.close()
return None
###################################################
+74
View File
@@ -0,0 +1,74 @@
import sys
import os
from datetime import datetime, timedelta
########################################################
class MultiStream:
"""
Clase para redirigir la salida a múltiples flujos (consola y archivo).
"""
def __init__(self, *streams):
self.streams = streams
def write(self, data):
for stream in self.streams:
try:
stream.write(data)
except UnicodeEncodeError:
# Fallback para consola (sys.stdout/stderr) que no soporte UTF-8
stream.write(data.encode('cp1252', 'replace').decode('cp1252'))
stream.flush()
def flush(self):
for stream in self.streams:
stream.flush()
########################################################
def setup_logger():
"""
Configura la redirección de stdout y stderr a un archivo de log y a la consola.
Retorna la ruta del archivo de log creado.
"""
log_dir = "logs"
if not os.path.exists(log_dir):
os.makedirs(log_dir)
# Nombre del archivo: YYYY-MM-DD_HH-MM-SS.log
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
log_filename = f"{timestamp}.log"
log_path = os.path.join(log_dir, log_filename)
# Abrir archivo en modo append con encoding utf-8 para soportar iconos
log_file = open(log_path, "a", encoding="utf-8")
# Redirigir stdout y stderr
sys.stdout = MultiStream(sys.stdout, log_file)
sys.stderr = MultiStream(sys.stderr, log_file)
return 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).")
+42
View File
@@ -0,0 +1,42 @@
import hana_client
import sap_services
import core
import logger_utilities
from datetime import datetime
###################################################
def main():
registros = hana_client.obtener_registros()
session = sap_services.login()
for registro in registros:
print(f"\n▶▶ Procesando registro número: {registro['DocNum']} (DocEntry: {registro['DocEntry']}) - Usuario: {registro.get('UserCode', 'N/A')}")
core.procesar_registro(registro, session)
sap_services.logout(session)
###################################################
if __name__ == "__main__":
# Configurar Logger (Redirige stdout a archivo y consola)
log_path = logger_utilities.setup_logger()
logger_utilities.limpiar_logs_antiguos(dias=30)
inicio_ejecucion = datetime.now()
print("="*60)
print(f"🚀 INICIO DE EJECUCIÓN: {inicio_ejecucion.strftime('%Y-%m-%d %H:%M:%S')}")
print(f"📄 Log guardado en: {log_path}")
print("="*60)
main()
fin_ejecucion = datetime.now()
duracion = fin_ejecucion - inicio_ejecucion
print("\n" + "="*60)
print(f"🏁 FIN DE EJECUCIÓN: {fin_ejecucion.strftime('%Y-%m-%d %H:%M:%S')}")
print(f"⏱️ Duración total: {str(duracion).split('.')[0]}")
print("="*60)
+77
View File
@@ -0,0 +1,77 @@
import requests
import json
import traceback
import sys
import sap_utilities
from config_loader import CONFIG
###################################################
def login():
session = requests.Session()
sl_config = CONFIG.service_layer
hana_config = CONFIG.hana
body = {
"CompanyDB": hana_config.company,
"UserName": sl_config.username,
"Password": sl_config.password
}
try:
response = sap_utilities.post(session, "Login", body)
if response:
return session
except Exception as e:
print(f"❌ Error de conexión al SAP Service Layer (Login): {e}")
tb = traceback.extract_tb(sys.exc_info()[2])
print(tb[-1].name)
return None
###################################################
def logout(session):
try:
response = sap_utilities.post(session, "Logout")
if response:
return True
except Exception as e:
print(f"❌ Error de desconexión del SAP Service Layer (Logout): {e}")
tb = traceback.extract_tb(sys.exc_info()[2])
print(tb[-1].name)
return False
###################################################
def get_by_key(session, endpoint):
if not session:
print("❌ Error: No se pudo iniciar sesión en SAP Service Layer")
return None
recurso = endpoint
response = sap_utilities.get(session, recurso)
if response and response.status_code == 200:
return response.json()
print(f"❌ Error obteniendo datos de {recurso}")
return None
###################################################
def update_document(session, recurso, body):
if not session:
print("❌ Error: No se pudo iniciar sesión en SAP Service Layer")
return None
# print(f" 📤 [PATCH] Body enviado a {recurso}:")
# print(json.dumps(body, indent=2, default=str))
response = sap_utilities.patch(session, recurso, body)
if response and response.status_code == 204:
return response
status = response.status_code if response is not None else "Sin respuesta (None)"
print(f"❌ Error actualizando documento en SAP Service Layer: HTTP {status}")
return None
###################################################
+142
View File
@@ -0,0 +1,142 @@
import requests
import urllib3
from config_loader import CONFIG
###################################################
def post(session, recurso:str, json_body: dict = None):
url = CONFIG.service_layer.url
if not url.endswith('/'):
url += '/'
headers = {
"accept": "application/json",
"content-type": "application/json"
}
urllib3.disable_warnings()
end_point = f"{url}{recurso}"
try:
response = None
if json_body:
response = session.post(
end_point,
headers = headers,
json = json_body,
verify = False
)
else:
response = session.post(
end_point,
headers = headers,
verify = False
)
response.raise_for_status()
print(f" ✅ [POST] {recurso} - Status {response.status_code}")
return response
except requests.RequestException as ex:
print(f" ❌ [POST] {recurso} - Falló: {ex}")
return None
###################################################
def get(session, recurso:str):
SL_MAXPAGESIZE = CONFIG.service_layer.maxpagesize
url = CONFIG.service_layer.url
if not url.endswith('/'):
url += '/'
headers = {
"accept": "application/json",
"content-type": "application/json",
"Prefer": f"odata.maxpagesize={SL_MAXPAGESIZE}"
}
urllib3.disable_warnings()
end_point = f"{url}{recurso}"
try:
response = session.get(
end_point,
headers = headers,
verify = False
)
response.raise_for_status()
print(f" ✅ [GET] {recurso} - Status {response.status_code}")
return response
except requests.RequestException as ex:
print(f" ❌ [GET] {recurso} - Falló: {ex}")
return None
###################################################
def patch(session, recurso:str, json_body: dict):
url = CONFIG.service_layer.url
REPLACE_COLLECTIONS = CONFIG.service_layer.b1s_replacecollectionsonpatch
if not url.endswith('/'):
url += '/'
headers = {
"accept": "application/json",
"content-type": "application/json"
}
if REPLACE_COLLECTIONS:
headers["B1S-ReplaceCollectionsOnPatch"] = "true"
urllib3.disable_warnings()
end_point = f"{url}{recurso}"
try:
response = session.patch(
end_point,
headers = headers,
json = json_body,
verify = False
)
if response.status_code == 204:
print(f" ✅ [PATCH] {recurso} - Status {response.status_code}")
return response
if 400 <= response.status_code <= 499:
print(f" ❌ [PATCH] {recurso} - Error HTTP {response.status_code}")
try:
error_body = response.json()
import json as _json
error = error_body.get("error", {})
mensaje = error.get("message", {})
if isinstance(mensaje, dict):
mensaje = mensaje.get("value", str(mensaje))
codigo = error.get("code", "N/A")
print(f" 📋 SAP Error Code : {codigo}")
print(f" 📋 SAP Error Message: {mensaje}")
# TransactionNotification (validaciones de UDF, triggers, etc.)
notif = error_body.get("TransactionNotification") or error.get("innererror", {}).get("TransactionNotification")
if notif:
print(f" 📋 TransactionNotification: {notif}")
except Exception:
print(f" 📋 Respuesta SAP (raw): {response.text}")
return None
# Otros errores no esperados
response.raise_for_status()
return response
except requests.RequestException as ex:
print(f" ❌ [PATCH] {recurso} - Excepción: {ex}")
return None
###################################################
+22
View File
@@ -0,0 +1,22 @@
info:
name: Login
type: http
http:
method: POST
url: https://192.168.1.175:50000/b1s/v1/Login
body:
type: json
data: |-
{
"CompanyDB": "SBO_TAGUMEDICA_21",
"UserName": "manager",
"Password": "m1r1"
}
auth: inherit
settings:
encodeUrl: true
timeout: 0
followRedirects: true
maxRedirects: 5
+10
View File
@@ -0,0 +1,10 @@
opencollection: 1.0.0
info:
name: SAPBusinessOne
bundled: false
extensions:
bruno:
ignore:
- node_modules
- .git
+20191
View File
File diff suppressed because it is too large. Load diff