Dynamics

Add Validated Offline Scanning to Warehouse Workflows in Dynamics 365

Fast Validation

The Fast Validation page pattern has been added as a private preview in the 10.0.12 release.  This works in a similar way to the multi-asset scanning page pattern that was discussed here (link), however this new pattern allows for client-side (i.e. offline) validation against a user-defined list of values.  This can be very useful for building fast scanning applications that need to ensure accuracy of the data scanned.  This article will show you how to implement a basic fast validation pattern in the warehouse mobile app.

Demo Flow

As of 10.0.12 this feature has only been included in two flows in the core product – License Plate Consolidation and Catch weight tag scanning.  It is also possible to see the new functionality in the demo mode within the latest warehouse mobile app.  In the latest version (1.8 as of this writing) the demo menu looks like this:

The LP Consolidation menu item will give you a good feel for what this Fast Validation framework can provide.  Note that the actual LP Consolidation menu item within the product can support this framework if the correct flighting and feature flags are configured as detailed below.

The first screen will allow you to specify the Target LP (any value here will be fine):

The next screen shows the new UI for the Fast validation feature:

Scanning values here will be done offline with no communication to the server.  The system has downloaded the list of possible license plates to the mobile device and these are now used to validate the user data.

For example – entering “LPx” will show this error telling the user that “LPx” was not a valid License Plate to use for the consolidation:

However, entering “lp1” produces the following screen – indicating this is a valid license plate available for consolidation:

You can see the full list of valid license plates as well as what has been scanned already by clicking the menu icon in the lower left corner – this shows a new screen with the scanned and validated values (called “Merged” in this example) and the list of total possible license plates (called “Available”).

Finally – going back to the main screen you can see there is a check mark icon in the lower right.  This will allow you to submit the final list to the server for processing.  We will talk later about the API used to send the list of scanned values to the server-side X++ processing logic – it works very similar to the multi-asset scanning API.

Setup

As of 10.0.12 this feature can be used for custom development using the patterns discussed below.  If you want to see the flow(s) available in the core product, then you will need to enable the following flight as well as enable a feature within Feature Management.  As discussed above the two flows that have been enabled in 10.0.12 are LP Consolidation and Catch Weight Tag scanning.

Flight Name: WHSRFFastValidationFeature

Once the above flight has been added to the development environment the following feature will need to be enabled:

In addition, the latest warehouse mobile app build will need to be utilized.  The required UI components are included in the 1.8 version of the app, which can be found in the Windows Store and Google Play store.

Sample Process

The provided solution works in tandem with the previous multi-asset scanning demo.  Imagine a scenario where the first station in the warehouse scans all the possible assets for a container.  Then the container goes through a round of packing where specific items are placed into the box – but we can limit the possible items to the ones previously scanned. If we want these scans to be as fast as possible, we can utilize the new offline scanning mode with the validation to ensure we only allow input from the list of possible assets.

The workflow in the demo looks like this:

Multi-Asset Scanning

  1. User logs into the multi-asset menu item
  2. Scans a Container ID
  3. Scans a set of Assets that are associated with the container.
    1. These are stored in the database associated with this specific ContainerID
    2. These scans are all performed offline using the multi-asset scanning mode previously released and documented

Fast Validation Asset Scanning

  1. Another user logs into the fast validation menu item
  2. Scans the same Container ID
  3. The super-set of possible Assets are retrieved from the database and used as the validation list in the Fast Validation UI
  4. The user can then scan Assets to store against this container – but the user input is restricted to the possible Assets associated in the previous step.

Multi-Asset Scanning

The first steps are done in the multi-asset scanning menu item described in the previous post here.  In this flow you can scan a container id and then scan any number of assets to associate with the container.  These will be saved into a custom WHSAsset table mapping the ContainerID to the new Assets.

Scanned Assets using the multi-scan UI:

These assets are stored into a custom table (WHSAsset) with a simple linking between the Container and Asset:

Fast Validation Flow

Now that the full list of assets have been scanned we can launch the Fast Validation scanning Menu Item.  This will again ask us to scan the Container ID – however it will now utilize the previously saved data to build the validation list.

In the sample provided – the initial Fast Validation screen is displayed like this:

The string values “Scan Assets” and “Assets to Scan” come from the setup parameters defined in the code as described below. Scanning a value that passes validation (i.e. it is within the list) will display the success scanning message like this:

Scanning an incorrect value (i.e. a value not in the valid list) will display the error message:

And scanning a duplicate value (i.e. if we scan 111 again) the duplicate message will be displayed:

Clicking the menu button will display a new screen with the list of already scanned values and possible valid data.  The titles of these tabs are also defined in the code detailed below.

For the demo flow let’s say we enter two more valid assets to pack into the container – “222” and “333.”  Then when we finalize the flow the mobile device will display the success message and the database should be updated to reflect the newly validated list of assets.

Again – remember that this is simply a demo flow to highlight how the offline scanning UI can be utilized in custom flows.  It does not reflect a realistic business scenario without some additional work.

API

Adding the Fast Validation page pattern to a page involves using the new class WHSFastValidationControlsBuildParameters.  When you construct one of these objects you can specify several parameters:

Input NameName of the input control to use for the stored data.  This is the “key” used to store the input data in the pass object.
Input LabelLabel to display to the user on the fast validation screen.
Input TypeThe EDT of the data to be scanned by the user.
DataA container with the possible values the user can scan against.
Fail MessageMessage to display to the user when a value is scanned that is not in the valid list.
Success MessageMessage to display to the user when a value is successfully scanned and validated against the data list.
Duplicate MessageMessage to display to the user when a value is scanned that has already been successfully scanned.
Title – Scanned ListThe text to display in the UI for the list of values that have been successfully scanned.
Title – To Be Scanned ListThe text to display in the UI for the list of possible values that can be scanned.
Sound on SuccessBoolean value that controls if a successful scan will trigger an audible feedback to the user.
Sound on FailureBoolean value that controls if a failed scan will trigger an audible feedback to the user.

In the sample app provided the above settings are all configured through the following code.  Note that this API assumes you are using the ProcessGuide framework, as a new method has been added to the ProcessGuidePage class.

protected void addDataControls(ProcessGuidePage _page)
    {
        WhsrfPassthrough pass = controller.parmSessionState().parmPass();
        WHSContainerId containerId = pass.lookupStr(ProcessGuideDataTypeNames::ContainerId);

        _page.addLabel(ProcessGuideDataTypeNames::ValidationScanLabel, 'Scan Assets', extendedTypeNum(WHSRFLabel));
        _page.addTextBox(ProcessGuideDataTypeNames::ContainerId, 'Container Id', extendedTypeNum(WHSContainerId), false, containerId);
        
        WHSFastValidationControlsBuildParameters  fastValidationControlsBuildParameters  = WHSFastValidationControlsBuildParameters::newFromParams(ProcessGuideDataTypeNames::ValidatedAssetList,
         'Assets to scan',
         extendedTypeNum(AssetIdList),
         this.getAssetIdList(containerId), 
         "@WAX:FastValidationMsgScanFailPick",
         "@WAX:FastValidationMsgScanSuccessPick",
         "@WAX:FastValidationMsgScanDuplicatedPick",
         "@WAX:FastValidationTitleListMergedLPs",
         "@WAX:FastValidationTitleListAvailableLPs",
         NoYes::Yes,
         NoYes::No);

        fastValidationControlsBuildParameters.scanTitle = "Scan Assets";
        _page.addFastValidationControls(fastValidationControlsBuildParameters);
    }

Processing Data

The mobile app will allow the user to stay on the scanning page and continue scanning data – it will not return to the server until the user taps the checkbox icon.  This will then submit the final list of values that have been scanned by the user for processing by the server-side code.  The values will be accessible to the server-side code in the pass object – under the key used for the “Input Name” value.  This will be a pipe separated list of values (there is an existing constant that defines the separator – ProcessGuidePageConstants::FastValidationListSeparator).

In the same project the following code is used to parse the incoming values – the highlighted line is the area where the scanned values are extracted from the pass object.

protected void doExecute()
    {
        WhsrfPassthrough pass = controller.parmSessionState().parmPass();
        WHSContainerId containerId = pass.lookupStr(ProcessGuideDataTypeNames::ContainerId);
        WHSAsset AssetTable;

        delete_from AssetTable
            where AssetTable.ContainerId == containerId;

        container selectedAssetsIds = str2con(pass.lookupStr(ProcessGuideDataTypeNames::ValidatedAssetList), ProcessGuidePageConstants::FastValidationListSeparator);

        int	len = conLen(selectedAssetsIds);

        for (int i = 1; i <= len; i++)
        {
            str AssetId = conPeek(selectedAssetsIds, i);
            WHSAsset newAsset;
            newAsset.ContainerId = containerId;
            newAsset.WHSAssetId = AssetId;
            newAsset.insert();
        }

        pass.remove(ProcessGuideDataTypeNames::ValidatedAssetList);
        pass.remove(ProcessGuideDataTypeNames::ContainerId);

        this.addProcessCompleteMessage();

        super();
            
    }

Conclusion

This article has explained the new Fast Validation scanning feature and how to enable it in a custom flow. You can download the complete solution here – hopefully it helps you to utilize this new capability we have added to the Dynamics product. As always please understand this code is intended for demonstration only and should not be used in a production system without extensive testing.

Posted by Zach in Development, Dynamics

Building Warehouse Applications with the ProcessGuide API

In my previous posts I have showed how to build custom workflows for the Warehouse mobile app in Dynamics.  All my previous examples have used the standard WHSWorkExecuteDisplay class framework.  One of the common complaints about this framework is the difficulty in which partners have with extending existing functionality or workflows.  To address these extensibility challenges the product team refactored the core API into a new framework called ProcessGuide which provides more opportunities to extend functionality at different points in the lifecycle of a warehouse workflow.  The full details of this change can be found here: https://docs.microsoft.com/en-us/dynamics365/supply-chain/warehousing/process-guide-framework

I would like to walk through the process of creating a new workflow in the ProcessGuide framework.  I will revisit my previous example and show how we would accomplish the same outcome, but this time utilizing the ProcessGuide framework.

The requirement is that we want a user to scan a set of assets against a single container within the warehouse.  These assets might be serial number controlled items or unique catch weight tags that you need to track within the system.  This article (https://cloudblogs.microsoft.com/dynamics365/it/2018/12/20/customizing-the-warehouse-mobile-app-multi-scan-pages/) discussed how we can use the offline multi-scan controller pattern to allow the user to scan these assets without having to do a round trip to the server with every scan.  Customers are using this pattern to enable high performance scanning workflows.

Process Guide Classes

The key objects to create in the ProcessGuide framework are documented in the above article, but I will copy them here for reference:

  • ProcessGuideController– This class orchestrates the overall execution of the business process. It defines the factories that instantiate the step and the navigation agent, which subsequently constitute the process execution, as well as the clean-up logic for cancellation or exiting the process.
  • ProcessGuideStep– This class represents one single step in the business process. This class contains a definition of the factories that instantiate a page builder, actions, and data processors and is responsible for invoking them in the correct sequence.
  • ProcessGuideNavigationAgent– This class is responsible for navigation between the steps. When a step is completed, the navigation agent is responsible for defining the next step and passes any parameters that the previous step may need to communicate to the next one.
  • ProcessGuidePageBuilder– This class is responsible for instantiating the user interface.
  • ProcessGuideAction– This class represents an action, shown as a button to the user.
  • ProcessGuideDataProcessor– This class is responsible for processing the user entered data in a field.

This can be somewhat hard to visualize based on the above descriptions – so I have tried to represent the major objects below.  What is key to see here is that when you create a new workflow you will have 1 or more ProcessGuideStep classes, and each one will have to define a ProcessGuidePageBuilder if they present a UI to the user.  It is possible to have a processing step with no UI, in which case there will be no PageBuilder class defined – we will see an example of this below.  The ProcessGuideNavigationAgent will define the transitions (or if you want to be all computer science-y about it – the edges in the directed graph) between the steps.

WHSWorkActivity and WHSWorkExecuteMode Enumerations

These two key enumerations still drive the core sysExtension Framework mapping – so one of the first steps is to define the new enumerations as an extension on the existing enumerations.  This process is unchanged from the previous articles.

ProcessGuideController

The class that defines the overall state-machine logic in the ProcessGuide framework is the ProcessGuideController.  This class will determine the discrete steps and conceptually maps to the high-level WHSWorkExecuteDisplay<ProcessName> class you might have defined previously.  When you create a custom workflow you will first define this class – however you will need to define the several other classes before you can finish the definition of the class itself.  For now, we will define the code like this:

[WHSWorkExecuteMode(WHSWorkExecuteMode::AssetScan)]
public class WHSProcessGuideAssetScanController extends ProcessGuideController
{
    protected ProcessGuideStepName initialStepName()
    {
        return null;
    }

    protected ProcessGuideNavigationRoute initializeNavigationRoute()
    {
        return null;
    }
}

Note that we are associating the Controller class with our new WHSWorkExecuteMode enumeration value in the class attribute so this controller class will be instantiated when the mobile app constructs these type of menu items.

As discussed above the Controller defines the navigation steps between the various state machine “steps.”  These steps are defined as classes derived from the ProcessGuideStep class.  Based on our workflow diagram above, we will have three steps in this workflow – one for each of the “states” (Scan ContainerID, Capture Assets, and Process Assets).

Scan ContainerID

We can define the first ProcessGuideStep with the following structure:

[ProcessGuideStepName(classStr(WHSProcessGuideValidateAssetScanContainerIdStep))]
public class WHSProcessGuideValidateAssetScanContainerIdStep extends ProcessGuideStep
{
    protected ProcessGuidePageBuilderName pageBuilderName()
    {
        return null;
    }
}

The pageBuilderName is required – this is where you will define the specific PageBuilder used to render this step. A PageBuilder class roughly corresponds to the displayForm method in the WHSWorkExecuteDisplay framework.  In the previous article this was the code used to construct this step (via the WHSWorkExecute framework):

private container getContainerStep(container _ret)
{
    _ret = this.buildGetContainerId(_ret);
    step = conWeighContainerStep::EnterWeight;

    return _ret;
}

container buildGetContainerId(container _con)
{
    container ret = _con;

    ret += [this.buildControl(#RFLabel, #Scan, ‘Scan a container’, 1, ”, #WHSRFUndefinedDataType, ”, 0)];
    ret += [this.buildControl(#RFText, conWHSControls::ContainerId, "@WAX1422", 1, pass.lookupStr(conWHSControls::ContainerId), extendedTypeNum(WHSContainerId), ”, 0)];
    ret += [this.buildControl(#RFButton, #RFOK, "@SYS5473", 1, ”, #WHSRFUndefinedDataType, ”, 1)];
    ret += [this.buildControl(#RFButton, #RFCancel, "@SYS50163", 1, ”, #WHSRFUndefinedDataType, ”, 0)];

    return ret;
}

The equivalent code in the ProcessGuide framework is below.  Note that you are constructing a ProcessGuidePageBuilder class and the data controls and action controls are split into separate methods (which allows for better extensibility).

[ProcessGuidePageBuilderName(classStr(WHSProcessGuideValidateAssetScanContainerIdPageBuilder))]
public class WHSProcessGuideValidateAssetScanContainerIdPageBuilder extends ProcessGuidePageBuilder
{
    protected final void addDataControls(ProcessGuidePage _page)
    {
        _page.addLabel(ProcessGuideDataTypeNames::ContainerIdLabelName, "Scan a container", extendedTypeNum(WHSRFUndefinedDataType));
        _page.addTextBox(ProcessGuideDataTypeNames::ContainerId, "@WAX1422", extendedTypeNum(WHSContainerId));
    }

    protected final void addActionControls(ProcessGuidePage _page)
    {
        #ProcessGuideActionNames
        _page.addButton(step.createAction(#ActionOK), true);
        _page.addButton(step.createAction(#ActionCancelExitProcess));
    }
}

Now that we have defined the PageBuilder class we can go back and update the Step class with the necessary details.  Specifically, the pageBuilderName method can be updated to return the class we just defined.  In addition, we need to define criteria for when this step is considered “complete” – this is done by overriding the isComplete method.  For this example we will require the ContainerId textbox to have data in it.

[ProcessGuideStepName(classStr(WHSProcessGuideValidateAssetScanContainerIdStep))]
public class WHSProcessGuideValidateAssetScanContainerIdStep extends ProcessGuideStep
{
    protected final boolean isComplete()
    {
        WhsrfPassthrough pass = controller.parmSessionState().parmPass();
        WHSContainerId containerId = pass.lookup(ProcessGuideDataTypeNames::ContainerId);
        return (containerId != '');
    }

    protected ProcessGuidePageBuilderName pageBuilderName()
    {
        return classStr(WHSProcessGuideValidateAssetScanContainerIdPageBuilder);
    }
}

Control Names

Note that I am using the ProcessGuideDataTypeNames class to define all the control names – this can be easily done with an extension class like below.  It also could be defined in another class – this is not a hard requirement; it just matches what the core ProcessGuide API is doing.

[ExtensionOf(classStr(ProcessGuideDataTypeNames))]
final static class ProcessGuideDataTypeNamesWHSTestPG_Extension
{
    public static const str ContainerId = "ContainerId";
    public static const str ContainerIdLabelName = "ContainerIdLabel";
    public static const str AssetId = "AssetId";
}

Capture Assets

We can now build the next step in the workflow – which means we need both a ProcessGuideStep and ProcessGuidePageBuilder class.  Since this step will be using the multi-scan pattern there will be one small addition to these two classes. The page builder will be defined in a similar manner to the previous class:

[ProcessGuidePageBuilderName(classStr(WHSProcessGuideAssetScanMultiScanPageBuilder))]
public class WHSProcessGuideAssetScanMultiScanPageBuilder extends ProcessGuidePageBuilder
{
    protected void addDataControls(ProcessGuidePage _page)
    {
        WhsrfPassthrough pass = controller.parmSessionState().parmPass();
        str assetIdList = pass.lookupStr(ProcessGuideDataTypeNames::AssetId);
        
        _page.addLabel(ProcessGuideDataTypeNames::ContainerIdLabelName,"Scan Assets", extendedTypeNum(WHSRFUndefinedDataType));
        _page.addTextBox(ProcessGuideDataTypeNames::AssetId, "Asset Id", extendedTypeNum(AssetIdList), true, assetIdList);
    }

    protected void addActionControls(ProcessGuidePage _page)
    {
        #ProcessGuideActionNames
        _page.addButton(step.createAction(#ActionOK), true);
        _page.addButton(step.createAction(#ActionCancelResetProcess));
    }

}

And the ProcessGuideStep will simply indicate that the specific PageBuilder class should be used:

[ProcessGuideStepName(classStr(WHSProcessGuideAssetScanMultiScanStep))]
public class WHSProcessGuideAssetScanMultiScanStep extends ProcessGuideStep 
{
    protected final ProcessGuidePageBuilderName pageBuilderName()
    {
        return classStr(WHSProcessGuideAssetScanMultiScanPageBuilder);
    }
}

What makes this step unique is the client-side multi-scan mode that is enabled for the user.  This allows the customer to scan any number of Assets in an offline mode and not have to wait for the server-side round trip for each scan.  In order to tell the mobile app that we want to be in this “multi-scan” mode we need to create our own DectoratorFactory object and return the Multi-Scan decorator for this specific step.  This will work the same way it did in the previous example; we will define a new MultiScan DecoratorFactory and have it return a new multi-scan WHSMobileAppServiceXMLDecorator at the specific step in the workflow we want the user to be in the multi-scan UI.  Recall that the WHSMobileAppServiceXMLDecorator class is used to direct the mobile app UI to change into the different major “modalities” – such as the standard scan control UI, the card view used for the worklist, and the multi-scan model.

We still do not ship a native MultiScan decorator factory, but it can be easily created for this specific flow with the following code.  This detects which step we are currently executing and if we are on the multi-scan step it will return the multi-scan decorator.

[WHSWorkExecuteMode(WHSWorkExecuteMode::AssetScan)]
public class WHSMobileAppServiceXMLDecoratorFactoryMultiScan implements WHSIMobileAppServiceXMLDecoratorFactory
{
    #WHSRF

    public WHSMobileAppServiceXMLDecorator getDecorator(container _con)
    {
        if (this.isMultiScanScreen(_con))
        {
            return new WHSMobileAppServiceXMLDecoratorMultiScan();
        }
        
        return new WHSMobileAppServiceXMLDecoratorFactoryDefault().getDecorator(_con);
    }

    /// <summary>
    /// Extracts the current step from the container.
    /// the current step should be preceeded by a string "CurrentStep"
    /// </summary>
    /// <param name = "_con">
    /// Contains information about the context
    /// </param>
    /// <returns>
    /// The current step listed in the context
    /// </returns>
    private str getCurrentStep(container _con)
    {
        container subCon = conPeek(_con, 2);
        for (int i  = 1; i <= conLen(subCon); i++)
        {
            if (conPeek(subCon, i - 1) == "CurrentStep")
            {
                return conPeek(subCon, i);
            }
        }

        //Default behavior.
        return conPeek(subCon, 8);
    }

    private boolean isMultiScanScreen(container _con)
    {
        const str MultiStep = classstr(WHSProcessGuideAssetScanMultiScanStep);

        str currStep = this.getCurrentStep(_con);

        return currStep == MultiStep;
    }

}

Process Assets

The final step in the workflow is to take the data entered by the user and submit it to the database.  For this we don’t need to display a UI, so we will create a step that derives from ProcessGuideStepWithoutPrompt.  This means we just need to provide an implementation for the doExecute method.  We will leverage the same code from the previous article – as a reminder here is the overview of the API used by the multi-scan page pattern API.


Multi-Scan API

Now that we know how to enable the Multi-Scan UI through a Page Pattern, we need to understand the basic API for passing the scanned items back and forth.  Once the MultiScan Page Pattern is requested, the first input control registered on the page will be used for the multi-scan input.  Remember that most of the UI interaction is all done client-side – so the only thing the server X++ code needs to do is define this control and the data that it contains.

When the user clicks that “submit” check box and sends the multi-scan data back to the X++ code, this is formatted in a very specific way.  The actual parsing of the data is done using the same interaction patterns as before – it will be stored in the result pass object for the specific control defined as the primary input of this page.  But the data will be passed in this format:

   <scanned value>, <number of scans>|<scanned value>, <number of scans>|…

Thus, in my demo example above the data that the server would receive would be the following:

   BC-001,2|BC-002,1|BC-003,1

In the X++ code you would then be responsible for parsing this string and storing the data in the necessary constructs.  We will see a simple example in a moment of how to parse this data.


The following is the code necessary to build the processing step:

[ProcessGuideStepName(classStr(WHSProcessGuideAssetScanProcessAssetsStep))]
public class WHSProcessGuideAssetScanProcessAssetsStep extends ProcessGuideStepWithoutPrompt
{
    protected final void doExecute()
    {
        WhsrfPassthrough pass = controller.parmSessionState().parmPass();
        
        if(pass.lookupStr(ProcessGuideDataTypeNames::AssetId) == "")
        {
            throw error('No assets found');
        }
        else
        {
            List assets = strSplit(pass.lookupStr(ProcessGuideDataTypeNames::AssetId),"|");
            ListEnumerator enumerator = assets.getEnumerator();
            while(enumerator.moveNext())
            {
                //save assetId
                str assetString = enumerator.current();
                if (assetString != "")
                {
                    str AssetId = subStr(assetString,1,strScan(assetString,",",1,strLen(assetString))-1 );
                    WHSAsset newAsset;
                    newAsset.ContainerId = pass.lookupStr(ProcessGuideDataTypeNames::ContainerId);
                    newAsset.WHSAssetId = AssetId;
                    newAsset.insert();
                }
            }
             this.addAssetsSavedProcessCompletionMessage();     
        }

        super();

        pass.remove(ProcessGuideDataTypeNames::ContainerId);
        pass.remove(ProcessGuideDataTypeNames::AssetId);
    }

    private void addAssetsSavedProcessCompletionMessage()
    {
        ProcessGuideMessageData messageData = ProcessGuideMessageData::construct();
        messageData.message = "Assets Saved";
        messageData.level = WHSRFColorText::Success;

        navigationParametersFrom = ProcessGuideNavigationParameters::construct();
        navigationParametersFrom.messageData = messageData;
    }

}

ProcessGuideController Finale

We now have all the pieces necessary to flesh out the controller class – which we just had as a stub earlier.  Update the code with the references to the three new ProcessGuideStep classes.

[WHSWorkExecuteMode(WHSWorkExecuteMode::ValidateAssetScan)]
public class WHSProcessGuideValidateAssetScanController extends ProcessGuideController
{
    protected ProcessGuideStepName initialStepName()
    {
        return classStr(WHSProcessGuideValidateAssetScanContainerIdStep);
    }

    protected ProcessGuideNavigationRoute initializeNavigationRoute()
    {
        ProcessGuideNavigationRoute navigationRoute = new ProcessGuideNavigationRoute();
        navigationRoute.addFollowingStep(classStr(WHSProcessGuideValidateAssetScanContainerIdStep), classStr(WHSProcessGuideValidateAssetScanValidateScanStep));
        navigationRoute.addFollowingStep(classStr(WHSProcessGuideValidateAssetScanValidateScanStep), classStr(WHSProcessGuideValidateAssetScanProcessAssetsStep));
        navigationRoute.addFollowingStep(classStr(WHSProcessGuideValidateAssetScanProcessAssetsStep), classStr(WHSProcessGuideValidateAssetScanContainerIdStep));

        return navigationRoute;
    }

}

This code overrides the two required methods in order to provide a definition of the state machine for this workflow – again using the ProcessGuideStep classes we defined above.  It should be clear how this code is a clear match to the state machine we defined at the beginning of the article – we are defining the specific states and the transition (edges) between them.

Example Workflow

Now that you have seen the code to enable this in a custom workflow, let’s walk through the screens in the mobile app.  You can download the complete code for this project in the link at the bottom of this post – you just need to get it up and running on a dev environment and configure the necessary menu items to enable the workflow for your system.

The initial screen shows the Container ID scanning field.  Note that in the sample project I have included the necessary class to default this to the scanning mode – however you will need to set these up in Dynamics as defined here.

Scanning a container id (CONT-000000001 works if you are in USMF in the Contoso demo data) will navigate you to the next screen and enable the multi-scan Page Pattern.

Here you can enter any number of assets and the app will store them into the local buffer.  As we described above you can view the scanned assets by clicking the icon in the lower left.  After a few scans we would see the UI updated:

Clicking the list icon would show the scans we have performed offline:

Finally clicking the “submit” button on the main screen will push the items to the server, which will then be saved to the custom WHSAsset table and the UI will display the success message.

Conclusion

Hopefully this shows you how to utilize the new ProcessGuide framework for your warehouse customizations.  The code used for this demo is available to download here – please note that this code is demonstration code only and should not be used in a production system without extensive testing.

  • Sorry about the code syntax highlighting – I am still working out the details on how to get X++ code to display correctly. For now it is just using C# formatting.

Posted by Zach in Development, Dynamics

Document Routing Agent Tips

The Document Routing Agent (DRA) is a client-side component for Dynamics that allows customers to connect their local printer resources with Dynamics so that printing from the server-side processes are directed to local devices.  Details here.  We have some customers utilizing very large deployments of these in order to facilitate quick turnaround time – especially in distribution scenarios where label printing timing is critical.

This article is a collection of known issues/workarounds that we have faced that might be valuable to you if you are working with the DRA.

“Key not valid for use in specific state”

This is an error you can hit if the underlying credentials used to connect to Dynamics have changed or expired.  The article posted here describes renaming the entire installation folder here (C:\ProgramData\Microsoft\Microsoft Dynamics 365 for Operations – Document Routing), which is effectively forcing s complete re-install.  Doing so we cause the installation process to create a new Guid for the ApplicationId and all previous printing registration mappings will be lost.  For some deployments this can be hundreds of printer registrations and can be a significant time investment.

You can avoid this re-registering by only removing the TokenCache.dat file instead of the entire directory – this will remove the stored session credentials and allow you to reconfigure them in the app.

Alternatively you can do the complete reinstall and then afterwards update the ida:ApplicationId value in the Microsoft.Dynamics.AX.Framework.DocumentRouting.config config file in the same location.  During the reinstall this value will be regenerated – but you can update the config file with the previously used Guid.


Polling Frequency 

The DRA polls Dynamics every 3 seconds for new documents to print.  This is fine for many of our customers, however large manufacturing and distribution customers often have very tight labeling requirements and expect the timing from scanning of a barcode to label printing to be less than 1 second.  One option I have seen customers adopt here is to install multiple DRA clients – thus you will have multiple polling “windows” and likely reduce the overall wait-time to less than 3 seconds.

Another option is to utilize a “hidden” configuration option in the DRA.  This default value can be overridden by adding the following key to the file: C:\ProgramData\Microsoft\Microsoft Dynamics 365 for Operations – Document Routing\Microsoft.Dynamics.AX.Framework.DocumentRouting.config

<add key=”ida:DocumentRoutingTimerNumSeconds” value=”X” />

This value can be as low as 1 (meaning it will poll every second).

Note that reducing this will have an impact on your Dynamics instance as a new OData call and DB connection will be initiated for every DRA polling event which could cause performance issues.  Be mindful of this and only this this technique if you really have this low printing latency requirements.


Platform Warning

There is a strong connection between the DRA and the platform version used to run Dynamics – the DRA is actually built as part of the platform build.  When you start the DRA after updating Dynamics to a new platform version (after a OneVersion monthly update for example) you will see the following warning message in the DRA:

Many customers view this message as an error and go through the process of reinstalling the DRA during each update cycle.  While this is best practice to ensure compatibility it is not strictly required – the DRA is backwards compatible to all supported platform releases (meaning the previous three monthly releases).  So if you want to avoid updating the DRA each cycle you can simply live with this warning.


Hope these tips were useful – I’ll update with more as I come across other interesting patterns.

Posted by Zach in Dynamics

Microsoft Business Application Summit – 2020

Hello,

Hopefully you were able to join our virtual Microsoft Business Application Summit this year – it was free and open to everyone.  You can access the on-demand content as well.

I did a session on best practices for implementing the Advanced Warehousing module in Dynamics 365 SCM, although much of the core guidance applies to any ERP system.  You can find the session here – and I have included the slides below.  Let me know what you think.

https://mymbas.microsoft.com/sessions/01286481-81c8-4b4d-b702-ae9634ae999f

MBAS WHS Best Practices

Posted by Zach in Dynamics

Warehouse Mobile Devices Portal (WMDP) Posts

Over the past few years I have published several articles about the technical details of Dynamics’ warehouse app, both in terms of architectural overviews as well as customization options.  These were published on an old Dynamics AX SCM blog that no longer exists so they have been moved to a central location.  I am linking to them all here and will be using this blog to post new updates to this series.

Posted by Zach in Dynamics