MOQ und andere Bestellbedingungen




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.