Fixed Kanbans in Dynamics 365 SCM with Warehouse Management

Small consumable items (Kanban)
Small consumable items (Kanban)

Fixed Kanbans in Dynamics 365 SCM with Warehouse Management

In a warehouse-managed environment, supplying production resources with small quantities of material is a common requirement. One possible solution is a Kanban flow, where the operator uses one container while the second container is replenished by the warehouse team. Once the first container is empty, it is returned to the replenishment cycle, ensuring that material is available at the resource without requiring the operator to create a warehouse request manually.

In Dynamics 365 Supply Chain Management, this process can be implemented using fixed Kanban rules in combination with Warehouse Management (WHS). The destination is defined by the activity in the production flow, whereas the source location in the main warehouse can be controlled by WHS location directives.

Configuration: Fixed Kanban Replenishment with WHS

  1. Navigate to Production control > Setup > Lean production flow > Value streams and create a value stream for the material supply process. The value stream provides the organisational context for the production flow and its activities. After that create a generic logistic Production flow model.
  2. Create the production flow with 1 transfer activity: Lean production flow > Production flows, associated with the value stream. Add a Version.
  3. Under the Activities button, use Create new plan activity, where in the wizard you create the transfer activity: Activity type = Transfer. The work cells are irrelevant; mark both Update on hand on receipt and Update on hand on pick.
  4. Choose the same “From” and “To” locations, namely the location of the resource: the activity represents the destination of the material flow. The actual source location can be determined separately by the WHS location directive.
  5. Enter something into the time, finish creating the activity, but then enter the Details mode and enable Auto complete by put line! This setting allows the transfer activity to be completed automatically with the last put step on the mobile device in the warehouse.
  6. Activate the flow version.
  7. Create the fixed Kanban rule: navigate to Product information management > Lean manufacturing > Kanban rules and create a rule of type Withdrawal.
  8. The same screw will be at more locations, potentially in containers of different sizes. Every Item-Location combination requires an own Kanban rule with a specific quantity.
  9. Set Replenishment strategy = Fixed. Select the transfer activity created in the previous steps as the First plan activity. This automatically populates the Receipt location of the resource. Set Register as empty = Manual.
  10. Specify the product and the Default quantity. This quantity defines how much material is requested when an empty container triggers replenishment i.e. the size of the box.
  11. Set the Fixed kanban quantity = 2 for the two-container replenishment cycle, or 1 if the replenishment is fast enough for a one-container system.
  12. At the bottom of the screen, you may pre-create the Kanban cards with the button + Add. Each becomes a unique barcode and a unique – and different from the barcode card number – Kanban number.
  13. Configure warehouse work classes and the work template:  under Warehouse management > Setup > Work, create the work classes required for Kanban picking and putting, then a work template for the Kanban picking work order type with the following sequence:
  14. Under Warehouse management > Setup > Location directives, configure a directive for the Kanban picking work type. Use the directive query and actions to determine the source location from which the material is picked. This is where the actual warehouse source is controlled, independently of the destination defined in the production flow.
  15. No separate put-away location directive nor a Kanban put away work template is required for this scenario because the destination is given by the transfer activity.
  16. Under Warehouse management > Setup > Waves > Wave templates, configure the template used to process Kanban replenishment demand. Enable automatic wave creation and automatic release of work. Configure the remaining wave settings according to the intended processing flow.
  17. Test the process: scan a Kanban card on the Transfer Kanban board (Production control > Kanban > Kanban board for transfer jobs) to make it empty.
  18. The system creates the replenishment demand and processes the corresponding wave to generate warehouse work. The warehouse worker executes the picking and put operations, replenishing the container with the fixed quantity defined in the Kanban rule.
  19. After the put operation, the material is available at the resource location, and Auto complete by put line allows the transfer activity to be completed through that process: the Kanban resets and becomes Completed automatically, awaiting the new cycle.

The fixed Kanban card cycle

To summarise:

  1. An empty container triggers replenishment: when a container is empty, the operator scans its Kanban card on the transfer-job Kanban board in empty-container mode. This triggers the replenishment process for that card.
  2. The system creates the replenishment demand: the Kanban is activated, and a Kanban wave is generated. Wave processing creates the warehouse work required to pick the fixed quantity and deliver it to the destination associated with the production-flow activity.
  3. The warehouse replenishes the container: the warehouse worker executes the picking and put operations. The source location is determined by the WHS location directive, while the destination is defined by the production-flow activity. The quantity to be replenished comes from the fixed Kanban rule.
  4. The replenished container returns to the resource: once the warehouse work is completed, the container is available for use at the resource. The other container can continue to supply material while this replenishment takes place.
  5. The cycle repeats: when the next container becomes empty, its Kanban card triggers another replenishment cycle. The circulating cards allow the process to repeat without creating a new Kanban rule for each replenishment.

Fixed Kanban or Min/Max replenishment?

For this process, the fixed Kanban rule provides a direct connection between the production resource, the container and the replenishment quantity. The empty Kanban card is the trigger, and the Kanban rule determines the quantity to be replenished. The WHS module then generates and lets execute the warehouse work, including the determination of the source location. The machine operator is in control.

Min/Max replenishment addresses a slighty different requirement. It is a warehouse replenishment mechanism based on inventory thresholds by location (!), whereas the fixed Kanban process is driven by the consumption and replenishment cycle of a defined container. The Dynamics 365 system periodically controls the min/max levels and hopefully reacts on time, but only as well, as good the quantities in the BOM are, and as timely the workers record material consumption.

Print a custom product label: a Template solution in Process Guide Framework, with detours

Print Product Label
Print Product Label

Print a custom product label: a Template solution in Process Guide Framework, with detours

I wanted to have a template code for a new Warehouse Management App menu item in Dynamics 365 for SCM. The guidance …/warehousing/process-guide-framework is good, but a few aspects are missing there. This sample takes an Item ID (released product), looks for a custom label for products, deducts a default printer and sends a requested number of product labels to the printer. It does not require any parameters but the mobile device menu item setup to perform this job, which is my credo: the less configuration I have, the less conversations with the customer must be led, and the less documentation must be written. As we all agree, writing documentation – including this one – is utterly boring.

ProcessGuideController and WHSWorkExecuteMode

The below mobile device menu item is an indirect one, i.e. it is not based on any warehouse work. A new menu item class is instantiated in connection with a new WHSWorkExecuteMode enumeration element, while an indirect menu item is driven by an Activity code in Warehouse management > Setup > Mobile device , which is a different enumeration: WHSWorkActivity:

This is why you have to extend each of the enums with a new element of the same name (here: WHSWorkExecuteMode::PrintProductLabel = WHSWorkActivity::PrintProductLabel). One is converted into another; you do not have to explicitly program the mapping anymore: it is enough for the elements to have precisely the same name.

The code in the controller is a no-brainer:

				
					[WHSWorkExecuteMode(WHSWorkExecuteMode::PrintProductLabel)]
public class ProcessGuideProductLabelController extends ProcessGuideController
{
    protected final ProcessGuideStepName initialStepName()
    {
        return classStr(ProcessGuideProductLabelItemIdStep);
    }

    protected ProcessGuideNavigationRoute initializeNavigationRoute()
    {
        ProcessGuideNavigationRoute navigationRoute = new ProcessGuideNavigationRoute();

        navigationRoute.addFollowingStep(classStr(ProcessGuideProductLabelItemIdStep), classStr(ProcessGuideProductLabelNoOfLabelsStep));
        navigationRoute.addFollowingStep(classStr(ProcessGuideProductLabelNoOfLabelsStep), classStr(ProcessGuideProductLabelItemIdStep));

        return navigationRoute;
    }
}
				
			

For the new process guide flow to come up, do not forget to SysFlushAOD: Refresh SysExtension cache in D365FO. For the Use process guide slider to automatically be set at the mobile device menu item (see above), we often use the following table extension:

				
					[ExtensionOf(tableStr(WHSRFMenuItemTable))]
internal final class WHSRFMenuItemTable_Extension
{ 
    protected boolean workActivityMustUseProcessGuideFramework()
    {
        boolean ret = next workActivityMustUseProcessGuideFramework();
        ret = ret || (this.WorkActivity == WHSWorkActivity::PrintProductLabel);
        return ret;
    }
}
				
			

Declarations to support detours

The menu item must be able not only to be called from the main mobile device menu, but also receive parameters from other menus / processes. It should be able to uptake the Item ID and print a product label right away. This is called a detour:  Configure detours for steps in mobile device menu items – Supply Chain Management | Dynamics 365 | Microsoft Learn.
We have to declare all possible steps and fields of the new menu item with a descendant of WHSMobileAppFlow:

				
					[WHSWorkExecuteMode(WHSWorkExecuteMode::PrintProductLabel)]
public final class WHSMobileAppFlowProductLabel extends WHSMobileAppFlow
{
    protected void initValues()
    {
        this.addStep(WHSMobileAppStepIds::ItemId);
        this.addStep(WHSMobileAppStepIds::WaveLblQty);

        this.addAvailableField(extendedTypeNum(ItemId));
        this.addAvailableField(extendedTypeNum(NumberOfLabels));
    }
}
				
			

Once declared and compiled, refresh the SysExtension cache again, open Warehouse management > Setup > Mobile device > Mobile device steps and use Create default setup there.

I also have a unique field “NumberOfLabels” to prompt the number of label copies. It must be declared with a class, a descendant of WHSField. It does not need a WHSControl descendant (see also my Input validation and messaging in the Process Guide Framework), because its behaviour is quite standard.

 

				
					[WHSFieldEDT(extendedTypeStr(NumberOfLabels))]
public class WHSFieldNumberOfLabels extends WHSField
{
    private const WHSFieldName             Name        = "@WAX:NumberOfLabels";
    private const WHSFieldDisplayPriority  Priority    = 10;
    private const WHSFieldDisplayPriority  SubPriority = 90;
    private const WHSFieldInputMode        InputMode   = WHSFieldInputMode::Manual;
    private const WHSFieldInputType        InputType   = WHSFieldInputType::Numeric;

    protected void initValues()
    {
        this.defaultName        = Name;
        this.defaultPriority    = Priority;
        this.defaultSubPriority = SubPriority;
        this.defaultInputMode   = InputMode;
        this.defaultInputType   = InputType;
    }
}
				
			

The extendedTypeNum property of the controls on “pages” (see below) must match exactly the above declaration. Once programmed, use the button Create default setup in Warehouse management > Setup > Mobile device > Warehouse app field names. The new field should appear in the list and can be used as a novel parameter in “Select fields to send” in a detour.  

Step 1: Screen to prompt the product number

The below 2 classes are trivial and I’ll keep them uncommented:

				
					[ProcessGuidePageBuilderName(classStr(ProcessGuideProductLabelItemIdPageBuilder))]
public class ProcessGuideProductLabelItemIdPageBuilder extends ProcessGuidePageBuilder
{
    protected final void addDataControls(ProcessGuidePage _page)
    {
        _page.addTextBox(ProcessGuideDataTypeNames::ItemId,
                         "@SYS14428",
                         extendedTypeNum(ItemId),
                         true,
                         controller.parmSessionState().parmPass().lookupStr(ProcessGuideDataTypeNames::ItemId));
    }
    protected final void addActionControls(ProcessGuidePage _page)
    {
        #ProcessGuideActionNames
        _page.addButton(step.createAction(#ActionOK), true);
        _page.addButton(step.createAction(#ActionCancelExitProcess));
    }
}

[ProcessGuideStepName(classStr(ProcessGuideProductLabelItemIdStep))]
public class ProcessGuideProductLabelItemIdStep extends ProcessGuideStep
{
    protected final ProcessGuidePageBuilderName pageBuilderName()
    {
        return classStr(ProcessGuideProductLabelItemIdPageBuilder);
    }
    protected final boolean isComplete()
    {
        WhsrfPassthrough pass = controller.parmSessionState().parmPass();
        return (pass.lookup(ProcessGuideDataTypeNames::ItemId) != "");
    }
}
				
			

Step 2: Screen to prompt the number of copies, then print

The next 2 classes are more complex.

Microsoft developers tend to design an additional step (here it would be the 3rd) which executes the core business logic, but a mobile app step comes with a UI – an additional screen looking like a confirmation. Yet every new screen is one click more for the worker to do, and we can initiate the printing right after the number of copies prompt.

It is essential to have super()  in front of the .doExecute() method, because it updates the “pass” with the latest user interaction (i.e. saves the “NumberOfLabels” in the session state).

				
					[ProcessGuidePageBuilderName(classStr(ProcessGuideProductLabelNoOfLabelsPageBuilder))]
public class ProcessGuideProductLabelNoOfLabelsPageBuilder extends ProcessGuidePageBuilder
{
    protected final void addDataControls(ProcessGuidePage _page)
    {
        WhsrfPassthrough pass = controller.parmSessionState().parmPass();

        if (! pass.exists(ProcessGuideDataTypeNames::NumberOfLabels))
        {
            pass.insert(ProcessGuideDataTypeNames::NumberOfLabels, 1);
        }
        _page.addTextBox(
            ProcessGuideDataTypeNames::NumberOfLabels,
            "@WAX:NumberOfLabels",
            extendedTypeNum(NumberOfLabels),
            true,
            WhsWorkExecuteDisplay::num2StrDisplay(pass.lookupNum(ProcessGuideDataTypeNames::NumberOfLabels)));

        _page.addLabel(
            ProcessGuideDataTypeNames::ItemInfo,
            InventProcessGuideInquiryItemHelper::generateItemInformation(pass.lookupStr(ProcessGuideDataTypeNames::ItemId), InventDim::findOrCreateBlank()),
            extendedTypeNum(WHSRFItemInformation));   
    }

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

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

    protected void doExecute()
    {
        super(); // process the controls

        // Identify the custom product label
        WHSLabelLayoutDataSource    labelDS;
        WHSLabelLayout              labelLayout;
        select firstonly labelLayout 
            where labelLayout.LayoutType == WHSLabelLayoutType::CustomLabel
        exists join labelDS
            where labelDS.CustomLabelRootDataSourceTable == tableStr(InventTable)
               && labelDS.LabelLayoutDataSourceId == labelLayout.LabelLayoutDataSource;
        if (! labelLayout)
        {
            return; 
        }        

        WhsrfPassthrough pass = controller.parmSessionState().parmPass();
        if (this.printCustomLabel(pass.lookupStr(ProcessGuideDataTypeNames::ItemId),
                                labelLayout,
                                WHSLabelPrinterSelector::construct()
                                    .withUserId(pass.parmUserId())
                                    .withWarehouseId(pass.parmInventLocationId())
                                    .selectPrinterForPrinterStockType(""),
                                pass.lookupNum(ProcessGuideDataTypeNames::NumberOfLabels)) > 0)
        {
            this.addReprintLabelProcessCompletionMessage();
        }
        
        this.passReset();
    }

    private int printCustomLabel(ItemId _itemId, WHSLabelLayout _labelLayout, WHSPrinterName _printerName, NumberOfLabels _noOfLabels)
    {
        InventTable     record = InventTable::find(_itemId);
        int             labelCount;
                        
        // Do not use a service to avoid a disruptive info message
        using (var batchPrintingContext = WhsBatchedDocumentRoutingContext::construct())
        {
            while (labelCount < _noOfLabels)
            {
                labelCount += WHSCustomLabelPrintCommandGenerator::printLabelsForRecord(_labelLayout, _printerName, record.RecId);
            }
            batchPrintingContext.printBatchedLabels();
        }
        return labelCount;
    }

    protected void passReset()
    {
        WhsrfPassthrough pass = controller.parmSessionState().parmPass();
        pass.remove(ProcessGuideDataTypeNames::ItemId);
        pass.remove(ProcessGuideDataTypeNames::NumberOfLabels);
    }

    private void addReprintLabelProcessCompletionMessage()
    {
        ProcessGuideMessageData messageData = ProcessGuideMessageData::construct();
        messageData.message = "@WAX3181";
        messageData.level = WHSRFColorText::Success;

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

Source code

You may download the example here: ProcessGuideProductLabel.axpp

Dissecting the Warehouse Management app layout

Dissecting the Warehouse Management app layout

The layout of the Dynamics 365 Warehouse Management app is managed by both the App code and the D365 SCM core to ensure a productive workflow. The app receives a state from the Dynamics 365 SCM core encapsulated in a “container,” comprising a list of controls to be presented on the current screen. These hints include the DisplayArea  assignment (the display area where controls that are awaiting input or confirmation are found, see Inspect details of active Warehouse Management mobile app sessions – Supply Chain Management | Dynamics 365 | Microsoft Learn) and the InstructionControl reference for item ID, quantity, location confirmations.

Typically, controls without default values are positioned in the Primary Input Area, awaiting user input through scanning or manual entry. In the above example, the next empty control is LP (license plate) i.e. the licence plate to pick the goods from. These controls which require a user input are presented one after another following the order of controls in the “container” or the explicit Display Priority, if provided by the programmer of the warehouse menu item UI. This is perceived by the user as a series of screens; internally, it remains the same screen.

To determine the DisplayArea, the D365 core employs heuristics. Controls with default values slide down to the Info and Secondary Input Area, except for confirmation fields like Confirm Location, which appear in the Primary Input Area despite seemingly having a default value. The initial value is still empty, but the InstructionControl let the warehouse app present a hint from a different field and display it in italic (here: BULK-010).

Upon assigning values to all controls, the screen is considered complete, triggering the button in the Primary Action Area.

Control placement on the Warehouse Management app screen can be influenced through the WHSMobileAppServiceDecoratorRule class family. For instance, a control with a non-empty default value can still be prompted in the primary input area, as demonstrated in the below code snippet:

				
					[SubscribesTo(classStr(WHSMobileAppServiceDecoratorRuleDefaultDisplayArea), staticDelegateStr(WHSMobileAppServiceDecoratorRuleDefaultDisplayArea, isTextInPrimaryInputAreaDelegate))]
public static void WHSMobileAppServiceDecorator_isTextInPrimaryInputAreaDelegate(boolean _enabled, Map _controlMap, str _data, WHSMenuItemName _menuItemName, EventHandlerResult _result)
{
   if (_enabled && _controlMap.lookup(#XMLControlName) == #MyControl)
   {
      _result.result(true);
   }
}
				
			
It is important to remove (to be exact, not to rebuild after the last user interaction) such a control from the screen once it receives its input, otherwise it will be prompted repeatedly in an infinite loop. The second example showcases a custom control with an InstructionControl reference for requesting confirmations while presenting a hint in italic:
				
					[ExtensionOf(classStr(WHSMobileAppServiceDecoratorRuleInstruction))]
final class WHSMobileAppServiceDecoratorRuleInstruction_Extension
{
    #WHSRF
    protected WHSMobileAppControlName getInstructionControlName(WHSMobileAppControlName _controlName)
    {
        WHSMobileAppControlName confirmationControl = _controlName;
        WHSMobileAppControlName masterControl;
        masterControl = next getInstructionControlName(confirmationControl);
        if (confirmationControl == #MyControl)
        {
            masterControl = #MyMasterControl;
        }
        return masterControl;
    }
}