Startseite »
Ressourcen » Hier
Probabilistische Prognosen bieten die Möglichkeit, Prioritätenlisten für Einkäufe zu erstellen, bei denen jede zusätzliche Einheit, die gekauft werden soll, entsprechend der Unternehmenstreiber, wie etwa der Bruttogewinnspanne und der erwarteten Lagerhaltungskosten, geordnet wird. Doch Mindestbestellmengen (MOQ) sind mit nicht-linearen Bedingungen verbunden, die die Berechnung der Bestellmengen erschwert. Lokad hat zur Berücksichtigung dieser Bedingungen, die bei Lieferketten häufig vorkommen, einen numerischen Löser, eigens für MOQs entworfen. Dieser Löser kann auch in Fällen angewandt werden, bei denen MOQs mit Containerbedingungen kombiniert werden.
Die solve.moq
Aufruffunktion
Der MOQ-Löser ist ein bestimmter numerischer Löser, der in Envision als
Aufruffunktion genutzt werden kann. Der mathematische Hintergrund dreht sich um das
allgemeine MOQ-Problem, das ein Integer Programmierprogramm darstellt. Die Syntax der Funktion lautet, wie folgt:
G.Buy = solve.moq(
Item: Id
Quantity: G.Min
Reward: G.Reward
Cost: G.Cost
// Angabe eines dieser drei:
MaxCost: maxBudget
MaxTarget: maxTarget
MinTarget: minTarget
// Optional:
Target: G.Target
TargetGroup: Supplier
// Optional, doch es muss
//dieselbe Zahl für jedes haben, höchstens 8
GroupId: A, B
GroupQuantity: G.A, G.B
GroupMinQuantity: AMoq, BMoq)
Die Parameter lauten, wie folgt:
G
: die Tabelle, eine Tabelle, die gewöhnlich über extend.distrib() erhalten wird.Item
: die Kennung der SKUs oder Produkte, die für die MOQ-Optimierung relevant sind.Quantity
: die Tabellenmengen, die für die Sortierung der Zeilen der Tabelle genutzt wird.Reward
: die finanzielle Belohnung beim Kauf der Zeile der Tabelle.Cost
: die finanziellen Kosten beim Kauf des Zeile der Tabelle.MaxCost
: Schwelle für eines der drei Optimierungsmodi der Löser. Bei MaxCost
werden so viele Zeilen der Tabelle genommen, bis das Budget aufgebraucht ist, und keine weiteren Linien hinzugefügt werden können, ohne das Budget zu überschreiten.MaxTarget
: dasselbe. Wenn dies benutzt wird, wird das Ziel von unten erreicht; er können keine weiteren Zeilen der Tabelle hinzugefügt werden, ohne das Ziel zu überschreiten.MinTarget
: dasselbe. Wenn dies benutzt wird, wird das Ziel von oben erreicht; Es können keine weiteren Zeilen der Tabelle hinzugefügt werden, ohne unter dem Ziel zu liegen.Target
: der Zielbeitrag, der der dieser Zeile zugeordnet wird. Nur anwenden, wenn MaxTarget
oder MinTarget
angegeben wird.TargetGroup
: Wenn angegeben, wird eine separate Optimierung der Mindestbestellmenge für jede Gruppe vorgenommen. Der implizite Standardwert ist über alle Artikel hinweg eine Konstante.GroupId
: erkennt die Gruppen für die MOQ-Bedingungen.GroupQuantity
: der Beitrag der Zeile der Tabelle zu den MOQ-Bedingungen.GroupMinQuantity
: die untere Grenze der MOQ-Bedingungen.
GroupId
,
GroupQuantity
und
GroupMinQuantity
, die drei letzen Parameter können über mehrere Argumente verfügen, eines für jede MOQ-Bedingung. Doch in jedem Fall sollte dieselbe Anzahl an Argumenten für jeden Parameter angegeben werden. Insgesamt können bis zu 8 Argumente an den MOQ-Löser übergeben werden, was ebenso viele unterschiedliche MOQ-Bedingungen darstellt.
Mindestbestellmengen (MOQ) pro SKU
Lieferanten legen häufig Mindestbestellmengen (MOQs) für Ihre Kunden fest. Solche MOQ-Bedingungen können auf verschiedenen Ebenen angewandt werden: pro SKU, pro Kategorie, pro Bestellung, usw. Angenommen wir müssen MOQ-Bedingungen auf SKU-Ebene berücksichtigen, besteht eine Mindestmenge, die bestellt werden muss und unter dieser Schwelle muss der Auftraggeber entscheiden, ob es notwendig ist, zusätzliche Einheiten der bestimmten SKU zu bestellen oder nicht. Im unteren Skript gehen wir davon aus, dass die Datei
items eine
MOQ
Spalte enthält. Ist keine MOQ-Bedingungen vorhanden, wird erwartet, dass dieses Feld gleich 1 ist.
read "/sample/Lokad_Items.tsv"
read "/sample/Lokad_Orders.tsv" as Orders
read "/sample/Lokad_PurchaseOrders.tsv" as PO
//Filtern auf geschlossene Aufträge
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
// % in Bezug auf den Verkaufspreis
oosPenalty := 0.25
// % jährliche Lagerhaltungskosten in Bezug auf den Kaufpreis
carryingCost := 0.3
// % jährlicher Abzinsungsfaktor
discount := 0.20
M = SellPrice - BuyPrice
S = - 0.25 * SellPrice // Bestrafung für Fehlbestände
//% '0.3' als jährliche Lagerhaltungskosten
C = - 0.3 * BuyPrice * mean(Leadtime) / 365
// Auftragsrückstand
MB = 0.5 * SellPrice
MBU = MB * uniform(1, Backorder)
// Auftragsrückstand
SB = 0.5 * SellPrice
SBU = SB * uniform(1, Backorder)
// Chance zu späteren Kauf
AM = 0.3
// % '0.2' als jährlicher Abzinsungsfaktor
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 // einfache erneute Zusammensetzung
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
Dieses Skript erstellt ein Dashboard, in dem die MOQ-Bedingungen für alle Zeilen der Liste korrekt berücksichtigt werden. Um diese Bedingungen zu erfüllen, nutzen wir die besondere
moqsolv
-Funktion von Envision. Im vorliegenden Fall besteht nur 1 Typ von MOQ-Bedingung, doch die
moqsolv
-Funktion kann auch für mehrere MOQ-Bedingungen angewandt werden. Die
moqsolv
-Funktion gibt für die Zeilen der Tabelle, die als Teil des Endergebnisses gewählt werden,
true
zurück. Im Hintergrund benutzt
moqsolv
einen erweiterten nichtlinearen Optimierer, der speziell für MOQ-Probleme entwickelt wurde.
Mindestbestellmengen (MOQ) pro SKU-Gruppe
In den vorangehenden Absätzen haben wir gezeigt, wie man MOQ-Bedingungen auf SKU-Ebene bewältigen kann. Nun lassen Sie uns zeigen, wie man solche Bedingungen auf einer höhere Aggregationsebene einbauen kann. Gehen wir davon aus, dass die MOQ-Grenze als Teil der
items-Datei vorhanden ist. Da sich MOQ-Bedingungen auf eine bestimmte Gruppenebene beziehen, können wir der Konsequenz halber davon ausgehen, dass alle Artikel, die zu derselben MOQ-Gruppe gehören, den gleichen MOQ-Wert haben. Das untere Skript veranschaulicht, wie diese Bedingung mit mehreren SKUs berücksichtigt werden kann.
read "/sample/Lokad_Items.tsv"
read "/sample/Lokad_Orders.tsv" as Orders
read "/sample/Lokad_PurchaseOrders.tsv" as PO
// snipped ..
G.Eligible = solve.moq(
Item: Id
Quantity: G.Min
Reward: G.Reward
Cost: G.Cost
MaxCost: budget
GroupId: SubCategory
GroupQuantity: G.Q
// MOQ pro subcategory
GroupMinQuantity: MinOrderQuantity)
// snipped ...
Der obere Skript ist fast identisch zum Skript im vorangehenden Abschnitt. Der Übersichtlichkeit halber wird nur der Aufruf von
moqsolv
angezeigt, da diese die einzige Zeile ist, die sich verändert, wenn sie eine alternative MOQ-Bedingung als Argument aufgenommen wird.
Multiplikatoren von Losgrößen für SKU
Manchmal können SKUs nur in bestimmtem Mengen bestellt werden. Im Gegensatz zu den oben genannten Bedingungen bei Mindestbestellmengen (MOQ), müssen die Lose ein Vielfaches einer Grundmenge "base" sein. Wenn ein Produkt beispielsweise nur in Kisten mit 12 Einheiten verkauft werden kann, ist es nicht möglicht, 13 Einheiten zu bestellen, sondern nur 12 oder 24. So beziehen wir uns auf die Menge, die multipliziert werden muss als
lot multiplier (Multiplikator von Losgrößen). Es ist möglich, die Logik zur Priorisierung dieser Bedingung anzupassen.
read "/sample/Lokad_Items.tsv"
read "/sample/Lokad_Orders.tsv" as Orders
read "/sample/Lokad_PurchaseOrders.tsv" as PO
LotMultiplier = 5
//Filtern auf geschlossenen Aufträgen
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
// % in Bezug auf den Verkaufspreis
oosPenalty := 0.25
// % jährliche Lagerhaltungskosten bezüglich des Kaufpreises
carryingCost := 0.3
// % jährlicher Nachlass
discount := 0.20
M = SellPrice - BuyPrice
// Bestrafung für Fehlbestände
S = - 0.25 * SellPrice
// % '0.3' als jährliche Lagerhaltungskosten
C = - 0.3 * BuyPrice * mean(Leadtime) / 365
// Auftragsrückstand
MB = 0.5 * SellPrice
MBU = MB * uniform(1, Backorder)
// Auftragsrückstand
SB = 0.5 * SellPrice
SBU = SB * uniform(1, Backorder)
// Chance späteren Kaufs
AM = 0.3
// % '0.2' als jährlicher Abzinsungsfaktor
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 // einfache erneute Zusammensetzung
Stock = StockOnHand + StockOnOrder
DBO = Demand >> BackOrder
// das dritte Argument ist 'LotMultiplier'
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
// eine Beispiels-MOQ-Bedingung
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
Das obere Skript nutzt das besondere Verhalten der
extend.distrib()
-Funktion, das genau zur Erfassung von Bedingungen bei Multiplikatoren von Losgrößen gedacht ist. Das dritte Argument dieser Funktion ist diese Anzahl des Multiplikators für Losgrößen.
Zielcontainerkapazität pro Lieferant
Bei dem Überseeimport tritt oft die Bedingung auf, so viel einzukaufen, bis ein Container voll bzw. halbvoll ist. Das Volumen des Containers ist bekannt und in diesem Beispiel gehen wir davon aus, dass das Volumen aller gekauften Artikel auch bekannt ist. Das Ziel ist die Erstellung einer Liste mit der engeren Wahl von Artikel, die den Inhalt des nächsten zu bestellenden Containers darstellt.
Um dies noch etwas komplexer zu gestalten, gehen wir auch davon aus, dass
verschiedene Lieferanten nicht zusammen liefern. Daher sollten alle Einkaufszeilen bei der Erstellung eines Containers mit demselben Lieferanten verbunden sein. Dies impliziert, dass man zuerst die wichtigsten Lieferanten feststellen sollte und dann entsprechend den Container füllen sollte. Wir nehmen an, die
Artikeldateien beinhalten eine Spalte
S
(für
Supplier), die den Hauptanbieter angibt, angenommen wir befinden uns in einem Single-Sourcing-Szenarium (d.h. jeder Artikel hat genau einen Lieferanten).
read "/sample/Lokad_Items.tsv"
read "/sample/Lokad_Orders.tsv" as Orders
read "/sample/Lokad_PurchaseOrders.tsv" as PO
//Filtern auf geschlossenen Aufträgen
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)
// erwartetes Containervolumenr (m3)
cV := 15
// erwartete Schwele zum Wechsel vom Container
cJT := 2 * cV
// % im Bezug zum Verkaufspreis
oosPenalty := 0.25
// % jährliche Lagerhaltungskosten im Bezug zum Kaufpreis
carryingCost := 0.3
// % jährlicher Abzinsungsfaktor
discount := 0.20
M = SellPrice - BuyPrice
// Bestrafung für Fehlbestände
S = - 0.25 * SellPrice
// % '0.3' als jährliche Lagerhaltungskosten
C = - 0.3 * BuyPrice * mean(Leadtime) / 365
// Auftragsrückstand
MB = 0.5 * SellPrice
MBU = MB * uniform(1, Backorder)
// Auftragsrückstand
SB = 0.5 * SellPrice
SBU = SB * uniform(1, Backorder)
// Chance auf späteren Kauf
AM = 0.3
// % '0.2' als jährlicher Abzinsungsfaktor
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 // einfache erneute Zusammensetzung
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) // Belohnung
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' steht für Lieferant
G.CId = priopack(G.V, cV, cJT, Id) by S sort G.Rk
// Füllung des Container für den dringlichsten
//Lieferanten
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
Dieses Dashboard erstellt eine einzelne Tabelle, in der die Pakete beschrieben werde, die in absteigender Reihenfolge nach Belohnung sortiert ist. Die Berechnung der Bestandsbelohnung erfolgt auf Grundlage der
stockrwd
-Funktion. Die Paket-Logik, also die Aufteilung der Mengen auf verschiedene Container, wird mit der
priopack
-Funktion vorgenommen. Diese Funktion wurde spezifisch in Envision eingeführt, um die Einschränkungen der Container zu berücksichtigen.