Tuesday, 21 February 2017

Performance improvement on sp_WHSOnHand / sp_WHSOnHandWithDelta

These two stored procedure are generated by AX during synchronization (there're others as well, but not the focus of this post).
- sp_WHSOnHand
- sp_WHSOnHandWithDelta

In our environment (with certain amount of data), it tooks around 250ms to execute, although this number looks small, but when you have 100 of them being executed, that's 25,000ms (25 seconds), it is a different story then. A wait of 25s isn't something nice for the user.

Analyzing the query plan shows that it starts from InventDim, getting a large number of records and then nested loop to WHSInventReserve and so on. Looking at the thickness of the arrow next to InventDim, that's an indicator that a large number of rows are returned from that operator. That means it is operating that large number of rows against the WHSInventReserve.

This is the major bottleneck of the query.
We've tried update statistics and reindex and clear the cache to make sure it generate a new plan, but it still generate the same execution plan.

Due to this stored procedure has an ItemId provided, it should limit the number of rows from the WHSInventReserve, using this limited number of rows and nested loop against the InventDim would be much quicker, but somehow the optimizer didn't think so.

Bad query plan

To resolve this problem, I'm splitting this query into two parts.
First, filter the WHSInventReserve with the ItemId provided and insert it into a table variable.
Then join this table variable to the InventDim. Although the sum of the estimated subtree cost of the original query and the new 2-parts query is similar, but it runs much quicker than the original version.

Better query plan

With this new change, the stored procedure completed in 7ms instead of 250ms.

Original query join

New query join

The stored procedure should be changed from within AX at the 'WHSOnHandSPHelper' class. When synchronization is run, it regenerate this stored procedure. It also regenerate when stock dimension setup is changed.

Below is a sample of complete stored procedure code generated by the 'WHSOnHandSPHelper' class (Do take note, the selection field changed based on the stock dimension setup).

=================================================
CREATE PROCEDURE [dbo].[sp_WHSOnHand]
    @PARTITION bigint,
    @LEVEL int,
    @DATAAREAID nvarchar (4),
    @ITEMID nvarchar (20),
    @INVENTSITEID nvarchar (10) = NULL,
    @INVENTSITEIDLEVEL int = 1024,
    @INVENTLOCATIONID nvarchar (10) = NULL,
    @INVENTLOCATIONIDLEVEL int = 1024,
    @WMSLOCATIONID nvarchar (10) = NULL,
    @WMSLOCATIONIDLEVEL int = 1024,
    @WMSPALLETID nvarchar (18) = NULL,
    @WMSPALLETIDLEVEL int = 1024,
    @INVENTSTATUSID nvarchar (10) = NULL,
    @INVENTSTATUSIDLEVEL int = 1024,
    @LICENSEPLATEID nvarchar (25) = NULL,
    @LICENSEPLATEIDLEVEL int = 1024,
    @INVENTBATCHID nvarchar (20) = NULL,
    @INVENTBATCHIDLEVEL int = 1024,
    @INVENTSERIALID nvarchar (20) = NULL,
    @INVENTSERIALIDLEVEL int = 1024,
    @INVENTPROFILEID_RU nvarchar (10) = NULL,
    @INVENTPROFILEID_RULEVEL int = 1024,
    @INVENTOWNERID_RU nvarchar (20) = NULL,
    @INVENTOWNERID_RULEVEL int = 1024,
    @INVENTGTDID_RU nvarchar (30) = NULL,
    @INVENTGTDID_RULEVEL int = 1024,
    @CONFIGID nvarchar (10) = NULL,
    @INVENTSIZEID nvarchar (10) = NULL,
    @INVENTCOLORID nvarchar (10) = NULL,
    @INVENTSTYLEID nvarchar (10) = NULL,
    -- Output parameters
    @AVAILPHYSICAL  numeric(32, 16) OUTPUT,
    @AVAILORDERED  numeric(32, 16) OUTPUT
AS

-- Without nocount on the procedure will return number of affected rows in the result set which is
-- harmful to performance and requires us to adjust field retrieval when calling the stored procedure
SET NOCOUNT ON;

-- We need to supress warnings when trying to aggregate NULL columns. This is a valid business scenario
-- and happens e.g. for a non-LP controlled location with batch below item.
SET ANSI_WARNINGS OFF;

-- The @RESULT table variable is used to store intermediate quantities.
DECLARE @RESULT TABLE(
    AVAILPHYSICALRESULT numeric(32, 16) NOT NULL, -- Aggregated minimum qtys from lower level in @RESULT
    AVAILORDEREDRESULT numeric(32, 16) NOT NULL, -- Aggregated minimum qtys from lower level in @RESULT
    AVAILPHYSICALFOUND numeric(32, 16), -- Aggregated qty for current level in WHSInventReserve
    AVAILORDEREDFOUND numeric(32, 16), -- Aggregated qty for current level in WHSInventReserve
    HIERARCHYLEVEL int NOT NULL,
    CONFIGID nvarchar (10) NULL,
    INVENTSIZEID nvarchar (10) NULL,
    INVENTCOLORID nvarchar (10) NULL,
    INVENTSTYLEID nvarchar (10) NULL,
    INVENTSITEID nvarchar (10) NULL,
    INVENTLOCATIONID nvarchar (10) NULL,
    WMSLOCATIONID nvarchar (10) NULL,
    WMSPALLETID nvarchar (18) NULL,
    INVENTSTATUSID nvarchar (10) NULL,
    LICENSEPLATEID nvarchar (25) NULL,
    INVENTBATCHID nvarchar (20) NULL,
    INVENTSERIALID nvarchar (20) NULL,
    INVENTPROFILEID_RU nvarchar (10) NULL,
    INVENTOWNERID_RU nvarchar (20) NULL,
    INVENTGTDID_RU nvarchar (30) NULL);

DECLARE @DIMLIST TABLE(
    INVENTDIMID nvarchar (20) NULL,
    AVAILPHYSICAL numeric(32, 16) NOT NULL,
    AVAILORDERED numeric(32, 16) NOT NULL);

-- The algorithm of the stored procedure is as follows:
-- 1. Initialize the @RESULT table with aggregated qtys in WHSInventReserve for the level requested by user
-- 2. Traverse the hierarchy up and for each level:
--     1) Aggregate qtys from previous level in @RESULT grouping by dimensions of current hierarchy level
--     2) Aggregate qtys from current level in WHSInventReserve grouping by dimensions of current hierarchy level
--     3) Select the minimum of the above qtys and insert into @RESULT
-- 3. When hierarchy level 0 is reached, return the sum of aggregated qtys in @RESULT

-- In the original query (the one after below query), it is joining InventDim and WHSInventReserve and filter records based on some criteria.
-- But in the execution plan, it doesn't seems to generate a good plan (even after UPDATE STATISTICS). In the original execution plan, it always
-- Get records from InventDim first, which returns lot of rows, then only join it to WHSInventReserve and continue filter from there.
-- If we makes it get records from WHSInventReserve first, filter it, then with lesser rows from here, join it to InventDim, it will be quicker.
-- So the optimization is to filter WHSInventResreve first, then use this result at the original query. In the test, this has reduce the execution
-- time from ~250ms to ~7ms
  INSERT INTO @DIMLIST(INVENTDIMID, AVAILPHYSICAL, AVAILORDERED)
  SELECT INVENTDIMID, AVAILPHYSICAL, AVAILORDERED
    FROM WHSINVENTRESERVE WHSINVENTRESERVE
   WHERE WHSINVENTRESERVE.ITEMID         = @ITEMID
     AND WHSINVENTRESERVE.PARTITION      = @PARTITION
     AND WHSINVENTRESERVE.DATAAREAID     = @DATAAREAID
     AND WHSINVENTRESERVE.HIERARCHYLEVEL = @LEVEL

-- Initialize the @RESULT variable.
-- Loads aggregated data for the lowest level requested by user.
INSERT INTO @RESULT (AVAILPHYSICALRESULT, AVAILORDEREDRESULT, AVAILPHYSICALFOUND, AVAILORDEREDFOUND, HIERARCHYLEVEL, CONFIGID, INVENTSIZEID, INVENTCOLORID, INVENTSTYLEID, INVENTSITEID, INVENTLOCATIONID, WMSLOCATIONID, WMSPALLETID, INVENTSTATUSID, LICENSEPLATEID, INVENTBATCHID, INVENTSERIALID, INVENTPROFILEID_RU, INVENTOWNERID_RU, INVENTGTDID_RU)
SELECT SUM(DIMLIST.AVAILPHYSICAL),
    SUM(DIMLIST.AVAILORDERED),
    NULL,
    NULL,
    @LEVEL,
    INVENTDIM.CONFIGID,
    INVENTDIM.INVENTSIZEID,
    INVENTDIM.INVENTCOLORID,
    INVENTDIM.INVENTSTYLEID,
    CASE WHEN @LEVEL + -1 >= @INVENTSITEIDLEVEL THEN INVENTDIM.INVENTSITEID ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @INVENTLOCATIONIDLEVEL THEN INVENTDIM.INVENTLOCATIONID ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @WMSLOCATIONIDLEVEL THEN INVENTDIM.WMSLOCATIONID ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @WMSPALLETIDLEVEL THEN INVENTDIM.WMSPALLETID ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @INVENTSTATUSIDLEVEL THEN INVENTDIM.INVENTSTATUSID ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @LICENSEPLATEIDLEVEL THEN INVENTDIM.LICENSEPLATEID ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @INVENTBATCHIDLEVEL THEN INVENTDIM.INVENTBATCHID ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @INVENTSERIALIDLEVEL THEN INVENTDIM.INVENTSERIALID ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @INVENTPROFILEID_RULEVEL THEN INVENTDIM.INVENTPROFILEID_RU ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @INVENTOWNERID_RULEVEL THEN INVENTDIM.INVENTOWNERID_RU ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @INVENTGTDID_RULEVEL THEN INVENTDIM.INVENTGTDID_RU ELSE NULL END
    FROM INVENTDIM INVENTDIM
    JOIN @DIMLIST DIMLIST
      ON INVENTDIM.INVENTDIMID = DIMLIST.INVENTDIMID
    WHERE   INVENTDIM.DATAAREAID = @DATAAREAID
        AND INVENTDIM.PARTITION = @PARTITION
        AND (@CONFIGID IS NULL OR INVENTDIM.CONFIGID = @CONFIGID)
        AND (@INVENTSIZEID IS NULL OR INVENTDIM.INVENTSIZEID = @INVENTSIZEID)
        AND (@INVENTCOLORID IS NULL OR INVENTDIM.INVENTCOLORID = @INVENTCOLORID)
        AND (@INVENTSTYLEID IS NULL OR INVENTDIM.INVENTSTYLEID = @INVENTSTYLEID)
        AND (@LEVEL < @INVENTSITEIDLEVEL OR @INVENTSITEID IS NULL OR @INVENTSITEID = INVENTDIM.INVENTSITEID)
        AND (@LEVEL < @INVENTLOCATIONIDLEVEL OR @INVENTLOCATIONID IS NULL OR @INVENTLOCATIONID = INVENTDIM.INVENTLOCATIONID)
        AND (@LEVEL < @WMSLOCATIONIDLEVEL OR @WMSLOCATIONID IS NULL OR @WMSLOCATIONID = INVENTDIM.WMSLOCATIONID)
        AND (@LEVEL < @WMSPALLETIDLEVEL OR @WMSPALLETID IS NULL OR @WMSPALLETID = INVENTDIM.WMSPALLETID)
        AND (@LEVEL < @INVENTSTATUSIDLEVEL OR @INVENTSTATUSID IS NULL OR @INVENTSTATUSID = INVENTDIM.INVENTSTATUSID)
        AND (@LEVEL < @LICENSEPLATEIDLEVEL OR @LICENSEPLATEID IS NULL OR @LICENSEPLATEID = INVENTDIM.LICENSEPLATEID)
        AND (@LEVEL < @INVENTBATCHIDLEVEL OR @INVENTBATCHID IS NULL OR @INVENTBATCHID = INVENTDIM.INVENTBATCHID)
        AND (@LEVEL < @INVENTSERIALIDLEVEL OR @INVENTSERIALID IS NULL OR @INVENTSERIALID = INVENTDIM.INVENTSERIALID)
        AND (@LEVEL < @INVENTPROFILEID_RULEVEL OR @INVENTPROFILEID_RU IS NULL OR @INVENTPROFILEID_RU = INVENTDIM.INVENTPROFILEID_RU)
        AND (@LEVEL < @INVENTOWNERID_RULEVEL OR @INVENTOWNERID_RU IS NULL OR @INVENTOWNERID_RU = INVENTDIM.INVENTOWNERID_RU)
        AND (@LEVEL < @INVENTGTDID_RULEVEL OR @INVENTGTDID_RU IS NULL OR @INVENTGTDID_RU = INVENTDIM.INVENTGTDID_RU)
GROUP BY
    INVENTDIM.CONFIGID,
    INVENTDIM.INVENTSIZEID,
    INVENTDIM.INVENTCOLORID,
    INVENTDIM.INVENTSTYLEID,
    CASE WHEN @LEVEL + -1 >= @INVENTSITEIDLEVEL THEN INVENTDIM.INVENTSITEID ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @INVENTLOCATIONIDLEVEL THEN INVENTDIM.INVENTLOCATIONID ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @WMSLOCATIONIDLEVEL THEN INVENTDIM.WMSLOCATIONID ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @WMSPALLETIDLEVEL THEN INVENTDIM.WMSPALLETID ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @INVENTSTATUSIDLEVEL THEN INVENTDIM.INVENTSTATUSID ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @LICENSEPLATEIDLEVEL THEN INVENTDIM.LICENSEPLATEID ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @INVENTBATCHIDLEVEL THEN INVENTDIM.INVENTBATCHID ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @INVENTSERIALIDLEVEL THEN INVENTDIM.INVENTSERIALID ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @INVENTPROFILEID_RULEVEL THEN INVENTDIM.INVENTPROFILEID_RU ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @INVENTOWNERID_RULEVEL THEN INVENTDIM.INVENTOWNERID_RU ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @INVENTGTDID_RULEVEL THEN INVENTDIM.INVENTGTDID_RU ELSE NULL END;

WHILE (@LEVEL > 0)
BEGIN

  DELETE FROM @DIMLIST

  INSERT INTO @DIMLIST(INVENTDIMID, AVAILPHYSICAL, AVAILORDERED)
  SELECT INVENTDIMID, AVAILPHYSICAL, AVAILORDERED
    FROM WHSINVENTRESERVE WHSINVENTRESERVE
   WHERE WHSINVENTRESERVE.HIERARCHYLEVEL = @LEVEL + -1
     AND WHSINVENTRESERVE.ITEMID         = @ITEMID
     AND WHSINVENTRESERVE.PARTITION      = @PARTITION
     AND WHSINVENTRESERVE.DATAAREAID     = @DATAAREAID

    INSERT INTO @RESULT (AVAILPHYSICALRESULT, AVAILORDEREDRESULT, AVAILPHYSICALFOUND, AVAILORDEREDFOUND, HIERARCHYLEVEL, CONFIGID, INVENTSIZEID, INVENTCOLORID, INVENTSTYLEID, INVENTSITEID, INVENTLOCATIONID, WMSLOCATIONID, WMSPALLETID, INVENTSTATUSID, LICENSEPLATEID, INVENTBATCHID, INVENTSERIALID, INVENTPROFILEID_RU, INVENTOWNERID_RU, INVENTGTDID_RU)
SELECT SUM(CASE WHEN RESULT.AVAILPHYSICALFOUND < RESULT.AVAILPHYSICALRESULT THEN RESULT.AVAILPHYSICALFOUND ELSE RESULT.AVAILPHYSICALRESULT END),
    SUM(CASE WHEN RESULT.AVAILORDEREDFOUND < RESULT.AVAILORDEREDRESULT THEN RESULT.AVAILORDEREDFOUND ELSE RESULT.AVAILORDEREDRESULT END),
    SUM(FOUND.AVAILPHYSICAL),
    SUM(FOUND.AVAILORDERED),
    @LEVEL + -1,
    RESULT.CONFIGID AS CONFIGID,
    RESULT.INVENTSIZEID AS INVENTSIZEID,
    RESULT.INVENTCOLORID AS INVENTCOLORID,
    RESULT.INVENTSTYLEID AS INVENTSTYLEID,
    CASE WHEN @LEVEL + -1 >= @INVENTSITEIDLEVEL THEN RESULT.INVENTSITEID ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @INVENTLOCATIONIDLEVEL THEN RESULT.INVENTLOCATIONID ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @WMSLOCATIONIDLEVEL THEN RESULT.WMSLOCATIONID ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @WMSPALLETIDLEVEL THEN RESULT.WMSPALLETID ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @INVENTSTATUSIDLEVEL THEN RESULT.INVENTSTATUSID ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @LICENSEPLATEIDLEVEL THEN RESULT.LICENSEPLATEID ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @INVENTBATCHIDLEVEL THEN RESULT.INVENTBATCHID ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @INVENTSERIALIDLEVEL THEN RESULT.INVENTSERIALID ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @INVENTPROFILEID_RULEVEL THEN RESULT.INVENTPROFILEID_RU ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @INVENTOWNERID_RULEVEL THEN RESULT.INVENTOWNERID_RU ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @INVENTGTDID_RULEVEL THEN RESULT.INVENTGTDID_RU ELSE NULL END
FROM @RESULT RESULT
LEFT OUTER JOIN (
SELECT SUM(DIMLIST.AVAILPHYSICAL) AS AVAILPHYSICAL,
    SUM(DIMLIST.AVAILORDERED) AS AVAILORDERED,
    INVENTDIM.CONFIGID AS CONFIGID,
    INVENTDIM.INVENTSIZEID AS INVENTSIZEID,
    INVENTDIM.INVENTCOLORID AS INVENTCOLORID,
    INVENTDIM.INVENTSTYLEID AS INVENTSTYLEID,
    CASE WHEN @LEVEL + -1 >= @INVENTSITEIDLEVEL THEN INVENTDIM.INVENTSITEID ELSE NULL END AS INVENTSITEID,
    CASE WHEN @LEVEL + -1 >= @INVENTLOCATIONIDLEVEL THEN INVENTDIM.INVENTLOCATIONID ELSE NULL END AS INVENTLOCATIONID,
    CASE WHEN @LEVEL + -1 >= @WMSLOCATIONIDLEVEL THEN INVENTDIM.WMSLOCATIONID ELSE NULL END AS WMSLOCATIONID,
    CASE WHEN @LEVEL + -1 >= @WMSPALLETIDLEVEL THEN INVENTDIM.WMSPALLETID ELSE NULL END AS WMSPALLETID,
    CASE WHEN @LEVEL + -1 >= @INVENTSTATUSIDLEVEL THEN INVENTDIM.INVENTSTATUSID ELSE NULL END AS INVENTSTATUSID,
    CASE WHEN @LEVEL + -1 >= @LICENSEPLATEIDLEVEL THEN INVENTDIM.LICENSEPLATEID ELSE NULL END AS LICENSEPLATEID,
    CASE WHEN @LEVEL + -1 >= @INVENTBATCHIDLEVEL THEN INVENTDIM.INVENTBATCHID ELSE NULL END AS INVENTBATCHID,
    CASE WHEN @LEVEL + -1 >= @INVENTSERIALIDLEVEL THEN INVENTDIM.INVENTSERIALID ELSE NULL END AS INVENTSERIALID,
    CASE WHEN @LEVEL + -1 >= @INVENTPROFILEID_RULEVEL THEN INVENTDIM.INVENTPROFILEID_RU ELSE NULL END AS INVENTPROFILEID_RU,
    CASE WHEN @LEVEL + -1 >= @INVENTOWNERID_RULEVEL THEN INVENTDIM.INVENTOWNERID_RU ELSE NULL END AS INVENTOWNERID_RU,
    CASE WHEN @LEVEL + -1 >= @INVENTGTDID_RULEVEL THEN INVENTDIM.INVENTGTDID_RU ELSE NULL END AS INVENTGTDID_RU
FROM INVENTDIM INVENTDIM
JOIN @DIMLIST DIMLIST
     ON INVENTDIM.PARTITION = @PARTITION
    AND INVENTDIM.DATAAREAID  = @DATAAREAID
    AND INVENTDIM.INVENTDIMID = DIMLIST.INVENTDIMID
GROUP BY
    INVENTDIM.CONFIGID,
    INVENTDIM.INVENTSIZEID,
    INVENTDIM.INVENTCOLORID,
    INVENTDIM.INVENTSTYLEID,
    CASE WHEN @LEVEL + -1 >= @INVENTSITEIDLEVEL THEN INVENTDIM.INVENTSITEID ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @INVENTLOCATIONIDLEVEL THEN INVENTDIM.INVENTLOCATIONID ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @WMSLOCATIONIDLEVEL THEN INVENTDIM.WMSLOCATIONID ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @WMSPALLETIDLEVEL THEN INVENTDIM.WMSPALLETID ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @INVENTSTATUSIDLEVEL THEN INVENTDIM.INVENTSTATUSID ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @LICENSEPLATEIDLEVEL THEN INVENTDIM.LICENSEPLATEID ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @INVENTBATCHIDLEVEL THEN INVENTDIM.INVENTBATCHID ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @INVENTSERIALIDLEVEL THEN INVENTDIM.INVENTSERIALID ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @INVENTPROFILEID_RULEVEL THEN INVENTDIM.INVENTPROFILEID_RU ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @INVENTOWNERID_RULEVEL THEN INVENTDIM.INVENTOWNERID_RU ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @INVENTGTDID_RULEVEL THEN INVENTDIM.INVENTGTDID_RU ELSE NULL END
    ) AS FOUND ON (RESULT.CONFIGID = FOUND.CONFIGID)
        AND (RESULT.INVENTSIZEID = FOUND.INVENTSIZEID)
        AND (RESULT.INVENTCOLORID = FOUND.INVENTCOLORID)
        AND (RESULT.INVENTSTYLEID = FOUND.INVENTSTYLEID)
        AND (@LEVEL + -1 < @INVENTSITEIDLEVEL OR RESULT.INVENTSITEID = FOUND.INVENTSITEID)
        AND (@LEVEL + -1 < @INVENTLOCATIONIDLEVEL OR RESULT.INVENTLOCATIONID = FOUND.INVENTLOCATIONID)
        AND (@LEVEL + -1 < @WMSLOCATIONIDLEVEL OR RESULT.WMSLOCATIONID = FOUND.WMSLOCATIONID)
        AND (@LEVEL + -1 < @WMSPALLETIDLEVEL OR RESULT.WMSPALLETID = FOUND.WMSPALLETID)
        AND (@LEVEL + -1 < @INVENTSTATUSIDLEVEL OR RESULT.INVENTSTATUSID = FOUND.INVENTSTATUSID)
        AND (@LEVEL + -1 < @LICENSEPLATEIDLEVEL OR RESULT.LICENSEPLATEID = FOUND.LICENSEPLATEID)
        AND (@LEVEL + -1 < @INVENTBATCHIDLEVEL OR RESULT.INVENTBATCHID = FOUND.INVENTBATCHID)
        AND (@LEVEL + -1 < @INVENTSERIALIDLEVEL OR RESULT.INVENTSERIALID = FOUND.INVENTSERIALID)
        AND (@LEVEL + -1 < @INVENTPROFILEID_RULEVEL OR RESULT.INVENTPROFILEID_RU = FOUND.INVENTPROFILEID_RU)
        AND (@LEVEL + -1 < @INVENTOWNERID_RULEVEL OR RESULT.INVENTOWNERID_RU = FOUND.INVENTOWNERID_RU)
        AND (@LEVEL + -1 < @INVENTGTDID_RULEVEL OR RESULT.INVENTGTDID_RU = FOUND.INVENTGTDID_RU)
WHERE RESULT.HIERARCHYLEVEL = @LEVEL
GROUP BY
    RESULT.CONFIGID,
    RESULT.INVENTSIZEID,
    RESULT.INVENTCOLORID,
    RESULT.INVENTSTYLEID,
    CASE WHEN @LEVEL + -1 >= @INVENTSITEIDLEVEL THEN RESULT.INVENTSITEID ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @INVENTLOCATIONIDLEVEL THEN RESULT.INVENTLOCATIONID ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @WMSLOCATIONIDLEVEL THEN RESULT.WMSLOCATIONID ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @WMSPALLETIDLEVEL THEN RESULT.WMSPALLETID ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @INVENTSTATUSIDLEVEL THEN RESULT.INVENTSTATUSID ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @LICENSEPLATEIDLEVEL THEN RESULT.LICENSEPLATEID ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @INVENTBATCHIDLEVEL THEN RESULT.INVENTBATCHID ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @INVENTSERIALIDLEVEL THEN RESULT.INVENTSERIALID ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @INVENTPROFILEID_RULEVEL THEN RESULT.INVENTPROFILEID_RU ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @INVENTOWNERID_RULEVEL THEN RESULT.INVENTOWNERID_RU ELSE NULL END,
    CASE WHEN @LEVEL + -1 >= @INVENTGTDID_RULEVEL THEN RESULT.INVENTGTDID_RU ELSE NULL END;


    SET @LEVEL = @LEVEL - 1
END; -- WHILE

SELECT @AVAILPHYSICAL = SUM(CASE WHEN AVAILPHYSICALFOUND < AVAILPHYSICALRESULT THEN AVAILPHYSICALFOUND ELSE AVAILPHYSICALRESULT END),
    @AVAILORDERED = SUM(CASE WHEN AVAILORDEREDFOUND < AVAILORDEREDRESULT THEN AVAILORDEREDFOUND ELSE AVAILORDEREDRESULT END)
FROM @RESULT
WHERE HIERARCHYLEVEL = 0;

GO
=================================================

Below is the XPO extract of the updated 'WHSOnHandSPHelper' class
=================================================
Exportfile for AOT version 1.0 or later
Formatversion: 1

***Element: CLS

; Microsoft Dynamics AX Class: WHSOnHandSPHelper unloaded
; --------------------------------------------------------------------------------
  CLSVERSION 1
  
  CLASS #WHSOnHandSPHelper
    PROPERTIES
      Name                #WHSOnHandSPHelper
      Origin              #{E0CDDBB5-8AE5-4BEF-9B5B-A41A300F36E5}
    ENDPROPERTIES
    
    METHODS
      SOURCE #buildCreateSPStmt
        #private str buildCreateSPStmt(boolean _includeDelta)
        #{
        #    str             ret = '';
        #    ListEnumerator  enabledDimsEnumerator;
        #    ListEnumerator  productDimensionsEnumerator;
        #    FieldId         dimFieldId;
        #    str             dimFieldName;
        #    boolean         isSerialEnabled;
        #
        #    if(useOptimizedVersion)
        #    {
        #        ret = this.TEC_buildCreateSPStmt(_includeDelta);
        #    }
        #    else
        #    {
        #        InventDimMetaDataCache::clearCache(); //in some cases the cache is not updated before sync making below SP
        #
        #        enabledDimsEnumerator = this.enabledDimensions().getEnumerator();
        #        productDimensionsEnumerator = this.productDimensions().getEnumerator();
        #        isSerialEnabled = this.getEnabledDimsParametersMap().exists(fieldNum(InventDim, InventSerialId));
        #
        #        /*
        #CREATE PROCEDURE [sp_WHSOnHand]
        #    @PARTITION bigint,
        #    @LEVEL int,
        #    @DATAAREAID nvarchar (4),
        #    @ITEMID nvarchar (20),
        #    @INVENTSITEID nvarchar (10) = NULL,
        #    @INVENTSITEIDLEVEL int = 1024,
        #    @INVENTLOCATIONID nvarchar (10) = NULL,
        #    @INVENTLOCATIONIDLEVEL int = 1024,
        #    @WMSLOCATIONID nvarchar (10) = NULL,
        #    @WMSLOCATIONIDLEVEL int = 1024,
        #    @WMSPALLETID nvarchar (18) = NULL,
        #    @WMSPALLETIDLEVEL int = 1024,
        #    @INVENTSTATUSID nvarchar (10) = NULL,
        #    @INVENTSTATUSIDLEVEL int = 1024,
        #    @LICENSEPLATEID nvarchar (25) = NULL,
        #    @LICENSEPLATEIDLEVEL int = 1024,
        #    @INVENTBATCHID nvarchar (20) = NULL,
        #    @INVENTBATCHIDLEVEL int = 1024,
        #    @INVENTSERIALID nvarchar (20) = NULL,
        #    @INVENTSERIALIDLEVEL int = 1024,
        #    @INVENTPROFILEID_RU nvarchar (10) = NULL,
        #    @INVENTPROFILEID_RULEVEL int = 1024,
        #    @INVENTOWNERID_RU nvarchar (20) = NULL,
        #    @INVENTOWNERID_RULEVEL int = 1024,
        #    @INVENTGTDID_RU nvarchar (30) = NULL,
        #    @INVENTGTDID_RULEVEL int = 1024,
        #    @CONFIGID nvarchar (10) = NULL,
        #    @INVENTSIZEID nvarchar (10) = NULL,
        #    @INVENTCOLORID nvarchar (10) = NULL,
        #    @INVENTSTYLEID nvarchar (10) = NULL,
        #    -- Output parameters
        #    @AVAILPHYSICAL  numeric(32, 16) OUTPUT,
        #    @AVAILORDERED  numeric(32, 16) OUTPUT
        #AS
        #
        #-- Without nocount on the procedure will return number of affected rows in the result set which is
        #-- harmful to performance and requires us to adjust field retrieval when calling the stored procedure
        #SET NOCOUNT ON;
        #
        #-- We need to supress warnings when trying to aggregate NULL columns. This is a valid business scenario
        #-- and happens e.g. for a non-LP controlled location with batch below item.
        #SET ANSI_WARNINGS OFF;
        #
        #-- The @RESULT table variable is used to store intermediate quantities.
        #DECLARE @RESULT TABLE(
        #    AVAILPHYSICALRESULT numeric(32, 16) NOT NULL, -- Aggregated minimum qtys from lower level in @RESULT
        #    AVAILORDEREDRESULT numeric(32, 16) NOT NULL, -- Aggregated minimum qtys from lower level in @RESULT
        #    AVAILPHYSICALFOUND numeric(32, 16), -- Aggregated qty for current level in WHSInventReserve
        #    AVAILORDEREDFOUND numeric(32, 16), -- Aggregated qty for current level in WHSInventReserve
        #    HIERARCHYLEVEL int NOT NULL,
        #    CONFIGID nvarchar (10) NULL,
        #    INVENTSIZEID nvarchar (10) NULL,
        #    INVENTCOLORID nvarchar (10) NULL,
        #    INVENTSTYLEID nvarchar (10) NULL,
        #    INVENTSITEID nvarchar (10) NULL,
        #    INVENTLOCATIONID nvarchar (10) NULL,
        #    WMSLOCATIONID nvarchar (10) NULL,
        #    WMSPALLETID nvarchar (18) NULL,
        #    INVENTSTATUSID nvarchar (10) NULL,
        #    LICENSEPLATEID nvarchar (25) NULL,
        #    INVENTBATCHID nvarchar (20) NULL,
        #    INVENTSERIALID nvarchar (20) NULL,
        #    INVENTPROFILEID_RU nvarchar (10) NULL,
        #    INVENTOWNERID_RU nvarchar (20) NULL,
        #    INVENTGTDID_RU nvarchar (30) NULL);
        #
        #-- The algorithm of the stored procedure is as follows:
        #-- 1. Initialize the @RESULT table with aggregated qtys in WHSInventReserve for the level requested by user
        #-- 2. Traverse the hierarchy up and for each level:
        #--     1) Aggregate qtys from previous level in @RESULT grouping by dimensions of current hierarchy level
        #--     2) Aggregate qtys from current level in WHSInventReserve grouping by dimensions of current hierarchy level
        #--     3) Select the minimum of the above qtys and insert into @RESULT
        #-- 3. When hierarchy level 0 is reached, return the sum of aggregated qtys in @RESULT
        #
        #-- Initialize the @RESULT variable.
        #-- Loads aggregated data for the lowest level requested by user.
        #INSERT INTO @RESULT (AVAILPHYSICALRESULT, AVAILORDEREDRESULT, AVAILPHYSICALFOUND, AVAILORDEREDFOUND, HIERARCHYLEVEL, CONFIGID, INVENTSIZEID, INVENTCOLORID, INVENTSTYLEID, INVENTSITEID, INVENTLOCATIONID, WMSLOCATIONID, WMSPALLETID, INVENTSTATUSID, LICENSEPLATEID, INVENTBATCHID, INVENTSERIALID, INVENTPROFILEID_RU, INVENTOWNERID_RU, INVENTGTDID_RU)
        #SELECT SUM(WHSINVENTRESERVE.AVAILPHYSICAL),
        #    SUM(WHSINVENTRESERVE.AVAILORDERED),
        #    NULL,
        #    NULL,
        #    @LEVEL,
        #    INVENTDIM.CONFIGID,
        #    INVENTDIM.INVENTSIZEID,
        #    INVENTDIM.INVENTCOLORID,
        #    INVENTDIM.INVENTSTYLEID,
        #    CASE WHEN @LEVEL + -1 >= @INVENTSITEIDLEVEL THEN INVENTDIM.INVENTSITEID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTLOCATIONIDLEVEL THEN INVENTDIM.INVENTLOCATIONID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @WMSLOCATIONIDLEVEL THEN INVENTDIM.WMSLOCATIONID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @WMSPALLETIDLEVEL THEN INVENTDIM.WMSPALLETID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTSTATUSIDLEVEL THEN INVENTDIM.INVENTSTATUSID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @LICENSEPLATEIDLEVEL THEN INVENTDIM.LICENSEPLATEID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTBATCHIDLEVEL THEN INVENTDIM.INVENTBATCHID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTSERIALIDLEVEL THEN INVENTDIM.INVENTSERIALID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTPROFILEID_RULEVEL THEN INVENTDIM.INVENTPROFILEID_RU ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTOWNERID_RULEVEL THEN INVENTDIM.INVENTOWNERID_RU ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTGTDID_RULEVEL THEN INVENTDIM.INVENTGTDID_RU ELSE NULL END
        #    FROM WHSINVENTRESERVE WHSINVENTRESERVE
        #    JOIN INVENTDIM ON INVENTDIM.INVENTDIMID = WHSINVENTRESERVE.INVENTDIMID AND INVENTDIM.DATAAREAID = WHSINVENTRESERVE.DATAAREAID AND INVENTDIM.PARTITION = WHSINVENTRESERVE.PARTITION
        #    WHERE WHSINVENTRESERVE.ITEMID           = @ITEMID
        #        AND WHSINVENTRESERVE.PARTITION      = @PARTITION
        #        AND WHSINVENTRESERVE.DATAAREAID     = @DATAAREAID
        #        AND WHSINVENTRESERVE.HIERARCHYLEVEL = @LEVEL
        #        AND (@CONFIGID IS NULL OR INVENTDIM.CONFIGID = @CONFIGID)
        #        AND (@INVENTSIZEID IS NULL OR INVENTDIM.INVENTSIZEID = @INVENTSIZEID)
        #        AND (@INVENTCOLORID IS NULL OR INVENTDIM.INVENTCOLORID = @INVENTCOLORID)
        #        AND (@INVENTSTYLEID IS NULL OR INVENTDIM.INVENTSTYLEID = @INVENTSTYLEID)
        #        AND (@LEVEL < @INVENTSITEIDLEVEL OR @INVENTSITEID IS NULL OR @INVENTSITEID = INVENTDIM.INVENTSITEID)
        #        AND (@LEVEL < @INVENTLOCATIONIDLEVEL OR @INVENTLOCATIONID IS NULL OR @INVENTLOCATIONID = INVENTDIM.INVENTLOCATIONID)
        #        AND (@LEVEL < @WMSLOCATIONIDLEVEL OR @WMSLOCATIONID IS NULL OR @WMSLOCATIONID = INVENTDIM.WMSLOCATIONID)
        #        AND (@LEVEL < @WMSPALLETIDLEVEL OR @WMSPALLETID IS NULL OR @WMSPALLETID = INVENTDIM.WMSPALLETID)
        #        AND (@LEVEL < @INVENTSTATUSIDLEVEL OR @INVENTSTATUSID IS NULL OR @INVENTSTATUSID = INVENTDIM.INVENTSTATUSID)
        #        AND (@LEVEL < @LICENSEPLATEIDLEVEL OR @LICENSEPLATEID IS NULL OR @LICENSEPLATEID = INVENTDIM.LICENSEPLATEID)
        #        AND (@LEVEL < @INVENTBATCHIDLEVEL OR @INVENTBATCHID IS NULL OR @INVENTBATCHID = INVENTDIM.INVENTBATCHID)
        #        AND (@LEVEL < @INVENTSERIALIDLEVEL OR @INVENTSERIALID IS NULL OR @INVENTSERIALID = INVENTDIM.INVENTSERIALID)
        #        AND (@LEVEL < @INVENTPROFILEID_RULEVEL OR @INVENTPROFILEID_RU IS NULL OR @INVENTPROFILEID_RU = INVENTDIM.INVENTPROFILEID_RU)
        #        AND (@LEVEL < @INVENTOWNERID_RULEVEL OR @INVENTOWNERID_RU IS NULL OR @INVENTOWNERID_RU = INVENTDIM.INVENTOWNERID_RU)
        #        AND (@LEVEL < @INVENTGTDID_RULEVEL OR @INVENTGTDID_RU IS NULL OR @INVENTGTDID_RU = INVENTDIM.INVENTGTDID_RU)
        #GROUP BY
        #    INVENTDIM.CONFIGID,
        #    INVENTDIM.INVENTSIZEID,
        #    INVENTDIM.INVENTCOLORID,
        #    INVENTDIM.INVENTSTYLEID,
        #    CASE WHEN @LEVEL + -1 >= @INVENTSITEIDLEVEL THEN INVENTDIM.INVENTSITEID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTLOCATIONIDLEVEL THEN INVENTDIM.INVENTLOCATIONID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @WMSLOCATIONIDLEVEL THEN INVENTDIM.WMSLOCATIONID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @WMSPALLETIDLEVEL THEN INVENTDIM.WMSPALLETID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTSTATUSIDLEVEL THEN INVENTDIM.INVENTSTATUSID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @LICENSEPLATEIDLEVEL THEN INVENTDIM.LICENSEPLATEID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTBATCHIDLEVEL THEN INVENTDIM.INVENTBATCHID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTSERIALIDLEVEL THEN INVENTDIM.INVENTSERIALID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTPROFILEID_RULEVEL THEN INVENTDIM.INVENTPROFILEID_RU ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTOWNERID_RULEVEL THEN INVENTDIM.INVENTOWNERID_RU ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTGTDID_RULEVEL THEN INVENTDIM.INVENTGTDID_RU ELSE NULL END;
        #
        #WHILE (@LEVEL > 0)
        #BEGIN
        #    INSERT INTO @RESULT (AVAILPHYSICALRESULT, AVAILORDEREDRESULT, AVAILPHYSICALFOUND, AVAILORDEREDFOUND, HIERARCHYLEVEL, CONFIGID, INVENTSIZEID, INVENTCOLORID, INVENTSTYLEID, INVENTSITEID, INVENTLOCATIONID, WMSLOCATIONID, WMSPALLETID, INVENTSTATUSID, LICENSEPLATEID, INVENTBATCHID, INVENTSERIALID, INVENTPROFILEID_RU, INVENTOWNERID_RU, INVENTGTDID_RU)
        #SELECT SUM(CASE WHEN RESULT.AVAILPHYSICALFOUND < RESULT.AVAILPHYSICALRESULT THEN RESULT.AVAILPHYSICALFOUND ELSE RESULT.AVAILPHYSICALRESULT END),
        #    SUM(CASE WHEN RESULT.AVAILORDEREDFOUND < RESULT.AVAILORDEREDRESULT THEN RESULT.AVAILORDEREDFOUND ELSE RESULT.AVAILORDEREDRESULT END),
        #    SUM(FOUND.AVAILPHYSICAL),
        #    SUM(FOUND.AVAILORDERED),
        #    @LEVEL + -1,
        #    RESULT.CONFIGID AS CONFIGID,
        #    RESULT.INVENTSIZEID AS INVENTSIZEID,
        #    RESULT.INVENTCOLORID AS INVENTCOLORID,
        #    RESULT.INVENTSTYLEID AS INVENTSTYLEID,
        #    CASE WHEN @LEVEL + -1 >= @INVENTSITEIDLEVEL THEN RESULT.INVENTSITEID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTLOCATIONIDLEVEL THEN RESULT.INVENTLOCATIONID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @WMSLOCATIONIDLEVEL THEN RESULT.WMSLOCATIONID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @WMSPALLETIDLEVEL THEN RESULT.WMSPALLETID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTSTATUSIDLEVEL THEN RESULT.INVENTSTATUSID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @LICENSEPLATEIDLEVEL THEN RESULT.LICENSEPLATEID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTBATCHIDLEVEL THEN RESULT.INVENTBATCHID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTSERIALIDLEVEL THEN RESULT.INVENTSERIALID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTPROFILEID_RULEVEL THEN RESULT.INVENTPROFILEID_RU ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTOWNERID_RULEVEL THEN RESULT.INVENTOWNERID_RU ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTGTDID_RULEVEL THEN RESULT.INVENTGTDID_RU ELSE NULL END
        #FROM @RESULT RESULT
        #LEFT OUTER JOIN (
        #SELECT SUM(WHSINVENTRESERVE.AVAILPHYSICAL) AS AVAILPHYSICAL,
        #    SUM(WHSINVENTRESERVE.AVAILORDERED) AS AVAILORDERED,
        #    INVENTDIM.CONFIGID AS CONFIGID,
        #    INVENTDIM.INVENTSIZEID AS INVENTSIZEID,
        #    INVENTDIM.INVENTCOLORID AS INVENTCOLORID,
        #    INVENTDIM.INVENTSTYLEID AS INVENTSTYLEID,
        #    CASE WHEN @LEVEL + -1 >= @INVENTSITEIDLEVEL THEN INVENTDIM.INVENTSITEID ELSE NULL END AS INVENTSITEID,
        #    CASE WHEN @LEVEL + -1 >= @INVENTLOCATIONIDLEVEL THEN INVENTDIM.INVENTLOCATIONID ELSE NULL END AS INVENTLOCATIONID,
        #    CASE WHEN @LEVEL + -1 >= @WMSLOCATIONIDLEVEL THEN INVENTDIM.WMSLOCATIONID ELSE NULL END AS WMSLOCATIONID,
        #    CASE WHEN @LEVEL + -1 >= @WMSPALLETIDLEVEL THEN INVENTDIM.WMSPALLETID ELSE NULL END AS WMSPALLETID,
        #    CASE WHEN @LEVEL + -1 >= @INVENTSTATUSIDLEVEL THEN INVENTDIM.INVENTSTATUSID ELSE NULL END AS INVENTSTATUSID,
        #    CASE WHEN @LEVEL + -1 >= @LICENSEPLATEIDLEVEL THEN INVENTDIM.LICENSEPLATEID ELSE NULL END AS LICENSEPLATEID,
        #    CASE WHEN @LEVEL + -1 >= @INVENTBATCHIDLEVEL THEN INVENTDIM.INVENTBATCHID ELSE NULL END AS INVENTBATCHID,
        #    CASE WHEN @LEVEL + -1 >= @INVENTSERIALIDLEVEL THEN INVENTDIM.INVENTSERIALID ELSE NULL END AS INVENTSERIALID,
        #    CASE WHEN @LEVEL + -1 >= @INVENTPROFILEID_RULEVEL THEN INVENTDIM.INVENTPROFILEID_RU ELSE NULL END AS INVENTPROFILEID_RU,
        #    CASE WHEN @LEVEL + -1 >= @INVENTOWNERID_RULEVEL THEN INVENTDIM.INVENTOWNERID_RU ELSE NULL END AS INVENTOWNERID_RU,
        #    CASE WHEN @LEVEL + -1 >= @INVENTGTDID_RULEVEL THEN INVENTDIM.INVENTGTDID_RU ELSE NULL END AS INVENTGTDID_RU
        #FROM WHSINVENTRESERVE WHSINVENTRESERVE
        #JOIN INVENTDIM ON INVENTDIM.PARTITION = WHSINVENTRESERVE.PARTITION
        #    AND INVENTDIM.DATAAREAID  = WHSINVENTRESERVE.DATAAREAID
        #    AND INVENTDIM.INVENTDIMID = WHSINVENTRESERVE.INVENTDIMID
        #WHERE WHSINVENTRESERVE.HIERARCHYLEVEL = @LEVEL + -1
        #    AND WHSINVENTRESERVE.ITEMID       = @ITEMID
        #    AND WHSINVENTRESERVE.PARTITION    = @PARTITION
        #    AND WHSINVENTRESERVE.DATAAREAID   = @DATAAREAID
        #GROUP BY
        #    INVENTDIM.CONFIGID,
        #    INVENTDIM.INVENTSIZEID,
        #    INVENTDIM.INVENTCOLORID,
        #    INVENTDIM.INVENTSTYLEID,
        #    CASE WHEN @LEVEL + -1 >= @INVENTSITEIDLEVEL THEN INVENTDIM.INVENTSITEID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTLOCATIONIDLEVEL THEN INVENTDIM.INVENTLOCATIONID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @WMSLOCATIONIDLEVEL THEN INVENTDIM.WMSLOCATIONID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @WMSPALLETIDLEVEL THEN INVENTDIM.WMSPALLETID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTSTATUSIDLEVEL THEN INVENTDIM.INVENTSTATUSID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @LICENSEPLATEIDLEVEL THEN INVENTDIM.LICENSEPLATEID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTBATCHIDLEVEL THEN INVENTDIM.INVENTBATCHID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTSERIALIDLEVEL THEN INVENTDIM.INVENTSERIALID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTPROFILEID_RULEVEL THEN INVENTDIM.INVENTPROFILEID_RU ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTOWNERID_RULEVEL THEN INVENTDIM.INVENTOWNERID_RU ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTGTDID_RULEVEL THEN INVENTDIM.INVENTGTDID_RU ELSE NULL END
        #    ) AS FOUND ON (RESULT.CONFIGID = FOUND.CONFIGID)
        #        AND (RESULT.INVENTSIZEID = FOUND.INVENTSIZEID)
        #        AND (RESULT.INVENTCOLORID = FOUND.INVENTCOLORID)
        #        AND (RESULT.INVENTSTYLEID = FOUND.INVENTSTYLEID)
        #        AND (@LEVEL + -1 < @INVENTSITEIDLEVEL OR RESULT.INVENTSITEID = FOUND.INVENTSITEID)
        #        AND (@LEVEL + -1 < @INVENTLOCATIONIDLEVEL OR RESULT.INVENTLOCATIONID = FOUND.INVENTLOCATIONID)
        #        AND (@LEVEL + -1 < @WMSLOCATIONIDLEVEL OR RESULT.WMSLOCATIONID = FOUND.WMSLOCATIONID)
        #        AND (@LEVEL + -1 < @WMSPALLETIDLEVEL OR RESULT.WMSPALLETID = FOUND.WMSPALLETID)
        #        AND (@LEVEL + -1 < @INVENTSTATUSIDLEVEL OR RESULT.INVENTSTATUSID = FOUND.INVENTSTATUSID)
        #        AND (@LEVEL + -1 < @LICENSEPLATEIDLEVEL OR RESULT.LICENSEPLATEID = FOUND.LICENSEPLATEID)
        #        AND (@LEVEL + -1 < @INVENTBATCHIDLEVEL OR RESULT.INVENTBATCHID = FOUND.INVENTBATCHID)
        #        AND (@LEVEL + -1 < @INVENTSERIALIDLEVEL OR RESULT.INVENTSERIALID = FOUND.INVENTSERIALID)
        #        AND (@LEVEL + -1 < @INVENTPROFILEID_RULEVEL OR RESULT.INVENTPROFILEID_RU = FOUND.INVENTPROFILEID_RU)
        #        AND (@LEVEL + -1 < @INVENTOWNERID_RULEVEL OR RESULT.INVENTOWNERID_RU = FOUND.INVENTOWNERID_RU)
        #        AND (@LEVEL + -1 < @INVENTGTDID_RULEVEL OR RESULT.INVENTGTDID_RU = FOUND.INVENTGTDID_RU)
        #WHERE RESULT.HIERARCHYLEVEL = @LEVEL
        #GROUP BY
        #    RESULT.CONFIGID,
        #    RESULT.INVENTSIZEID,
        #    RESULT.INVENTCOLORID,
        #    RESULT.INVENTSTYLEID,
        #    CASE WHEN @LEVEL + -1 >= @INVENTSITEIDLEVEL THEN RESULT.INVENTSITEID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTLOCATIONIDLEVEL THEN RESULT.INVENTLOCATIONID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @WMSLOCATIONIDLEVEL THEN RESULT.WMSLOCATIONID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @WMSPALLETIDLEVEL THEN RESULT.WMSPALLETID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTSTATUSIDLEVEL THEN RESULT.INVENTSTATUSID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @LICENSEPLATEIDLEVEL THEN RESULT.LICENSEPLATEID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTBATCHIDLEVEL THEN RESULT.INVENTBATCHID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTSERIALIDLEVEL THEN RESULT.INVENTSERIALID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTPROFILEID_RULEVEL THEN RESULT.INVENTPROFILEID_RU ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTOWNERID_RULEVEL THEN RESULT.INVENTOWNERID_RU ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTGTDID_RULEVEL THEN RESULT.INVENTGTDID_RU ELSE NULL END;
        #
        #
        #    SET @LEVEL = @LEVEL - 1
        #END; -- WHILE
        #
        #SELECT @AVAILPHYSICAL = SUM(CASE WHEN AVAILPHYSICALFOUND < AVAILPHYSICALRESULT THEN AVAILPHYSICALFOUND ELSE AVAILPHYSICALRESULT END),
        #    @AVAILORDERED = SUM(CASE WHEN AVAILORDEREDFOUND < AVAILORDEREDRESULT THEN AVAILORDEREDFOUND ELSE AVAILORDEREDRESULT END)
        #FROM @RESULT
        #WHERE HIERARCHYLEVEL = 0;
        #        */
        #
        #        ret += strFmt('CREATE PROCEDURE [%1]\n', WhsOnHandSPHelper::storedProcedureName(_includeDelta));
        #        ret +=        '    @PARTITION bigint,\n';
        #        ret += strFmt('    %1 int,\n', this.levelExpression(0));
        #        ret += strFmt('    @DATAAREAID %1,\n', this.sqlTypeNameForField(tableNum(InventTable), fieldNum(InventTable, DataAreaId)));
        #        ret += strFmt('    @ITEMID %1,\n', this.sqlTypeNameForField(tableNum(InventTable), fieldNum(InventTable, ItemId)));
        #        if (_includeDelta)
        #        {
        #            ret += strFmt('    %1 %2,\n', #TTSIdParmName, this.sqlTypeNameForField(tableNum(WHSInventReserveDelta), fieldNum(WHSInventReserveDelta, TTSId)));
        #        }
        #
        #        // Generate parameters for supported dimensions
        #        enabledDimsEnumerator.reset();
        #        while (enabledDimsEnumerator.moveNext())
        #        {
        #            [dimFieldId, dimFieldName] = enabledDimsEnumerator.current();
        #            ret += strFmt('    %1 %2 = NULL,\n', this.dimensionParm(dimFieldName), this.sqlTypeNameForField(tableNum(InventDim), dimFieldId));
        #            ret += strFmt('    %1 int = %2,\n', this.dimensionLevelParm(dimFieldName), #INVALIDHIERARCHYLEVEL);
        #        }
        #
        #        // Generate parameters for product dimensions
        #        productDimensionsEnumerator.reset();
        #        while (productDimensionsEnumerator.moveNext())
        #        {
        #            [dimFieldId, dimFieldName] = productDimensionsEnumerator.current();
        #            ret += strFmt('    %1 %2 = NULL,\n', this.dimensionParm(dimFieldName), this.sqlTypeNameForField(tableNum(InventDim), dimFieldId));
        #        }
        #
        #        ret +=        '    -- Output parameters\n';
        #        ret += strFmt('    %1  numeric(32, 16) OUTPUT,\n', #AvailPhysicalParmName);
        #        ret += strFmt('    %1  numeric(32, 16) OUTPUT\n', #AvailOrderedParmName);
        #        ret +=        'AS\n\n';
        #
        #        ret +=        '-- Without nocount on the procedure will return number of affected rows in the result set which is\n';
        #        ret +=        '-- harmful to performance and requires us to adjust field retrieval when calling the stored procedure\n';
        #        ret +=        'SET NOCOUNT ON;\n\n';
        #
        #        ret +=        '-- We need to supress warnings when trying to aggregate NULL columns. This is a valid business scenario\n';
        #        ret +=        '-- and happens e.g. for a non-LP controlled location with batch below item.\n';
        #        ret +=        'SET ANSI_WARNINGS OFF;\n\n';
        #
        #        ret += this.declareResultTableVariable() + '\n';
        #
        #        ret +=        '-- The algorithm of the stored procedure is as follows:\n';
        #        ret +=        '-- 1. Initialize the @RESULT table with aggregated qtys in WHSInventReserve for the level requested by user\n';
        #        ret +=        '-- 2. Traverse the hierarchy up and for each level:\n';
        #        ret +=        '--     1) Aggregate qtys from previous level in @RESULT grouping by dimensions of current hierarchy level\n';
        #        ret +=        '--     2) Aggregate qtys from current level in WHSInventReserve grouping by dimensions of current hierarchy level\n';
        #        ret +=        '--     3) Select the minimum of the above qtys and insert into @RESULT\n';
        #        ret +=        '-- 3. When hierarchy level 0 is reached, return the sum of aggregated qtys in @RESULT\n\n';
        #
        #        ret +=        '-- Initialize the @RESULT variable.\n';
        #        ret +=        '-- Loads aggregated data for the lowest level requested by user.\n';
        #        ret +=        'INSERT INTO @RESULT (' + this.resultTableVariableFields() + ')\n';
        #
        #        ret += this.selectFromWHSInventReserveStmt(_includeDelta) + '\n';
        #
        #        ret += strFmt('WHILE (%1 > 0)\n', this.levelExpression(0));
        #        ret +=        'BEGIN\n';
        #        ret += strFmt('    INSERT INTO @RESULT (%1)\n', this.resultTableVariableFields());
        #
        #        ret += this.selectFromResultJoinWHSInventReserveStmt(_includeDelta) + '\n\n';
        #
        #        ret += strFmt('    SET %1 = %1 - 1\n', this.levelExpression(0));
        #        ret +=        'END; -- WHILE\n\n';
        #
        #        ret += strFmt('SELECT %1 = SUM(CASE WHEN %2 < %3 THEN %2 ELSE %3 END),\n',
        #                        #AvailPhysicalParmName,
        #                        #AVAILPHYSICALFOUNDFieldName,
        #                        #AVAILPHYSICALRESULTFieldName);
        #        ret += strFmt('    %1 = SUM(CASE WHEN %2 < %3 THEN %2 ELSE %3 END)\n',
        #                        #AvailOrderedParmName,
        #                        #AVAILORDEREDFOUNDFieldName,
        #                        #AVAILORDEREDRESULTFieldName);
        #        ret +=        'FROM @RESULT\n';
        #        ret += strFmt('WHERE %1 = 0;\n', #HIERARCHYLEVELFieldName);
        #    }
        #
        #    return ret;
        #}
      ENDSOURCE
      SOURCE #classDeclaration
        #///
        #///     The WHSOnHandSPHelper class contains helper methods to manipulate the WHS on-hand stored procedure.
        #///
        #public class WhsOnHandSPHelper
        #{
        #    #Define.CacheScope('WHSOnHandSPHelper')
        #    #Define.EnabledDimsParametersCacheKey('EnabledDimensionsCache')
        #    #Define.ProductDimsParametersCacheKey('ProductDimensionsCache')
        #
        #    #Define.AvailPhysicalParmName('@AVAILPHYSICAL')
        #    #Define.AvailPhysicalMinName('@AVAILPHYSICALMIN')
        #    #Define.AvailOrderedParmName('@AVAILORDERED')
        #    #Define.AvailOrderedMinName('@AVAILORDERMIN')
        #    #Define.TTSIdParmName('@TTSId')
        #
        #    #Define.AVAILPHYSICALRESULTFieldName('AVAILPHYSICALRESULT')
        #    #Define.AVAILORDEREDRESULTFieldName('AVAILORDEREDRESULT')
        #    #Define.AVAILPHYSICALFOUNDFieldName('AVAILPHYSICALFOUND')
        #    #Define.AVAILORDEREDFOUNDFieldName('AVAILORDEREDFOUND')
        #    #Define.HIERARCHYLEVELFieldName('HIERARCHYLEVEL')
        #
        #    #WHSOnHandSP
        #    #WHSReservationHierarchy
        #
        #    str resultTableVariableFields; // access using the resultTableVariableFields() method
        #
        #    boolean     useOptimizedVersion;
        #}
      ENDSOURCE
      SOURCE #new
        #protected void new()
        #{
        #    resultTableVariableFields = '';
        #
        #    useOptimizedVersion = true;
        #}
      ENDSOURCE
      SOURCE #selectFromWHSInventReserveStmt
        #private str selectFromWHSInventReserveStmt(boolean _includeDelta)
        #{
        #    str             ret = '';
        #    ListEnumerator  enabledDimsEnumerator = this.enabledDimensions().getEnumerator();
        #    ListEnumerator  productDimsEnumerator = this.productDimensions().getEnumerator();
        #    str             dimFieldName;
        #    str             levelExpr = this.levelExpression(-1);
        #    str             inventReserveAlias = this.whsInventReserveTableAlias(_includeDelta);
        #    boolean         hasDoneFirstIteration;
        #
        #    if(useOptimizedVersion)
        #    {
        #        ret = this.TEC_selectFromWHSInventReserveStmt(_includeDelta);
        #    }
        #    else
        #    {
        #        ret += strFmt('SELECT SUM(%1.AVAILPHYSICAL),\n', inventReserveAlias);
        #        ret += strFmt('    SUM(%1.AVAILORDERED),\n', inventReserveAlias);
        #        ret +=        '    NULL,\n';
        #        ret +=        '    NULL,\n';
        #        ret += strFmt('    %1,\n', this.levelExpression(0));
        #
        #        // Select product dimensions
        #        productDimsEnumerator.reset();
        #        while (productDimsEnumerator.moveNext())
        #        {
        #            dimFieldName = conPeek(productDimsEnumerator.current(), 2);
        #            ret += strFmt('    INVENTDIM.%1,\n', dimFieldName);
        #        }
        #
        #        // Select enabled dimensions
        #        enabledDimsEnumerator.reset();
        #        hasDoneFirstIteration = false;
        #        while (enabledDimsEnumerator.moveNext())
        #        {
        #            if (hasDoneFirstIteration)
        #            {
        #                ret += ',\n';
        #            }
        #
        #            dimFieldName = conPeek(enabledDimsEnumerator.current(), 2);
        #            ret += strFmt('    CASE WHEN %1 >= %2 THEN INVENTDIM.%3 ELSE NULL END',
        #                            this.levelExpression(-1),
        #                            this.dimensionLevelParm(dimFieldName),
        #                            dimFieldName);
        #
        #            hasDoneFirstIteration = true;
        #        }
        #        ret += '\n';
        #
        #        ret += strFmt('    FROM %1 %2\n', this.whsInventReserveTableName(_includeDelta), inventReserveAlias);
        #        ret += strFmt('    JOIN INVENTDIM ON INVENTDIM.INVENTDIMID = %1.INVENTDIMID AND INVENTDIM.DATAAREAID = %1.DATAAREAID AND INVENTDIM.PARTITION = %1.PARTITION\n', inventReserveAlias);
        #        ret += strFmt('    WHERE %1.ITEMID           = @ITEMID\n', inventReserveAlias);
        #        ret += strFmt('        AND %1.PARTITION      = @PARTITION\n', inventReserveAlias);
        #        ret += strFmt('        AND %1.DATAAREAID     = @DATAAREAID\n', inventReserveAlias);
        #        ret += strFmt('        AND %1.HIERARCHYLEVEL = %2\n', inventReserveAlias, this.levelExpression(0));
        #
        #        // Add product dimensions range
        #        productDimsEnumerator.reset();
        #        while (productDimsEnumerator.moveNext())
        #        {
        #            dimFieldName = conPeek(productDimsEnumerator.current(), 2);
        #            ret += strFmt('        AND (%2 IS NULL OR INVENTDIM.%1 = %2)\n',
        #                            dimFieldName,
        #                            this.dimensionParm(dimFieldName));
        #        }
        #
        #        // Add enabled dimensions range
        #        enabledDimsEnumerator.reset();
        #        while (enabledDimsEnumerator.moveNext())
        #        {
        #            dimFieldName = conPeek(enabledDimsEnumerator.current(), 2);
        #            ret += strFmt('        AND (%1 < %2 OR %3 IS NULL OR %3 = INVENTDIM.%4)\n',
        #                            this.levelExpression(0),
        #                            this.dimensionLevelParm(dimFieldName),
        #                            this.dimensionParm(dimFieldName),
        #                            dimFieldName);
        #        }
        #
        #        ret +=        'GROUP BY\n';
        #
        #        // Add grouping by product dimensions
        #        productDimsEnumerator.reset();
        #        while (productDimsEnumerator.moveNext())
        #        {
        #            dimFieldName = conPeek(productDimsEnumerator.current(), 2);
        #            ret += strFmt('    INVENTDIM.%1,\n', dimFieldName);
        #        }
        #
        #        // Add grouping by enabled dimensions
        #        enabledDimsEnumerator.reset();
        #        hasDoneFirstIteration = false;
        #        while (enabledDimsEnumerator.moveNext())
        #        {
        #            if (hasDoneFirstIteration)
        #            {
        #                ret += ',\n';
        #            }
        #
        #            dimFieldName = conPeek(enabledDimsEnumerator.current(), 2);
        #            ret += strFmt('    CASE WHEN %1 >= %2 THEN INVENTDIM.%3 ELSE NULL END',
        #                            this.levelExpression(-1),
        #                            this.dimensionLevelParm(dimFieldName),
        #                            dimFieldName);
        #
        #            hasDoneFirstIteration = true;
        #        }
        #
        #        ret += ';\n';
        #    }
        #
        #    return ret;
        #}
      ENDSOURCE
      SOURCE #selectInnerFromWHSInventReserveStmt
        #private str selectInnerFromWHSInventReserveStmt(boolean _includeDelta)
        #{
        #    str             ret = '';
        #    ListEnumerator  enabledDimsEnumerator = this.enabledDimensions().getEnumerator();
        #    ListEnumerator  productDimsEnumerator = this.productDimensions().getEnumerator();
        #    str             dimFieldName;
        #    str             levelExpr = this.levelExpression(-1);
        #    str             inventReserveAlias = this.whsInventReserveTableAlias(_includeDelta);
        #    boolean         hasDoneFirstIteration;
        #
        #    if(useOptimizedVersion)
        #    {
        #        ret = this.TEC_selectInnerFromWHSInventReserveStmt(_includeDelta);
        #    }
        #    else
        #    {
        #        ret += strFmt('SELECT SUM(%1.AVAILPHYSICAL) AS AVAILPHYSICAL,\n', inventReserveAlias);
        #        ret += strFmt('    SUM(%1.AVAILORDERED) AS AVAILORDERED,\n', inventReserveAlias);
        #
        #        productDimsEnumerator.reset();
        #        while (productDimsEnumerator.moveNext())
        #        {
        #            dimFieldName = conPeek(productDimsEnumerator.current(), 2);
        #            ret += strFmt('    INVENTDIM.%1 AS %1,\n', dimFieldName);
        #        }
        #
        #        enabledDimsEnumerator.reset();
        #        hasDoneFirstIteration = false;
        #        while (enabledDimsEnumerator.moveNext())
        #        {
        #            if (hasDoneFirstIteration)
        #            {
        #                ret += ',\n';
        #            }
        #
        #            dimFieldName = conPeek(enabledDimsEnumerator.current(), 2);
        #            ret += strFmt('    CASE WHEN %1 >= %2 THEN INVENTDIM.%3 ELSE NULL END AS %3',
        #                            levelExpr,
        #                            this.dimensionLevelParm(dimFieldName),
        #                            dimFieldName);
        #
        #            hasDoneFirstIteration = true;
        #        }
        #        ret += '\n';
        #
        #        ret += strFmt('FROM %1 %2\n', this.whsInventReserveTableName(_includeDelta), inventReserveAlias);
        #        ret += strFmt('JOIN INVENTDIM ON INVENTDIM.PARTITION = %1.PARTITION\n', inventReserveAlias);
        #        ret += strFmt('    AND INVENTDIM.DATAAREAID  = %1.DATAAREAID\n', inventReserveAlias);
        #        ret += strFmt('    AND INVENTDIM.INVENTDIMID = %1.INVENTDIMID\n', inventReserveAlias);
        #        ret += strFmt('WHERE %1.HIERARCHYLEVEL = %2\n', inventReserveAlias, levelExpr);
        #        ret += strFmt('    AND %1.ITEMID       = @ITEMID\n', inventReserveAlias);
        #        ret += strFmt('    AND %1.PARTITION    = @PARTITION\n', inventReserveAlias);
        #        ret += strFmt('    AND %1.DATAAREAID   = @DATAAREAID\n', inventReserveAlias);
        #        ret +=          'GROUP BY\n';
        #
        #        // Add groupping by product dimensions
        #        productDimsEnumerator.reset();
        #        while (productDimsEnumerator.moveNext())
        #        {
        #            dimFieldName = conPeek(productDimsEnumerator.current(), 2);
        #            ret += strFmt('    INVENTDIM.%1,\n', dimFieldName);
        #        }
        #
        #        // Add groupping by enabled dimensions
        #        enabledDimsEnumerator.reset();
        #        hasDoneFirstIteration = false;
        #        while (enabledDimsEnumerator.moveNext())
        #        {
        #            if (hasDoneFirstIteration)
        #            {
        #                ret += ',\n';
        #            }
        #
        #            dimFieldName = conPeek(enabledDimsEnumerator.current(), 2);
        #            ret += strFmt('    CASE WHEN %1 >= %2 THEN INVENTDIM.%3 ELSE NULL END',
        #                            levelExpr,
        #                            this.dimensionLevelParm(dimFieldName),
        #                            dimFieldName);
        #
        #            hasDoneFirstIteration = true;
        #        }
        #        ret += '\n';
        #    }
        #
        #    return ret;
        #}
      ENDSOURCE
      SOURCE #TEC_buildCreateSPStmt
        #private str TEC_buildCreateSPStmt(boolean _includeDelta)
        #{
        #    str             ret = '';
        #    ListEnumerator  enabledDimsEnumerator;
        #    ListEnumerator  productDimensionsEnumerator;
        #    FieldId         dimFieldId;
        #    str             dimFieldName;
        #    boolean         isSerialEnabled;
        #    str             inventReserveAlias = this.whsInventReserveTableAlias(_includeDelta);
        #
        #    InventDimMetaDataCache::clearCache(); //in some cases the cache is not updated before sync making below SP
        #
        #    enabledDimsEnumerator = this.enabledDimensions().getEnumerator();
        #    productDimensionsEnumerator = this.productDimensions().getEnumerator();
        #    isSerialEnabled = this.getEnabledDimsParametersMap().exists(fieldNum(InventDim, InventSerialId));
        #
        #    /*
        #CREATE PROCEDURE [dbo].[sp_WHSOnHand]
        #    @PARTITION bigint,
        #    @LEVEL int,
        #    @DATAAREAID nvarchar (4),
        #    @ITEMID nvarchar (20),
        #    @INVENTSITEID nvarchar (10) = NULL,
        #    @INVENTSITEIDLEVEL int = 1024,
        #    @INVENTLOCATIONID nvarchar (10) = NULL,
        #    @INVENTLOCATIONIDLEVEL int = 1024,
        #    @WMSLOCATIONID nvarchar (10) = NULL,
        #    @WMSLOCATIONIDLEVEL int = 1024,
        #    @WMSPALLETID nvarchar (18) = NULL,
        #    @WMSPALLETIDLEVEL int = 1024,
        #    @INVENTSTATUSID nvarchar (10) = NULL,
        #    @INVENTSTATUSIDLEVEL int = 1024,
        #    @LICENSEPLATEID nvarchar (25) = NULL,
        #    @LICENSEPLATEIDLEVEL int = 1024,
        #    @INVENTBATCHID nvarchar (20) = NULL,
        #    @INVENTBATCHIDLEVEL int = 1024,
        #    @INVENTSERIALID nvarchar (20) = NULL,
        #    @INVENTSERIALIDLEVEL int = 1024,
        #    @INVENTPROFILEID_RU nvarchar (10) = NULL,
        #    @INVENTPROFILEID_RULEVEL int = 1024,
        #    @INVENTOWNERID_RU nvarchar (20) = NULL,
        #    @INVENTOWNERID_RULEVEL int = 1024,
        #    @INVENTGTDID_RU nvarchar (30) = NULL,
        #    @INVENTGTDID_RULEVEL int = 1024,
        #    @CONFIGID nvarchar (10) = NULL,
        #    @INVENTSIZEID nvarchar (10) = NULL,
        #    @INVENTCOLORID nvarchar (10) = NULL,
        #    @INVENTSTYLEID nvarchar (10) = NULL,
        #    -- Output parameters
        #    @AVAILPHYSICAL  numeric(32, 16) OUTPUT,
        #    @AVAILORDERED  numeric(32, 16) OUTPUT
        #AS
        #
        #-- Without nocount on the procedure will return number of affected rows in the result set which is
        #-- harmful to performance and requires us to adjust field retrieval when calling the stored procedure
        #SET NOCOUNT ON;
        #
        #-- We need to supress warnings when trying to aggregate NULL columns. This is a valid business scenario
        #-- and happens e.g. for a non-LP controlled location with batch below item.
        #SET ANSI_WARNINGS OFF;
        #
        #-- The @RESULT table variable is used to store intermediate quantities.
        #DECLARE @RESULT TABLE(
        #    AVAILPHYSICALRESULT numeric(32, 16) NOT NULL, -- Aggregated minimum qtys from lower level in @RESULT
        #    AVAILORDEREDRESULT numeric(32, 16) NOT NULL, -- Aggregated minimum qtys from lower level in @RESULT
        #    AVAILPHYSICALFOUND numeric(32, 16), -- Aggregated qty for current level in WHSInventReserve
        #    AVAILORDEREDFOUND numeric(32, 16), -- Aggregated qty for current level in WHSInventReserve
        #    HIERARCHYLEVEL int NOT NULL,
        #    CONFIGID nvarchar (10) NULL,
        #    INVENTSIZEID nvarchar (10) NULL,
        #    INVENTCOLORID nvarchar (10) NULL,
        #    INVENTSTYLEID nvarchar (10) NULL,
        #    INVENTSITEID nvarchar (10) NULL,
        #    INVENTLOCATIONID nvarchar (10) NULL,
        #    WMSLOCATIONID nvarchar (10) NULL,
        #    WMSPALLETID nvarchar (18) NULL,
        #    INVENTSTATUSID nvarchar (10) NULL,
        #    LICENSEPLATEID nvarchar (25) NULL,
        #    INVENTBATCHID nvarchar (20) NULL,
        #    INVENTSERIALID nvarchar (20) NULL,
        #    INVENTPROFILEID_RU nvarchar (10) NULL,
        #    INVENTOWNERID_RU nvarchar (20) NULL,
        #    INVENTGTDID_RU nvarchar (30) NULL);
        #
        #DECLARE @DIMLIST TABLE(
        #    INVENTDIMID nvarchar (20) NULL,
        #    AVAILPHYSICAL numeric(32, 16) NOT NULL,
        #    AVAILORDERED numeric(32, 16) NOT NULL);
        #
        #-- The algorithm of the stored procedure is as follows:
        #-- 1. Initialize the @RESULT table with aggregated qtys in WHSInventReserve for the level requested by user
        #-- 2. Traverse the hierarchy up and for each level:
        #--     1) Aggregate qtys from previous level in @RESULT grouping by dimensions of current hierarchy level
        #--     2) Aggregate qtys from current level in WHSInventReserve grouping by dimensions of current hierarchy level
        #--     3) Select the minimum of the above qtys and insert into @RESULT
        #-- 3. When hierarchy level 0 is reached, return the sum of aggregated qtys in @RESULT
        #
        #-- In the original query (the one after below query), it is joining InventDim and WHSInventReserve and filter records based on some criteria.
        #-- But in the execution plan, it doesn't seems to generate a good plan (even after UPDATE STATISTICS). In the original execution plan, it always
        #-- Get records from InventDim first, which returns lot of rows, then only join it to WHSInventReserve and continue filter from there.
        #-- If we makes it get records from WHSInventReserve first, filter it, then with lesser rows from here, join it to InventDim, it will be quicker.
        #-- So the optimization is to filter WHSInventResreve first, then use this result at the original query. In the test, this has reduce the execution
        #-- time from ~250ms to ~7ms
        #  INSERT INTO @DIMLIST(INVENTDIMID, AVAILPHYSICAL, AVAILORDERED)
        #  SELECT INVENTDIMID, AVAILPHYSICAL, AVAILORDERED
        #    FROM WHSINVENTRESERVE WHSINVENTRESERVE
        #   WHERE WHSINVENTRESERVE.ITEMID         = @ITEMID
        #     AND WHSINVENTRESERVE.PARTITION      = @PARTITION
        #     AND WHSINVENTRESERVE.DATAAREAID     = @DATAAREAID
        #     AND WHSINVENTRESERVE.HIERARCHYLEVEL = @LEVEL
        #
        #-- Initialize the @RESULT variable.
        #-- Loads aggregated data for the lowest level requested by user.
        #INSERT INTO @RESULT (AVAILPHYSICALRESULT, AVAILORDEREDRESULT, AVAILPHYSICALFOUND, AVAILORDEREDFOUND, HIERARCHYLEVEL, CONFIGID, INVENTSIZEID, INVENTCOLORID, INVENTSTYLEID, INVENTSITEID, INVENTLOCATIONID, WMSLOCATIONID, WMSPALLETID, INVENTSTATUSID, LICENSEPLATEID, INVENTBATCHID, INVENTSERIALID, INVENTPROFILEID_RU, INVENTOWNERID_RU, INVENTGTDID_RU)
        #SELECT SUM(DIMLIST.AVAILPHYSICAL),
        #    SUM(DIMLIST.AVAILORDERED),
        #    NULL,
        #    NULL,
        #    @LEVEL,
        #    INVENTDIM.CONFIGID,
        #    INVENTDIM.INVENTSIZEID,
        #    INVENTDIM.INVENTCOLORID,
        #    INVENTDIM.INVENTSTYLEID,
        #    CASE WHEN @LEVEL + -1 >= @INVENTSITEIDLEVEL THEN INVENTDIM.INVENTSITEID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTLOCATIONIDLEVEL THEN INVENTDIM.INVENTLOCATIONID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @WMSLOCATIONIDLEVEL THEN INVENTDIM.WMSLOCATIONID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @WMSPALLETIDLEVEL THEN INVENTDIM.WMSPALLETID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTSTATUSIDLEVEL THEN INVENTDIM.INVENTSTATUSID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @LICENSEPLATEIDLEVEL THEN INVENTDIM.LICENSEPLATEID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTBATCHIDLEVEL THEN INVENTDIM.INVENTBATCHID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTSERIALIDLEVEL THEN INVENTDIM.INVENTSERIALID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTPROFILEID_RULEVEL THEN INVENTDIM.INVENTPROFILEID_RU ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTOWNERID_RULEVEL THEN INVENTDIM.INVENTOWNERID_RU ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTGTDID_RULEVEL THEN INVENTDIM.INVENTGTDID_RU ELSE NULL END
        #    FROM INVENTDIM INVENTDIM
        #    JOIN @DIMLIST DIMLIST
        #      ON INVENTDIM.INVENTDIMID = DIMLIST.INVENTDIMID
        #    WHERE   INVENTDIM.DATAAREAID = @DATAAREAID
        #        AND INVENTDIM.PARTITION = @PARTITION
        #        AND (@CONFIGID IS NULL OR INVENTDIM.CONFIGID = @CONFIGID)
        #        AND (@INVENTSIZEID IS NULL OR INVENTDIM.INVENTSIZEID = @INVENTSIZEID)
        #        AND (@INVENTCOLORID IS NULL OR INVENTDIM.INVENTCOLORID = @INVENTCOLORID)
        #        AND (@INVENTSTYLEID IS NULL OR INVENTDIM.INVENTSTYLEID = @INVENTSTYLEID)
        #        AND (@LEVEL < @INVENTSITEIDLEVEL OR @INVENTSITEID IS NULL OR @INVENTSITEID = INVENTDIM.INVENTSITEID)
        #        AND (@LEVEL < @INVENTLOCATIONIDLEVEL OR @INVENTLOCATIONID IS NULL OR @INVENTLOCATIONID = INVENTDIM.INVENTLOCATIONID)
        #        AND (@LEVEL < @WMSLOCATIONIDLEVEL OR @WMSLOCATIONID IS NULL OR @WMSLOCATIONID = INVENTDIM.WMSLOCATIONID)
        #        AND (@LEVEL < @WMSPALLETIDLEVEL OR @WMSPALLETID IS NULL OR @WMSPALLETID = INVENTDIM.WMSPALLETID)
        #        AND (@LEVEL < @INVENTSTATUSIDLEVEL OR @INVENTSTATUSID IS NULL OR @INVENTSTATUSID = INVENTDIM.INVENTSTATUSID)
        #        AND (@LEVEL < @LICENSEPLATEIDLEVEL OR @LICENSEPLATEID IS NULL OR @LICENSEPLATEID = INVENTDIM.LICENSEPLATEID)
        #        AND (@LEVEL < @INVENTBATCHIDLEVEL OR @INVENTBATCHID IS NULL OR @INVENTBATCHID = INVENTDIM.INVENTBATCHID)
        #        AND (@LEVEL < @INVENTSERIALIDLEVEL OR @INVENTSERIALID IS NULL OR @INVENTSERIALID = INVENTDIM.INVENTSERIALID)
        #        AND (@LEVEL < @INVENTPROFILEID_RULEVEL OR @INVENTPROFILEID_RU IS NULL OR @INVENTPROFILEID_RU = INVENTDIM.INVENTPROFILEID_RU)
        #        AND (@LEVEL < @INVENTOWNERID_RULEVEL OR @INVENTOWNERID_RU IS NULL OR @INVENTOWNERID_RU = INVENTDIM.INVENTOWNERID_RU)
        #        AND (@LEVEL < @INVENTGTDID_RULEVEL OR @INVENTGTDID_RU IS NULL OR @INVENTGTDID_RU = INVENTDIM.INVENTGTDID_RU)
        #GROUP BY
        #    INVENTDIM.CONFIGID,
        #    INVENTDIM.INVENTSIZEID,
        #    INVENTDIM.INVENTCOLORID,
        #    INVENTDIM.INVENTSTYLEID,
        #    CASE WHEN @LEVEL + -1 >= @INVENTSITEIDLEVEL THEN INVENTDIM.INVENTSITEID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTLOCATIONIDLEVEL THEN INVENTDIM.INVENTLOCATIONID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @WMSLOCATIONIDLEVEL THEN INVENTDIM.WMSLOCATIONID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @WMSPALLETIDLEVEL THEN INVENTDIM.WMSPALLETID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTSTATUSIDLEVEL THEN INVENTDIM.INVENTSTATUSID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @LICENSEPLATEIDLEVEL THEN INVENTDIM.LICENSEPLATEID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTBATCHIDLEVEL THEN INVENTDIM.INVENTBATCHID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTSERIALIDLEVEL THEN INVENTDIM.INVENTSERIALID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTPROFILEID_RULEVEL THEN INVENTDIM.INVENTPROFILEID_RU ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTOWNERID_RULEVEL THEN INVENTDIM.INVENTOWNERID_RU ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTGTDID_RULEVEL THEN INVENTDIM.INVENTGTDID_RU ELSE NULL END;
        #
        #WHILE (@LEVEL > 0)
        #BEGIN
        #
        #  DELETE FROM @DIMLIST
        #
        #  INSERT INTO @DIMLIST(INVENTDIMID, AVAILPHYSICAL, AVAILORDERED)
        #  SELECT INVENTDIMID, AVAILPHYSICAL, AVAILORDERED
        #    FROM WHSINVENTRESERVE WHSINVENTRESERVE
        #   WHERE WHSINVENTRESERVE.HIERARCHYLEVEL = @LEVEL + -1
        #     AND WHSINVENTRESERVE.ITEMID         = @ITEMID
        #     AND WHSINVENTRESERVE.PARTITION      = @PARTITION
        #     AND WHSINVENTRESERVE.DATAAREAID     = @DATAAREAID
        #
        #INSERT INTO @RESULT (AVAILPHYSICALRESULT, AVAILORDEREDRESULT, AVAILPHYSICALFOUND, AVAILORDEREDFOUND, HIERARCHYLEVEL, CONFIGID, INVENTSIZEID, INVENTCOLORID, INVENTSTYLEID, INVENTSITEID, INVENTLOCATIONID, WMSLOCATIONID, WMSPALLETID, INVENTSTATUSID, LICENSEPLATEID, INVENTBATCHID, INVENTSERIALID, INVENTPROFILEID_RU, INVENTOWNERID_RU, INVENTGTDID_RU)
        #SELECT SUM(CASE WHEN RESULT.AVAILPHYSICALFOUND < RESULT.AVAILPHYSICALRESULT THEN RESULT.AVAILPHYSICALFOUND ELSE RESULT.AVAILPHYSICALRESULT END),
        #    SUM(CASE WHEN RESULT.AVAILORDEREDFOUND < RESULT.AVAILORDEREDRESULT THEN RESULT.AVAILORDEREDFOUND ELSE RESULT.AVAILORDEREDRESULT END),
        #    SUM(FOUND.AVAILPHYSICAL),
        #    SUM(FOUND.AVAILORDERED),
        #    @LEVEL + -1,
        #    RESULT.CONFIGID AS CONFIGID,
        #    RESULT.INVENTSIZEID AS INVENTSIZEID,
        #    RESULT.INVENTCOLORID AS INVENTCOLORID,
        #    RESULT.INVENTSTYLEID AS INVENTSTYLEID,
        #    CASE WHEN @LEVEL + -1 >= @INVENTSITEIDLEVEL THEN RESULT.INVENTSITEID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTLOCATIONIDLEVEL THEN RESULT.INVENTLOCATIONID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @WMSLOCATIONIDLEVEL THEN RESULT.WMSLOCATIONID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @WMSPALLETIDLEVEL THEN RESULT.WMSPALLETID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTSTATUSIDLEVEL THEN RESULT.INVENTSTATUSID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @LICENSEPLATEIDLEVEL THEN RESULT.LICENSEPLATEID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTBATCHIDLEVEL THEN RESULT.INVENTBATCHID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTSERIALIDLEVEL THEN RESULT.INVENTSERIALID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTPROFILEID_RULEVEL THEN RESULT.INVENTPROFILEID_RU ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTOWNERID_RULEVEL THEN RESULT.INVENTOWNERID_RU ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTGTDID_RULEVEL THEN RESULT.INVENTGTDID_RU ELSE NULL END
        #FROM @RESULT RESULT
        #LEFT OUTER JOIN (
        #SELECT SUM(DIMLIST.AVAILPHYSICAL) AS AVAILPHYSICAL,
        #    SUM(DIMLIST.AVAILORDERED) AS AVAILORDERED,
        #    INVENTDIM.CONFIGID AS CONFIGID,
        #    INVENTDIM.INVENTSIZEID AS INVENTSIZEID,
        #    INVENTDIM.INVENTCOLORID AS INVENTCOLORID,
        #    INVENTDIM.INVENTSTYLEID AS INVENTSTYLEID,
        #    CASE WHEN @LEVEL + -1 >= @INVENTSITEIDLEVEL THEN INVENTDIM.INVENTSITEID ELSE NULL END AS INVENTSITEID,
        #    CASE WHEN @LEVEL + -1 >= @INVENTLOCATIONIDLEVEL THEN INVENTDIM.INVENTLOCATIONID ELSE NULL END AS INVENTLOCATIONID,
        #    CASE WHEN @LEVEL + -1 >= @WMSLOCATIONIDLEVEL THEN INVENTDIM.WMSLOCATIONID ELSE NULL END AS WMSLOCATIONID,
        #    CASE WHEN @LEVEL + -1 >= @WMSPALLETIDLEVEL THEN INVENTDIM.WMSPALLETID ELSE NULL END AS WMSPALLETID,
        #    CASE WHEN @LEVEL + -1 >= @INVENTSTATUSIDLEVEL THEN INVENTDIM.INVENTSTATUSID ELSE NULL END AS INVENTSTATUSID,
        #    CASE WHEN @LEVEL + -1 >= @LICENSEPLATEIDLEVEL THEN INVENTDIM.LICENSEPLATEID ELSE NULL END AS LICENSEPLATEID,
        #    CASE WHEN @LEVEL + -1 >= @INVENTBATCHIDLEVEL THEN INVENTDIM.INVENTBATCHID ELSE NULL END AS INVENTBATCHID,
        #    CASE WHEN @LEVEL + -1 >= @INVENTSERIALIDLEVEL THEN INVENTDIM.INVENTSERIALID ELSE NULL END AS INVENTSERIALID,
        #    CASE WHEN @LEVEL + -1 >= @INVENTPROFILEID_RULEVEL THEN INVENTDIM.INVENTPROFILEID_RU ELSE NULL END AS INVENTPROFILEID_RU,
        #    CASE WHEN @LEVEL + -1 >= @INVENTOWNERID_RULEVEL THEN INVENTDIM.INVENTOWNERID_RU ELSE NULL END AS INVENTOWNERID_RU,
        #    CASE WHEN @LEVEL + -1 >= @INVENTGTDID_RULEVEL THEN INVENTDIM.INVENTGTDID_RU ELSE NULL END AS INVENTGTDID_RU
        #FROM INVENTDIM INVENTDIM
        #JOIN @DIMLIST DIMLIST
        #     ON INVENTDIM.PARTITION = @PARTITION
        #    AND INVENTDIM.DATAAREAID  = @DATAAREAID
        #    AND INVENTDIM.INVENTDIMID = DIMLIST.INVENTDIMID
        #GROUP BY
        #    INVENTDIM.CONFIGID,
        #    INVENTDIM.INVENTSIZEID,
        #    INVENTDIM.INVENTCOLORID,
        #    INVENTDIM.INVENTSTYLEID,
        #    CASE WHEN @LEVEL + -1 >= @INVENTSITEIDLEVEL THEN INVENTDIM.INVENTSITEID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTLOCATIONIDLEVEL THEN INVENTDIM.INVENTLOCATIONID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @WMSLOCATIONIDLEVEL THEN INVENTDIM.WMSLOCATIONID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @WMSPALLETIDLEVEL THEN INVENTDIM.WMSPALLETID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTSTATUSIDLEVEL THEN INVENTDIM.INVENTSTATUSID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @LICENSEPLATEIDLEVEL THEN INVENTDIM.LICENSEPLATEID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTBATCHIDLEVEL THEN INVENTDIM.INVENTBATCHID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTSERIALIDLEVEL THEN INVENTDIM.INVENTSERIALID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTPROFILEID_RULEVEL THEN INVENTDIM.INVENTPROFILEID_RU ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTOWNERID_RULEVEL THEN INVENTDIM.INVENTOWNERID_RU ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTGTDID_RULEVEL THEN INVENTDIM.INVENTGTDID_RU ELSE NULL END
        #    ) AS FOUND ON (RESULT.CONFIGID = FOUND.CONFIGID)
        #        AND (RESULT.INVENTSIZEID = FOUND.INVENTSIZEID)
        #        AND (RESULT.INVENTCOLORID = FOUND.INVENTCOLORID)
        #        AND (RESULT.INVENTSTYLEID = FOUND.INVENTSTYLEID)
        #        AND (@LEVEL + -1 < @INVENTSITEIDLEVEL OR RESULT.INVENTSITEID = FOUND.INVENTSITEID)
        #        AND (@LEVEL + -1 < @INVENTLOCATIONIDLEVEL OR RESULT.INVENTLOCATIONID = FOUND.INVENTLOCATIONID)
        #        AND (@LEVEL + -1 < @WMSLOCATIONIDLEVEL OR RESULT.WMSLOCATIONID = FOUND.WMSLOCATIONID)
        #        AND (@LEVEL + -1 < @WMSPALLETIDLEVEL OR RESULT.WMSPALLETID = FOUND.WMSPALLETID)
        #        AND (@LEVEL + -1 < @INVENTSTATUSIDLEVEL OR RESULT.INVENTSTATUSID = FOUND.INVENTSTATUSID)
        #        AND (@LEVEL + -1 < @LICENSEPLATEIDLEVEL OR RESULT.LICENSEPLATEID = FOUND.LICENSEPLATEID)
        #        AND (@LEVEL + -1 < @INVENTBATCHIDLEVEL OR RESULT.INVENTBATCHID = FOUND.INVENTBATCHID)
        #        AND (@LEVEL + -1 < @INVENTSERIALIDLEVEL OR RESULT.INVENTSERIALID = FOUND.INVENTSERIALID)
        #        AND (@LEVEL + -1 < @INVENTPROFILEID_RULEVEL OR RESULT.INVENTPROFILEID_RU = FOUND.INVENTPROFILEID_RU)
        #        AND (@LEVEL + -1 < @INVENTOWNERID_RULEVEL OR RESULT.INVENTOWNERID_RU = FOUND.INVENTOWNERID_RU)
        #        AND (@LEVEL + -1 < @INVENTGTDID_RULEVEL OR RESULT.INVENTGTDID_RU = FOUND.INVENTGTDID_RU)
        #WHERE RESULT.HIERARCHYLEVEL = @LEVEL
        #GROUP BY
        #    RESULT.CONFIGID,
        #    RESULT.INVENTSIZEID,
        #    RESULT.INVENTCOLORID,
        #    RESULT.INVENTSTYLEID,
        #    CASE WHEN @LEVEL + -1 >= @INVENTSITEIDLEVEL THEN RESULT.INVENTSITEID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTLOCATIONIDLEVEL THEN RESULT.INVENTLOCATIONID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @WMSLOCATIONIDLEVEL THEN RESULT.WMSLOCATIONID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @WMSPALLETIDLEVEL THEN RESULT.WMSPALLETID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTSTATUSIDLEVEL THEN RESULT.INVENTSTATUSID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @LICENSEPLATEIDLEVEL THEN RESULT.LICENSEPLATEID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTBATCHIDLEVEL THEN RESULT.INVENTBATCHID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTSERIALIDLEVEL THEN RESULT.INVENTSERIALID ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTPROFILEID_RULEVEL THEN RESULT.INVENTPROFILEID_RU ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTOWNERID_RULEVEL THEN RESULT.INVENTOWNERID_RU ELSE NULL END,
        #    CASE WHEN @LEVEL + -1 >= @INVENTGTDID_RULEVEL THEN RESULT.INVENTGTDID_RU ELSE NULL END;
        #
        #
        #    SET @LEVEL = @LEVEL - 1
        #END; -- WHILE
        #
        #SELECT @AVAILPHYSICAL = SUM(CASE WHEN AVAILPHYSICALFOUND < AVAILPHYSICALRESULT THEN AVAILPHYSICALFOUND ELSE AVAILPHYSICALRESULT END),
        #    @AVAILORDERED = SUM(CASE WHEN AVAILORDEREDFOUND < AVAILORDEREDRESULT THEN AVAILORDEREDFOUND ELSE AVAILORDEREDRESULT END)
        #FROM @RESULT
        #WHERE HIERARCHYLEVEL = 0;
        #    */
        #
        #    ret += strFmt('CREATE PROCEDURE [%1]\n', WhsOnHandSPHelper::storedProcedureName(_includeDelta));
        #    ret +=        '    @PARTITION bigint,\n';
        #    ret += strFmt('    %1 int,\n', this.levelExpression(0));
        #    ret += strFmt('    @DATAAREAID %1,\n', this.sqlTypeNameForField(tableNum(InventTable), fieldNum(InventTable, DataAreaId)));
        #    ret += strFmt('    @ITEMID %1,\n', this.sqlTypeNameForField(tableNum(InventTable), fieldNum(InventTable, ItemId)));
        #    if (_includeDelta)
        #    {
        #        ret += strFmt('    %1 %2,\n', #TTSIdParmName, this.sqlTypeNameForField(tableNum(WHSInventReserveDelta), fieldNum(WHSInventReserveDelta, TTSId)));
        #    }
        #
        #    // Generate parameters for supported dimensions
        #    enabledDimsEnumerator.reset();
        #    while (enabledDimsEnumerator.moveNext())
        #    {
        #        [dimFieldId, dimFieldName] = enabledDimsEnumerator.current();
        #        ret += strFmt('    %1 %2 = NULL,\n', this.dimensionParm(dimFieldName), this.sqlTypeNameForField(tableNum(InventDim), dimFieldId));
        #        ret += strFmt('    %1 int = %2,\n', this.dimensionLevelParm(dimFieldName), #INVALIDHIERARCHYLEVEL);
        #    }
        #
        #    // Generate parameters for product dimensions
        #    productDimensionsEnumerator.reset();
        #    while (productDimensionsEnumerator.moveNext())
        #    {
        #        [dimFieldId, dimFieldName] = productDimensionsEnumerator.current();
        #        ret += strFmt('    %1 %2 = NULL,\n', this.dimensionParm(dimFieldName), this.sqlTypeNameForField(tableNum(InventDim), dimFieldId));
        #    }
        #
        #    ret +=        '    -- Output parameters\n';
        #    ret += strFmt('    %1  numeric(32, 16) OUTPUT,\n', #AvailPhysicalParmName);
        #    ret += strFmt('    %1  numeric(32, 16) OUTPUT\n', #AvailOrderedParmName);
        #    ret +=        'AS\n\n';
        #
        #    ret +=        '-- Without nocount on the procedure will return number of affected rows in the result set which is\n';
        #    ret +=        '-- harmful to performance and requires us to adjust field retrieval when calling the stored procedure\n';
        #    ret +=        'SET NOCOUNT ON;\n\n';
        #
        #    ret +=        '-- We need to supress warnings when trying to aggregate NULL columns. This is a valid business scenario\n';
        #    ret +=        '-- and happens e.g. for a non-LP controlled location with batch below item.\n';
        #    ret +=        'SET ANSI_WARNINGS OFF;\n\n';
        #
        #    ret += this.declareResultTableVariable() + '\n';
        #
        #    ret +=        'DECLARE @DIMLIST TABLE(\n';
        #    ret +=        '    INVENTDIMID nvarchar (20) NULL,\n';
        #    ret +=        '    AVAILPHYSICAL numeric(32, 16) NOT NULL,\n';
        #    ret +=        '    AVAILORDERED numeric(32, 16) NOT NULL);\n\n';
        #
        #    ret +=        '-- The algorithm of the stored procedure is as follows:\n';
        #    ret +=        '-- 1. Initialize the @RESULT table with aggregated qtys in WHSInventReserve for the level requested by user\n';
        #    ret +=        '-- 2. Traverse the hierarchy up and for each level:\n';
        #    ret +=        '--     1) Aggregate qtys from previous level in @RESULT grouping by dimensions of current hierarchy level\n';
        #    ret +=        '--     2) Aggregate qtys from current level in WHSInventReserve grouping by dimensions of current hierarchy level\n';
        #    ret +=        '--     3) Select the minimum of the above qtys and insert into @RESULT\n';
        #    ret +=        '-- 3. When hierarchy level 0 is reached, return the sum of aggregated qtys in @RESULT\n\n';
        #
        #    ret +=        '-- In the original query (the one after below query), it is joining InventDim and WHSInventReserve and filter records based on some criteria.\n';
        #    ret +=        '-- But in the execution plan, it doesn\'t seems to generate a good plan (even after UPDATE STATISTICS). In the original execution plan, it always\n';
        #    ret +=        '-- Get records from InventDim first, which returns lot of rows, then only join it to WHSInventReserve and continue filter from there.\n';
        #    ret +=        '-- If we makes it get records from WHSInventReserve first, filter it, then with lesser rows from here, join it to InventDim, it will be quicker.\n';
        #    ret +=        '-- So the optimization is to filter WHSInventResreve first, then use this result at the original query. In the test, this has reduce the execution\n';
        #    ret +=        '-- time from ~250ms to ~7ms\n';
        #    ret +=        '  INSERT INTO @DIMLIST(INVENTDIMID, AVAILPHYSICAL, AVAILORDERED)\n';
        #    ret +=        '  SELECT INVENTDIMID, AVAILPHYSICAL, AVAILORDERED\n';
        #    ret += strFmt('    FROM %1 %2\n', this.whsInventReserveTableName(_includeDelta), inventReserveAlias);
        #    ret += strFmt('   WHERE %1.ITEMID         = @ITEMID\n', inventReserveAlias);
        #    ret += strFmt('     AND %1.PARTITION      = @PARTITION\n', inventReserveAlias);
        #    ret += strFmt('     AND %1.DATAAREAID     = @DATAAREAID\n', inventReserveAlias);
        #    ret += strFmt('     AND %1.HIERARCHYLEVEL = %2\n\n', inventReserveAlias, this.levelExpression(0));
        #
        #    ret +=        '-- Initialize the @RESULT variable.\n';
        #    ret +=        '-- Loads aggregated data for the lowest level requested by user.\n';
        #    ret +=        'INSERT INTO @RESULT (' + this.resultTableVariableFields() + ')\n';
        #
        #    ret += this.selectFromWHSInventReserveStmt(_includeDelta) + '\n';
        #
        #    ret += strFmt('WHILE (%1 > 0)\n', this.levelExpression(0));
        #    ret +=        'BEGIN\n';
        #
        #    ret += strFmt('  DELETE FROM @DIMLIST\n\n');
        #
        #    ret += strFmt('  INSERT INTO @DIMLIST(INVENTDIMID, AVAILPHYSICAL, AVAILORDERED)\n');
        #    ret += strFmt('  SELECT INVENTDIMID, AVAILPHYSICAL, AVAILORDERED\n');
        #    ret += strFmt('    FROM %1 %2\n', this.whsInventReserveTableName(_includeDelta), inventReserveAlias);
        #    ret += strFmt('   WHERE %1.HIERARCHYLEVEL = %2 + -1\n', inventReserveAlias, this.levelExpression(0));
        #    ret += strFmt('     AND %1.ITEMID         = @ITEMID\n', inventReserveAlias);
        #    ret += strFmt('     AND %1.PARTITION      = @PARTITION\n', inventReserveAlias);
        #    ret += strFmt('     AND %1.DATAAREAID     = @DATAAREAID\n\n', inventReserveAlias);
        #
        #    ret += strFmt('    INSERT INTO @RESULT (%1)\n', this.resultTableVariableFields());
        #
        #    ret += this.selectFromResultJoinWHSInventReserveStmt(_includeDelta) + '\n\n';
        #
        #    ret += strFmt('    SET %1 = %1 - 1\n', this.levelExpression(0));
        #    ret +=        'END; -- WHILE\n\n';
        #
        #    ret += strFmt('SELECT %1 = SUM(CASE WHEN %2 < %3 THEN %2 ELSE %3 END),\n',
        #                    #AvailPhysicalParmName,
        #                    #AVAILPHYSICALFOUNDFieldName,
        #                    #AVAILPHYSICALRESULTFieldName);
        #    ret += strFmt('    %1 = SUM(CASE WHEN %2 < %3 THEN %2 ELSE %3 END)\n',
        #                    #AvailOrderedParmName,
        #                    #AVAILORDEREDFOUNDFieldName,
        #                    #AVAILORDEREDRESULTFieldName);
        #    ret +=        'FROM @RESULT\n';
        #    ret += strFmt('WHERE %1 = 0;\n', #HIERARCHYLEVELFieldName);
        #
        #    return ret;
        #}
      ENDSOURCE
      SOURCE #TEC_selectFromWHSInventReserveStmt
        #private str TEC_selectFromWHSInventReserveStmt(boolean _includeDelta)
        #{
        #    str             ret = '';
        #    ListEnumerator  enabledDimsEnumerator = this.enabledDimensions().getEnumerator();
        #    ListEnumerator  productDimsEnumerator = this.productDimensions().getEnumerator();
        #    str             dimFieldName;
        #    str             levelExpr = this.levelExpression(-1);
        #    str             inventReserveAlias = this.whsInventReserveTableAlias(_includeDelta);
        #    str             dimList = 'DIMLIST';
        #    boolean         hasDoneFirstIteration;
        #
        #    ret += strFmt('SELECT SUM(%1.AVAILPHYSICAL),\n', dimList);
        #    ret += strFmt('    SUM(%1.AVAILORDERED),\n', dimList);
        #    ret +=        '    NULL,\n';
        #    ret +=        '    NULL,\n';
        #    ret += strFmt('    %1,\n', this.levelExpression(0));
        #
        #    // Select product dimensions
        #    productDimsEnumerator.reset();
        #    while (productDimsEnumerator.moveNext())
        #    {
        #        dimFieldName = conPeek(productDimsEnumerator.current(), 2);
        #        ret += strFmt('    INVENTDIM.%1,\n', dimFieldName);
        #    }
        #
        #    // Select enabled dimensions
        #    enabledDimsEnumerator.reset();
        #    hasDoneFirstIteration = false;
        #    while (enabledDimsEnumerator.moveNext())
        #    {
        #        if (hasDoneFirstIteration)
        #        {
        #            ret += ',\n';
        #        }
        #
        #        dimFieldName = conPeek(enabledDimsEnumerator.current(), 2);
        #        ret += strFmt('    CASE WHEN %1 >= %2 THEN INVENTDIM.%3 ELSE NULL END',
        #                        this.levelExpression(-1),
        #                        this.dimensionLevelParm(dimFieldName),
        #                        dimFieldName);
        #
        #        hasDoneFirstIteration = true;
        #    }
        #    ret += '\n';
        #
        #    //ret += strFmt('    FROM %1 %2\n', this.whsInventReserveTableName(_includeDelta), inventReserveAlias);
        #    //ret += strFmt('    JOIN INVENTDIM ON INVENTDIM.INVENTDIMID = %1.INVENTDIMID AND INVENTDIM.DATAAREAID = %1.DATAAREAID AND INVENTDIM.PARTITION = %1.PARTITION\n', inventReserveAlias);
        #    //ret += strFmt('    WHERE %1.ITEMID           = @ITEMID\n', inventReserveAlias);
        #    //ret += strFmt('        AND %1.PARTITION      = @PARTITION\n', inventReserveAlias);
        #    //ret += strFmt('        AND %1.DATAAREAID     = @DATAAREAID\n', inventReserveAlias);
        #    //ret += strFmt('        AND %1.HIERARCHYLEVEL = %2\n', inventReserveAlias, this.levelExpression(0));
        #    ret += strFmt('    FROM INVENTDIM INVENTDIM\n');
        #    ret += strFmt('    JOIN @DIMLIST %1\n', dimList);
        #    ret += strFmt('      ON INVENTDIM.INVENTDIMID = %1.INVENTDIMID\n', dimList);
        #    ret += strFmt('    WHERE   INVENTDIM.DATAAREAID = @DATAAREAID\n');
        #    ret += strFmt('        AND INVENTDIM.PARTITION = @PARTITION\n');
        #
        #    // Add product dimensions range
        #    productDimsEnumerator.reset();
        #    while (productDimsEnumerator.moveNext())
        #    {
        #        dimFieldName = conPeek(productDimsEnumerator.current(), 2);
        #        ret += strFmt('        AND (%2 IS NULL OR INVENTDIM.%1 = %2)\n',
        #                        dimFieldName,
        #                        this.dimensionParm(dimFieldName));
        #    }
        #
        #    // Add enabled dimensions range
        #    enabledDimsEnumerator.reset();
        #    while (enabledDimsEnumerator.moveNext())
        #    {
        #        dimFieldName = conPeek(enabledDimsEnumerator.current(), 2);
        #        ret += strFmt('        AND (%1 < %2 OR %3 IS NULL OR %3 = INVENTDIM.%4)\n',
        #                        this.levelExpression(0),
        #                        this.dimensionLevelParm(dimFieldName),
        #                        this.dimensionParm(dimFieldName),
        #                        dimFieldName);
        #    }
        #
        #    ret +=        'GROUP BY\n';
        #
        #    // Add grouping by product dimensions
        #    productDimsEnumerator.reset();
        #    while (productDimsEnumerator.moveNext())
        #    {
        #        dimFieldName = conPeek(productDimsEnumerator.current(), 2);
        #        ret += strFmt('    INVENTDIM.%1,\n', dimFieldName);
        #    }
        #
        #    // Add grouping by enabled dimensions
        #    enabledDimsEnumerator.reset();
        #    hasDoneFirstIteration = false;
        #    while (enabledDimsEnumerator.moveNext())
        #    {
        #        if (hasDoneFirstIteration)
        #        {
        #            ret += ',\n';
        #        }
        #
        #        dimFieldName = conPeek(enabledDimsEnumerator.current(), 2);
        #        ret += strFmt('    CASE WHEN %1 >= %2 THEN INVENTDIM.%3 ELSE NULL END',
        #                        this.levelExpression(-1),
        #                        this.dimensionLevelParm(dimFieldName),
        #                        dimFieldName);
        #
        #        hasDoneFirstIteration = true;
        #    }
        #
        #    ret += ';\n';
        #
        #    return ret;
        #}
      ENDSOURCE
      SOURCE #TEC_selectInnerFromWHSInventReserveStmt
        #private str TEC_selectInnerFromWHSInventReserveStmt(boolean _includeDelta)
        #{
        #    str             ret = '';
        #    ListEnumerator  enabledDimsEnumerator = this.enabledDimensions().getEnumerator();
        #    ListEnumerator  productDimsEnumerator = this.productDimensions().getEnumerator();
        #    str             dimFieldName;
        #    str             levelExpr = this.levelExpression(-1);
        #    str             inventReserveAlias = this.whsInventReserveTableAlias(_includeDelta);
        #    str             dimList = 'DIMLIST';
        #    boolean         hasDoneFirstIteration;
        #
        #    ret += strFmt('SELECT SUM(%1.AVAILPHYSICAL) AS AVAILPHYSICAL,\n', dimList);
        #    ret += strFmt('    SUM(%1.AVAILORDERED) AS AVAILORDERED,\n', dimList);
        #
        #    productDimsEnumerator.reset();
        #    while (productDimsEnumerator.moveNext())
        #    {
        #        dimFieldName = conPeek(productDimsEnumerator.current(), 2);
        #        ret += strFmt('    INVENTDIM.%1 AS %1,\n', dimFieldName);
        #    }
        #
        #    enabledDimsEnumerator.reset();
        #    hasDoneFirstIteration = false;
        #    while (enabledDimsEnumerator.moveNext())
        #    {
        #        if (hasDoneFirstIteration)
        #        {
        #            ret += ',\n';
        #        }
        #
        #        dimFieldName = conPeek(enabledDimsEnumerator.current(), 2);
        #        ret += strFmt('    CASE WHEN %1 >= %2 THEN INVENTDIM.%3 ELSE NULL END AS %3',
        #                        levelExpr,
        #                        this.dimensionLevelParm(dimFieldName),
        #                        dimFieldName);
        #
        #        hasDoneFirstIteration = true;
        #    }
        #    ret += '\n';
        #
        #    //ret += strFmt('FROM %1 %2\n', this.whsInventReserveTableName(includeDelta), inventReserveAlias);
        #    //ret += strFmt('JOIN INVENTDIM ON INVENTDIM.PARTITION = %1.PARTITION\n', inventReserveAlias);
        #    //ret += strFmt('    AND INVENTDIM.DATAAREAID  = %1.DATAAREAID\n', inventReserveAlias);
        #    //ret += strFmt('    AND INVENTDIM.INVENTDIMID = %1.INVENTDIMID\n', inventReserveAlias);
        #    //ret += strFmt('WHERE %1.HIERARCHYLEVEL = %2\n', inventReserveAlias, levelExpr);
        #    //ret += strFmt('    AND %1.ITEMID       = @ITEMID\n', inventReserveAlias);
        #    //ret += strFmt('    AND %1.PARTITION    = @PARTITION\n', inventReserveAlias);
        #    //ret += strFmt('    AND %1.DATAAREAID   = @DATAAREAID\n', inventReserveAlias);
        #    ret += strFmt('FROM INVENTDIM INVENTDIM\n');
        #    ret += strFmt('JOIN @DIMLIST %1\n', dimList);
        #    ret += strFmt('     ON INVENTDIM.PARTITION = @PARTITION\n');
        #    ret += strFmt('    AND INVENTDIM.DATAAREAID  = @DATAAREAID\n');
        #    ret += strFmt('    AND INVENTDIM.INVENTDIMID = DIMLIST.INVENTDIMID\n');
        #    ret +=          'GROUP BY\n';
        #
        #    // Add groupping by product dimensions
        #    productDimsEnumerator.reset();
        #    while (productDimsEnumerator.moveNext())
        #    {
        #        dimFieldName = conPeek(productDimsEnumerator.current(), 2);
        #        ret += strFmt('    INVENTDIM.%1,\n', dimFieldName);
        #    }
        #
        #    // Add groupping by enabled dimensions
        #    enabledDimsEnumerator.reset();
        #    hasDoneFirstIteration = false;
        #    while (enabledDimsEnumerator.moveNext())
        #    {
        #        if (hasDoneFirstIteration)
        #        {
        #            ret += ',\n';
        #        }
        #
        #        dimFieldName = conPeek(enabledDimsEnumerator.current(), 2);
        #        ret += strFmt('    CASE WHEN %1 >= %2 THEN INVENTDIM.%3 ELSE NULL END',
        #                        levelExpr,
        #                        this.dimensionLevelParm(dimFieldName),
        #                        dimFieldName);
        #
        #        hasDoneFirstIteration = true;
        #    }
        #    ret += '\n';
        #
        #    return ret;
        #}
      ENDSOURCE
    ENDMETHODS
  ENDCLASS

***Element: END
=================================================


Friday, 16 December 2016

Cannot alter 'PURCHLINEVIEW_IN' because it is not a table

We hit this issue in DEV environment, not sure how it happenned in the first place, but this table "PurchLineView_IN" has been created as a View in SQL.





Due to it shouldn't appear as View in the first place, so I deleted it straight away from SQL, then rerun the sync from AX. It then created the "PurchLineView_IN" as table in SQL instead of as a View. The sync issue is resolved.

Wednesday, 28 September 2016

Unable to edit AX7 element properties (dimmed/grayed out)

Quick tips: In AX2012 and the version before, when you click on an element in the AOT, (assuming the properties window is opened) they're editable right away. But in AX7, if you click on an element in your project, the properties is not editable (grayed out, read only). You'll need to double click to open the element in the editor window, then the properties will be enabled for editing.

Tuesday, 20 September 2016

Workflow error - The condition could not be evaluated. No data has been returned

Error
Stopped (error): X++ Exception: The condition could not be evaluated. No data has been returned.
 at SysWorkflowEvaluationProvider-evaluateCondition
SysWorkflowEvaluationProvider-evaluate
SysWorkflowQueue-resume


Details
During one of the workflow development, the requirement is to have a batch job to auto-submit all records that meet certain condition. Let's call this TableA for now, auto-submit all records where FieldA == 12. Then workflow configuration is set to auto-approve records if certain conditions are met.

The development started with creating a query object.
Then use this query in the Workflow wizard and created all the related classes, menu item, etc.
Then a batch job created to auto-submit all TableA records which FieldA == 12.
The batch job uses the same query object created earlier.
And the mistake is the FieldA is added into the query object as a range field with fixed enum value 12.

This is due to during the workflow approval process, this FieldA will be updated into other status.
When it is updated to other status than 12, this causes the record doesn't meet the criteria (Range) in the query object, hence, the error "The condition could not be evaluated. No data has been returned.".

The query object (used in workflow document class) is used throughout the approval process, not just the initial submission. So the query shouldn't have any conditions added to it, especially those field which the value will change throughout the process. To fix this, the range (FieldA = 12) has been removed from the query object. Then added during runtime of the batch job. Eg. SysQuery::findOrCreateRange(qbds, fieldNum(TableA,FieldA)).value(...).


For my scenario, the error can be replicated from: (Module) > Setup > **** workflows > (Select the related workflow> Edit > Properties > Automatic actions > Test.


Wednesday, 29 June 2016

SysQueryRangeUtil - Custom query function

In one of old post (Dynamics AX batch job relative day parameter), I've mention regarding using function in query range. There're a set of standard functions available in the SysQueryRangeUtil class. But if you happen to need a custom function (especially with more complex logic), you can develop it in this class and use it just like you'll do with the standard methods.

Example:





You can use this function in query object, under the "Range" node, or anywhere that provides filtering functionality.


Tuesday, 21 June 2016

Replenishment error - Location xxxxxx failed due to the strategy

Just a quick post for non-developer who hit this error and looking for solution.

Path to functionality:

  • Warehouse management > Periodic > Replenishments
  • Warehouse management > Inquiries > History > Work creation history log

The error message "Location xxxxxx failed due to the strategy" in "Work creation history log" is due to the "Total available" is less than "Physical stock".

Examples:
Before replenishment run:
- License plate 90, Physical stock = 106, Total available = 96
- License plate 91, Physical stock = 100, Total available = 100

When the replenishment run, it check whether it can locate a full license plate, if it can’t, then it give the “Location xxxxxx failed due to the strategy.” error.

The way it finds the full license plate is by checking the Total available against the Physical stock (this is a simplified explanation of the logic, it does much more than these two checking).

In this instance, LP 01C02E is not fully available (Physical stock is 106, but Total available is only 96), hence, error.
Then it check next LP, 01C03E, this is fully available (Physical stock is 100, Total available is 100), hence, successfully use it.




The code which checks and produce the error is location at this path: \Classes\WHSLocationDirective\performStrategyLPLocate

Wednesday, 15 June 2016

"The endpoint does not exist or the action is not allowed" on Payment journal voucher send

Just a quick post for non-developer who hit this error and looking for solution.

If you're setting up AIF Outbound port for Payment journal, you'll need to add the "LedgerVendorPaymentService.find" operation to the port.

Without this port, when you click on the "Send..." button on the Payment journal voucher, you'll first get the error "No endPoints are configured for the action".


But the "Send document..." dialog will still shows up, and if you continue to select the Port name and click "OK", you'll get the error "The endpoint does not exist or the action is not allowed".



To fix this issue:

  1. Go to: System administration > Setup > Services and Application Integration Framework > Outbound ports
  2. Select the respective port name and click “Deactivate”
  3. Click on “Service operations” button under the “Service contract customizations” FastTab
  4. Double click on the “LedgerVendorPaymentService.find” service operation under the “Remaining service operations” list to add it into the “Selected service operations”
  5. Close the form
  6. Click “Activate” button to activate the Outbound port again


Friday, 20 May 2016

Find AIF service class name by Axd document class name

Just a quick snippet to find AIF service class name by Axd document class name.


static void GetAxdClassNameByServiceName(Args _args)
{
    #avifiles
    str                     mySearchAxdName = "AxdCountingJournal"; //Change this to your Axd class name
    List                    list;
    ListEnumerator          listEnumerator;
    SysDictClass            sysDictClass, serviceDictClass;
    AxdWizardParameters     axdWizardParameters;
    int                     progressTotal, progressCount;
    SysOperationProgress    progressBar;
   
    sysDictClass = new SysDictClass(classnum(AifDocumentService));
    list = sysDictClass.extendedBy();
   
    progressTotal = list.elements();
    progressBar   = SysOperationProgress::newGeneral(#aviUpdate, 'Axd/service class search', progressTotal);
   
    listEnumerator = list.getEnumerator();
    while (listEnumerator.moveNext())
    {
        serviceDictClass = new SysDictClass(listEnumerator.current());
       
        progressCount++;
        progressBar.incCount();
        progressBar.setText(strfmt("Searching: %1 of %2 (%3)", progressCount, progressTotal, serviceDictClass.name()));       
        progressBar.update(true);
       
        if (serviceDictClass.id() != classNum(AifGenericDocumentService))
        {
            axdWizardParameters = AifServiceClassGenerator::getServiceParameters(serviceDictClass.name());
           
            if(axdWizardParameters && axdWizardParameters.parmName() == mySearchAxdName)
            {
                info(strFmt("Aif service class name: %1", axdWizardParameters.parmAifServiceClassName()));
                info(strFmt("Document name: %1", axdWizardParameters.parmAifEntityClassName()));
                info(strFmt("Axd name: %1", axdWizardParameters.parmName()));
                info(strFmt("Query name: %1", axdWizardParameters.parmQueryName()));
               
                break;
            }
        }
    }
   
    /*
    //Use this if you know the Document class name and wanted to find the Axd class name
   
    str                     documentClassName = "LedgerGeneralJournal";
    SysDictClass            documentDictClass;
    AifDocument             documentInstance;
    AxdBase                 axdBase;
   
    documentDictClass = new SysDictClass(className2Id(documentClassName));
    documentInstance = documentDictClass.makeObject();
    axdBase = documentInstance.getAxdInstance();
   
    info(strFmt("Document name: %1", documentClassName));
    info(strFmt("Axd name: %1", classId2Name(classIdGet(axdBase))));
    info(strFmt("Query name: %1", axdBase.getQueryName()));
    */
}

Tuesday, 26 April 2016

UTCDateTime field in AX & daylight saving

This is an addition to an old post "UTCDateTime field in AX & filtering" (http://mybhat.blogspot.my/2011/10/utcdatetime-field-in-ax-filtering.html).

When using the DateTimeUtil::newDateTime() method to create a datetime value, if you're using daylight saving, then you'll probably need to take extra care with the optional parameter in this method - Timezone.

Let's walk-through with an example.
Today is 26/04/2016, and this year the daylight saving ends on 30 Oct for UK.
The job below generate datetime value from 25 Oct to 5 Nov, with the time a minute before midnight (11:59:59pm). One with the Timezone parameter and the other one without.


Below is the data if you view it in AX.
You'll notice the field "DateTimeTest" for records between 26/10 - 30/10 isn't showing 11:59:59pm, instead, it is showing 12:59:59am, Then looking at the field "DateTimeTestWithTimezone", it correctly display all the time value at 11:59:59pm.


And if you look at the backend data in SQL, it was the other way round, the "DateTimeTest" field saved in all the record has the time value of 23:59:59.000, which is different from how it is shown in AX. If you look at the "DateTimeTestWithTimezone", for records between 25/10 - 29/10, it is saved with 22:59:59.000 and records after 29/10 is saved with 23:59:59.000.


AX will show time value based on your computer settings.
If you use daylight saving, it will automatically convert the time accordingly.
In the above example, the field "DateTimeTest" is assigned with a datetime value generated without the timezone parameter, so it is saved as 23:59:59.000 in SQL, when this value is shown in AX at a machine with daylight saving, it automatically fast forward an hour for the dates before the daylight saving ends, hence, 23:59:59 (11:59:59pm) becomes the next day 00:59:59 (12:59:59am).

It works the same way if user enter a datetime value. Says, user enter 01/05/2016, 5:00:00pm with a machine with daylight saving on, this value will be saved as 01/05/2016, 4:00:00pm in SQL.

Enjoy playing around with UTCDateTime!!!

Friday, 2 October 2015

Unable to move or delete files after reading it

Applies to: AX2012

Scenario
- Open file and read
- Move the file to another folder after reading is completed

Issue
While not running it in CIL, the code works, but once run it in CIL, it give the error "System.IO.IOException: The process cannot access the file because it is being used by another process".

Those who came from AX 2009 used to depends on AX to finalize an object by just leaving the scope. Ref: https://msdn.microsoft.com/en-us/library/io.finalize.aspx

But when running in CIL, the finalize needs to be called manually.

Below is a sample service class called by a controller, when running the code in CIL, if the 'finalize()' is called, no error occur. But if the 'finalize()' is not called, it give the error as shown above.

=================================================
class DemoService extends SysOperationServiceBase
{
}

=================================================
public void processDemo(DemoDataContract _contract)
{
    ;

    this.processFile(_contract.parmFileNameFrom());
    
    this.moveFile(_contract.parmFileNameFrom(), _contract.parmFileNameTo());
}

=================================================
void processFile(Filename _fileNameFrom)
{
    #File
    FileIoPermission                perm;
    TextIo                          importFile;
    container                       record;
    int                             totalRecords;
    ;

    perm = new FileIoPermission(_fileNameFrom, #io_read);
    perm.assert();

    importFile = new TextIo(_fileNameFrom, #io_read);

    while(importFile.status() ==  IO_Status::Ok)
    {
        record = importFile.read();

        totalRecords++;
    }
        
    if(totalRecords)
        info(strFmt("Row count: %1", totalRecords));

    //importFile.finalize(); //Enable and disable this comment to test the effect
}

=================================================
void moveFile(Filename _sourceFile, Filename _destinationFile)
{
    #File
    FileIoPermission            perm;
    ;

    perm = new FileIoPermission(_destinationFile, #io_write);
    perm.assert();

    System.IO.File::Move(_sourceFile, _destinationFile);
}


Friday, 8 May 2015

AIF process stuck / never ending (due to AIFResponse deletion)

Scenario of the issue
  • Created a document service (Eg. TableA) and Inbound port to test it
  • While running the test, the receive (AifGateWayReceiveService) is OK, but the processing (AifInboundProcessingService) seems to stuck there / never ending

In details
  • Start up 2 AX client, Eg ClientA and ClientB.
  • ClientA: The testing starts with getting the Inbound port (file adaptor) and XML file ready
  • Dump the XML into the inbound folder
  • ClientA: Run a simple job which contain this line new AifGateWayReceiveService().run();
  • ClientA: Check "Queue manager" (Path: System administration > Periodic > Service and AIF) and confirmed the message has gone in
  • ClientB: Run another simple job which contain this line new AifInboundProcessingService().run();
  • ClientA: Check the "Queue manager" and confirmed the line is gone and a new record has been inserted into the table used by the document service (Eg. TableA)
  • ClientB: The job seems not finishing, AX is frozen and not responding

Below is a sample of the simple job used in above steps (just toggle around the options).

boolean runReceive = true,
        runProcess = false;
    
if(runReceive)
{
    new AifGateWayReceiveService().run();
}
    
if(runProcess)
{
    new AifInboundProcessingService().run();
}


Investigation result
The frozen isn't actually hanging, instead, it is running some record deletion, which took AGES, hence, not responding.

Below is the Trace Parser screenshot showing the trace file captured for around 30 seconds.

Within this short period, there're thousands of calls to database server for a DELETE statement.

This DELETE statement came from the AIF which tries to delete the expired AIFResponse records.



This DELETE statement supposed to delete any records that's older than the expired datetime (used today's datetime minus the AIFGlobalSettings.ResponseCacheLifetime value). Typically a 24 hours timeframe is a common value. But this table contains millions of records (up to 2 years old of data is in this table).

The solution is to get rid to this data from SQL. Since this is development environment, truncate table would do. But if this production environment, it would need more care on how the data is deleted (performance, time of performing the deletion, recover mode & backup chain, etc).

Wednesday, 25 February 2015

Sort container and Array for Dynamics AX

Just a quick snippet for sorting container and Array object.

//Method to sort container
static container sortContainer(container _con)
{
    anytype     temp1;
    anytype     temp2;
    int         i;
    int         j;
    container   sortedCon;
    ;

    sortedCon = _con;

    // Sort the container
    for (i = 1; i <= conlen(sortedCon); i++)
    {
        for (j = i + 1; j <= conlen(sortedCon); j++)
        {
            temp1 = conpeek(sortedCon, j);
            temp2 = conpeek(sortedCon, i);

            if (temp1 < temp2)
            {
                sortedCon = condel(sortedCon, j, 1);
                sortedCon = conins(sortedCon, j, temp2);
                sortedCon = condel(sortedCon, i, 1);
                sortedCon = conins(sortedCon, i, temp1);
            }
        }
    }

    return sortedCon;
}

//Method to sort Array
static Array sortArray(Array _array)
{
    int         i;
    int         arrayCount = _array.lastIndex();
    container   con, sortedCon;

    //Switch from Array to container
    for(i = 1; i <= arrayCount; i++)
    {
        con = conIns(con, i, _array.value(i));
    }
    
    //Sort the container
    sortedCon = sortContainer(con);
    
    //Switch from container to Array
    for(i = 1; i <= conLen(sortedCon); i++)
    {
        _array.value(i, conPeek(sortedCon, i));
    }
    
    return _array;
}

Thursday, 12 February 2015

Copy table buffer between two table with similar structure

Just a quick snippet for those who need it.
The standard buf2Buf() method works if the field Id is identical, but in case the field name is same but have different Id, the code snippet below does the job. It is modified from the buf2Buf() to cater to two tables with similar structure and field name but different field Id.

static void buf2BufByName(
    Common  _from,
    Common  _to
    )
{
    DictTable   dictTableFrom   = new DictTable(_from.TableId);
    DictTable   dictTableTo     = new DictTable(_to.TableId);
    DictField   dictFieldFrom;
    FieldId     fieldIdFrom     = dictTableFrom.fieldNext(0);
    FieldId     fieldIdTo
    ;

    while (fieldIdFrom && ! isSysId(fieldIdFrom))
    {
        dictFieldFrom   = new DictField(_from.TableId, fieldIdFrom);

        if(dictFieldFrom)
        {
            fieldIdTo = dictTableTo.fieldName2Id(dictFieldFrom.name());

            if(fieldIdTo)
                _to.(fieldIdTo) = _from.(fieldIdFrom);
        }

        fieldIdFrom = dictTableFrom.fieldNext(fieldIdFrom);
    }
}