Files
2026-05-28 15:08:25 -05:00

356 lines
13 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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.")
###################################################