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
[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
X++ programming blog series
Further reading:
Print a custom product label: a Template solution in Process Guide Framework, with detours
Extending SysOperation contracts with DataMemberAttribute
X++ Decorator pattern in Dynamics 365
Get a cost center in D365FO
Find and merge a DimensionAttributeValue
SysExtension framework pitfall: avoid new()
Input validation and messaging in the Process Guide Framework