Step by step: end to end solution to add a new step in WHMS mobile app
Wassup guys?!!!
I am back with a brand new topic for WHMS: how to add a new screen in your WHMS mobile app, from the scratch.
You might be wondering, what is new in it: we already have one in place from Microsoft, that aptly explains the process to add a new screen to mobile app:
https://learn.microsoft.com/en-us/dynamics365/supply-chain/supply-chain-dev/process-guide-framework
The above solution tells you to create a controller class, that contains a method called: initializeNavigationRoute, that contains the sequence of the step of classes/navigatiion for your screens:
protected ProcessGuideNavigationRoute initializeNavigationRoute()
{
ProcessGuideNavigationRoute navigationRoute = new ProcessGuideNavigationRoute();
navigationRoute.addFollowingStep(classStr(ProdProcessGuidePromptProductionIdStep), classStr(ProdProcessGuideConfirmProductionOrderStep));
navigationRoute.addFollowingStep(classStr(ProdProcessGuideConfirmProductionOrderStep), classStr(ProdProcessGuideStartProductionOrderStep));
navigationRoute.addFollowingStep(classStr(ProdProcessGuideStartProductionOrderStep), classStr(ProdProcessGuidePromptProductionIdStep));
return navigationRoute;
}
But my case is different. Think of a situation, where you need to create a purchase order screen, that needs to iteratively need to create lines to orders, unless the user 'Completes' the order. For such cases, the above solution won't work.
And this article is here to help you setup the navigation, where you conditionally need to add a step to your screen iteratively.
Here go the steps to implement the steps:
a. Extend the Enum: WHSWorkActivity >> Add a new element called 'CreatePurchOrder'
b. In the WHMS >> Setup >> Mobile device >> Mobile Device Menu Items >> create a new record with this newly created work activity:
c. In the WHMS >> Setup >> Mobile Device >> Mobile Device Menu >> Make necessary alterations to make the above menu appear in desired Main menu.
d. Lastly ensure this menu is selected as under WHMS >> Setup >> Worker >> create/select any user with the above menu.
e. Create a similar Enum value called 'CreatePurchOrder' under 'WHSWorkExecuteMode'
The entire logic of navigation is based on this above Enum. The standard code has a map written berween this two enums in the table WHSRFMenuItemTable. This is done in the method: getWHSWorkExecuteMode. You need to do a CoC to this method, like this, to enable tthe mapping between the enum elements:
public static WHSWorkExecuteMode getWHSWorkExecuteMode(WHSRFMenuItemTable _menuItem)
{
WHSWorkExecuteMode ret = next getWHSWorkExecuteMode(_menuItem);
if (_menuItem.WorkActivity == WHSWorkActivity::CreatePurchOrder)
{
return WHSWorkExecuteMode::CreatePurchOrder;
}
return ret;
}
f. Create a new class called: ProcessGuidePurchOrderController, that should look like as follows:
[WHSWorkExecuteMode(WHSWorkExecuteMode::CreatePurchOrder)]
internal final class ProcessGuidePurchOrderController extends ProcessGuideController
{
protected ProcessGuideStepName initialStepName()
{
return classStr(ProcessGuidePromptPurchOrderWarehouseStep);
}
protected ProcessGuideNavigationAgentAbstractFactory navigationAgentFactory()
{
return new ProcessGuidePromptPurchOrderControllerAgentFactory();
}
}
It also is saying which is the first step to begin the navigation. In our case it's ProcessGuidePromptPurchOrderWarehouseStep: with a navigation agent factory as: ProcessGuidePromptPurchOrderControllerAgentFactory. Next, let us look, what's inside this agent factory.
g. Create a new class called: ProcessGuidePromptPurchOrderControllerAgentFactory: where we are going to mention the various navigation steps:
[WHSWorkExecuteMode(WHSWorkExecuteMode::CreatePurchOrder)]
internal final class ProcessGuidePromptPurchOrderControllerAgentFactory extends ProcessGuideNavigationAgentAbstractFactory
{
public final ProcessGuideNavigationAgent createNavigationAgent(ProcessGuideINavigationAgentCreationParameters _parameters)
{
ProcessGuideNavigationAgentCreationParameters creationParameters = _parameters as ProcessGuideNavigationAgentCreationParameters;
if (!creationParameters)
{
throw error(Error::wrongUseOfFunction(funcName()));
}
return this.initializeNavigationAgent(creationParameters.stepName, creationParameters.controller);
}
private ProcessGuideNavigationAgent initializeNavigationAgent(
ProcessGuideStepName _currentStep,
ProcessGuideController _controller)
{
WhsrfPassthrough pass = _controller.parmSessionState().parmPass();
PurchId orderId = pass.lookupStr(PurchMobileAppConstants::PurchId);
switch (_currentStep)
{
case classStr(ProcessGuidePromptPurchOrderWarehouseStep) :
return new ProcessGuidePurchOrderPromptToWarehouseNavigationAgent(_controller);
case classStr(PurchOrderCreateGuidedCompleteOrderStep) :
if (! orderId)
{
return new ProcessGuidePurchOrderCompleteOrderNavigationAgent();
}
else
{
pass.remove(PurchMobileAppConstants::PurchId)
return new ProcessGuidePurchOrderPromptToWarehouseNavigationAgent(_controller);
}
case classStr(ProcessGuidePromptPurchOrderFromItemIdStep) :
return new ProcessGuidePurchOrderPromptItemIdNavigationAgent(_controller);
default :
return new ProcessGuidePurchOrderPromptToWarehouseNavigationAgent(_controller);
}
}
}
Let me try to explain what's going on here:
>> I have created a class called: PurchMobileAppConstants, where I have defined various constants:
public static const str PurchId = 'PurchId';
>> The first step is actually prompting for user to select the warehouse in which this PO will be created. We are using this class for this purpose: ProcessGuidePromptPurchOrderWarehouseStep.
>> The next step is prompting for ItemId, and hence we are using the class: ProcessGuidePromptPurchOrderFromItemIdStep.
>> The next step, is either the user would select Complete order, or would select Ok to continue adding itemIds: hence for completing the order, I am using: ProcessGuidePurchOrderCompleteOrderNavigationAgent. However, as we would see, if we click on 'Complete order', the loop calls back again on this step, againa and again - resulting in an infinite look. Hence to break it, I am checking if the order Id is not present, then only go for the step completion, else fall back to the first screen: ProcessGuidePurchOrderPromptToWarehouseNavigationAgent.
And yes, you have guessed it right, you have to create Navigation agent classes, for each of the above switch cases:
I. For Warehouse prompt this is our navigation agent:
internal final class ProcessGuidePurchOrderPromptToWarehouseNavigationAgent extends ProcessGuideNavigationAgent
{
private ProcessGuideController controller;
public void new(ProcessGuideController _controller)
{
controller = _controller;
}
protected ProcessGuideStepName calculateNextStepName()
{
return classStr(ProcessGuidePromptPurchOrderFromItemIdStep);
}
}
Which means, when I clicking on Ok at this step, it will take me to the next step to select Item.
II. For navigation agent to select Items, this is what it looks like:
internal final class ProcessGuidePromptPurchOrderFromItemIdNavigationAgent extends ProcessGuideNavigationAgent
{
private ProcessGuideController controller;
public void new(ProcessGuideController _controller)
{
controller = _controller;
}
protected ProcessGuideStepName calculateNextStepName()
{
if (controller.parmClickedAction() == PurchMobileAppConstants::ActionCompleteOrder)
{
return classStr(PurchOrderCreateGuidedCompleteOrderStep);
}
return classStr(ProcessGuidePromptPurchOrderFromItemIdStep);
}
}
This code checks, if the order is 'Action complete', then put it to Complete order step, else keep on iterating back to ProcessGuidePromptPurchOrderFromItemIdStep step.
Uh oh, PurchMobileAppConstants::ActionCompleteOrder means I actually have created a constant in the above class PurchMobileAppConstants:
#ProcessGuideActionNames
public static const str ActionCompleteOrder = #ActionCompleteOrder;
That's the trick, guys. This would keep the user loop back between selecting/ading more items to your sales line and then when he selects Complete order, it will come out of the loop.
Rest part is very easy:
III. Create a page builder class for each step you want to keep to your process. For my example above, I created two page builder classes for two of my steps: to select warehouse, and to select Item Id.
IV. A Process guide step for each of the steps. Likewise I had two process guide steps.
Okay -- before we proceeed any further, I would like to remind you that this kind of activities always are associated with a staging table. The staging table could be thought of having a status as Initiated. As the mobile app completes, the status could change to 'ready to process'. From there we can write a batch to create multi-threaded tasks to create purchase orders.
Exactly this what happens when you create Transfer orders from mobile app: it creates a record in a table called: WHSMobileDeviceQueueMessage, from which a 'mobile app event batch job' processes to create transfer order with associated lines.
Let's see the structure of process guide classes now:
A typical process guide step looks like this:
[ProcessGuideStepName(classStr(ProcessGuidePromptPurchOrderFromItemIdStep ))]
internal final class ProcessGuidePromptPurchOrderFromItemIdStep extends ProcessGuideStep
{
#ProcessGuideActionNames
ItemId itemId;
Qty qty;
/// <summary>
/// Marks if the process is complete
/// </summary>
/// <returns>True if completed; flase otherwise</returns>
protected boolean isComplete()
{
WhsrfPassthrough pass = controller.parmSessionState().parmPass();
itemId itemId = pass.lookup(ProcessGuideDataTypeNames::ItemId);
if (controller.parmClickedAction() == #ActionOk)
{
return itemId != '');
}
return true;
}
/// <summary>
/// Refers to the page builder name
/// </summary>
/// <returns>The page builder name for the step</returns>
protected ProcessGuidePageBuilderName pageBuilderName()
{
return classStr(ProcessGuidePromptItemIdPageBuilder);
}
/// <summary>
/// Puts the control validation for the step
/// </summary>
protected void validateControls()
{
if (controller.parmClickedAction() == #ActionOK)
{
//Do validations
}
super();
}
protected void doExecute()
{
super();
WhsrfPassthrough pass = controller.parmSessionState().parmPass();
if (!processingResult.isErrorState
&& controller.parmClickedAction() == #ActionOK)
{
this.createStagingTable(pass);
}
}
private RefRecId createStagingTable(WhsrfPassthrough _pass)
{
//create logic for populating staging records
}
}
The process guide class refer to a pageBuilder that looks like this:
[ProcessGuidePageBuilderName(classStr(ProcessGuidePromptItemIdPageBuilder))]
internal final class ProcessGuidePromptItemIdPageBuilder extends ProcessGuidePageBuilder
{
/// <summary>
/// Adds the data control to the page
/// </summary>
/// <param name = "_page">The page in which the controls are to be added</param>
protected void addDataControls(ProcessGuidePage _page)
{
WhsrfPassthrough pass = controller.parmSessionState().parmPass();
_page.addTextBox(ProcessGuideDataTypeNames::ItemId, "Item Id", extendedTypeNum(ItemId));
_page.addTextBox(ProcessGuideDataTypeNames::VendAcount, "Vendor", extendedTypeNum(vendAccount), this);
}
protected void addActionControls(ProcessGuidePage _page)
{
#ProcessGuideActionNames
_page.addButton(step.createAction(#ActionOK), true);
WhsrfPassthrough pass = controller.parmSessionState().parmPass();
if (pass.exists(PurchMobileAppConstants::PurchId) &&
(/*Logic to see if more than one item exists in the cart*/))
{
_page.addButton(step.createAction(#ActionCompleteOrder));
}
_page.addButton(step.createAction(#ActionCancelExitProcess));
}
private VendAccount getVendAccount()
{
//Logic to obtain vendor account
}
}
Consequently, the logic checks if the PO has more than one item in the plate or not, hence based on that it will make the 'Complete order' button visible.
V. Lastly you need to add a step called complete order, that should extend: ProcessGuideStepWithoutPrompt and should look like --
[ProcessGuideStepName(classStr(PurchOrderCreateGuidedCompleteOrderStep))]
internal final class PurchOrderCreateGuidedCompleteOrderStep extends ProcessGuideStepWithoutPrompt
{
#ProcessGuideActionNames
/// <summary>
/// Implies if the transaction is complete
/// </summary>
/// <returns>True if yes, false otherwise</returns>
protected boolean isComplete()
{
return true;
}
/// <summary>
/// Override for execution of the clicked event
/// </summary>
protected void doExecute()
{
super();
if (!processingResult.isErrorState
&& controller.parmClickedAction() == PurchMobileAppConstants::ActionCompleteOrder)
{
WhsrfPassthrough pass = controller.parmSessionState().parmPass();
this.updateStagingRecords(pass);
}
}
private void this.updateStagingRecords(WhsrfPassthrough _pass)
{
//updates the staging record as appropriate
this.addCompletionMessage(orderIdentifier);
}
/// <summary>
/// Adds the completion message at the end of the wizard
/// </summary>
/// <param name = "_orderIdentifier">The sales order Id</param>
private void addCompletionMessage(purchId _orderIdentifier)
{
ProcessGuideMessageData messageData = ProcessGuideMessageData::construct();
messageData.message = strFmt("Purchase order completed for %", _orderIdentifier);
messageData.level = WHSRFColorText::Success;
navigationParametersFrom = ProcessGuideNavigationParameters::construct();
navigationParametersFrom.messageData = messageData;
}
}
Yes, that completes the process. You are essentially updating the staging table records as ready to process, so that you can write a required batch job to process the records (create PO with its lines).
That's all for today. Hoping to see you again with a new blog soon -- much love and namaste, as always 💓💓💓
Comments
Post a Comment