Continua a imparare con
LOKAD TV
solve.moq
G.Buy = solve.moq( Item: Id Quantity: G.Min Reward: G.Reward Cost: G.Cost // Obbligatorio uno tra: MaxCost: maxBudget MaxTarget: maxTarget MinTarget: minTarget // Facoltativo: Target: G.Target TargetGroup: Supplier // Facoltativo, ma deve avere // lo stesso numero per ognuno, massimo 8 GroupId: A, B GroupQuantity: G.A, G.B GroupMinQuantity: AMoq, BMoq)
G
: è la griglia, una tabella ottenuta di solito attraverso extend.distrib();Item
(articolo): identificativi della SKU o dei prodotti rilevanti dal punto di vista dell'ottimizzazione dei MOQ;Quantity
(quantità): quantità della griglia, utilizzata per ordinare le righe della griglia;Reward
(rendimento): ritorno economico associato all'acquisto della riga della griglia;Cost
(costo): costo economico associato all'acquisto della riga della griglia;MaxCost
(costo massimo): soglia per una delle tre modalità di ottimizzazione da scegliere per il solutore. MaxCost
indica che le righe della griglia sono considerate fino a che il budget non viene raggiunto e che nessuna riga può essere aggiunta senza superare il budget;MaxTarget
(obiettivo massimo): soglia per una delle tre modalità di ottimizzazione da scegliere per il solutore. Se usata, l'obiettivo viene raggiunto partendo dal basso; nessuna altra riga potrà essere aggiunta senza superare l'obiettivo;MinTarget
(obiettivo minimo): soglia per una delle tre modalità di ottimizzazione da scegliere per il solutore. Se usata, l'obiettivo viene raggiunto partendo dall'alto; nessuna altra riga potrà essere aggiunta senza restare al di sotto dell'obiettivo;Target
(obiettivo): contributo all'obiettivo associato alla riga della griglia. Da applicare solo se sono già specificati MaxTarget
o MinTarget
;TargetGroup
: se specificato, per ogni gruppo viene eseguita un'ottimizzazione MOQ distinta. Il valore predefinito implicito è una costante per tutti gli articoli;GroupId
(id gruppo): identifica il raggruppamento valido per il vincolo MOQ;GroupQuantity
(quantità gruppo): contributo della riga della griglia al vincolo MOQ;GroupMinQuantity
(quantità minima gruppo): limite inferiore del vincolo MOQ.GroupId
, GroupQuantity
e GroupMinQuantity
, possono avere più argomenti, uno per ogni diverso vincolo MOQ; da notare, però, che ogni parametro dovrà avere lo stesso numero di argomenti. I solutori MOQ possono gestire fino a 8 argomenti, uno per vincolo MOQ.MOQ
. Se non abbiamo un simile vincolo, allora questo campo sarà uguale a 1.read "/sample/Lokad_Items.tsv" read "/sample/Lokad_Orders.tsv" as Orders read "/sample/Lokad_PurchaseOrders.tsv" as PO // Filtro per PO archiviati where PO.DeliveryDate > PO.Date 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 // % relativa al prezzo di vendita oosPenalty := 0.25 // % di costi annui di mantenimento a magazzino relativa al prezzo di acquisto carryingCost := 0.3 // % di sconto economico annuale discount := 0.20 M = SellPrice - BuyPrice S = - 0.25 * SellPrice // svantaggio per rottura di stock // % dello 0,3 come costo annuo di mantenimento a magazzino C = - 0.3 * BuyPrice * mean(Leadtime) / 365 // in caso di ordini arretrati MB = 0.5 * SellPrice MBU = MB * uniform(1, Backorder) // in caso di ordini arretrati SB = 0.5 * SellPrice SBU = SB * uniform(1, Backorder) // opportunità di acquistare più avanti AM = 0.3 // % dello 0,2 come sconto economico annuale AC = 1 - 0.2 * mean(Leadtime) / 365 RM = MBU + (stockrwd.m(Demand, AM) * M) >> Backorder RS = SBU + zoz(stockrwd.s(Demand) * S) >> Backorder RC = (stockrwd.c(Demand, AC) * C) >> BackOrder R = RM + RS + RC // ricomposizione semplice Stock = StockOnHand + StockOnOrder DBO = Demand >> BackOrder table G = extend.distrib(DBO, Stock) G.Q = G.Max - G.Min + 1 G.Reward = int(R, G.Min, G.Max) G.Cost = BuyPrice * G.Q where G.Max >= Stock 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" unit: "$" sum(BuyPrice * G.Q) as "Purchase Cost" unit: "$" group by Id order by [sum(G.Reward) / sum(G.Cost)] desc
moqsolv
. In questo caso particolare, abbiamo un solo vincolo di questo tipo, ma la funzione è in grado di gestire anche più vincoli legati al quantitativo minimo d'ordine. La funzione moqsolv
restituisce true
(vero) per le righe della griglia che possono concorrere al risultato finale. Per fare ciò, moqsolv
utilizza un ottimizzatore non lineare avanzato, pensato proprio per gestire il problema MOQ.read "/sample/Lokad_Items.tsv" read "/sample/Lokad_Orders.tsv" as Orders read "/sample/Lokad_PurchaseOrders.tsv" as PO // ... G.Eligible = solve.moq( Item: Id Quantity: G.Min Reward: G.Reward Cost: G.Cost MaxCost: budget GroupId: SubCategory GroupQuantity: G.Q // MOQ per sottocategoria GroupMinQuantity: MinOrderQuantity) // ...
moqsolv
: questa è l'unica riga che cambia, poiché contiene come argomento un vincolo MOQ alternativo.read "/sample/Lokad_Items.tsv" read "/sample/Lokad_Orders.tsv" as Orders read "/sample/Lokad_PurchaseOrders.tsv" as PO LotMultiplier = 5 // Filtro per PO archiviati where PO.DeliveryDate > PO.Date 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 // % relativa al prezzo di vendita oosPenalty := 0.25 // % di costi annui di mantenimento a magazzino relativa al prezzo di acquisto carryingCost := 0.3 // % di sconto economico annuale discount := 0.20 M = SellPrice - BuyPrice // svantaggio per rottura di stock S = - 0.25 * SellPrice // % dello 0,3 come costi annui di mantenimento a magazzino C = - 0.3 * BuyPrice * mean(Leadtime) / 365 // in caso di ordini arretrati MB = 0.5 * SellPrice MBU = MB * uniform(1, Backorder) // in caso di ordini arretrati SB = 0.5 * SellPrice SBU = SB * uniform(1, Backorder) // opportunità di acquistare più avanti AM = 0.3 // % dello 0,2 come sconto economico annuale AC = 1 - 0.2 * mean(Leadtime) / 365 RM = MBU + (stockrwd.m(Demand, AM) * M) >> Backorder RS = SBU + zoz(stockrwd.s(Demand) * S) >> Backorder RC = (stockrwd.c(Demand, AC) * C) >> BackOrder R = RM + RS + RC // ricomposizione semplice Stock = StockOnHand + StockOnOrder DBO = Demand >> BackOrder // il terzo argomento è "LotMultiplier" (moltiplicatore di partite) table G = extend.distrib(DBO, Stock, LotMultiplier) G.Q = G.Max - G.Min + 1 G.Reward = int(R, G.Min, G.Max) G.Cost = BuyPrice * G.Q where G.Max > Stock // vincolo MOQ fittizio 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" unit: "$" sum(BuyPrice * G.Q) as "Purchase Cost" unit: "$" group by Id order by [sum(G.Reward) / sum(G.Cost)] desc
extend.distrib()
, che mira proprio a "catturare" i vincoli relativi al moltiplicatore di partite. Il terzo argomento della funzione, infatti, è proprio la quantità del moltiplicatore di partite.S
(fornitore), che indichi il venditore principale, e che a ogni articolo corrisponda esattamente un fornitore.read "/sample/Lokad_Items.tsv" read "/sample/Lokad_Orders.tsv" as Orders read "/sample/Lokad_PurchaseOrders.tsv" as PO // Filtro per PO archiviati where PO.DeliveryDate > PO.Date 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) // volume atteso del container (in m3) cV := 15 // soglia di salto attesa per il container cJT := 2 * cV // % relativa al prezzo di vendita oosPenalty := 0.25 // % dei costi annui di mantenimento a magazzino relativa al prezzo di acquisto carryingCost := 0.3 // % di sconto economico annuale discount := 0.20 M = SellPrice - BuyPrice // svantaggio per rottura di stock S = - 0.25 * SellPrice // % dello 0,3 come costo annuo di mantenimento a magazzino C = - 0.3 * BuyPrice * mean(Leadtime) / 365 // in caso di ordini arretrati MB = 0.5 * SellPrice MBU = MB * uniform(1, Backorder) // in caso di ordini arretrati SB = 0.5 * SellPrice SBU = SB * uniform(1, Backorder) // opportunità di acquistare più avanti AM = 0.3 // % dello 0,2 come sconto economico annuale AC = 1 - 0.2 * mean(Leadtime) / 365 RM = MBU + (stockrwd.m(Demand, AM) * M) >> Backorder RS = SBU + zoz(stockrwd.s(Demand) * S) >> Backorder RC = (stockrwd.c(Demand, AC) * C) >> BackOrder R = RM + RS + RC // ricomposizione semplice Stock = StockOnHand + StockOnOrder DBO = Demand >> BackOrder table G = extend.distrib(DBO, Stock) G.Q = G.Max - G.Min + 1 G.Rwd = int(R, G.Min, G.Max) // ricompensa G.Score = G.Rwd / max(1, BuyPrice * G.Q) G.V = Volume * G.Q where G.Max > Stock G.Rk = rank(G.Score, Id, -G.Max) // "S" sta per fornitore (dall'inglese "supplier") G.CId = priopack(G.V, cV, cJT, Id) by S sort G.Rk // riempire il container per il // fornitore più urgente where sum(G.Q) > 0 show table "Containers \{cV}m3" a1f4 tomato with same(Supplier) as "Supplier" G.CId as "Container" Id as "Id" sum(G.Q) as "Quantity" sum(G.Rwd) as "Reward" unit:"$" sum(BuyPrice * G.Q) as "Investment" unit:"$" sum(G.V) as "Volume{ m3}" group by [G.CId, Id] order by [avg(sum(G.Rwd) by [S, G.CId])] desc
stockrwd
. La logica di raggruppamento (ossia quella utilizzata per suddividere le quantità tra vari container) è la funzione priopack
, che è stata introdotta in Envision proprio per le situazioni in cui è necessario suddividere gli acquisti in container.