How to prioritize purchases economically

This guide shows how to generate a purchase priority list from the 1-echelon 2017 dataset.

Step 1: Open the dataset session

Use the preloaded session so the /sample folder is already available.

Step 2: Create the script

Create a new Envision project, paste the script below, and save it.

read form with
  budget : number

read "/sample/Lokad_Items.tsv" as Items[id] max 1m with
  "Id" as id : text
  Supplier: text
  Category: text
  SubCategory: text
  SellPrice : number
  BuyPrice : number
  StockOnHand : number
  StockOnOrder : number

read "/sample/Lokad_Orders.tsv.gz" as Orders max 1m expect [id, date] with
  "Id" as id : text
  "Date" as date : date
  Quantity : number
read "/sample/Lokad_PurchaseOrders.tsv" as PO max 1m expect [id, date] with
  "Id" as id : text
  "Date" as date : date
  DeliveryDate : date

Items.Backorder = 0
Items.LotMultiplier = 1

orderingLeadtime = 7 // 7 days

// exclude non-delivered POs
where PO.DeliveryDate > PO.date
  Items.Leadtime = forecast.leadtime(
    hierarchy: (Items.Category, Items.SubCategory),
    present: (max(Orders.date) by 1 + 1),
    leadtimeDate: PO.date,
    leadtimeValue: PO.DeliveryDate - PO.date + 1)

Items.Demand = forecast.demand(
  horizon: Items.Leadtime + dirac(orderingLeadtime),
  hierarchy: (Items.Category, Items.SubCategory),
  present: (max(Orders.date) by 1) + 1,
  demandDate: Orders.date,
  demandValue: Orders.Quantity)

show form "Purchase simulator" with
  budget as "Max budget"

Items.M = Items.SellPrice - Items.BuyPrice
// stock-out penalty
Items.S = - 0.25 * Items.SellPrice
// % '0.3' as annual carrying cost
Items.C = - 0.3 * Items.BuyPrice * mean(Items.Leadtime) / 365
// back-order case
Items.MB = 0.5 * Items.SellPrice
Items.MBU = Items.MB * uniform(1, Items.Backorder)
// back-order case
Items.SB = 0.5 * Items.SellPrice
Items.SBU = Items.SB * uniform(1, Items.Backorder)
// opportunity to buy later
Items.AM = 0.3
// % '0.2' as annual economic discount
Items.AC = 1 - 0.2 * mean(Items.Leadtime) / 365

Items.RM = Items.MBU + shift((stockrwd.m(Items.Demand, Items.AM) * Items.M), Items.Backorder)
Items.RS = Items.SBU + shift(zoz(stockrwd.s(Items.Demand) * Items.S), Items.Backorder)
Items.RC = shift((stockrwd.c(Items.Demand, Items.AC) * Items.C), Items.Backorder)
// plain recomposition
Items.R = Items.RM + Items.RS + Items.RC

Items.Stock = Items.StockOnHand + Items.StockOnOrder

Items.DBO = Items.Demand + Items.Backorder
table G max 100k = extend.ranvar(Items.DBO, Items.Stock, Items.LotMultiplier)

where G.Max > Items.Stock
  G.Q = G.Max - G.Min + 1
  // integral of the stock reward function
  G.Reward = int(Items.R, G.Min, G.Max)
  G.Score = G.Reward / max(1, Items.BuyPrice * G.Q)

  // top scores first
  // but preserve ordering of pairs (Id, G.Max)
  G.Rank = rank(G.Score, id, -G.Max)
  G.Invest = cumsum(Items.BuyPrice * G.Q) scan [G.Rank]

  where G.Invest < budget
  where any(G.Q > 0) into Items
    show table "Purchase priority list with $\{budget}" with
      id as "id"
      Items.Supplier as "Supplier"
      Items.StockOnHand as "OnHand"
      Items.StockOnOrder as "OnOrder"
      sum(G.Q) as "Qty"
      mean(Items.Leadtime) as "Leadtime"
      sum(G.Reward) as "Reward" {unit:"$"}
      sum(Items.BuyPrice * G.Q) as "Cost" {unit:"$"}
      group by id
      order by avg(G.Score) desc

Step 3: Run and read the list

Run the script, set the budget in the form, and review the table in order. Purchase the items starting from the top until the budget is exhausted.

User Contributed Notes
0 notes + add a note