<- "2024-12-29"
start_date
<- as.Date(start_date, format = "%Y-%m-%d")
start_date
start_date
[1] "2024-12-29"
R programming, supply chain, demand supply planning, S&OP, End to End, E2E, planr
The idea is to show how to create an End to End (E2E) view, using the R package planr.
The planr package is for Supply Chain Management.
The goal is to provide some functions to perform quickly some classic operations in the scope of Demand and Supply Planning or to run the S&OP (Sales & Operations Planning) process.
We will see here how to use a few functions from the package, especially to calculate projected inventories and a constrained demand.
To know more about planr, you can visit the website : planr - Demand & Supply Planning | S&OP :: with R
Let’s consider 2 entities as follow:
Entity 1 : is the receiver
Entity 2 : is the supplier
Sales are performed by the Entity 1, which manages a local stock and gets a Supply from the Entity 2.
The Entity 1 :
provides to the Entity 2 a Supply Plan, composed of Purchase Orders and Replenishment Forecasts
would like to know whether this Supply Plan will be delivered on time by the Entity 2
At the same time, the Entity 2 would like to better understand the projected stocks of the Entity 1, to see whether there are any local risks in case of delays.
The purpose of the End-to-End (E2E) view is then :
to provide a shared visibility on the projected inventories of both Entities
update the projected inventories of the Entity 1, considering the actual supply plan of the Entity 2
We will proceed in 3 steps :
Step 1 : Get from the Entity 2 the actual Supplier’s answer
Calculate Entity 1’s projected inventories, and get the Constrained Demand, using the const_dmd() function from the R package planr
Translate this supply answer to the Entity 1, converting the ETD (Estimated Time of Departure) to the ETA (Estimated Time of Arrival)
Step 2 : Calculate the projected in-transit
Step 3 : Update the Entity 1’s actual projected inventories
From Entity 1
We need 5 variables :
Sales Forecasts
Opening Stock On Hand
Supply Plan, usually composed of 3 variables
Current in-transit
Pending Purchase Orders (POs)
Future Replenishment Plan (not necesarilly materialized through a PO)
From Entity 2
We just need 2 variables :
Opening Stock On Hand
Production Plan
Transit Lead Time
eventually some SKUs Dimensions
Start date
<- "2024-12-29"
start_date
<- as.Date(start_date, format = "%Y-%m-%d")
start_date
start_date
[1] "2024-12-29"
We get 2 standard outputs
For the Supplier (Entity 2) :
projected inventories and calculation of the constrained demand
a cockpit (for a portfolio of products) : to visualize easily the OOS & Alerts, as well as the overstocks situations, at Supplier level
For both entities : End to End (E2E) view
displayed as a table with the projected inventories of both Entities next to each other
a cockpit (for a portfolio of products) : to visualize easily the OOS & Alerts, as well as the overstocks situations, at Receiver (Entity 1) level
#————————————————————————
# ETL
library(tidyverse)
library(sparkline)
library(htmltools)
# Charts
library(highcharter)
# Tables
library(reactable)
library(reactablefmtr)
# special Supply Chain calculations
library(planr)
#————————————————————————
Let’s a create a simple demo dataset, with only one product, called “Product A” .
Let’s create some simple monthly sales forecasts, and split them into weekly bucket :
# create variables and dataframe
<- c(1000, 1000, 2000, 1000, 1000, 2000,
Demand 1000, 1000, 2000, 1000, 1000, 2000)
<- c("2025-01-01", "2025-02-01", "2025-03-01", "2025-04-01",
Period "2025-05-01", "2025-06-01", "2025-07-01", "2025-08-01",
"2025-09-01", "2025-10-01", "2025-11-01", "2025-12-01")
<- data.frame(Period,
df1
Demand)
# formatting
$Period <- as.Date(df1$Period, format = "%Y-%m-%d")
df1
# add a DFU
$DFU <- "Product A"
df1
glimpse(df1)
Rows: 12
Columns: 3
$ Period <date> 2025-01-01, 2025-02-01, 2025-03-01, 2025-04-01, 2025-05-01, 20…
$ Demand <dbl> 1000, 1000, 2000, 1000, 1000, 2000, 1000, 1000, 2000, 1000, 100…
$ DFU <chr> "Product A", "Product A", "Product A", "Product A", "Product A"…
Now, let’s split it into weekly bucket. We apply the function month_to_week() from the package planr.
By default, we consider the demand split evenly between different weeks.
If we want a different pattern, we can use the function month_to_weekx().
# apply month_to_week()
<- planr::month_to_week(dataset = df1, DFU, Period, Demand) df1
Joining with `by = join_by(Monthly.Week.no)`
Joining with `by = join_by(Day.no)`
Joining with `by = join_by(Monthly.Period, DFU)`
# formatting
$Period <- as.Date(df1$Period, format = "%Y-%m-%d")
df1
# keep results
<- df1
demand_data
glimpse(df1)
Rows: 53
Columns: 3
$ DFU <chr> "Product A", "Product A", "Product A", "Product A", "Product A"…
$ Period <date> 2024-12-29, 2025-01-05, 2025-01-12, 2025-01-19, 2025-01-26, 20…
$ Demand <dbl> 142.8571, 250.0000, 250.0000, 250.0000, 142.8571, 250.0000, 250…
Let’s put a random value, for example 1500 units.
We will affect this value to the first period of time, which is here the start_date (“2024-12-29”).
# create dataframe
<- data.frame(
df1 DFU = c("Product A"),
Period = c(start_date),
Opening = 1500
)
# keep results
<- df1
opening_data
glimpse(df1)
Rows: 1
Columns: 3
$ DFU <chr> "Product A"
$ Period <date> 2024-12-29
$ Opening <dbl> 1500
Let’s affect 2 random values, at ETA (Estimated Date of Arrival) :
600 units on the week of “2025-01-12”
1200 units on the week of “2025-02-09”
# create dataframe
<- data.frame(
df1 DFU = c(rep("Product A", 2)),
Period = c( "2025-01-12", "2025-02-09"),
In_transit = c(600, 1200)
)
# formatting
$Period <- as.Date(df1$Period, format = "%Y-%m-%d")
df1
# keep results
<- df1
in_transit_data
glimpse(df1)
Rows: 2
Columns: 3
$ DFU <chr> "Product A", "Product A"
$ Period <date> 2025-01-12, 2025-02-09
$ In_transit <dbl> 600, 1200
Let’s affect 1 random value, at ETA (Estimated Date of Arrival) :
# create dataframe
<- data.frame(
df1 DFU = c("Product A"),
Period = c( "2025-03-16"),
Pending_PO = c(2000)
)
# formatting
$Period <- as.Date(df1$Period, format = "%Y-%m-%d")
df1
# keep results
<- df1
pending_po_data
glimpse(df1)
Rows: 1
Columns: 3
$ DFU <chr> "Product A"
$ Period <date> 2025-03-16
$ Pending_PO <dbl> 2000
Now, let’s say that we have some monthly Replenishment Forecasts for the following 6 months :
# create dataframe
<- data.frame(
df1 DFU = c(rep("Product A", 6)),
Period = c( "2025-04-13", "2025-05-11", "2025-06-15",
"2025-07-13", "2025-08-17", "2025-09-14"),
Replenishment_Plan = c(3000, 1000, 2000, 1000, 1000, 2000)
)
# formatting
$Period <- as.Date(df1$Period, format = "%Y-%m-%d")
df1
# keep results
<- df1
replenishment_plan_data
glimpse(df1)
Rows: 6
Columns: 3
$ DFU <chr> "Product A", "Product A", "Product A", "Product A",…
$ Period <date> 2025-04-13, 2025-05-11, 2025-06-15, 2025-07-13, 20…
$ Replenishment_Plan <dbl> 3000, 1000, 2000, 1000, 1000, 2000
Now let’s combine those 3 parts (Demand | Opening | Supply) to get a template for the projected inventories of the Entity 1 :
# merge
<- left_join(demand_data, opening_data) df1
Joining with `by = join_by(DFU, Period)`
<- left_join(df1, in_transit_data) df1
Joining with `by = join_by(DFU, Period)`
<- left_join(df1, pending_po_data) df1
Joining with `by = join_by(DFU, Period)`
<- left_join(df1, replenishment_plan_data) df1
Joining with `by = join_by(DFU, Period)`
# replace missing values by zero
$Opening <- df1$Opening |> replace_na(0)
df1$In_transit <- df1$In_transit |> replace_na(0)
df1$Pending_PO <- df1$Pending_PO |> replace_na(0)
df1$Replenishment_Plan <- df1$Replenishment_Plan |> replace_na(0)
df1
# calculate the total supply
$Supply <- df1$In_transit + df1$Pending_PO + df1$Replenishment_Plan
df1
# keep results
<- df1
Entity1_template_data
glimpse(df1)
Rows: 53
Columns: 8
$ DFU <chr> "Product A", "Product A", "Product A", "Product A",…
$ Period <date> 2024-12-29, 2025-01-05, 2025-01-12, 2025-01-19, 20…
$ Demand <dbl> 142.8571, 250.0000, 250.0000, 250.0000, 142.8571, 2…
$ Opening <dbl> 1500, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
$ In_transit <dbl> 0, 0, 600, 0, 0, 0, 1200, 0, 0, 0, 0, 0, 0, 0, 0, 0…
$ Pending_PO <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2000, 0, 0, 0, 0, …
$ Replenishment_Plan <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3000, …
$ Supply <dbl> 0, 0, 600, 0, 0, 0, 1200, 0, 0, 0, 0, 2000, 0, 0, 0…
We’re going to calculate the theoretical projected inventories at the Entity 1 level.
Theoretical because we assume that all the planned quantities will be delivered on time. We will need to verify this later on, using the End to End (E2E) view.
Let’s apply the function light_proj_inv() from the package planr :
# set a working df
<- Entity1_template_data
df1
# keep only needed variables
<- df1 |> select(DFU, Period, Demand, Opening, Supply)
df1
# calculate the projected inventories
<- planr::light_proj_inv(dataset = df1,
df1 DFU = DFU,
Period = Period,
Demand = Demand,
Opening = Opening,
Supply = Supply)
Joining with `by = join_by(DFU, Period)`
# keep results
<- df1
entity1_calculated_projection_data
glimpse(df1)
Rows: 53
Columns: 7
$ DFU <chr> "Product A", "Product A", "Product A", …
$ Period <date> 2024-12-29, 2025-01-05, 2025-01-12, 20…
$ Demand <dbl> 142.8571, 250.0000, 250.0000, 250.0000,…
$ Opening <dbl> 1500, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
$ Calculated.Coverage.in.Periods <dbl> 5.9, 4.9, 6.1, 5.1, 4.1, 3.1, 4.5, 3.5,…
$ Projected.Inventories.Qty <dbl> 1357.1, 1107.1, 1457.1, 1207.1, 1064.3,…
$ Supply <dbl> 0, 0, 600, 0, 0, 0, 1200, 0, 0, 0, 0, 2…
#-------------------
# Get data
#-------------------
# set a working df
<- entity1_calculated_projection_data
df1
#-------------------
# Transform
#-------------------
# keep only the needed columns
<- df1 |> select(Period,
df1
Demand,
Calculated.Coverage.in.Periods,
Projected.Inventories.Qty,
Supply)
# create a f_colorpal field
<- df1 |> mutate(f_colorpal = case_when( Calculated.Coverage.in.Periods > 6 ~ "#FFA500",
df1 > 2 ~ "#32CD32",
Calculated.Coverage.in.Periods > 0 ~ "#FFFF99",
Calculated.Coverage.in.Periods TRUE ~ "#FF0000" ))
#-------------------
# Table
#-------------------
# create reactable
reactable(df1, resizable = TRUE, showPageSizeOptions = TRUE,
striped = TRUE, highlight = TRUE, compact = TRUE,
defaultPageSize = 20,
columns = list(
Demand = colDef(
name = "Demand (units)",
cell = data_bars(df1,
fill_color = "#3fc1c9",
text_position = "outside-end"
)
),
Calculated.Coverage.in.Periods = colDef(
name = "Coverage (Periods)",
maxWidth = 90,
cell= color_tiles(df1, color_ref = "f_colorpal")
),
f_colorpal = colDef(show = FALSE), # hidden, just used for the coverages
`Projected.Inventories.Qty`= colDef(
name = "Projected Inventories (units)",
format = colFormat(separators = TRUE, digits=0),
style = function(value) {
if (value > 0) {
<- "#008000"
color else if (value < 0) {
} <- "#e00000"
color else {
} <- "#777"
color
}list(color = color
#fontWeight = "bold"
)
}
),
Supply = colDef(
name = "Supply (units)",
cell = data_bars(df1,
fill_color = "#3CB371",
text_position = "outside-end"
)
)
# close columns lits
),
columnGroups = list(
colGroup(name = "Projected Inventories", columns = c("Calculated.Coverage.in.Periods",
"Projected.Inventories.Qty"))
)
# close reactable )
Let’s put a random value, for example 3000 units.
We will affect this value to the first period of time, which is here the start_date (“2024-12-29”).
# create dataframe
<- data.frame(
df1 DFU = c("Product A"),
Period = c(start_date),
Opening = 3000
)
# keep results
<- df1
opening_data
glimpse(df1)
Rows: 1
Columns: 3
$ DFU <chr> "Product A"
$ Period <date> 2024-12-29
$ Opening <dbl> 3000
Let’s affect 3 random values, at Production date :
2000 units on the week of “2025-03-02”
3000 units on the week of “2025-04-20”
3000 units on the week of “2025-05-04”
# create dataframe
<- data.frame(
df1 DFU = c(rep("Product A", 3)),
Period = c("2025-03-02", "2025-04-20", "2025-05-04"),
Supply = c(2000, 3000, 3000)
)
# formatting
$Period <- as.Date(df1$Period, format = "%Y-%m-%d")
df1
# keep results
<- df1
production_plan_data
glimpse(df1)
Rows: 3
Columns: 3
$ DFU <chr> "Product A", "Product A", "Product A"
$ Period <date> 2025-03-02, 2025-04-20, 2025-05-04
$ Supply <dbl> 2000, 3000, 3000
The Demand to the Entity 2 is the expected supply from the Entity 1, excluding the current in transit :
Pending Purchase Orders
Future Replenishment Plan
So, first, let’s get those variables, at ETA date :
# set a working df
<- Entity1_template_data
df1
# keep only needed variables
<- df1 |> select(DFU, Period, Pending_PO, Replenishment_Plan)
df1
# calculate total demand
$Demand <- df1$Pending_PO + df1$Replenishment_Plan
df1
glimpse(df1)
Rows: 53
Columns: 5
$ DFU <chr> "Product A", "Product A", "Product A", "Product A",…
$ Period <date> 2024-12-29, 2025-01-05, 2025-01-12, 2025-01-19, 20…
$ Pending_PO <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2000, 0, 0, 0, 0, …
$ Replenishment_Plan <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 3000, …
$ Demand <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2000, 0, 0, 0, 300…
Now, let’s convert this ETA Demand into ETD date, using the transit leadtime.
We just need to offset the Demand by the transit leadtime (60days)
# offset of the transit leadtime
$Period <- df1$Period - 60 # because the transit lead time is 60 days
df1
# make sure the new Period fits to the beginning of a week period
$Period <- floor_date(df1$Period, unit = "week")
df1
# it's possible that the new ETD ends up on the similar weekly period
# so let's aggregate to avoid any issue
<- df1 |> group_by(DFU, Period) |>
df1 summarise(Demand = sum(Demand)
)
`summarise()` has grouped output by 'DFU'. You can override using the `.groups`
argument.
# keep only the relevant period of time
<- df1 |> filter(Period >= start_date)
df1
# keep results
<- df1 demand_etd_data
Note : if we have SKUs with different transit leadtimes, we just need to have in our master data a dataset with the transit leadtimes by SKU and merge this dataset with the above table.
To keep it simple in this example, we just offset the transit leadtime straight in the original table
Now let’s combine those 3 parts (Demand | Opening | Supply) to get a template to calculate the projected inventories and the Constrained Demand of the Entity 2 :
# merge
<- left_join(demand_etd_data, opening_data) df1
Joining with `by = join_by(DFU, Period)`
<- left_join(df1, production_plan_data) df1
Joining with `by = join_by(DFU, Period)`
# replace missing values by zero
$Opening <- df1$Opening |> replace_na(0)
df1$Supply <- df1$Supply |> replace_na(0)
df1
# keep results
<- df1
Entity2_template_data
glimpse(df1)
Rows: 44
Columns: 5
Groups: DFU [1]
$ DFU <chr> "Product A", "Product A", "Product A", "Product A", "Product A…
$ Period <date> 2024-12-29, 2025-01-05, 2025-01-12, 2025-01-19, 2025-01-26, 2…
$ Demand <dbl> 0, 0, 2000, 0, 0, 0, 3000, 0, 0, 0, 1000, 0, 0, 0, 0, 2000, 0,…
$ Opening <dbl> 3000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
$ Supply <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 2000, 0, 0, 0, 0, 0, 0, 3000, 0, 30…
Let’s apply the function const_dmd() from the package planr.
# set a working df
<- Entity2_template_data
df1
# calculate the Proj Inv & Const Dmd
<- planr::const_dmd(dataset = df1,
df1 DFU = DFU,
Period = Period,
Demand = Demand,
Opening = Opening,
Supply = Supply)
Joining with `by = join_by(DFU, Period)`
Joining with `by = join_by(DFU, Period)`
Joining with `by = join_by(DFU, Period)`
Joining with `by = join_by(DFU, Period)`
# keep results
<- df1
entity2_calculated_projection_data
glimpse(df1)
Rows: 44
Columns: 9
$ DFU <chr> "Product A", "Product A", "Product A", …
$ Period <date> 2024-12-29, 2025-01-05, 2025-01-12, 20…
$ Demand <dbl> 0, 0, 2000, 0, 0, 0, 3000, 0, 0, 0, 100…
$ Opening <dbl> 3000, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
$ Calculated.Coverage.in.Periods <dbl> 5.3, 4.3, 3.3, 2.3, 1.3, 0.3, 0.0, 0.0,…
$ Projected.Inventories.Qty <dbl> 3000, 3000, 1000, 1000, 1000, 1000, -20…
$ Supply <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 2000, 0, 0, …
$ Constrained.Demand <dbl> 0, 0, 2000, 0, 0, 0, 1000, 0, 0, 2000, …
$ Current.Stock.Available.Tag <dbl> 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
Let’s create a function to display a badge :
# A Function to define a Badge Status in the reactable
<- function(color = "#aaa", width = "9px", height = width) {
status_badge span(style = list(
display = "inline-block",
marginRight = "8px",
width = width,
height = height,
backgroundColor = color,
borderRadius = "50%"
)) }
Now let’s display the table :
# set a working df
<- entity2_calculated_projection_data
df1
#----------------
# Create the table
#----------------
# remove not needed column
<- df1 |> select(-DFU)
df1
# reorder variables
<- df1 |> select(Period, Demand, Constrained.Demand, Current.Stock.Available.Tag,
df1
Calculated.Coverage.in.Periods, Projected.Inventories.Qty, Supply
)
# create a f_colorpal field
<- df1 |> mutate(f_colorpal = case_when(Calculated.Coverage.in.Periods > 6 ~ "#FFA500",
df1 > 2 ~ "#32CD32",
Calculated.Coverage.in.Periods > 0 ~ "#FFFF99",
Calculated.Coverage.in.Periods TRUE ~ "#FF0000" ))
# adjust Current.Stock.Available.Tag
$Current.Stock.Available.Tag <- if_else(df1$Current.Stock.Available.Tag == 1, "Available", "")
df1
#-------------------------
# Create Table
reactable(df1, resizable = TRUE, showPageSizeOptions = TRUE,
striped = TRUE, highlight = TRUE, compact = TRUE,
defaultPageSize = 20,
columns = list(
Demand = colDef(
name = "Demand (units)",
cell = data_bars(df1,
fill_color = "#3fc1c9",
text_position = "outside-end")
),
Constrained.Demand = colDef(
name = "Constrained Demand (units)",
cell = data_bars(df1,
fill_color = "gold",
text_position = "outside-end")
),
Calculated.Coverage.in.Periods = colDef(
name = "Coverage (Periods)",
maxWidth = 90,
cell= color_tiles(df1, color_ref = "f_colorpal")
),
f_colorpal = colDef(show = FALSE), # hidden, just used for the coverages
`Projected.Inventories.Qty`= colDef(
name = "Projected Inventories (units)",
format = colFormat(separators = TRUE, digits=0),
style = function(value) {
if (value > 0) {
<- "#008000"
color else if (value < 0) {
} <- "#e00000"
color else {
} <- "#777"
color
}list(color = color
#fontWeight = "bold"
)
}
),
Current.Stock.Available.Tag = colDef(
name = "Current Stock Available Tag",
cell = function(value) {
<- switch(
color
value,No = "hsl(120,61%,50%)",
Available = "rgb(135,206,250)"
)<- status_badge(color = color)
badge tagList(badge, value)
}),
Supply = colDef(
name = "Supply (units)",
cell = data_bars(df1,
fill_color = "#3CB371",
text_position = "outside-end"
)
)
# close columns lits
),
columnGroups = list(
colGroup(name = "Projected Inventories", columns = c("Calculated.Coverage.in.Periods",
"Projected.Inventories.Qty"))
)
# close reactable )
We can see that the Entity 2 will be late to supply the Demand coming from the Entity 1, from beginning of February until beginning of May.
Creating the E2E view later on, we will figure out if there are any impact at the Entity 1 level.
Let’s use the current in transit at ETA date.
# set a working df
<- in_transit_data
df1
# aggregate
<- df1 |> group_by(DFU) |>
df1 summarise(Current.GIT = sum(In_transit)
)
# Add Start.Date
$Period <- start_date
df1
# keep results
<- df1
opening_GIT_data
glimpse(df1)
Rows: 1
Columns: 3
$ DFU <chr> "Product A"
$ Current.GIT <dbl> 1800
$ Period <date> 2024-12-29
# set a working df
<- in_transit_data
df1
# add Current_GIT_DB
<- left_join(df1, opening_GIT_data) df1
Joining with `by = join_by(DFU, Period)`
# replace missing values by zero
$Current.GIT <- df1$Current.GIT |> replace_na(0)
df1
# rename
<- df1 |> rename(ETA.Current.GIT = In_transit)
df1
# add Tag.In.Transit
$Tag.In.Transit <- if_else(df1$ETA.Current.GIT > 0, "GIT", "")
df1
# keep results
<- df1
Current_GIT_data
glimpse(df1)
Rows: 2
Columns: 5
$ DFU <chr> "Product A", "Product A"
$ Period <date> 2025-01-12, 2025-02-09
$ ETA.Current.GIT <dbl> 600, 1200
$ Current.GIT <dbl> 0, 0
$ Tag.In.Transit <chr> "GIT", "GIT"
We will use the Entity 2’s Projected Inventories, with the calculated constrained demand.
Based on the Constrained Demand
# set a working df
<- entity2_calculated_projection_data
df1
# keep only the needed variables
<- df1 |> select(DFU,
df1
Period,
Constrained.Demand,
Current.Stock.Available.Tag)
# rename
<- df1 |> rename(Supplier.Date = Period,
df1 ETD.Future.In.Transit.Qty = Constrained.Demand,
Supplier.Current.Stock.Available.Tag = Current.Stock.Available.Tag
)
# keep Results
<- df1
ETD_data
glimpse(df1)
Rows: 44
Columns: 4
$ DFU <chr> "Product A", "Product A", "Produc…
$ Supplier.Date <date> 2024-12-29, 2025-01-05, 2025-01-…
$ ETD.Future.In.Transit.Qty <dbl> 0, 0, 2000, 0, 0, 0, 1000, 0, 0, …
$ Supplier.Current.Stock.Available.Tag <dbl> 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, …
We need to consider the Transit Lead Time of each DFU.
In this example, we only have one transit Transit Lead Time : 60 days
# set a working df
<- ETD_data
df1
# Add the Transit Times
$Transit.Time <- 60 # in days
df1
# calculate the Receiver.Date
$Receiver.Date <- df1$Supplier.Date + df1$Transit.Time
df1
# get start of the week day using lubridate
$Receiver.Date <- floor_date(df1$Receiver.Date, unit = "week")
df1
# keep only needed variables
<- df1 |> select(DFU,
df1
Receiver.Date,
ETD.Future.In.Transit.Qty,
Supplier.Current.Stock.Available.Tag)
# rename
<- df1 |> rename(Period = Receiver.Date,
df1 ETA.Future.In.Transit.Qty = ETD.Future.In.Transit.Qty,
Receiver.Current.Stock.Available.Tag = Supplier.Current.Stock.Available.Tag)
# aggregate
<- df1 |> group_by(DFU, Period, Receiver.Current.Stock.Available.Tag) |>
df1 summarise(ETA.Future.In.Transit.Qty = sum(ETA.Future.In.Transit.Qty))
`summarise()` has grouped output by 'DFU', 'Period'. You can override using the
`.groups` argument.
# keep Results
<- df1
ETA_data
glimpse(df1)
Rows: 44
Columns: 4
Groups: DFU, Period [44]
$ DFU <chr> "Product A", "Product A", "Produc…
$ Period <date> 2025-02-23, 2025-03-02, 2025-03-…
$ Receiver.Current.Stock.Available.Tag <dbl> 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, …
$ ETA.Future.In.Transit.Qty <dbl> 0, 0, 2000, 0, 0, 0, 1000, 0, 0, …
Get final dataset with both ETD & ETA
# rename
<- ETD_data |> rename(Period = Supplier.Date)
ETD_data
# merge
<- merge(ETD_data, ETA_data, all = TRUE)
df1
# replace missing values by zero
$ETD.Future.In.Transit.Qty <- df1$ETD.Future.In.Transit.Qty |> replace_na(0)
df1$ETA.Future.In.Transit.Qty <- df1$ETA.Future.In.Transit.Qty |> replace_na(0)
df1
# keep Results
<- df1
ETD_ETA_data
glimpse(df1)
Rows: 52
Columns: 6
$ DFU <chr> "Product A", "Product A", "Produc…
$ Period <date> 2024-12-29, 2025-01-05, 2025-01-…
$ ETD.Future.In.Transit.Qty <dbl> 0, 0, 2000, 0, 0, 0, 1000, 0, 0, …
$ Supplier.Current.Stock.Available.Tag <dbl> 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, …
$ Receiver.Current.Stock.Available.Tag <dbl> NA, NA, NA, NA, NA, NA, NA, NA, 0…
$ ETA.Future.In.Transit.Qty <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 200…
We are going to combine 2 parts :
current in transit
future in transit
Current In Transit :
Current GIT : i.e. the Opening In Transit
ETA Current GIT
Future (or next) In Transit :
ETD.Next.GIT
ETA.Next.GIT
# merge
<- merge(Current_GIT_data, ETD_ETA_data, all = TRUE)
df1
# keep results
<- df1
Interim_data
glimpse(df1)
Rows: 52
Columns: 9
$ DFU <chr> "Product A", "Product A", "Produc…
$ Period <date> 2024-12-29, 2025-01-05, 2025-01-…
$ ETA.Current.GIT <dbl> NA, NA, 600, NA, NA, NA, 1200, NA…
$ Current.GIT <dbl> NA, NA, 0, NA, NA, NA, 0, NA, NA,…
$ Tag.In.Transit <chr> NA, NA, "GIT", NA, NA, NA, "GIT",…
$ ETD.Future.In.Transit.Qty <dbl> 0, 0, 2000, 0, 0, 0, 1000, 0, 0, …
$ Supplier.Current.Stock.Available.Tag <dbl> 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, …
$ Receiver.Current.Stock.Available.Tag <dbl> NA, NA, NA, NA, NA, NA, NA, NA, 0…
$ ETA.Future.In.Transit.Qty <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 200…
# Add the Transit Times
$Transit.Time <- 60 # in days
df1
# keep only needed variables
<- df1 |> select(DFU,
df1
Period,
Current.GIT,
ETA.Current.GIT,
ETD.Future.In.Transit.Qty,
ETA.Future.In.Transit.Qty,
Transit.Time
)
# rename
<- df1 |> rename(ETD.Next.GIT = ETD.Future.In.Transit.Qty,
df1 ETA.Next.GIT = ETA.Future.In.Transit.Qty)
# convert the Transit.Time in weeks
$Transit.Time <- df1$Transit.Time / 7
df1
# round
$Transit.Time <- round(df1$Transit.Time)
df1
# keep results
<- df1
In_Transit_data
glimpse(df1)
Rows: 52
Columns: 7
$ DFU <chr> "Product A", "Product A", "Product A", "Product A", "P…
$ Period <date> 2024-12-29, 2025-01-05, 2025-01-12, 2025-01-19, 2025-…
$ Current.GIT <dbl> NA, NA, 0, NA, NA, NA, 0, NA, NA, NA, NA, NA, NA, NA, …
$ ETA.Current.GIT <dbl> NA, NA, 600, NA, NA, NA, 1200, NA, NA, NA, NA, NA, NA,…
$ ETD.Next.GIT <dbl> 0, 0, 2000, 0, 0, 0, 1000, 0, 0, 2000, 0, 0, 0, 0, 0, …
$ ETA.Next.GIT <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2000, 0, 0, 0, 1000, 0, …
$ Transit.Time <dbl> 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, …
# set a working df
<- In_Transit_data
df1
# calculate the ETD.Period
$ETD.Period <- df1$Period - (df1$Transit.Time * 7)
df1
# keep only needed variables
<- df1 |> select(DFU,
df1
ETD.Period,
ETA.Current.GIT)
# rename
<- df1 |> rename(Period = ETD.Period,
df1 ETD.Current.GIT = ETA.Current.GIT)
# get the beginning of the week for the Period
# to ensure we are following an english standard
$Period <- floor_date(as.Date(df1$Period, "%Y-%m-%d"), unit = "week")
df1
# aggregate
<- df1 |> group_by(DFU, Period) |>
df1 summarise(ETD.Current.GIT = sum(ETD.Current.GIT))
`summarise()` has grouped output by 'DFU'. You can override using the `.groups`
argument.
# add back to initial dataset
<- merge(In_Transit_data, df1, all = TRUE)
df1
# replace missing values by zero
$ETA.Current.GIT <- df1$ETA.Current.GIT |> replace_na(0)
df1$ETD.Current.GIT <- df1$ETD.Current.GIT |> replace_na(0)
df1
# keep results
<- df1
Interim_data
glimpse(df1)
Rows: 61
Columns: 8
$ DFU <chr> "Product A", "Product A", "Product A", "Product A", "P…
$ Period <date> 2024-10-27, 2024-11-03, 2024-11-10, 2024-11-17, 2024-…
$ Current.GIT <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, 0, NA, NA,…
$ ETA.Current.GIT <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 600, 0, 0, 0, 1200, 0…
$ ETD.Next.GIT <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, 0, 0, 2000, 0, 0, …
$ ETA.Next.GIT <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, 0, 0, 0, 0, 0, 0, …
$ Transit.Time <dbl> NA, NA, NA, NA, NA, NA, NA, NA, NA, 9, 9, 9, 9, 9, 9, …
$ ETD.Current.GIT <dbl> 0, 0, 600, 0, 0, 0, 1200, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0…
# set a working df
<- Interim_data
df1
# accumulate data
<- df1 |> group_by(DFU, Period) |>
df1 summarise(
ETA.Current.GIT = sum(ETA.Current.GIT),
ETD.Current.GIT = sum(ETD.Current.GIT)
|>
)
mutate(
acc_ETA.Current.GIT = cumsum(ETA.Current.GIT),
acc_ETD.Current.GIT = cumsum(ETD.Current.GIT)
)
`summarise()` has grouped output by 'DFU'. You can override using the `.groups`
argument.
# calculate projected Current In Transit
$Proj.Current.GIT <- df1$acc_ETD.Current.GIT - df1$acc_ETA.Current.GIT
df1
# keep only needed columns
<- df1 |> select(DFU, Period, Proj.Current.GIT)
df1
# keep Results
<- df1
Proj_Current_In_Transit_data
glimpse(df1)
Rows: 61
Columns: 3
Groups: DFU [1]
$ DFU <chr> "Product A", "Product A", "Product A", "Product A", "…
$ Period <date> 2024-10-27, 2024-11-03, 2024-11-10, 2024-11-17, 2024…
$ Proj.Current.GIT <dbl> 0, 0, 600, 600, 600, 600, 1800, 1800, 1800, 1800, 180…
# set a working df
<- In_Transit_data
df1
# accumulate data
<- df1 |> group_by(DFU, Period) |>
df1 summarise(
ETD.Next.GIT = sum(ETD.Next.GIT),
ETA.Next.GIT = sum(ETA.Next.GIT)
|>
)
mutate(
acc_ETD.Next.GIT = cumsum(ETD.Next.GIT),
acc_ETA.Next.GIT = cumsum(ETA.Next.GIT)
)
`summarise()` has grouped output by 'DFU'. You can override using the `.groups`
argument.
# calculate projected Future In Transit
$Proj.Future.GIT <- if_else(df1$acc_ETD.Next.GIT > df1$acc_ETA.Next.GIT,
df1$acc_ETD.Next.GIT - df1$acc_ETA.Next.GIT,
df10)
# keep only needed columns
<- df1 |> select(DFU, Period, Proj.Future.GIT)
df1
# keep Results
<- df1
Proj_Future_In_Transit_data
glimpse(df1)
Rows: 52
Columns: 3
Groups: DFU [1]
$ DFU <chr> "Product A", "Product A", "Product A", "Product A", "P…
$ Period <date> 2024-12-29, 2025-01-05, 2025-01-12, 2025-01-19, 2025-…
$ Proj.Future.GIT <dbl> 0, 0, 2000, 2000, 2000, 2000, 3000, 3000, 3000, 5000, …
# merge everything
<- left_join(Interim_data, Proj_Current_In_Transit_data) df1
Joining with `by = join_by(DFU, Period)`
<- left_join(df1, Proj_Future_In_Transit_data) df1
Joining with `by = join_by(DFU, Period)`
# replace missing values by zero
$ETA.Current.GIT <- df1$ETA.Current.GIT |> replace_na(0)
df1$ETA.Next.GIT <- df1$ETA.Next.GIT |> replace_na(0)
df1$ETD.Next.GIT <- df1$ETD.Next.GIT |> replace_na(0)
df1$Current.GIT <- df1$Current.GIT |> replace_na(0)
df1
$ETD.Current.GIT <- df1$ETD.Current.GIT |> replace_na(0)
df1$Proj.Current.GIT <- df1$Proj.Current.GIT |> replace_na(0)
df1$Proj.Future.GIT <- df1$Proj.Future.GIT |> replace_na(0)
df1
# Calculate Total Projected In Transit
$Proj.GIT <- df1$Proj.Current.GIT + df1$Proj.Future.GIT
df1
# keep only relevant Periods, i.e. > Start Date
<- filter(df1, df1$Period >= start_date)
df1
# formatting
<- as.data.frame(df1)
df1
# keep results
<- df1
Projected_GIT_data
glimpse(df1)
Rows: 52
Columns: 11
$ DFU <chr> "Product A", "Product A", "Product A", "Product A", "…
$ Period <date> 2024-12-29, 2025-01-05, 2025-01-12, 2025-01-19, 2025…
$ Current.GIT <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
$ ETA.Current.GIT <dbl> 0, 0, 600, 0, 0, 0, 1200, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
$ ETD.Next.GIT <dbl> 0, 0, 2000, 0, 0, 0, 1000, 0, 0, 2000, 0, 0, 0, 0, 0,…
$ ETA.Next.GIT <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2000, 0, 0, 0, 1000, 0,…
$ Transit.Time <dbl> 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9, 9,…
$ ETD.Current.GIT <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
$ Proj.Current.GIT <dbl> 1800, 1800, 1200, 1200, 1200, 1200, 0, 0, 0, 0, 0, 0,…
$ Proj.Future.GIT <dbl> 0, 0, 2000, 2000, 2000, 2000, 3000, 3000, 3000, 5000,…
$ Proj.GIT <dbl> 1800, 1800, 3200, 3200, 3200, 3200, 3000, 3000, 3000,…
This time we will consider the actual (i.e. constrained) supply plan, based on the Entity 2’s supply answer.
# set a working df
<- Projected_GIT_data
df1
# get supply
$Supply <- df1$ETA.Current.GIT + df1$ETA.Next.GIT
df1
# keep only needed variables
<- df1 |> select(DFU,
df1
Period,
Supply)
# keep results
<- df1
const_supply_data
glimpse(df1)
Rows: 52
Columns: 3
$ DFU <chr> "Product A", "Product A", "Product A", "Product A", "Product A"…
$ Period <date> 2024-12-29, 2025-01-05, 2025-01-12, 2025-01-19, 2025-01-26, 20…
$ Supply <dbl> 0, 0, 600, 0, 0, 0, 1200, 0, 0, 0, 2000, 0, 0, 0, 1000, 0, 0, 2…
# set a working df
<- Entity1_template_data
df1
# keep only needed variables
<- df1 |> select(DFU, Period, Demand, Opening)
df1
glimpse(df1)
Rows: 53
Columns: 4
$ DFU <chr> "Product A", "Product A", "Product A", "Product A", "Product A…
$ Period <date> 2024-12-29, 2025-01-05, 2025-01-12, 2025-01-19, 2025-01-26, 2…
$ Demand <dbl> 142.8571, 250.0000, 250.0000, 250.0000, 142.8571, 250.0000, 25…
$ Opening <dbl> 1500, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
# merge
<- left_join(df1, const_supply_data) df1
Joining with `by = join_by(DFU, Period)`
glimpse(df1)
Rows: 53
Columns: 5
$ DFU <chr> "Product A", "Product A", "Product A", "Product A", "Product A…
$ Period <date> 2024-12-29, 2025-01-05, 2025-01-12, 2025-01-19, 2025-01-26, 2…
$ Demand <dbl> 142.8571, 250.0000, 250.0000, 250.0000, 142.8571, 250.0000, 25…
$ Opening <dbl> 1500, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
$ Supply <dbl> 0, 0, 600, 0, 0, 0, 1200, 0, 0, 0, 2000, 0, 0, 0, 1000, 0, 0, …
# calculate
<- planr::light_proj_inv(data = df1,
df1 DFU = DFU,
Period = Period,
Demand = Demand,
Opening = Opening,
Supply = Supply)
Joining with `by = join_by(DFU, Period)`
# keep results
<- df1
Calculated_Entity1_Const_Proj_Inv_data
glimpse(df1)
Rows: 53
Columns: 7
$ DFU <chr> "Product A", "Product A", "Product A", …
$ Period <date> 2024-12-29, 2025-01-05, 2025-01-12, 20…
$ Demand <dbl> 142.8571, 250.0000, 250.0000, 250.0000,…
$ Opening <dbl> 1500, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
$ Calculated.Coverage.in.Periods <dbl> 5.9, 4.9, 6.1, 5.1, 4.1, 3.1, 4.5, 3.5,…
$ Projected.Inventories.Qty <dbl> 1357.1, 1107.1, 1457.1, 1207.1, 1064.3,…
$ Supply <dbl> 0, 0, 600, 0, 0, 0, 1200, 0, 0, 0, 2000…
Add back the info about Current.Stock.Available.Tag, using the dataset ETD_ETA_data
# set a working df
<- ETD_ETA_data
df1
# replace missing values by zero
$Receiver.Current.Stock.Available.Tag <- df1$Receiver.Current.Stock.Available.Tag |> replace_na(0)
df1
# keep only unique values
<- unique(df1)
df1
# merge
<- left_join(Calculated_Entity1_Const_Proj_Inv_data, df1) df1
Joining with `by = join_by(DFU, Period)`
# replace missing values by zero
$Receiver.Current.Stock.Available.Tag <- df1$Receiver.Current.Stock.Available.Tag |> replace_na(0)
df1
# keep results
<- df1
Calculated_Entity1_Const_Proj_Inv_data
glimpse(df1)
Rows: 53
Columns: 11
$ DFU <chr> "Product A", "Product A", "Produc…
$ Period <date> 2024-12-29, 2025-01-05, 2025-01-…
$ Demand <dbl> 142.8571, 250.0000, 250.0000, 250…
$ Opening <dbl> 1500, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
$ Calculated.Coverage.in.Periods <dbl> 5.9, 4.9, 6.1, 5.1, 4.1, 3.1, 4.5…
$ Projected.Inventories.Qty <dbl> 1357.1, 1107.1, 1457.1, 1207.1, 1…
$ Supply <dbl> 0, 0, 600, 0, 0, 0, 1200, 0, 0, 0…
$ ETD.Future.In.Transit.Qty <dbl> 0, 0, 2000, 0, 0, 0, 1000, 0, 0, …
$ Supplier.Current.Stock.Available.Tag <dbl> 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, …
$ Receiver.Current.Stock.Available.Tag <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, …
$ ETA.Future.In.Transit.Qty <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 200…
# set a working df
<- Calculated_Entity1_Const_Proj_Inv_data
df1
# rename
<- df1 |> rename(
df1 sales_forecasts_qty = Demand,
Receiver_opening_qty = Opening,
Receiver_Calculated.Coverage.in.Periods = Calculated.Coverage.in.Periods,
Receiver_Projected.Inventories.Qty = Projected.Inventories.Qty,
Receiver_Supply_qty = Supply
)
# keep results
<- df1
Receiver_PI_data
glimpse(df1)
Rows: 53
Columns: 11
$ DFU <chr> "Product A", "Product A", "Pro…
$ Period <date> 2024-12-29, 2025-01-05, 2025-…
$ sales_forecasts_qty <dbl> 142.8571, 250.0000, 250.0000, …
$ Receiver_opening_qty <dbl> 1500, 0, 0, 0, 0, 0, 0, 0, 0, …
$ Receiver_Calculated.Coverage.in.Periods <dbl> 5.9, 4.9, 6.1, 5.1, 4.1, 3.1, …
$ Receiver_Projected.Inventories.Qty <dbl> 1357.1, 1107.1, 1457.1, 1207.1…
$ Receiver_Supply_qty <dbl> 0, 0, 600, 0, 0, 0, 1200, 0, 0…
$ ETD.Future.In.Transit.Qty <dbl> 0, 0, 2000, 0, 0, 0, 1000, 0, …
$ Supplier.Current.Stock.Available.Tag <dbl> 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, …
$ Receiver.Current.Stock.Available.Tag <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
$ ETA.Future.In.Transit.Qty <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
# set a working df
<- entity2_calculated_projection_data
df1
# keep only needed variables
<- df1 |> select(DFU ,
df1
Period,
Demand,
Constrained.Demand,
Current.Stock.Available.Tag,
Calculated.Coverage.in.Periods,
Projected.Inventories.Qty,
Opening,
Supply)
# rename
<- df1 |> rename(
df1 Supplier_Total.Demand_qty = Demand,
Supplier_Confirmed.Supply.Plan_qty = Constrained.Demand,
Supplier_Current.Stock.Available.Tag = Current.Stock.Available.Tag,
Supplier_Opening_qty = Opening,
Supplier_Calculated.Coverage.in.Periods = Calculated.Coverage.in.Periods,
Supplier_Projected.Inventories.Qty = Projected.Inventories.Qty,
Supplier_Supply_qty = Supply
)
# keep results
<- df1
Supplier_PI_data
glimpse(df1)
Rows: 44
Columns: 9
$ DFU <chr> "Product A", "Product A", "Pro…
$ Period <date> 2024-12-29, 2025-01-05, 2025-…
$ Supplier_Total.Demand_qty <dbl> 0, 0, 2000, 0, 0, 0, 3000, 0, …
$ Supplier_Confirmed.Supply.Plan_qty <dbl> 0, 0, 2000, 0, 0, 0, 1000, 0, …
$ Supplier_Current.Stock.Available.Tag <dbl> 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, …
$ Supplier_Calculated.Coverage.in.Periods <dbl> 5.3, 4.3, 3.3, 2.3, 1.3, 0.3, …
$ Supplier_Projected.Inventories.Qty <dbl> 3000, 3000, 1000, 1000, 1000, …
$ Supplier_Opening_qty <dbl> 3000, 0, 0, 0, 0, 0, 0, 0, 0, …
$ Supplier_Supply_qty <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 200…
# set a working df
<- Projected_GIT_data
df1
# keep only needed variables
<- df1 |> select(DFU,
df1
Period,
Current.GIT,
ETA.Current.GIT,
ETD.Next.GIT,
ETA.Next.GIT,
Proj.Current.GIT,
Proj.Future.GIT,
Proj.GIT)
# keep results
<- df1
GIT_data
glimpse(df1)
Rows: 52
Columns: 9
$ DFU <chr> "Product A", "Product A", "Product A", "Product A", "…
$ Period <date> 2024-12-29, 2025-01-05, 2025-01-12, 2025-01-19, 2025…
$ Current.GIT <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,…
$ ETA.Current.GIT <dbl> 0, 0, 600, 0, 0, 0, 1200, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
$ ETD.Next.GIT <dbl> 0, 0, 2000, 0, 0, 0, 1000, 0, 0, 2000, 0, 0, 0, 0, 0,…
$ ETA.Next.GIT <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2000, 0, 0, 0, 1000, 0,…
$ Proj.Current.GIT <dbl> 1800, 1800, 1200, 1200, 1200, 1200, 0, 0, 0, 0, 0, 0,…
$ Proj.Future.GIT <dbl> 0, 0, 2000, 2000, 2000, 2000, 3000, 3000, 3000, 5000,…
$ Proj.GIT <dbl> 1800, 1800, 3200, 3200, 3200, 3200, 3000, 3000, 3000,…
#-------------------------------------
# Merge Receiver and Supplier Projected Inventories
#-------------------------------------
# merge
<- left_join(Supplier_PI_data, Receiver_PI_data) df1
Joining with `by = join_by(DFU, Period)`
#-------------------------------------
# Add In Transit
#-------------------------------------
# merge
<- left_join(df1, GIT_data) df1
Joining with `by = join_by(DFU, Period)`
# keep results
<- df1
Set_Up_E2E_data
glimpse(df1)
Rows: 44
Columns: 25
$ DFU <chr> "Product A", "Product A", "Pro…
$ Period <date> 2024-12-29, 2025-01-05, 2025-…
$ Supplier_Total.Demand_qty <dbl> 0, 0, 2000, 0, 0, 0, 3000, 0, …
$ Supplier_Confirmed.Supply.Plan_qty <dbl> 0, 0, 2000, 0, 0, 0, 1000, 0, …
$ Supplier_Current.Stock.Available.Tag <dbl> 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, …
$ Supplier_Calculated.Coverage.in.Periods <dbl> 5.3, 4.3, 3.3, 2.3, 1.3, 0.3, …
$ Supplier_Projected.Inventories.Qty <dbl> 3000, 3000, 1000, 1000, 1000, …
$ Supplier_Opening_qty <dbl> 3000, 0, 0, 0, 0, 0, 0, 0, 0, …
$ Supplier_Supply_qty <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 200…
$ sales_forecasts_qty <dbl> 142.8571, 250.0000, 250.0000, …
$ Receiver_opening_qty <dbl> 1500, 0, 0, 0, 0, 0, 0, 0, 0, …
$ Receiver_Calculated.Coverage.in.Periods <dbl> 5.9, 4.9, 6.1, 5.1, 4.1, 3.1, …
$ Receiver_Projected.Inventories.Qty <dbl> 1357.1, 1107.1, 1457.1, 1207.1…
$ Receiver_Supply_qty <dbl> 0, 0, 600, 0, 0, 0, 1200, 0, 0…
$ ETD.Future.In.Transit.Qty <dbl> 0, 0, 2000, 0, 0, 0, 1000, 0, …
$ Supplier.Current.Stock.Available.Tag <dbl> 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, …
$ Receiver.Current.Stock.Available.Tag <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
$ ETA.Future.In.Transit.Qty <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
$ Current.GIT <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
$ ETA.Current.GIT <dbl> 0, 0, 600, 0, 0, 0, 1200, 0, 0…
$ ETD.Next.GIT <dbl> 0, 0, 2000, 0, 0, 0, 1000, 0, …
$ ETA.Next.GIT <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
$ Proj.Current.GIT <dbl> 1800, 1800, 1200, 1200, 1200, …
$ Proj.Future.GIT <dbl> 0, 0, 2000, 2000, 2000, 2000, …
$ Proj.GIT <dbl> 1800, 1800, 3200, 3200, 3200, …
#------------------------------
# Get data
<- Set_Up_E2E_data
df1
#------------------------------
# Filter
# filter Period to display a shorter horizon
<- df1 |> filter(Period <= "2025-06-30")
df1
#------------------------------
# Transform
# keep only needed variables
<- df1 |> select(Period,
df1
# Receiver
sales_forecasts_qty,
Receiver_Calculated.Coverage.in.Periods,
Receiver_Projected.Inventories.Qty,
ETA.Current.GIT,
ETA.Next.GIT,
Receiver.Current.Stock.Available.Tag,
# Supplier
Supplier_Total.Demand_qty,
Supplier_Confirmed.Supply.Plan_qty,
Supplier_Calculated.Coverage.in.Periods,
Supplier_Projected.Inventories.Qty,
Supplier_Supply_qty
)
# format Coverage at one digit after comma
$Receiver_Calculated.Coverage.in.Periods <- format(round(df1$Receiver_Calculated.Coverage.in.Periods, 1), nsmall = 1)
df1
$Supplier_Calculated.Coverage.in.Periods <- format(round(df1$Supplier_Calculated.Coverage.in.Periods, 1), nsmall = 1)
df1
# replace Current Stock Available Tag from numerical to characters
$Receiver.Current.Stock.Available.Tag <- if_else(df1$Receiver.Current.Stock.Available.Tag == 1, "ok", "")
df1
#-----------------
# create a f_colorpal field
#-----------------
#-----------------
# for Receiver
# formatting
$Receiver_Calculated.Coverage.in.Periods <- as.numeric(df1$Receiver_Calculated.Coverage.in.Periods)
df1
<- df1 |> mutate(Receiver_f_colorpal =
df1 case_when( Receiver_Calculated.Coverage.in.Periods > 16 ~ "lightblue",
> 4 ~ "white",
Receiver_Calculated.Coverage.in.Periods > 0 ~ "yellow",
Receiver_Calculated.Coverage.in.Periods <= 0 ~ "tomato",
Receiver_Calculated.Coverage.in.Periods TRUE ~ "#FFFFFF"
))
#-----------------
# for Supplier
# formatting
$Supplier_Calculated.Coverage.in.Periods <- as.numeric(df1$Supplier_Calculated.Coverage.in.Periods)
df1
<- df1 |> mutate(Supplier_f_colorpal = case_when( Supplier_Calculated.Coverage.in.Periods > 16 ~ "lightblue",
df1 > 4 ~ "white",
Supplier_Calculated.Coverage.in.Periods > 0 ~ "yellow",
Supplier_Calculated.Coverage.in.Periods TRUE ~ "#FF0000" ))
# formatting for a better display
$sales_forecasts_qty <- as.integer(df1$sales_forecasts_qty)
df1
#-------------------------
# Create Table
#reactable(df1)
reactable(df1, resizable = TRUE, showPageSizeOptions = TRUE,
striped = TRUE, highlight = TRUE, compact = TRUE,
defaultPageSize = 50,
columns = list(
#------------------------
# Receiver
sales_forecasts_qty = colDef(
name = "Sales Forecasts (units)",
minWidth = 150,
cell = data_bars(df1,
#round_edges = TRUE
#value <- format(value, big.mark = ","),
#number_fmt = big.mark = ",",
fill_color = "#3fc1c9",
#fill_opacity = 0.8,
text_position = "outside-end"
)
),
Receiver_Calculated.Coverage.in.Periods = colDef(
name = "Entity 1 Coverage (weeks)",
maxWidth = 90,
cell= color_tiles(df1, color_ref = "Receiver_f_colorpal")
),
Receiver_f_colorpal = colDef(show = FALSE), # hidden, just used for the coverages
Receiver_Projected.Inventories.Qty = colDef(
name = "Entity 1 Projected Inventories (units)",
format = colFormat(separators = TRUE, digits=0),
style = function(value) {
if (value > 0) {
<- "#008000"
color else if (value < 0) {
} <- "#e00000"
color else {
} <- "#777"
color
}list(color = color
#fontWeight = "bold"
)
}
),
ETA.Next.GIT = colDef(
name = "[ETA] actual Supply Plan (units)",
minWidth = 150,
cell = data_bars(df1,
#round_edges = TRUE
#value <- format(value, big.mark = ","),
#number_fmt = big.mark = ",",
fill_color = "#FFD700",
#fill_opacity = 0.8,
text_position = "outside-end"
)#format = colFormat(separators = TRUE, digits=0)
#number_fmt = big.mark = ","
),
Receiver.Current.Stock.Available.Tag = colDef(
name = "Entity 2 Supply Availability",
cell = function(value) {
<- switch(
color
value,ok = "hsl(120,61%,50%)",
#OK = "hsl(30, 97%, 70%)",
not = "hsl(3, 69%, 50%)"
)<- status_badge(color = color)
badge tagList(badge, value)
}),
#------------------------
# Supplier
Supplier_Total.Demand_qty = colDef(
name = "Total Requested Shipment (units)",
minWidth = 150,
cell = data_bars(df1,
#round_edges = TRUE
#value <- format(value, big.mark = ","),
#number_fmt = big.mark = ",",
fill_color = "#3fc1c9",
#fill_opacity = 0.8,
text_position = "outside-end"
)
),
Supplier_Confirmed.Supply.Plan_qty = colDef(
name = "[ETD] Possible Shipment (units)",
minWidth = 150,
cell = data_bars(df1,
#round_edges = TRUE
#value <- format(value, big.mark = ","),
#number_fmt = big.mark = ",",
fill_color = "#FFD700",
#fill_opacity = 0.8,
text_position = "outside-end"
)#format = colFormat(separators = TRUE, digits=0)
#number_fmt = big.mark = ","
),
Supplier_Calculated.Coverage.in.Periods = colDef(
name = "Entity 2 Coverage (weeks)",
maxWidth = 90,
cell= color_tiles(df1, color_ref = "Supplier_f_colorpal")
),
Supplier_f_colorpal = colDef(show = FALSE), # hidden, just used for the coverages
Supplier_Projected.Inventories.Qty = colDef(
name = "Entity 2 Projected Inventories (units)",
format = colFormat(separators = TRUE, digits=0),
style = function(value) {
if (value > 0) {
<- "#008000"
color else if (value < 0) {
} <- "#e00000"
color else {
} <- "#777"
color
}list(color = color
#fontWeight = "bold"
)
}
),
Supplier_Supply_qty = colDef(
name = "Production Plan (units)",
minWidth = 150,
cell = data_bars(df1,
fill_color = "#3CB371",
text_position = "outside-end"
)
),
#------------------------
# In Transit
ETA.Current.GIT = colDef(
name = "Current In Transit (units)",
minWidth = 150,
cell = data_bars(df1,
fill_color = "#F08080",
text_position = "outside-end"
)
)
# close columns list
),
columnGroups = list(
colGroup(name = "Entity 1", columns = c("sales_forecasts_qty",
"Receiver_Calculated.Coverage.in.Periods",
"Receiver_Projected.Inventories.Qty",
"ETA.Current.GIT",
"ETA.Next.GIT",
"Receiver.Current.Stock.Available.Tag")),
colGroup(name = "Entity 2", columns = c("Supplier_Total.Demand_qty",
"Supplier_Confirmed.Supply.Plan_qty",
"Supplier_Calculated.Coverage.in.Periods",
"Supplier_Projected.Inventories.Qty",
"Supplier_Supply_qty"))
)
# close reactable )
We can notice that despite the fact that the Entity 2 will be late to supply, with some backorders in February, there won’t be, until June, any risk of shortage at the Entity 1 level.