优先订货与订货限制

MOQ 和其他订货限制


首页 » 资源 » 此处

利用概率预测,可以生成每个要采购的增量单元依据预期毛利润和预期存货持有成本等商业推动因素进行排列的采购优先级列表。经证明,采购优先级列表也能灵活地适应供应限制。在本页面,我们将详细说明可通过我们的数据处理工具来编写脚本的一些常见采购场景,例如最小订单量 (MOQ) 或集装箱容量。

在本指南中,我们将上一篇指南中的采购优先级列表作为切入点。为了简明起见,在此不赘述生成该列表的详细信息。


每个供应商的目标集装箱容量

在从海外进货时,时常会遇到采购的商品要装满整个或半个集装箱的限制。集装箱的体积已知,本例中假定所采购的所有项目的体积也已知。我们的目标是构建一个待采购项目列表,且该列表中的项目应几乎装满整个集装箱。

为了考量更复杂一点的情形,我们假定供应商之间不会进行集运。这样在对集装箱装货时,所有采购项目应来自同一个供应商。这意味着应先确定最紧迫的供应商,然后相应填装集装箱。假设项目文件包含有指示主要供应商 Supplier 列,且假设考量的是单一采购场景(即所有项目来自同一个供应商)。

read "/sample" all

Horizon = call forecast.leadtime(
hierarchy: Category, SubCategory
present: (max(Orders.Date) by 1) + 1
leadtimeDate: PurchaseOrders.Date
leadtimeValue: PurchaseOrders.DeliveryDate - PurchaseOrders.Date + 1)

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

cVolume := 15 // expected volume of the container (m3)

oosPenalty := 0.25 // % relative to selling price
carryingCost := 0.3 // % annual carrying cost relative to purchase price
discount := 0.20 // % annual economic discount

M = SellPrice - BuyPrice
S = - oosPenalty * SellPrice
C = - carryingCost * BuyPrice * LeadTime / 365
A = 1 - discount * LeadTime / 365

Reward = stockrwd(Demand, M, S, C, A)

table G = extend.distrib(Demand, StockOnHand + StockOnOrder)
G.Q = G.Max - G.Min + 1
G.Reward = int(Reward, 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)

// filling the container for the most pressing supplier
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

该仪表板将生成单独的一个表,表中包含批次列表的详细信息,且这些批次按效益递减的顺序排列。库存效益根据 stockrwd 函数计算。分批逻辑(也即对多个集装箱划分数量)则使用 priopack 函数进行。在 Envision 中,我们针对具有集装箱限制的购买情况专门介绍过这个函数。

另请参阅解决一般的 MOQ 问题

每个 SKU 的最小订单量 (MOQ)

供应商经常向客户施加最小订单量 (MOQ) 限制。此类 MOQ 限制可在不同级别应用:SKU、目标、订单等等。假定 SKU 级别存在 MOQ 限制:每个 SKU 存在最小订单量,除此阈值外,负责订购商品的人还可以决定是否针对该 SKU 进行额外订货。在下面的脚本中,假定项目文件包含 MOQ 列。如果没有适用的 MOQ 限制,则这个字段应为 1。

read "/sample" all

Horizon = call forecast.leadtime(
hierarchy: Category, SubCategory
present: (max(Orders.Date) by 1) + 1
leadtimeDate: PurchaseOrders.Date
leadtimeValue: PurchaseOrders.DeliveryDate - PurchaseOrders.Date + 1)

Demand = call 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 // % relative to selling price
carryingCost := 0.3 // % annual carrying cost relative to purchase price
discount := 0.20 // % annual economic discount

M = SellPrice - BuyPrice
S = - oosPenalty * SellPrice
C = - carryingCost * BuyPrice * LeadTime / 365
A = 1 - discount * LeadTime / 365

Reward = stockrwd(Demand, M, S, C, A)

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

where G.Max >= StockOnHand + StockOnOrder
G.Eligible = moqsolv(Id, G.Min, G.Reward, G.Cost, G.Cost, budget, \
Id, G.Q, MinOrderQuantity) // the MOQ constraint itself
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

这段脚本将生成列表中的所有行均满足 MOQ 限制的仪表板。为了满足这些限制,我们使用了 moqsolv 这个特殊的 Envision 函数。尽管在示例中只有一种类型的 MOQ 限制,但函数 moqsolv 也可处理多种 MOQ 限制。对于网格中被选定为最终结果的行,函数 moqsolv 将返回 true。从本质上说,moqsolv 使用了专门为解决 MOQ 问题而量身定制的高级非线性优化器。

每组 SKU 的最小订单量 (MOQ)

在上一节中我们了解了如何处理 SKU 级别的 MOQ 限制,接下来将了解如何在更高聚合级别处理此类限制。假设 items 文件中提供了 MOQ 阈值。鉴于 MOQ 限制要应用于特定群组级别,为了一致起见,我们假设所有项目属于具有相同 MOQ 值的相同 MOQ 组。下面这段脚本说明了这种多 SKU 限制的处理方法。

read "/sample" all

// snipped ..

Grid.Eligible = moqsolv(Id, G.Min, G.Reward, G.Cost, G.Cost, budget, \
SubCategory, G.Q, MinOrderQuantity) // MOQ per subcategory
// snipped ...

上面这段脚本其实与上一节中的脚本大致相同。为了清楚起见,我们只显示对 moqsolv 的调用,因为只有这一行是不同的,这一行使用了一种替代性的 MOQ 限制来作为参数。

每个 SKU 的批次倍数

有时 SKU 只能按特定的数量订购,与上文详述的最小订单量 (MOQ) 限制不同,我们需要将订货数量乘以某个"基数"。举个例子,某种产品只能按每箱 12 件订购。不能订购 13 件,只能订购 12 件或 24 件。我们将所要乘的数量称之为批次倍数。您也可以调整这种优先化逻辑,以适合这一限制。

read "/sample" all

LotMultiplier = 5

Horizon = call forecast.leadtime(
hierarchy: Category, SubCategory
present: (max(Orders.Date) by 1) + 1
leadtimeDate: PurchaseOrders.Date
leadtimeValue: PurchaseOrders.DeliveryDate - PurchaseOrders.Date + 1)

Demand = call 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 // % relative to selling price
carryingCost := 0.3 // % annual carrying cost relative to purchase price
discount := 0.20 // % annual economic discount

M = SellPrice - BuyPrice
S = - oosPenalty * SellPrice
C = - carryingCost * BuyPrice * LeadTime / 365
A = 1 - discount * LeadTime / 365

Reward = stockrwd(Demand, M, S, C, A)

// the third argument is 'LotMultiplier'
table G = extend.distrib(Demand, StockOnHand + StockOnOrder, LotMultiplier)
G.Q = G.Max - G.Min + 1
G.Reward = int(Reward, G.Min, G.Max)
G.Cost = BuyPrice * G.Q

where G.Max > StockOnHand + StockOnOrder
// a mock MOQ constraint
G.Eligible = moqsolv(Id, G.Min, G.Reward, G.Cost, G.Cost, budget, Id, G.Q, 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

上面这段脚本将分位数网格行合并成了匹配批次倍数信息(此信息将作为输入传递)的更大的行。如果没有批次倍数限制,批次倍数值应为 1。通过进行合并,得出的采购优先级列表将包含预期数量的所有倍数。

上述逻辑中的主要不足在于分位数网格并非总是以 +1 为增量来组成各行。因此,这种逻辑是使用批次倍数四舍五入来进行有限的近似计算,这种情况只有在分位数网格的增量为 +10 或更高时才发生。

这段脚本利用了 extend.distrib() 函数的一种特殊行为,即准确捕获批次倍数限制。此函数的第三个参数即为批次倍数的数量。