Post order run is used to fine-tune the AGR order proposal. Through this very specialized and advanced logic can be applied to any conditions we seek to adjust. We use post order run instead of changing the AGR code order logic itself. Typical example is the allocaiton logic seen at the bottom here where we automatically adjust the order proposal if we exhaust WH stock with the store demand. The overall goal should be to improve the system and reduce manual steps needed by the customer.
Post order run behaves slightly differently on manual or scheduled orders. The latter type can take in more custom actions if needed. Often, we only want to run specific post order run steps on larger scheduled orders (often orders created from specific MBE order reports).
Those orders have slightly different structure for possible post order runs. If needed we can put special logic into dbo.scheduled_orders_post_order_run_custom
. Procedure dbo.post_order_run
runs on all orders.
dbo.scheduled_orders_run_execute
dbo.order_run_execute
dbo.post_order_run
dbo.scheduled_orders_post_order_run
(adds “Scheduled order: " prefix to order Description
field)
dbo.scheduled_orders_post_order_run_custom
(post processing only on scheduled orders)dbo.scheduled_orders_post
In standard AGR these are the procedures run in manual orders:
dbo.order_run_execute
dbo.post_order_run
(which can then call custom logic as this examples)
dbo.post_order_run_
allocateStep 1
Make sure the following code is in dbo.order_run_execute
. Set in after dbo.order_run_post_populate
is executed. This step is normally already done by AGR install.
EXEC [dbo].[post_order_run]
@batch_id = @batch_id, @origin_id = @origin_id, @parent_id = @event_id,
@order_run_id = @order_run_id
Step 2. The main dbo.post_order_run procedure
This procedure is run every time a manual or scheduled order is created. This example shows how a complicated setup can be broken into easily understood modules making this procedure and its dependencies much easier to develop, test and maintain.
USE [agr_prod]
GO
/****** Object: StoredProcedure [dbo].[post_order_run] Script Date: 03.12.2020 09.59.20 ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author: Sigurdur Skarphedinsson
-- Description: Post order run logic run on all orders
--
-- 20.12.2019.GH Removal of dependencies on scheduled order post order run, simplified flow
-- =============================================
ALTER PROCEDURE [dbo].[post_order_run]
(
@batch_id INT = NULL,
@origin_id TINYINT = NULL,
@parent_id INT = NULL,
@order_run_id INT
)
AS
BEGIN
SET NOCOUNT ON
BEGIN TRY
DECLARE @event_id INT
EXEC @event_id = core.event_task_start @task_name = @@PROCID, @batch_id = @batch_id, @origin_id = @origin_id, @parent_id = @parent_id, @print = 1
SELECT 1 as returnValue
EXEC core.event_task_complete @event_id = @event_id, @print = 1
END TRY
BEGIN CATCH
EXEC core.catch_error @event_id = @event_id
END CATCH
END
Break logic up into independent parts. For clarity sake it is better to best to split post order run actions into separate procedures and then call them from dbo.post_order_run
as seen below. Then dbo.post_order_run
itself will be smaller and becomes much simpler to manage. This is a good principle to follow, even on smaller, simpler setups. Then each post order run part can be improved, tested and logged independent of the other logic in place. If we are dealing with many post order run procedures it is helpful to have them all use the same prefix, like dbo.post_order_run_[postfix]
.
EXEC dbo.post_order_run_allocate
@batch_id = @batch_id, @origin_id = @origin_id, @parent_id = @event_id,
@order_run_id = @order_run_id
Step 3. Adding custom post order run code as needed
This allocates order from stores to warehouse. It processes all item numbers, and if stock in warehouse < sum of orders from stores for the item, it then allocates to all the stores by prioritizing the orders from highest to lowest store demand. We then start by giving the stores order multiples until we run out of stock. Have in mind that in this procedure, available WH stock has already been calculated and is stored in table dbo.item_extra_info.
USE [AGR_prod]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author: Elena
-- Description: Post order allocation
-- 13.03.2017.EL Only for stores ordering from WH
-- If the total amount stores are ordering from WH is greater than available stock at WH,
-- then the amount is adjusted to available WH amount proportionally to store demand and
-- rounded by min order and order multiple. If the amount is still larger than available stock at WH
-- then the stores ordered by their demand, reduce order qty by order multiple starting from the store with the smallest demand
-- 28.06.2017.GB Optimized by adding indexes and changing update
-- 28.06.2017.GB Added locking and unlocking of orderlines
-- 14.02.2019.GH Changed wh_stocks to wh_stocks_available column (which excludes previous sales lines to stores)
-- 28.06.2019.GH Added "Allocation. No available WH stocks" text to initial update query (if no available WH Stocks)
-- 04.11.2019.GH Logging of #order_allocation table for debugging porposes, minor syntax fixes
-- 05.05.2020.GH Changed to use WH stock available comm to support multipl replenishment order runs in one schedule
-- =============================================
-- exec [dbo].[post_order_run] 123, 527
ALTER PROCEDURE [dbo].[post_order_run_allocate]
(
@batch_id INT = NULL,
@origin_id TINYINT = NULL,
@parent_id INT = NULL,
@order_run_id AS INT
)
AS
BEGIN
SET NOCOUNT ON;
DECLARE @extra_info_string nvarchar(100)
SET @extra_info_string = 'Order run id: ' + cast(@order_run_id AS NVARCHAR(100))
EXEC @action_exec_id = dbo.log_action_execution @action_exec_id = @action_exec_id, @proc_id = @@PROCID, @extra_info = @extra_info_string
BEGIN TRY
DECLARE @order_run_iid int = @order_run_id
DECLARE @wh_orders int
SET @wh_orders =
(select count(*) from dbo.orders
where order_run_id = @order_run_iid
and order_from_location_id in (select id from dbo.locations where location_type = 'warehouse')
)
if @wh_orders > 0
BEGIN
-- Let's put order lines to zero if we have no Available WH stock
UPDATE ol
SET ol.[unit_qty_chg] = 0
, ol.description = CASE WHEN len(ol.description) > 0 THEN ol.description + '; ' + 'Allocation. No available WH stocks'
ELSE 'Allocation. No available WH stocks'
END
FROM [dbo].[order_lines] ol
join
(
SELECT
ol.id,
ei.wh_stocks_available_comm
FROM [dbo].[orders] o
INNER JOIN [dbo].[order_lines] ol ON o.id = ol.order_id
INNER JOIN dbo.items i ON ol.[item_id] = i.id
inner join dbo.item_extra_info ei on ei.item_id = i.id
WHERE o.[status] = 0 AND o.[deleted] = 0 AND o.[order_run_id] = @order_run_iid
and ISNULL(ei.wh_stocks_available_comm,0) <= 0
) ol_wh on ol.id = ol_wh.id
-- Create the working temp table
--28.06.2017.GB added priority into the clustered index
CREATE TABLE #order_allocation
(
[order_run_id] [int] NOT NULL,
[order_id] [int] NOT NULL,
[item_id] [int] NOT NULL,
[item_no] [nvarchar](255) NOT NULL,
[min_order] [int] NULL,
[order_multiple] [int] NOT NULL,
[min_order_multiple] [int] NOT NULL,
[unit_qty] [decimal](18, 4) NULL,
[new_unit_qty] [decimal](18, 4) NULL,
[total_unit_qty] [decimal](38, 4) NULL,
[pct] [decimal](38, 20) NULL,
[location_id] [int] NOT NULL,
[order_from_location_id] [int] NOT NULL,
[id] [int] NOT NULL,
[stock_available] [decimal](18, 4) NOT NULL,
[priority] [int] NOT NULL,
[description] [nvarchar](500) NULL,
[done] [int] NULL,
[used] [int] NULL,
PRIMARY KEY CLUSTERED (priority,order_run_id,[order_id],[item_id])
)
-- Let's populate the temp table with initial values
;WITH order_run_total
AS
(
SELECT
o.[order_run_id]
,i.item_no
,SUM(ol.[unit_qty_chg]) AS total_unit_qty
--, ei.wh_stocks_available
, ei.wh_stocks_available_comm
,o.[order_from_location_id]
FROM [dbo].[orders] o
INNER JOIN [dbo].[order_lines] ol ON o.id = ol.order_id
INNER JOIN dbo.items i ON ol.[item_id] = i.id
INNER JOIN dbo.item_extra_info ei on ei.item_id = i.id
WHERE o.[status] = 0 AND o.[deleted] = 0 AND o.[order_run_id] = @order_run_iid
GROUP BY o.[order_run_id], i.[item_no], o.[order_from_location_id], ei.wh_stocks_available_comm
having SUM(ol.[unit_qty_chg]) > ISNULL(ei.wh_stocks_available_comm,0) and ISNULL(ei.wh_stocks_available_comm,0) > 0
)
INSERT INTO #order_allocation
SELECT
o.[order_run_id]
,ol.[order_id]
,ol.[item_id]
,i.item_no
,ior.min_order
,ior.order_multiple
, case when ior.order_multiple > ior.min_order then ior.order_multiple else ior.min_order end
,ol.[unit_qty_chg]
,ol.[unit_qty_chg]
,ort.total_unit_qty
,ISNULL(ol.[unit_qty_chg]/NULLIF(ort.total_unit_qty, 0),0) AS pct
,o.[location_id]
,o.[order_from_location_id]
,ofi.id
,ort.wh_stocks_available_comm
,ROW_NUMBER() OVER(PARTITION BY o.[order_run_id], i.item_no, o.order_from_location_id ORDER BY ol.unit_qty_calculated desc, o.[location_id] )
-- ,''
,'Allocation. Demand exceeds available WH stock, qty adjusted'
,0
,0
FROM [dbo].[orders] o
INNER JOIN [dbo].[order_lines] ol ON o.id = ol.order_id
INNER JOIN [dbo].[item_order_routes] ior ON ol.item_id = ior.item_id AND o.order_from_location_id = ior.order_from_location_id
INNER JOIN dbo.items i ON ol.[item_id] = i.id
INNER JOIN dbo.items ofi ON o.order_from_location_id = ofi.location_id AND i.item_no = ofi.item_no
INNER JOIN order_run_total ort ON ort.[order_run_id] = o.order_run_id AND i.item_no = ort.item_no
INNER JOIN dbo.locations l ON l.id = o.order_from_location_id
WHERE o.[status] = 0 AND o.[deleted] = 0 AND ort.total_unit_qty > ort.wh_stocks_available_comm and ort.wh_stocks_available_comm > 0
GROUP BY o.[order_run_id]
,ol.[order_id]
,ol.[item_id]
,ior.min_order
,ior.order_multiple
,ol.[unit_qty_chg]
,ort.total_unit_qty
,o.[location_id]
,o.[order_from_location_id]
,ofi.id
,l.location_no
,i.item_no
,ort.wh_stocks_available_comm
,ol.unit_qty_calculated
--28.06.2017.GB added index
CREATE NONCLUSTERED INDEX [IX_temp_OrderAllocation_item_no]
ON [#order_allocation] ([item_no])
INCLUDE ([order_run_id],[order_id],[item_id])
--28.06.2017.GB added index
CREATE NONCLUSTERED INDEX [IX_temp_order_allocation_item_id]
ON [#order_allocation] ([item_id])
--select * from #order_allocation
--order by item_no, priority
-- Set qty to zero if available stock < 1 or unit_qty is negative
UPDATE oa
SET new_unit_qty = 0,
used = 1,
done = 1
FROM
#order_allocation oa
WHERE stock_available <= 0 or unit_qty < 0
UPDATE oa
SET new_unit_qty =
case
WHEN floor(stock_available*pct/order_multiple)*order_multiple < min_order_multiple then 0
ELSE floor(stock_available*pct/order_multiple)*order_multiple
end
FROM
#order_allocation oa
WHERE done = 0
UPDATE oa
SET stock_available = oa.stock_available - used.used_qty
FROM #order_allocation oa
INNER JOIN
(
SELECT
order_run_id
, item_no
, order_from_location_id
, SUM([new_unit_qty]) as used_qty
, COUNT(*) AS order_count
FROM #order_allocation oa
WHERE done = 0
GROUP BY [order_run_id], item_no, order_from_location_id
) used ON used.item_no = oa.item_no AND used.order_from_location_id = oa.order_from_location_id
UPDATE oa
SET done = 1,
used = 1
FROM
#order_allocation oa
WHERE done = 0 AND ((new_unit_qty = 0 and stock_available < min_order_multiple) or (new_unit_qty > 0 and stock_available < order_multiple))
--select * from #order_allocation
--order by item_no, priority
DECLARE @item_id INT
DECLARE @item_no [nvarchar](255)
DECLARE @order_from_location_id INT
DECLARE @order_multiple INT
DECLARE @min_order_multiple INT
DECLARE @unit_qty [decimal](18, 4)
DECLARE @stock_available [decimal](18, 4)
DECLARE @done INT
declare @orig_unit_qty [decimal](18, 4)
WHILE (SELECT count(*) FROM #order_allocation WHERE done = 0) > 0
BEGIN
SET @done = 0
SET @unit_qty = 0
IF (SELECT count(*) FROM #order_allocation WHERE used = 0) <= 0
BEGIN
UPDATE #order_allocation
SET used = 0
WHERE done = 0
END
SELECT TOP (1)
@item_id =item_id,
@order_multiple = order_multiple,
@order_from_location_id = order_from_location_id,
@item_no = item_no,
@stock_available = stock_available,
@min_order_multiple = min_order_multiple,
@unit_qty = new_unit_qty,
@orig_unit_qty = unit_qty
FROM #order_allocation
WHERE done = 0 and used = 0
ORDER BY priority
IF @unit_qty >= 0 and @orig_unit_qty > 0
begin
if @stock_available >= @order_multiple -- enough stock to allocate
begin
SET @unit_qty = @order_multiple
end
else -- not enough stock to allocate
begin
set @done = 1
end
end
else -- no stock allocated before
begin
if @stock_available >= @min_order_multiple
begin
SET @unit_qty = @min_order_multiple
end
else -- not enough stock to allocate
begin
set @done = 1
end
end
IF @done = 0 -- still stock available
BEGIN
UPDATE oa
SET new_unit_qty = new_unit_qty + @unit_qty,
done = @done,
used = 1
FROM #order_allocation oa
where item_id = @item_id
UPDATE oa
SET stock_available = stock_available-@unit_qty
FROM #order_allocation oa
where item_no = @item_no AND order_from_location_id = @order_from_location_id
END
ELSE
BEGIN
UPDATE oa
SET done = @done,
used = 1
FROM #order_allocation oa
where item_id = @item_id
END
update #order_allocation
set @done = 1
where item_no = @item_no
and stock_available <= 0
--select * from #order_allocation
--order by item_no, priority
END
update oa
set total_unit_qty = toa.new_qty
from #order_allocation oa
join (
SELECT SUM(new_unit_qty) AS new_qty, item_no, order_from_location_id
FROM #order_allocation
GROUP BY item_no, order_from_location_id
) toa ON oa.item_no = toa.item_no and oa.order_from_location_id = toa.order_from_location_id
--select * from #order_allocation
--order by item_no, priority
-- 04.11.2019.GH Let's store the temp values in an actual table for later review
-- But only do this on scheduled orders each day (origin 2)
INSERT INTO cus.post_order_run_allocation_raw
(order_run_id, order_id, item_id, item_no, min_order, order_multiple, min_order_multiple, unit_qty, new_unit_qty, total_unit_qty, pct,
location_id, order_from_location_id, id, stock_available, priority, description, done, used)
SELECT
oa.order_run_id, oa.order_id, oa.item_id, oa.item_no, oa.min_order, oa.order_multiple, oa.min_order_multiple, oa.unit_qty, oa.new_unit_qty, oa.total_unit_qty, pct,
oa.location_id, oa.order_from_location_id, oa.id, oa.stock_available, oa.priority, oa.description, oa.done, oa.used
FROM
#order_allocation oa
INNER JOIN dbo.orders o ON o.id = oa.order_id
WHERE
o.origin_id = 2
AND o.description like '%Daily orders%'
-- Actual update of order lines from the working table
UPDATE ol
SET ol.unit_qty_chg = oa.new_unit_qty,
ol.description =
CASE
WHEN len(ol.description) > 0 THEN ol.description + '; ' + oa.description + ' to ' + cast(CAST(oa.new_unit_qty AS INT) AS varchar(10))
ELSE oa.description + ' to ' + cast(CAST(oa.new_unit_qty as int) AS varchar(10))
END
OUTPUT inserted.id, 8, inserted.unit_qty_chg INTO cus.bi_log_order_line_changes (order_line_id, event_type_id, qty)
FROM dbo.order_lines ol
INNER JOIN #order_allocation oa ON ol.order_id = oa.order_id AND ol.item_id = oa.item_id
WHERE oa.new_unit_qty != ol.unit_qty_chg -- 12.09.2018.GH Only update lines that are actually changed
END
--@wh_orders > 0
END TRY
BEGIN CATCH
EXEC core.catch_error @event_id = @event_id
END CATCH
END
It is strongly recommended to write useful information into the order line “Description” column when post order adjusts the order line in any way. This helps the customers make sense of what is going on and can also dramatically speed up consulting debugging on order lines. It is best to include as much useful information as possible (prior qty value, which logic changed it), without being too verbose. Care should be taken not to overwrite already exiting description texts and thus we test for it in the example below and append to the end of the string, if any.
When not to write to the Description field. Example of this use might be something like special qty rounding rules that would update too many order lines to be useful to flag them as changed, then we avoid both the green line and the Description field updates.
UPDATE ol
SET
ol.unit_qty_chg = ior.order_multiple,
ol.description =
CASE
WHEN LEN(ol.description) > 0
THEN CAST(ol.description, '; ', ol.unit_qty_chg AS INT), ' adjusted to ' , cast(ior.order_multiple AS INT) ,' (#4)')
ELSE
CAST(ol.unit_qty_chg AS INT), ' adjusted to ' , CAST(ior.order_multiple AS INT) ,' (#4)')
END
FROM dbo.orders o
INNER JOIN dbo.order_lines ol ON ol.order_id = o.id
INNER JOIN dbo.items i ON i.id = ol.item_id
INNER JOIN dbo.item_order_routes ior ON ior.item_id = i.id
WHERE
o.description like '%Forbruk alle dage%'
AND o.order_run_id = @order_run_id
Often, we only want to run the post order run on some orders, like orders created specific report name. In the example below we only run this logic on the large Scheduled daily order. Manual order from that same MBE report would not activate this logic.
IF (@origin_id = 1 AND (@order_description LIKE '%Daily orders%' '))
BEGIN
EXEC dbo.post_order_run_super_planner @action_exec_id = @action_exec_id, @order_run_id = @order_run_id
END
Labeling
Often it is useful to give each post order run procedure a “label” or number, since then we can talk about “#4 seems to be wrong on this product group” instead of something vague.
With one simple change to the post order run procedures we can vastly improve logging of their operations and make debugging much quicker. We only need to adjust the following to start writing better info to the logging tables.
DECLARE @extra_info_string NVARCHAR(100)
SET @extra_info_string = 'Order run id: ' + CAST(@order_run_id AS NVARCHAR(100))
EXEC @action_exec_id = dbo.log_action_execution @action_exec_id = @action_exec_id, @proc_id = @@PROCID, @extra_info = @extra_info_string
This would look something like this example of a main scheduled daily order showing all post order run operations on it.
Some customers do not want to see the green vertical line, indicating an edited order line, when they open their orders after post order run has completed. Seeing the green line can cause confusion as to if a person has perhaps already edited the order line already or not.
In those cases, we simply update both columns unit_qty
and unit_qty_chg
in the post order run updates. We could update the “Description” field even in those cases - if that makes sense in those conditions.