Pedidos de compra con limitaciones de pedido

MOQ y otras restricciones de pedido











Inicio » Recursos » Aquí Los pronósticos probabilísticos ofrecen la posibilidad de generar una lista de prioridad de compra en la que cada unidad incremental que debe comprarse se clasifica con respecto a sus impulsores comerciales, como el margen bruto y el coste de almacenamiento de inventario esperados.

Los pronósticos probabilísticos ofrecen la posibilidad de generar una lista de prioridad de compra en la que cada unidad progresiva que debe comprarse se clasifica con respecto a sus impulsores comerciales, como el margen bruto y el coste de almacenamiento de inventario esperados. Las MOQ (cantidades de orden mínimas), sin embargo, introducen limitaciones no lineales que complican el cálculo de las cantidades de pedido de compra. Para satisfacer este requisito que a menudo se encuentra en las cadenas de suministro, Lokad ha diseñado un solver numérico pensado específicamente para las MOQ. El solver se aplica a la situación en la que las MOQ se combinan con una limitación de contenedor.


La función de llamada solve.moq

El solver de MOQ es un solver numérico especializado al que puede accederse dentro de Envision como función de llamada. El marco de referencia matemático es el problema general de MOQ, que representa un problema de programación entera. La sintaxis de esta función se ilustra a continuación.

G.Buy = solve.moq(
Item: Id 
Quantity: G.Min
Reward: G.Reward 
Cost: G.Cost
// Proporcionar una de estas tres: 
MaxCost: maxBudget
MaxTarget: maxTarget
MinTarget: minTarget
// Opcional:
Target: G.Target 
// Opcional, pero debe tener el mismo número para cada una, máx. 8
GroupId: A, B
GroupQuantity: G.A, G.B
GroupMinQuantity: AMoq, BMoq)

Los parámetros son los siguientes:

  • G: la grilla, una tabla que generalmente se obtiene a través de extend.distrib().
  • Item: los identificadores de las SKU o de los productos relevantes para la optimización de la MOQ.
  • Quantity: la cantidad de grilla, utilizada para ordenar las líneas de la grilla.
  • Reward: la recompensa económica asociada con la compra de la línea de la grilla.
  • Cost: el costo económico de la compra de la línea de la grilla.
  • MaxCost: umbral de uno de los tres modos de optimización para el solver. El MaxCost indica que se tomarán líneas de la grilla hasta que se haya agotado el presupuesto, y no se podrá agregar ninguna otra línea sin superar el presupuesto.
  • MaxTarget: ídem. Cuando se utiliza, se llega al objetivo desde abajo; no es posible agregar más líneas de grilla sin exceder el objetivo.
  • MinTarget: ídem. Cuando se utiliza, se llega al objetivo desde arriba; no es posible agregar más líneas de grilla sin quedar por debajo del objetivo.
  • Target: la contribución objetivo asociada con la línea de grilla. Aplicar solo cuando se hayan especificado MaxTarget o MinTarget.
  • GroupId: identifica la agrupación para limitaciones de MOQ.
  • GroupQuantity: la contribución de la línea de grilla a la limitación de MOQ.
  • GroupMinQuantity: el límite inferior de la limitación de MOQ.

Los tres parámetros GroupId, GroupQuantity y GroupMinQuantity pueden tener varios argumentos, uno para cada limitación de MOQ; sin embargo, debería proporcionarse la misma cantidad de argumentos para cada parámetro. Pueden pasarse hasta 8 argumentos a los solvers de MOQ, que representan la misma cantidad de limitaciones de MOQ diferentes.

Cantidad de orden mínima (MOQ) por SKU

Los proveedores a menudo imponen cantidades de orden mínima (MOQ) a sus clientes. Tales limitaciones de MOQ pueden aplicarse en varios niveles: por SKU, por categoría, por pedido, etc. Supongamos que estamos frente a limitaciones MOQ a nivel de SQU: por cada SKU, hay una cantidad mínima que debe pedirse, y superado ese umbral, queda a criterio de la persona que pide los artículos decidir si es necesario pedir unidades adicionales de esa SKU específica o no. En el script a continuación, suponemos que el archivo items contiene una columna MOQ. Si no existe una limitación de MOQ aplicable, se espera que este campo sea igual a 1.

read "/sample/Lokad_Items.tsv"
read "/sample/Lokad_Orders.tsv" as Orders
read "/sample/Lokad_PurchaseOrders.tsv" as PO

where PO.DeliveryDate > PO.Date //Filtrado de PO cerrados
Horizon = forecast.leadtime(
hierarchy: Category, SubCategory
present: (max(Orders.Date) by 1) + 1
leadtimeDate: PO.Date
leadtimeValue: PO.DeliveryDate - PO.Date + 1)

Demand = forecast.demand(
horizon: Horizon
hierarchy: Category, SubCategory
present: (max(Orders.Date) by 1) + 1
demandDate: Orders.Date
demandValue: Orders.Quantity)

budget := 1000
MinOrderQuantity = 5

oosPenalty := 0.25 // % relativo al precio de venta
carryingCost := 0.3 // % costo de almacenamiento relativo al precio de compra
discount := 0.20 // % descuento económico anual

M = SellPrice - BuyPrice
S = - 0.25 * SellPrice // penalización por desabastecimiento
C = - 0.3 * BuyPrice * mean(Leadtime) / 365 // % '0,3' como costo de almacenamiento anual
MB = 0.5 * SellPrice // caso de pedido pendiente
SB = 0.5 * SellPrice // caso de pedido pendiente
AM = 0.3 // oportunidad de comprar más tarde
AC = 1 - 0.2 * mean(Leadtime) / 365 // % '0,2' como descuento económico anual

RM = MB * uniform(1, Backorder) + (stockrwd.m(Demand, AM) * M) >> Backorder
RS = SB * uniform(1, Backorder) + zoz(stockrwd.s(Demand) * S) >> Backorder
RC = (stockrwd.c(Demand AC) * C) >> BackOrder
R = RM + RS + RC // recomposición simple

table G = extend.distrib(Demand >> BackOrder, StockOnHand + StockOnOrder)
G.Q = G.Max - G.Min + 1
G.Reward = int(R, G.Min, G.Max)
G.Cost = BuyPrice * G.Q

where G.Max >= StockOnHand + StockOnOrder
G.Eligible = solve.moq(
Item: Id
Quantity: G.Min
Reward: G.Reward
Cost: G.Cost
MaxCost: budget
GroupId: Id
GroupQuantity: G.Q
GroupMinQuantity: MinOrderQuantity)

where G.Eligible & sum(G.Eligible ? 1 : 0) > 0
show table "Purchase priority list (budget: $\{budget})" a1f4 tomato with
Id as "Id"
MinOrderQuantity as "MOQ"
sum(G.Q) as "Quantity"
sum(G.Reward) as "Reward{$}"
sum(BuyPrice * G.Q) as "Purchase Cost{$}"
group by Id
order by sum(G.Reward) / sum(G.Cost) desc

Este script produce un panel de información en el que las limitaciones de MOQ se satisfacen para todas las líneas de la lista. Para satisfacer esas limitaciones, utilizamos la función especial moqsolv de Envision. En el caso actual, tenemos solo un tipo de limitaciones de MOQ, pero la función moqsolv también funciona con varias limitaciones de MOQ. La función moqsolv devuelve el valor true en las líneas de la grilla elegidas para que formen parte del resultado final. Si se la analiza, moqsolv está utilizando un optimizador no lineal avanzado que ha sido creado específicamente para el problema de MOQ.

Cantidades de orden mínima (MOQ) por grupos de SKU

Hemos visto en la sección anterior cómo gestionar las limitaciones de MOQ a nivel de SKU. Ahora, veamos de qué modo se pueden gestionar estas limitaciones en un nivel de agregación superior. Supongamos que el umbral de MOQ está disponible como parte del archivo items. Debido a que la limitación de MOQ se aplica a un determinado nivel de agrupación, suponemos, para mantener la coherencia, que todos los artículos que pertenecen al mismo grupo de MOQ tienen el mismo valor MOQ. El script a continuación ilustra el modo en que se puede gestionar esta limitación multi-SKU.

read "/sample/Lokad_Items.tsv"
read "/sample/Lokad_Orders.tsv" as Orders
read "/sample/Lokad_PurchaseOrders.tsv" as PO

// fragmentado ..

G.Eligible = solve.moq(
Item: Id
Quantity: G.Min
Reward: G.Reward
Cost: G.Cost
MaxCost: budget
GroupId: SubCategory
GroupQuantity: G.Q
GroupMinQuantity: MinOrderQuantity) // MOQ por subcategoría

// fragmentado ...

El script anterior es en realidad casi idéntico al script de la sección anterior. Para que resulte claro, solo mostramos la llamada a moqsolv, ya que es la única línea que cambia, debido a que toma una limitación de MOQ alternativa como argumento.

Multiplicadores de lote por SKU

A veces, las SKU solo pueden pedirse en ciertas cantidades y, a diferencia de la limitación de cantidad de orden mínima (MOQ) detallada anteriormente, la cantidad pedida debe ser múltiplo de una cantidad "base". Por ejemplo, un producto solo puede pedirse en cajas de 12 unidades. No es posible pedir 13 unidades, ni 12 o 24. A esa cantidad por la que se multiplica la llamamos multiplicador de lote. Es posible ajustar la lógica de priorización para que se ajuste también a esta limitación.

read "/sample/Lokad_Items.tsv"
read "/sample/Lokad_Orders.tsv" as Orders
read "/sample/Lokad_PurchaseOrders.tsv" as PO

LotMultiplier = 5

where PO.DeliveryDate > PO.Date //Filtrado de PO cerrados
Horizon = forecast.leadtime(
hierarchy: Category, SubCategory
present: (max(Orders.Date) by 1) + 1
leadtimeDate: PO.Date
leadtimeValue: PO.DeliveryDate - PO.Date + 1)

Demand = forecast.demand(
horizon: Horizon
hierarchy: Category, SubCategory
present: (max(Orders.Date) by 1) + 1
demandDate: Orders.Date
demandValue: Orders.Quantity)

show form "Purchase with lot multipliers" a1b2 tomato with Form.budget as "Max budget"
budget := Form.budget

oosPenalty := 0.25 // % relativo al precio de venta
carryingCost := 0.3 // % costo de almacenamiento relativo al precio de compra
discount := 0.20 // % descuento económico anual

M = SellPrice - BuyPrice
S = - 0.25 * SellPrice // penalización por desabastecimiento
C = - 0.3 * BuyPrice * mean(Leadtime) / 365 // % '0,3' como costo de almacenamiento anual
MB = 0.5 * SellPrice // caso de pedido pendiente
SB = 0.5 * SellPrice // caso de pedido pendiente
AM = 0.3 // oportunidad de comprar más tarde
AC = 1 - 0.2 * mean(Leadtime) / 365 // % '0,2' como descuento económico anual

RM = MB * uniform(1, Backorder) + (stockrwd.m(Demand, AM) * M) >> Backorder
RS = SB * uniform(1, Backorder) + zoz(stockrwd.s(Demand) * S) >> Backorder
RC = (stockrwd.c(Demand AC) * C) >> BackOrder
R = RM + RS + RC // recomposición simple

// el tercer argumento es 'LotMultiplier'
table G = extend.distrib(Demand >> BackOrder, StockOnHand + StockOnOrder, LotMultiplier)
G.Q = G.Max - G.Min + 1
G.Reward = int(R, G.Min, G.Max)
G.Cost = BuyPrice * G.Q

where G.Max > StockOnHand + StockOnOrder
// una limitación de MOQ ficticia
G.Eligible = solve.moq(
Item: Id
Quantity: G.Min
Reward: G.Reward
Cost: G.Cost
MaxCost: budget
GroupId: Id
GroupQuantity: G.Q
GroupMinQuantity: 1)

where G.Eligible & sum(G.Eligible ? 1 : 0) > 0
show table "Purchase priority list (budget: $\{budget})" a1f4 tomato with
Id as "Id"
sum(G.Q) as "Quantity"
sum(G.Reward) as "Reward{$}"
sum(BuyPrice * G.Q) as "Purchase Cost{$}"
group by Id
order by sum(G.Reward) / sum(G.Cost) desc

El script aprovecha un comportamiento especial de la función extend.distrib() que está precisamente pensado para captar limitaciones de multiplicador de lote. El tercer argumento de esta función es justamente la cantidad de multiplicador de lote.

Capacidad de contenedor objetivo por proveedor

Con las importaciones al extranjero, a menudo viene asociada la limitación de comprar hasta llenar un contenedor o medio contenedor. El volumen del contenedor es un dato que se conoce, y en este ejemplo suponemos que los volúmenes de todos los artículos que se compran también se conocen. El objetivo es componer una lista de selecciones de artículos que represente los contenidos del próximo contenedor lleno por comprar.

Para complicar un poco más las cosas, supongamos que no hay consolidación de envío entre proveedores. Así, para componer un contenedor, todas las líneas de compra deberían asociarse al mismo proveedor. Esto implica que uno primero debería identificar al proveedor más apremiante y, luego, llenar el contenedor de consecuencia. Supongamos que el archivo items contiene una columna Supplier que indica el proveedor principal, suponiendo que estamos en un escenario de mono-suministro (es decir, cada artículo tiene exactamente un proveedor).

read "/sample/Lokad_Items.tsv"
read "/sample/Lokad_Orders.tsv" as Orders
read "/sample/Lokad_PurchaseOrders.tsv" as PO

where PO.DeliveryDate > PO.Date //Filtrado de PO cerrados
Horizon = forecast.leadtime(
hierarchy: Category, SubCategory
present: (max(Orders.Date) by 1) + 1
leadtimeDate: PO.Date
leadtimeValue: PO.DeliveryDate - PO.Date + 1)

Demand = forecast.demand(
horizon: Horizon
hierarchy: Category, SubCategory
present: (max(Orders.Date) by 1) + 1
demandDate: Orders.Date
demandValue: Orders.Quantity)

cVolume := 15 // volumen esperado del contenedor (m3)

oosPenalty := 0.25 // % relativo al precio de venta
carryingCost := 0.3 // % costo de almacenamiento relativo al precio de compra
discount := 0.20 // % descuento económico anual

M = SellPrice - BuyPrice
S = - 0.25 * SellPrice // penalización por desabastecimiento
C = - 0.3 * BuyPrice * mean(Leadtime) / 365 // % '0,3' como costo de almacenamiento anual
MB = 0.5 * SellPrice // caso de pedido pendiente
SB = 0.5 * SellPrice // caso de pedido pendiente
AM = 0.3 // oportunidad de comprar más tarde
AC = 1 - 0.2 * mean(Leadtime) / 365 // % '0,2' como descuento económico anual

RM = MB * uniform(1, Backorder) + (stockrwd.m(Demand, AM) * M) >> Backorder
RS = SB * uniform(1, Backorder) + zoz(stockrwd.s(Demand) * S) >> Backorder
RC = (stockrwd.c(Demand AC) * C) >> BackOrder
R = RM + RS + RC // recomposición simple

table G = extend.distrib(Demand >> BackOrder, StockOnHand + StockOnOrder)
G.Q = G.Max - G.Min + 1
G.Reward = int(R, G.Min, G.Max)
G.Score = G.Reward / max(1, BuyPrice * G.Q)
G.Volume = Volume * G.Q

where G.Max > StockOnHand + StockOnOrder
G.Rank = rank(G.Score, Id, -G.Max)
G.CId = priopack(G.Rank, G.Volume, Supplier, cVolume, 2 * cVolume, Id)

// llenado del contenedor para el proveedor más apremiante
where sum(G.Q) > 0
show table "Containers (container size: \{cVolume})" a1f4 tomato with
same(Supplier) as "Supplier"
G.CId as "Container"
Id as "Id"
sum(G.Q) as "Quantity"
sum(G.Reward) as "Reward{$}"
sum(BuyPrice * G.Q) as "Investment{$}"
sum(G.Volume) as "Volume{ m3}"
group by (G.CId, Id)
order by avg(sum(G.Reward) by (Supplier, G.CId)) desc

Este panel de información elabora una sola tabla que contiene los detalles de la lista de lotes ordenados por recompensa decreciente. Las recompensas por existencias se calculan sobre la base de la función stockrwd. La lógica de loteo —es decir, la de dividir cantidades en diferentes contenedores— se realiza utilizando la función priopack. Esta función ha sido introducida en Envision específicamente para gestionar la compra con limitaciones por contenedor.