library(testthat)
# Load already included functions pkgload::load_all(export_all = FALSE)
#' Return the amount of a drug to be ordered #' #' @param forecast dataframe with two columns- mean_demand and sd_demand #' @param distribution distr6 object defining the distribution of order arrival #' @param min_stock number level below which we want a probability < p_min of falling #' @param max_stock number level above which we want a prob < p_max of rising #' @param p_min number probability of going into emergency stock #' @param p_max number probability of ordering too much #' @param inv_i int the inventory at the time of this (the i'th) order #' @param delta_pref int the preferred order interval for drug in question #' @param outstanding_orders int the number of units in outstanding orders #' @return number giving amount to order #' @export drug_quantity <- function(forecast, distribution, min_stock, max_stock, p_min, p_max, inv_i, delta_pref, outstanding_orders) { MaxLoops = 100 # maximum number of loops permitted in each while loop below Q_i = 0 # initialise order quantity Delta_i = delta_pref # initialise time to next order flag_suf = 0 # initialise a flag indicating (=1) that Q_i is sufficent flag_stor = 0 # initialise a flag indicating (=1) that Q_i is not too much Forecast_quantiles <- make_quantiles(forecast) tmp1 <- c(0,0) # dummy storage for Q_i and flag_suf dummy_counter <- 0 while(flag_suf < 1 & dummy_counter < MaxLoops){ # repeat until Q_i sufficient dummy_counter <- dummy_counter + 1 # returns whether Q_i sufficient and next Q_i to try tmp1 <- Q_enough_Q( distribution, inv_i, Q_i, outstanding_orders, Forecast_quantiles, Delta_i, min_stock, p_min) Q_i = tmp1[1] flag_suf = tmp1[2] } # ERROR HANDLING ON POSSIBLE TIME-OUT ABOVE tmp2 <- c(Q_i, flag_stor) # returns whether Q_i satisfies storage condition and next Q_i to try tmp2 <- Q_toomuch_Q(forecast_q = Forecast_quantiles, o_orders = outstanding_orders, choose_distribution = distribution, current_q_i = Q_i, max_stock = max_stock, p_max = p_max, inv_i = inv_i) Q_i <- tmp2[1] flag_stor <- tmp2[2] # if sufficient Q_i breaches storage, need to reduce and shorten Delta if(flag_stor < 1){ flag_suf <- 0 # reset sufficiency flag to zero as about to reduce Q_i dummy_counter_2 <- 0 # look for Q_i that meets storage condition while(flag_stor<1 & dummy_counter_2 < MaxLoops){ dummy_counter_2 <- dummy_counter_2 + 1 # returns whether Q_i satisfies storage condition and next Q_i to try tmp2 <- Q_toomuch_Q(forecast_q = Forecast_quantiles, o_orders = outstanding_orders, choose_distribution = distribution, current_q_i = Q_i, max_stock = max_stock, p_max = p_max, inv_i = inv_i) Q_i <- tmp2[1] flag_stor <- tmp2[2] } tmp3 <- c(0,0) # temporary storage of Delta_i and flag_suf dummy_counter_3 <- 0 # now reduce Delta_i until reduced Q_i sufficient while(flag_suf < 1 & dummy_counter_3 < MaxLoops){ dummy_counter_3 <- dummy_counter_3 + 1 tmp3 <- Q_enough_Delta(Forecast_quantiles, choose_distribution = distribution, d_i = Delta_i, inv_i = inv_i, current_q_i = Q_i, min_stock, p_min, outstanding_orders = outstanding_orders) Delta_i <- tmp3[1] flag_suf <- tmp3[2] } } return(list("Q_i" = Q_i, "Delta_i" = Delta_i)) } # set up as piece wise linear from input. pwlcdf <- function(Forecast_quantiles, q_vals, num_q_vals, time_point, x){ res <- numeric(length(x)) for(i in seq(1:length(x))){ low_cut <- Forecast_quantiles[time_point,1] high_cut <- Forecast_quantiles[time_point, num_q_vals] res[i][x[i] >= high_cut] = 1 for(j in seq(1, num_q_vals - 1, 1)) { res[i][x[i] >= Forecast_quantiles[time_point,j] & x[i] < Forecast_quantiles[time_point,j+1]] = q_vals[j] + (q_vals[j+1] - q_vals[j]) * (x[i] - Forecast_quantiles[time_point, j]) / (Forecast_quantiles[time_point,j+1] - Forecast_quantiles[time_point, j]) } res[i][x[i] <= low_cut] = 0 } return(res) } # set up as piece wise linear from input. pwlquant <- function(Forecast_quantiles, q_vals, num_q_vals, time_point, p){ res <- numeric(length(p)) for (i in seq(1, length(p))){ for(j in seq(1, num_q_vals - 1, 1)) { res[i][p[i] >= q_vals[j] & p[i] < q_vals[j + 1]] = Forecast_quantiles[time_point, j] + (Forecast_quantiles[time_point, j + 1] - Forecast_quantiles[time_point, j])*(p[i] - q_vals[j]) / (q_vals[j + 1] - q_vals[j]) } res[i][p[i] >= q_vals[num_q_vals]] = Forecast_quantiles[time_point, num_q_vals] } return(res) } Q_enough_Delta <- function(forecast_q, choose_distribution, d_i, inv_i, current_q_i, min_stock, p_min, outstanding_orders){ # determines whether Q_i is sufficient and, if not, suggests next Delta_i to try # function used when Q_i has been reduced to meet storage condition epsilon <- 0.01 flag1 <- 0 lmax <- length(choose_distribution) Delta_i <- d_i # prob that, if next order delivered at time y, Q_i for this # order will be insufficient to meet demand to then P_Q_insuff <- c(rep(1,Delta_i+lmax+1)) # initialise to zero - probability that the next order is delivered at time y Prob_y <-c(rep(0,Delta_i+lmax+1)) term_y <- c(rep(0,Delta_i+lmax+1)) Q_i <- current_q_i num_q_vals <- ncol(forecast_q) Forecast_quantiles <- forecast_q # set up vector of quantile levels - evenly spaced for now q_vals <- c(seq(0,1, 1 / (num_q_vals - 1))) Q_out <- sum(outstanding_orders) # now set probabilities for next delivery arriving at time y # and Q_i being insufficient if that is the case for (y in 1:Delta_i){ Prob_y[y] = 0 # probability that demand up to and including day y eats into emergency stock P_Q_insuff[y] <- 1 - pwlcdf(Forecast_quantiles, q_vals, num_q_vals, y, inv_i + Q_out + Q_i - min_stock) } # we only need to go just beyond (Delta_i + longest lead time) for (y in seq(Delta_i + 1, Delta_i + lmax, 1)) { Prob_y[y] = choose_distribution[y-Delta_i] # probability that demand up to and including day y eats into emergency stock P_Q_insuff[y] <- 1 - pwlcdf(Forecast_quantiles, q_vals, num_q_vals, y, inv_i + Q_out + Q_i - min_stock) } # loop over second set of values of y phi <- sum(term_y) if (phi > p_min) { # Q_i insufficient given current value of Delta_i sc <- (p_min / phi) * (1 - epsilon) # get reduction required in phi y_peak <- which.max(term_y) # find biggest term in phi # find target probability for next delivery being ordered at y P_target <- Prob_y[y_peak] * sc y_hat <- y_peak test <- Prob_y[y_hat] # effectively we are bringing the order of the next forward until we get # desired reduction in the (currently) biggest contribution # to the prob of Q_i being insufficient while(test > P_target){ y_hat <- y_hat + 1 test <- Prob_y[y_hat] } Delta_i <- Delta_i -(y_hat - y_peak) } else { flag1 = 1 } if (Delta_i < 1) { print("CANNOT BE SOLVED WITH CURRENT INPUTS - REVISE STORAGE CONSTRAINT, EMERGENCY STOCK OR TOLERANCES") } res <- c(Delta_i, flag1) return(res) } Q_enough_Q <- function(lead_time_dis, inv_i, current_q_i, outstanding_orders, Forecast_quantiles, Delta_i, min_stock, p_min){ # This function assesses whether Q_i will be sufficient and, if not, # suggests next Q_i to try epsilon <- 0.01 flag1 = 0 # initialise as insufficient phi = 1 # this is the probability that order is insufficient lmax <- length(lead_time_dis) # this is the prob that, if next order delivered at time y, Q_i for this # order will be insufficient to meet demand to then P_Q_insuff <- c(rep(1,Delta_i + lmax + 1)) # initialise to zero - this is the probability that the next order is # delivered at time y Prob_y <-c(rep(0,Delta_i+lmax+1)) term_y <- c(rep(0,Delta_i+lmax+1)) num_q_vals <- ncol(Forecast_quantiles) # set up vector of quantile levels - evenly spaced for now q_vals <- c(seq(0,1, 1 / (num_q_vals - 1))) Q_i <- current_q_i Q_out <- sum(outstanding_orders) # quantity associated with outstanding orders for (y in 1 : Delta_i){ Prob_y[y] = 0 # probability that demand up to and including day y eats into emergency stock P_Q_insuff[y] <- 1 - pwlcdf(Forecast_quantiles, q_vals, num_q_vals, y, inv_i + Q_out + Q_i - min_stock) } # we only need to go just beyond (Delta_i + longest lead time) for (y in seq(Delta_i + 1, Delta_i + lmax, 1)) { Prob_y[y] = lead_time_dis[y - Delta_i] # wrong code Martin sent # Prob_y[y] = lead_time_dis$cdf(y-Delta_i) # returns probability that demand up to and including day y eats into emergency stock P_Q_insuff[y] <- 1 - pwlcdf(Forecast_quantiles, q_vals, num_q_vals, y, inv_i + Q_out + Q_i - min_stock) } # loop over second set of values of y # joint probability delivery of next order occurs at y and that q_i insufficient if so term_y <- Prob_y * P_Q_insuff # sums over all possible delivery times y to give probability that Q_i insufficient phi <- sum(term_y) if (phi > p_min) { # if Q_i not sufficient # get scaling factor required to bring prob phi under tolerance value - # with slight over-adjustment to stop any asymptotic behaviour sc <- (p_min / phi) * (1 - epsilon) y_peak <- which.max(term_y) # find biggest term in phi # get target for reduced contribution from biggest term P_target <- P_Q_insuff[y_peak] * sc # get from forecast the demand associated with that target probability B <- pwlquant(Forecast_quantiles, q_vals, num_q_vals, y_peak, (1 - P_target)) # set Q_i to be sufficient reduce bring biggest term in phi by sc - # amount determined above Q_i <- B + min_stock - inv_i - Q_out } else { flag1 = 1 } res <- c(Q_i,flag1) return(res) } Q_toomuch_Q <- function(forecast_q, o_orders, choose_distribution, current_q_i, max_stock, p_max, inv_i) { # function indicates if Q_i meets storage condition and, if not, gives next Q_i to try epsilon <- 0.01 flag2 = 0 Q_i <- current_q_i Forecast_quantiles <- forecast_q num_q_vals <- ncol(Forecast_quantiles) # set up vector of quantile levels - evenly spaced for now q_vals <- c(seq(0,1, 1 / (num_q_vals - 1))) # NEW FOR OCTOBER 2021 lmax <- length(choose_distribution) # p if next order delivered at time y # Q_i for this order will be insufficient to meet demand to then P_Q_toomuch <- c(rep(1,lmax)) # initialise to zero - p that the next order is delivered at time y Prob_x <-c(rep(0,lmax)) term_x <- c(rep(0,lmax)) # P_Q_toomuch <- c(rep(1,Forecast_length)) # initialise as too much # Prob_x <-c(rep(0,Forecast_length)) # initialise to zero # term_x <- c(rep(0,Forecast_length)) Q_out <- o_orders # NEW FOR OCTOBER 2021 Prob_x <- choose_distribution # for (x in seq(1,Forecast_length,1)){ for (x in 1 : lmax){ # to reflect change in lead_time_dis from a Distr6 entity to a vector # Prob_x[x] <- lead_time_dis$cdf(x)- lead_time_dis$cdf(x-1) # probability that this delivery will be on day x # P_Q_toomuch[x] <- pnorm((Inv_i+Q_out+Q_i-Storage_constraint), # Forecast$mean_cumulative_demand[x],Forecast$sd_cumulative[x], lower.tail = TRUE) # P_Q_toomuch[x] <- Fcast[x]$cdf(Inv_i+Q_out+Q_i-Storage_constraint,lower.tail = TRUE ) # gives probability that, if this delivery arrives at time x, it will # breach storage constraint P_Q_toomuch[x] <- pwlcdf( Forecast_quantiles, q_vals, num_q_vals, x, inv_i + Q_out + Q_i - max_stock) } term_x <- Prob_x * P_Q_toomuch # joint probability that delivery occurs at x and that Q_i breaches storage if so gam <- sum(term_x) # summming over all possible x, this is prob that delivery will breach storage on arrival if (gam > p_max) { # compare to tolerance sc <- (p_max / gam) * (1 - epsilon) # get reduction required in prob of breach x_peak <- which.max(term_x) # find x with biggest contribution to prob of breach P_target <- P_Q_toomuch[x_peak] * sc # get target for reduced contribution from biggest term # B <- qnorm(P_target,Forecast$mean_cumulative_demand[x_peak],Forecast$sd_cumulative[x_peak],lower.tail = TRUE) # B <- Fcast[x_peak]$quantile(P_target,lower.tail = TRUE) B <- pwlquant(Forecast_quantiles,q_vals,num_q_vals,x_peak,P_target) # get from forecast the demand associated with target prob # set Q_i to meet that demand- this is not guaranteed to be enough of a reduction. Q_i <- B - inv_i - Q_out + max_stock } else { flag2 = 1 } res <- c(Q_i,flag2) return(res) } #' Forecast and run inventory management algorithm, returning reorder quantities #' by site and drug #' #' @param site String. Site code (selected from dynamic UI listing all sites) #' @param supplier String. Supplier (changes weekly, selected in Shiny interface) #' @param product dataframe. Contents of product_sup_profile, loaded in #' app_server.R #' @param w_order dataframe. Contents of w_order_log_df1, loaded in #' app_server.R #' #' @return dataframe containing drug name, stock level, days to order, etc. #' @export #' inventory_reorder <- function(site, supplier, product, w_order, requis, holidays, updateProgress = NULL){ product <- product %>% dplyr::filter(Site == site, Supplier_name == supplier) %>% dplyr::arrange(Drug_name) for(chem in 1:nrow(product)[1]){ starting_stock_level1 <- initial_product_list %>% dplyr::filter(Drug_code == product$Drug_code[chem]) starting_stock_level <- starting_stock_level1$stocklvl[1] w_trans_log_df2a <- w_trans_log_df1 %>% dplyr::filter(Drug_code == product$Drug_code[chem], Site == site, Date > as.Date("2022-02-25")) %>% dplyr::summarise(Total_Qty = sum(Qty)) w_order_log_df3 <- w_order_log_df2 %>% dplyr::filter(Drug_code == product$Drug_code[chem] & Site == site & Kind == "R" & DateReceived > as.Date("2022-02-25") & DateReceived < Sys.Date()) %>% dplyr::summarise(Rec_Qty = (sum(QtyRec) * product$Packsize[chem])) stock_level <- starting_stock_level + w_order_log_df3$Rec_Qty - w_trans_log_df2a product$stocklvl[chem] <- as.numeric(stock_level) product$stocklvl <- utils::type.convert(product$stocklvl) product$stocklvl[chem] <- ifelse(product$stocklvl[chem] < 0, 0, product$stocklvl[chem]) date_last_ordered <- w_order_log_df2 %>% dplyr::filter(Drug_code == product$Drug_code[chem], Site == site, Kind %in% c("D", "O", "I")) %>% dplyr::arrange(desc(DateOrdered)) product$lastordered[chem] <- date_last_ordered$DateOrdered[1] } # Settings which aren't yet provided within the code max_storage_capacity <- 100000 # add in other waste / expiry / adjustment codes waste_adjust_codes <- c("Adj", "ADJ","COMSP","EXP", "HADJ","HEXP", "HWAST", "MOCK", "RWAST", "TEST", "TRG", "WAST", "WASTE", "XXXX") # list of excluded suppliers for site 100 cross_site_order_exclusions <- c("HIGH", "MILL", "PHWRC", "RAMP") # takes two reactive inputs- site and supplier order_list <- product %>% dplyr::filter(Site == site, Supplier_name == supplier) %>% dplyr::arrange(Drug_name) # Add in order cycle - only needed for shadow testing order_list <- dplyr::left_join(order_list, product_ord_cycle, by = c("Site" = "Site", "Drug_code" = "Drug_code", "Supplier_name" = "Supplier_name")) # Identify new products which haven't been assigned an AAH ordercycle and # add in ordercycle details - only needed for shadow testing order_list$Order_cycle[is.na(order_list$Order_cycle)] <- "empty" order_list$Order_cycle <- ifelse(order_list$Order_cycle == "empty", "AAHb", order_list$Order_cycle) # select orders placed within the last 2 years order_log <- w_order %>% subset(DateOrdered > Sys.Date() - 730) %>% dplyr::filter(Site == site) # this is only needed for site 100 if(site == 100){ order_log <- order_log %>% dplyr::filter(!Supplier_name %in% cross_site_order_exclusions) } purrr::pmap_dfr(order_list, function(Drug_code, ProductID, ...){ comment <- "None" # cat(Drug_code) # Filter product for relevant product product_info <- product %>% dplyr::filter(.data$Drug_code == .env$Drug_code, Site == site) # calculate leadtimes ord_log <- order_log %>% dplyr::filter(Kind %in% c("D", "O", "I") & .data$Drug_code == .env$Drug_code) %>% dplyr::select(OrderNum, Drug_code, DateOrdered, QtyOrd) if(nrow(ord_log) <= 1){ return( data.frame(drug = Drug_code, order = NA, actual_order_qty = 0, days_to_order = NA, Drug_Name = product_info$Drug_name[1], Supplier_Tradename = product_info$SupplierTradeName[1], PackSize = product_info$Packsize[1], Units = product_info$PrintForm[1], Stock_Level = product_info$stocklvl[1], Last_Order_Date = product_info$lastordered[1], Min_stock_risk = NA, Min_stock_level = NA, Comments = "No history - order manually") ) } # Get rid of multiple lines for same order & received date rec_log <- order_log %>% dplyr::filter(Site == site & Kind == "R" & .data$Drug_code == .env$Drug_code) %>% dplyr::group_by(OrderNum, Drug_code, Supplier_name, DateOrdered, DateReceived, Site, Kind) %>% dplyr::summarise(QtyRec = sum(QtyRec)) %>% dplyr::ungroup() if(nrow(rec_log) <= 1){ return( data.frame(drug = Drug_code, order = NA, actual_order_qty = 0, days_to_order = NA, Drug_Name = product_info$Drug_name[1], Supplier_Tradename = product_info$SupplierTradeName[1], PackSize = product_info$Packsize[1], Units = product_info$PrintForm[1], Stock_Level = product_info$stocklvl[1], Last_Order_Date = product_info$lastordered[1], Min_stock_risk = NA, Min_stock_level = NA, Comments = "No history - order manually") ) } ord_rec_log <- dplyr::left_join(ord_log, rec_log, by = c("OrderNum" = "OrderNum", "Drug_code" = "Drug_code", "DateOrdered" = "DateOrdered")) %>% tidyr::replace_na(list(Supplier_name = supplier, DateReceived = as.Date("1999-12-30"), Site = site, Kind = "R", QtyRec = 0)) ord_rec_log <- ord_rec_log %>% dplyr::mutate(Outstanding_order = QtyOrd - QtyRec) orders_outstanding <- ord_rec_log %>% dplyr::group_by(Site, Supplier_name, Drug_code, OrderNum, DateOrdered, QtyOrd) %>% dplyr::summarise(TotalRecQty = sum(QtyRec)) %>% dplyr::mutate(Outstanding_order = QtyOrd - TotalRecQty) %>% dplyr::ungroup() leadtime <- ord_rec_log %>% dplyr::filter(DateReceived != "1999-12-30") %>% dplyr::rowwise() %>% dplyr::mutate(leadtime = n_weekdays(DateOrdered, DateReceived, holidays)) %>% subset(leadtime <= 14) if(nrow(leadtime) == 0){ return( data.frame(drug = Drug_code, order = NA, actual_order_qty = 0, days_to_order = NA, Drug_Name = product_info$Drug_name[1], Supplier_Tradename = product_info$SupplierTradeName[1], PackSize = product_info$Packsize[1], Units = product_info$PrintForm[1], Stock_Level = product_info$stocklvl[1], Last_Order_Date = product_info$lastordered[1], Min_stock_risk = NA, Min_stock_level = NA, Comments = "No history - order manually") ) } leadmin <- min(leadtime$leadtime) leadmax <- max(leadtime$leadtime) leadmode <- calc_mode(leadtime$leadtime) # set distribution for delivery lead time lead_time_distribution <- distr6::Triangular$new( lower = leadmin - 0.5, upper = leadmax + 0.5, # Have had to make upper +0.5 rather than -0.5 in # original code otherwise if min, max & mode is 1 distr6 doesn't work # as upper is less than mode mode = leadmode) lead_time_dis <- lead_time_distribution$pdf(seq(leadmin - 0.5, leadmax + 0.5, 1)) # find outstanding order quantity for orders placed within last 14 days outstanding_order_qty <- orders_outstanding %>% subset(DateOrdered > Sys.Date() - 14) %>% dplyr::mutate(out_ord_qty = Outstanding_order * product_info$Packsize[1]) %>% dplyr::summarise(total_out_ord_qty = sum(out_ord_qty)) # See if any outstanding orders from last 60 days add comment to shiny table outstanding_order_comment <- orders_outstanding %>% subset(DateOrdered > Sys.Date() - 60) %>% dplyr::filter(Outstanding_order > 0) %>% dplyr::arrange(desc(DateOrdered)) if(nrow(outstanding_order_comment) == 0){ comment <- "None" }else{ comment <- paste("Outstanding order from", outstanding_order_comment$DateOrdered[1], sep = " ") } # find outstanding requisitions which need fulfilling outstanding_requis <- requis %>% dplyr::filter(Site == site, .data$Drug_code == .env$Drug_code) if(nrow(outstanding_requis) == 0){ outstanding_requis_quantity <- 0L }else{ outstanding_requis <- outstanding_requis %>% subset(DateOrdered > Sys.Date() - 14) %>% dplyr::summarise(total_out_requis = sum(Outstanding)) outstanding_requis_quantity <- outstanding_requis$total_out_requis[1] comment <- "Outstanding requisition" } # Filter out issues to adjustment and waste codes # to consider in future deleting issues done in error i.e. # where issue is followed by a return to stock demand_data <- w_trans_log_df1 %>% dplyr::filter(Site == site, .data$Drug_code == .env$Drug_code, !Ward %in% waste_adjust_codes, Qty >= 0, Date < order_date, Date < Sys.Date()) %>% rbind(data.frame(WTranslogID = "NA", Date = Sys.Date(), Drug_code = Drug_code, Qty = 0, Ward = "NA", Site = site)) %>% dplyr::arrange(Date) %>% dplyr::group_by(Date) %>% dplyr::summarise(Total_Qty = sum(Qty)) %>% dplyr::ungroup() %>% tidyr::complete(Date = seq.Date(min(Date), max(Date), by="day")) %>% tidyr::replace_na(list(Total_Qty = 0)) # if no orders in this range jump out of function if(sum(demand_data$Total_Qty) == 0){ return( data.frame(drug = Drug_code, order = NA, actual_order_qty = 0, days_to_order = NA, Drug_Name = product_info$Drug_name[1], Supplier_Tradename = product_info$SupplierTradeName[1], PackSize = product_info$Packsize[1], Units = product_info$PrintForm[1], Stock_Level = product_info$stocklvl[1], Last_Order_Date = product_info$lastordered[1], Min_stock_risk = NA, Min_stock_level = NA, Comments = "No history - order manually") ) } # Calculate min_stock_level min_stock_level <- demand_data %>% subset(Date > Sys.Date() - 365) min_stock_level <- stats::quantile(min_stock_level$Total_Qty, .98, na.rm = TRUE) %>% ceiling() # Work out p_min from order scheduling product_ordercycle <- product_ord_cycle %>% dplyr::filter(.data$Drug_code == .env$Drug_code) sup_ordercycle <- sup_order_sched %>% dplyr::filter(Supplier == supplier & Order_cycle == product_ordercycle$Order_cycle[1]) ar <- (lubridate::interval(sup_ordercycle$Order_cycle_start_date[1], Sys.Date()) %>% as.numeric('days')) %% 14 time_til_next_order <- ord_sch %>% dplyr::filter(Day == sup_order_sched$Order_day[1] & x == ar) if(nrow(time_til_next_order) == 0){ return( data.frame(drug = Drug_code, order = NA, actual_order_qty = 0, days_to_order = NA, Drug_Name = product_info$Drug_name[1], Supplier_Tradename = product_info$SupplierTradeName[1], PackSize = product_info$Packsize[1], Units = product_info$PrintForm[1], Stock_Level = product_info$stocklvl[1], Last_Order_Date = product_info$lastordered[1], Min_stock_risk = NA, Min_stock_level = NA, Comments = "No history - order manually") ) } if(time_til_next_order$Delta_p[1] > 7){ risk_of_min_stock <- 0.15 risk_of_exceeding_max_stock <- 0.05 }else{ risk_of_min_stock <- 0.4 risk_of_exceeding_max_stock <- 0.05 } # Run forecast demand_data <- make_tsibble(demand_data, frequency = "Daily") daily_forecast <- forecast_series(demand_data, length(lead_time_dis) + 14, frequency = "Daily") actual_forecast <- daily_forecast %>% dplyr::filter(.model == "ARIMA") step <- drug_quantity( forecast = actual_forecast, distribution = lead_time_dis, min_stock = min_stock_level, max_stock = max_storage_capacity, p_min = risk_of_min_stock, p_max = risk_of_exceeding_max_stock, inv_i = product_info$stocklvl[1], delta_pref = time_til_next_order$Delta_p[1], outstanding_orders = outstanding_order_qty$total_out_ord_qty[1]) comment <- ifelse(comment != "None", comment, "None") to_return <- data.frame(drug = Drug_code, order = ceiling(step$Q_i / product_info$Packsize[1] + outstanding_requis_quantity), actual_order_qty = ceiling( step$Q_i / product_info$Packsize[1] + outstanding_requis_quantity), days_to_order = step$Delta_i, Drug_Name = product_info$Drug_name[1], Supplier_Tradename = product_info$SupplierTradeName[1], PackSize = product_info$Packsize[1], Units = product_info$PrintForm[1], Stock_Level = product_info$stocklvl[1], Last_Order_Date = product_info$lastordered[1], Min_stock_risk = risk_of_min_stock, Min_stock_level = min_stock_level, Comments = comment) # updateProgress(detail = paste0("Drug: ", to_return$drug, ", order:", # to_return$order)) # Populate order quantity & time 'til next order in output table return(to_return) }) }
test_that("inventory_functions works", { load("pharmacy.rda") lower_lead = 1.5 # discrete min - 0.5 upper_lead = 10.5 # discrete max - 0.5 mode_lead = 4 # discrete mode # set distribution for delivery lead time lead_time_distribution <- distr6::Triangular$new(lower = lower_lead, upper = upper_lead, mode = mode_lead) lead_time_dis <- lead_time_distribution$pdf( seq(lower_lead - 0.5, upper_lead + 0.5, 1)) lead_time_dis <- lead_time_distribution$pdf( seq(lower_lead - 0.5, upper_lead + 0.5, 1)) test_data <- pharmacy %>% dplyr::filter(Site1 == "Site C", NSVCode == "Drug A") daily_data <- make_tsibble(test_data, frequency = "Daily") test_forecast <- forecast_series(daily_data, length(lead_time_dis) + 14, frequency = "Daily") test_stock <- drug_quantity(forecast = test_forecast, outstanding_orders = 100, distribution = lead_time_dis, min_stock = 100, max_stock = 10000, p_min = 0.01, p_max = 0.05, inv_i = 150, delta_pref = 14) testthat::expect_equal(test_stock$Q_i, 10091.85, tolerance = 1) testthat::expect_equal(test_stock$Delta_i, 5) })
# Run but keep eval=FALSE to avoid infinite loop # Execute in the console directly fusen::inflate(flat_file = "dev/flat_inventory_functions.Rmd", vignette_name = NA)
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.