Microsoft Dynamics AX 2012 Warehouse Management RF Interface
Purpose: The purpose of this document is to illustrate how to extend Warehouse Management RF Interface in Microsoft Dynamics AX 2012 R3.
Challenge: Microsoft Dynamics AX 2012 R3 ships with a wide array of Warehouse Management features such as workflows, cluster picking, wave processing, inventory control, containerization, mobile devices, deferred reservation strategy, work, integration with quality control module, etc. Please find more info about Warehouse management features in Microsoft Dynamics AX 2012 R3 here: http://technet.microsoft.com/EN-US/library/dn716029.aspx. Often times for the purposes of POC or implementation you would need to automate certain business processes or introduce a new business process according to company’s specifics and make them available on mobile devices.
Solution: Microsoft Dynamics AX 2012 R3 provides an extensible Warehouse Management RF Interface. You can leverage framework classes to quickly build your own RF User Interface and the power of X++ to quickly implement/automate required business processes. As the result employees on warehouse floor will be able to take advantage of these business processes using mobile devices.
Scenario
Microsoft Dynamics AX 2012 R3 Warehouse Management introduces a concept of Work – a set of instructions (picks and puts) to move goods from one location to another location within a warehouse. Work instructions can be generated in advance or on the fly depending on business process. Then employees on warehouse floor can use mobile devices to execute these instructions. In some situations it would be nice to be able to execute a set of instructions at once without going through each instruction execution individually. In this particular walkthrough I’ll want to automate “Complete work” feature available on Work screen which allows the user to complete all instructions associated with particular Work ID at once, and make it available on mobile device.
I’ll start with some work generated which I want to be able to execute at once using mobile device. In my case this is Sales picking work which consists of 2 instructions for simplicity
Work
Using “Complete Work” feature in standard Microsoft Dynamics AX 2012 R3 you have to select a User and then Validate work to make sure there’re no errors before you Complete work
Work completion
In case there’re errors after validation step you will have to correct them before you can Complete Work. Often times these errors are associated with missing data which have to be entered in conjunction with setup of the system. For example, in my case I need to specify Target license plate ID for Pick instruction before I can Complete Work
Infolog
This validation business logic is included in \Classes\WHSWorkManualComplete\Methods\validateWork
In case I want to automate Complete Work feature I will need to find the way to check work instructions for errors programmatically and then automatically assign missing Target license plates to appropriate work instructions. In standard Microsoft Dynamics AX 2012 R3 on Work completion screen you can also select qualified License plates from lookup for work instructions. Lookup business logic is included in \Forms\WHSWorkComplete\Designs\Design\[Group:Lines]\[Grid:WorkLines]\StringEdit:WHSTmpCompleteWorkLine_TargetLicensePlateId\Methods\lookup which gives me a clue of how to implement required automation
Work completion – Target license plate lookup
For the sake of simplicity I’ll assume that a qualified license plate will already exist in the system and I just need to be able to automatically select it for work instruction when needed
License plate
Inventory transactions
For the purposes of my automation I’ll simply select the first qualified license plate present in the system. When all work looks good validation result will be “All work is valid”
Infolog
After that Complete work button becomes available in standard Microsoft Dynamics AX 2012 R3 on Work completion screen
Work completion
When work is completed you will be a message like this
Infolog
As the result all work instructions belonging to particular Work ID will be executed and closed
Work
My ultimate goal in this walkthrough is to be able to Complete work automatically using mobile device
I’ll start off with a simple job to Complete work based on Work ID
Source code
static void AlexCompleteWorkJob(Args _args)
{
#define.UserId("24")
#define.WorkId("USMF-000055")
Set setWorkHeaders = new Set(Types::Record);
WHSWorkManualComplete manualComplete;
WHSWorkCompleteForm workCompleteForm;
WHSTmpCompleteWorkTable tmpCompleteWorkTable;
WHSTmpCompleteWorkLine tmpCompleteWorkLine;
WHSWorkTable workTable;
WHSTargetLicensePlateId targetLicensePlateId;
WHSTargetLicensePlateId targetLicensePlateId(WHSTmpCompleteWorkLine _tmpCompleteWorkLine)
{
InventDim _inventDim, inventDim;
InventSum inventSum;
_inventDim = InventDim::find(_tmpCompleteWorkLine.InventDimId);
_inventDim.InventBatchId = _tmpCompleteWorkLine.InventBatchId;
_inventDim.wmsLocationId = _tmpCompleteWorkLine.wmsLocationId;
_inventDim.LicensePlateId = _tmpCompleteWorkLine.TargetLicensePlateId;
select firstOnly inventDim
join inventSum
where inventSum.InventDimId == inventDim.InventDimId &&
inventDim.InventLocationId == _inventDim.InventLocationId &&
inventDim.wmsLocationId == _inventDim.wmsLocationId &&
inventDim.InventColorId == _inventDim.InventColorId &&
inventDim.InventSizeId == _inventDim.InventSizeId &&
inventDim.ConfigId == _inventDim.ConfigId &&
inventDim.InventBatchId == _inventDim.InventBatchId &&
inventDim.InventStatusId == _inventDim.InventStatusId &&
inventDim.LicensePlateId != "" &&
inventSum.ItemId == _tmpCompleteWorkLine.ItemId &&
inventSum.PhysicalInvent == _tmpCompleteWorkLine.InventQtyRemain;
return inventDim.LicensePlateId;
}
workTable = WHSWorkTable::find(#WorkId);
setWorkHeaders.add(workTable);
if (!setWorkHeaders.empty())
{
manualComplete = new WHSWorkManualComplete();
workCompleteForm = new WHSWorkCompleteForm();
workCompleteForm.initFromSet(setWorkHeaders,
tmpCompleteWorkTable,
tmpCompleteWorkLine);
while select tmpCompleteWorkTable
{
while select forUpdate tmpCompleteWorkLine
where tmpCompleteWorkLine.WorkId == tmpCompleteWorkTable.WorkId
&& (tmpCompleteWorkLine.WorkStatus == WHSWorkStatus::Open
|| tmpCompleteWorkLine.WorkStatus == WHSWorkStatus::InProcess)
{
if ( (tmpCompleteWorkTable.WorkTransType == WHSWorkTransType::ProdPick
|| tmpCompleteWorkTable.WorkTransType == WHSWorkTransType::KanbanPick
|| tmpCompleteWorkTable.WorkTransType == WHSWorkTransType::Sales
|| tmpCompleteWorkTable.WorkTransType == WHSWorkTransType::TransferIssue
|| tmpCompleteWorkTable.WorkTransType == WHSWorkTransType::Replenishment)
&& tmpCompleteWorkLine.WorkType == WHSWorkType::Pick
&& !tmpCompleteWorkLine.isPutBefore()
&& WMSLocation::find(tmpCompleteWorkLine.wmsLocationId, tmpCompleteWorkTable.InventLocationId).whsLocationIsLPControlled())
{
if (!tmpCompleteWorkLine.TargetLicensePlateId)
{
//"@WAX2651"
targetLicensePlateId = targetLicensePlateId(tmpCompleteWorkLine);
if (targetLicensePlateId)
{
tmpCompleteWorkLine.TargetLicensePlateId = targetLicensePlateId;
tmpCompleteWorkLine.doUpdate();
}
}
else if (WHSLicensePlate::find(tmpCompleteWorkLine.TargetLicensePlateId).getTotalQtyOnLicensePlate(tmpCompleteWorkLine.TargetLicensePlateId) != tmpCompleteWorkLine.InventQtyRemain)
{
//@WAX4149"
}
}
}
while select forupdate tmpCompleteWorkLine
where tmpCompleteWorkLine.WorkId == tmpCompleteWorkTable.WorkId &&
tmpCompleteWorkLine.wmsLocationId == ''
{
//"@WAX2649"
}
}
if (manualComplete.validateWork(#UserId, tmpCompleteWorkTable, tmpCompleteWorkLine))
{
while select tmpCompleteWorkTable
{
manualComplete.executeWork(#UserId, tmpCompleteWorkTable, tmpCompleteWorkLine);
info(strFmt("@WAX2663", tmpCompleteWorkTable.WorkId));
}
delete_from tmpCompleteWorkTable;
}
}
else
{
warning("@WAX4778");
}
}
|
In this job I simply automated processes which take place when you Complete work using Work completion form (in User interface). Please note that targetLicensePlateId function was introduced to automatically assign missing Target license plates for work instructions just like you would normally do using Target license plate lookup on Work completion form (in User interface)
Okay, this is good, now it is time to make “Complete Work” feature available on mobile device
Microsoft Dynamics AX 2012 R3 provides RF Interface via IIS-hosted Web site (Warehouse mobile devices Portal) which is suited for industrial RF guns or any other devices supporting communication through HTTP/HTTPS. For the purposes of testing X++ form based emulator is also available (Menu Items > Action > WHSWorkExecute), please note that when using X++ form based emulator you can debug you code in X++ Debugger.
RF Interface in Microsoft Dynamics AX 2012 R3 is implemented using State machine principles. This means that when you implement a new business process you will have to identify screens a user will go through using mobile device (states) and circumstances under which one screen will change to another screen (transitions)
For example, in my “Complete Work” scenario I can represent my State machine as described below
Please note that circles represent states and arrows represent transitions
The idea is simple: I will first ask for Work ID with is associated with work instruction I work to execute. If Work ID provided is valid I’ll show Work ID and let the user confirm (kind of “Are you sure?”) the action, otherwise if Work ID is invalid I’ll display an error and ask for Work ID again. In case of valid Work ID after the user confirms the action I’ll programmatically execute work and display confirmation. After that I’ll ask for a new Work ID and so on and so forth.
At the very beginning I’ll need to introduce my “Complete work” business process as a new mobile device menu item
Mobile device menu items
Please note that for the sake of simplicity I added my menu item with Mode = Indirect
And then I can add this menu item to mobile device menu
Mobile device menu
I decided to place this menu item into Inventory section of mobile device menu
In order to add my “Complete work” business process I needed to modify WHSWorkExecuteMode and WHSWorkActivity base enums by adding new elements to the end
WHSWorkExecuteMode Base enum
WHSWorkActivity Base enum
After that I’ll introduce a new class called WHSWorkExecuteDisplayAlexCompleteWork which extends WHSWorkExecuteDisplay class. WHSWorkExecuteDisplay classes hierarchy is specifically responsible for mobile device user experience.
WHSWorkExecuteDisplay class
static WHSWorkExecuteDisplay construct(WHSWorkExecuteMode _mode)
{
switch (_mode)
{
case WHSWorkExecuteMode::Menu : return WHSWorkExecuteDisplayMenu::construct();
…
case WHSWorkExecuteMode::ChangeBatchDisposition : return WHSWorkExecuteDisplayChangeBatchDisp::construct();
//alex:>>
case WHSWorkExecuteMode::AlexCompleteWork : return WHSWorkExecuteDisplayAlexCompleteWork::construct();
//alex:<<
default : throw error("@WAX1238");
}
}
|
Please note that I modified construct method of WHSWorkExecuteDisplay class to support my custom business process.
Now I’ll implement WHSWorkExecuteDisplayAlexCompleteWork class which automates “Complete Work” feature
Source code
class WhsWorkExecuteDisplayAlexCompleteWork extends WHSWorkExecuteDisplay
{
}
|
container buildDoAlexWorkId(container _con, WHSWorkId _workId)
{
xSession session = new xSession();
container ret = _con;
ret += [this.buildControl(#RFText, #WorkId, "@WAX273", 1, _workId, extendedTypeNum(WHSWorkId), '', 0, false)];
ret += [this.buildControl(#RFButton, #RFOK, "@SYS5473", 1, '', #WHSRFUndefinedDataType, '', 1)];
return ret;
}
|
container buildGetAlexWorkId(container _con)
{
xSession session = new xSession();
container ret = _con;
ret = this.buildGetWorkId(ret);
return ret;
}
|
container displayForm(container _con, str _buttonClicked = '')
{
WHSWorkExecute workExecute = new WHSWorkExecute();
container ret = connull();
container con = _con;
int hasError = 0;
int startInfologLine;
pass = WHSRFPassthrough::create(conPeek(_con, 2));
hasError = this.hasError(_con);
if (pass.exists(#UserId))
{
userId = pass.lookup(#UserId);
}
startInfologLine = infologLine() + 1;
switch (step)
{
case 0:
ret = this.buildGetAlexWorkId(ret);
step = 1;
break;
case 1:
try
{
pass.insert(#WorkId, conPeek(conPeek(con, 4 + hasError), #data));
if (!workExecute.validateWorkIdInSystem(pass.lookup(#WorkId)))
{
throw error("@WAX1081");
}
ret = this.buildDoAlexWorkId(ret, pass.lookup(#WorkId));
step = 2;
}
catch
{
ret = this.addErrorLabelFromInfolog(ret, startInfologLine, WHSRFColorText::Error);
ret = this.buildGetAlexWorkId(ret);
}
break;
case 2:
try
{
this.executeWork(pass.lookup(#WorkId), userId);
ret = this.addErrorLabel(ret, "@WAX866", WHSRFColorText::Success);
}
catch
{
ret = this.addErrorLabel(ret, "@WAX866", WHSRFColorText::Error);
}
ret += [this.buildControl(#RFLabel, #WorkId, strFmt("Work Id: %1", pass.lookup(#WorkId)), 1, '', #WHSRFUndefinedDataType, '', 0)];
ret += [this.buildControl(#RFButton, #RFOK, "@SYS5473", 1, '', #WHSRFUndefinedDataType, '', 1)];
step = 3;
break;
case 3:
pass = this.resetPassthrough(ret, false);
ret = this.buildGetAlexWorkId(ret);
step = 1;
break;
default:
break;
}
ret = this.updateModeStepPass(ret, WHSWorkExecuteMode::AlexCompleteWork, step, pass);
ret = this.addCancelButton(ret, 1, true);
return ret;
}
|
void executeWork(WHSWorkId _workId, WHSUserId _userId)
{
Set setWorkHeaders = new Set(Types::Record);
WHSWorkManualComplete manualComplete;
WHSWorkCompleteForm workCompleteForm;
WHSTmpCompleteWorkTable tmpCompleteWorkTable;
WHSTmpCompleteWorkLine tmpCompleteWorkLine;
WHSWorkTable workTableLocal;
WHSTargetLicensePlateId targetLicensePlateId;
WHSTargetLicensePlateId targetLicensePlateId(WHSTmpCompleteWorkLine _tmpCompleteWorkLine)
{
InventDim _inventDim, inventDim;
InventSum inventSum;
_inventDim = InventDim::find(_tmpCompleteWorkLine.InventDimId);
_inventDim.InventBatchId = _tmpCompleteWorkLine.InventBatchId;
_inventDim.wmsLocationId = _tmpCompleteWorkLine.wmsLocationId;
_inventDim.LicensePlateId = _tmpCompleteWorkLine.TargetLicensePlateId;
select firstOnly inventDim
join inventSum
where inventSum.InventDimId == inventDim.InventDimId &&
inventDim.InventLocationId == _inventDim.InventLocationId &&
inventDim.wmsLocationId == _inventDim.wmsLocationId &&
inventDim.InventColorId == _inventDim.InventColorId &&
inventDim.InventSizeId == _inventDim.InventSizeId &&
inventDim.ConfigId == _inventDim.ConfigId &&
inventDim.InventBatchId == _inventDim.InventBatchId &&
inventDim.InventStatusId == _inventDim.InventStatusId &&
inventDim.LicensePlateId != "" &&
inventSum.ItemId == _tmpCompleteWorkLine.ItemId &&
inventSum.PhysicalInvent == _tmpCompleteWorkLine.InventQtyRemain;
return inventDim.LicensePlateId;
}
workTableLocal = WHSWorkTable::find(_workId);
setWorkHeaders.add(workTableLocal);
if (!setWorkHeaders.empty())
{
manualComplete = new WHSWorkManualComplete();
workCompleteForm = new WHSWorkCompleteForm();
workCompleteForm.initFromSet(setWorkHeaders,
tmpCompleteWorkTable,
tmpCompleteWorkLine);
while select tmpCompleteWorkTable
{
while select forUpdate tmpCompleteWorkLine
where tmpCompleteWorkLine.WorkId == tmpCompleteWorkTable.WorkId
&& (tmpCompleteWorkLine.WorkStatus == WHSWorkStatus::Open
|| tmpCompleteWorkLine.WorkStatus == WHSWorkStatus::InProcess)
{
if ( (tmpCompleteWorkTable.WorkTransType == WHSWorkTransType::ProdPick
|| tmpCompleteWorkTable.WorkTransType == WHSWorkTransType::KanbanPick
|| tmpCompleteWorkTable.WorkTransType == WHSWorkTransType::Sales
|| tmpCompleteWorkTable.WorkTransType == WHSWorkTransType::TransferIssue
|| tmpCompleteWorkTable.WorkTransType == WHSWorkTransType::Replenishment)
&& tmpCompleteWorkLine.WorkType == WHSWorkType::Pick
&& !tmpCompleteWorkLine.isPutBefore()
&& WMSLocation::find(tmpCompleteWorkLine.wmsLocationId, tmpCompleteWorkTable.InventLocationId).whsLocationIsLPControlled())
{
if (!tmpCompleteWorkLine.TargetLicensePlateId)
{
//"@WAX2651"
targetLicensePlateId = targetLicensePlateId(tmpCompleteWorkLine);
if (targetLicensePlateId)
{
tmpCompleteWorkLine.TargetLicensePlateId = targetLicensePlateId;
tmpCompleteWorkLine.doUpdate();
}
}
else if (WHSLicensePlate::find(tmpCompleteWorkLine.TargetLicensePlateId).getTotalQtyOnLicensePlate(tmpCompleteWorkLine.TargetLicensePlateId) != tmpCompleteWorkLine.InventQtyRemain)
{
//@WAX4149"
}
}
}
while select forupdate tmpCompleteWorkLine
where tmpCompleteWorkLine.WorkId == tmpCompleteWorkTable.WorkId &&
tmpCompleteWorkLine.wmsLocationId == ''
{
//"@WAX2649"
}
}
if (manualComplete.validateWork(_userId, tmpCompleteWorkTable, tmpCompleteWorkLine))
{
while select tmpCompleteWorkTable
{
manualComplete.executeWork(_userId, tmpCompleteWorkTable, tmpCompleteWorkLine);
info(strFmt("@WAX2663", tmpCompleteWorkTable.WorkId));
}
delete_from tmpCompleteWorkTable;
}
}
else
{
warning("@WAX4778");
}
}
|
protected void new()
{
}
|
static WHSWorkExecuteDisplayAlexCompleteWork construct()
{
WHSWorkExecuteDisplayAlexCompleteWork workExecuteDisplayAlexCompleteWork;
workExecuteDisplayAlexCompleteWork = new WHSWorkExecuteDisplayAlexCompleteWork();
return workExecuteDisplayAlexCompleteWork;
}
|
Let’s briefly discuss the source code above
buildGetAlexWorkId method is designed to display a control for user to enter Work ID
As you noticed buildGetAlexWorkId method calls standard buildGetWorkId method of WHSWorkExecuteDisplay class (\Classes\WHSWorkExecuteDisplay\Methods\buildGetWorkId)
Source code
container buildGetWorkId(container _con, str _extraText = '')
{
container ret = _con;
str finalLabel = _extraText ? _extraText : "@WAX729"; // Scan a Work Id
;
ret += [this.buildControl(#RFLabel, #Scan, finalLabel, 1, '', #WHSRFUndefinedDataType, '', 0)];
ret += [this.buildControl(#RFText, #WorkId, "@WAX273", 1, '', extendedTypeNum(WHSWorkId), '', 0)];
ret += [this.buildControl(#RFButton, #RFOK, "@SYS5473", 1, '', #WHSRFUndefinedDataType, '', 1)];
return ret;
}
|
Methods like this (for example, buildGetLicensePlateId, etc.) are already implemented in the framework that’s why it makes sense to use them when appropriate. You can also add new code in addition to using standard methods from parent class
buildDoAlexWorkId method is designed to display a greyed out control with entered valid Work ID
Also I added some new validation logic in \Classes\WHSWorkExecute\validateWorkIdInSystem to verify that Work ID entered does exist in the system. This method resembles another standard method \Classes\WHSWorkExecute\validateLicensePlateInSystem
Source code
boolean validateWorkIdInSystem(WHSWorkId _workId)
{
return WHSWorkTable::find(_workId).RecId != 0;
}
|
displayForm is a core method which implements all states (mobile device forms). Each state is associated with step, for each step there’s a next step (transition path) defined. Context is passed between states by means of container (con) and variables are passed by means of Map (pass). Cyclic flow is implemented with help of the code highlighted with Blue when I move over to step 1 after step 3. Code highlighted with Yellow is a required prerequisite code to work with pass Map, identify userId, add default “Cancel” button, etc. And I also highlighted with Green method calls required for my business process organization.
executeWork method is simply a wrapper for AlexCompleteWorkJob source code in order to execute all work instructions associated with particular Work ID
In this example I showcased how to use standard framework methods (buildGetWorkId, buildGetAlexWorkId) and write your own code (buildDoAlexWorkId and directly inside of displayForm) to implement mobile device user interface controls
After we are done with coding we need to Generate Incremental CIL
Now we are ready to test our “Complete Work” process on mobile device using X++ form based emulator
Login
Inventory
Alex Complete Work
For the Happy path we’ll enter existing valid Work ID
Scan a Work ID
Work ID
Work completed
Then we are into a next cycle of execution until we press “Cancel” button to exit
Scan a Work ID
To illustrate Error flow we’ll enter non-existing Work ID to generate an error
Scan a Work ID
The entry is not valid
You can also execute the same process in a Web Browser by navigating to http://localhost:8300/Execute/Display (URL on my Demo VM)
Sign in to AX
Inventory
Alex Complete Work
Scan a Work ID
Work ID
Work completed
Moreover if I connect my Demo VM to the internet using external Virtual Switch I can even execute this process on my Nokia Windows Phone. The only thing you need is to substitute localhost to IP address in Warehouse Mobile Devices Portal URL
Work completed
As the result all work instructions belonging to Work ID will be executed and closed, just like I’d do that on Work completion for in Microsoft Dynamics AX 2012 R3
Result
Summary: This document describes how extend Warehouse Management RF Interface in Microsoft Dynamics AX 2012 R3. You can effectively automate business processes and introduce a new business processes available on mobile devices in Microsoft Dynamics AX 2012 R3 by using robust and extensible RF Interface framework.
Tags: Microsoft Dynamics AX 2012 R3, Warehouse Management, WMS, Work, Pick, Put, Mobile Devices, RF Interface, RFID, IIS, X++, Process Automation, State machine.
Note: This document is intended for information purposes only, presented as it is with no warranties from the author. This document may be updated with more content to better outline the issues and describe the solutions.
Author: Alex Anikiev, PhD, MCP
Epic work. I can only recommend to make sure that before running this scenario (completing work) you have the items on a license plate with quantity, equal to the work ID.
ReplyDeletehi,
ReplyDeleteHow install and used x++ debug emulator ?
I have visual studio installed with ax tools and process attached to aos server with transport remote (no authentification).
I don't know how debug warehouse code.
Thanx
Thanks for your post. I’ve been thinking about writing a very comparable post over the last couple of weeks, I’ll probably keep it short and sweet and link to this instead if thats cool. Thanks.customlicenseplates.info
ReplyDeleteRandom question,
ReplyDeleteDo you know where the numbers on the buttons are being set?
Enjoyed your post. I knew this process (after being trained) but I feel you did a great job explaining the process and also adding visuals. I would love to see more of your posts for other processes in Dynamics AX.
ReplyDeleteExcellent blog post & thanks for sharing with us. To get more information about Microsoft Dynamics AX Support, Upgrade, Implementations, Visit at Microsoft Dynamics AX Manufacturing
ReplyDelete
ReplyDeleteNice blogpost and thanks for sharing such useful information about Microsoft Dynamics. If you are looking for how microsoft dynamics AX can help in your business growth then you should visit at Dynamics AX Partner Australia. and get Get help on your Microsoft Dynamics AX solution from our qualified experts.
Great blogspost and thanks for sharing such useful information about Microsoft Dynamics. if you are looking for microsoft jobs then visit:
ReplyDeletemicrosoft dynamic