Microsoft Dynamics AX 2012 AIF – Routes Web Service
Purpose: The purpose of this document is to illustrate how to develop custom Routes Web Service in Microsoft Dynamics AX 2012 RTM/FPK.
Challenge: Data model changes in Microsoft Dynamics AX 2012 related to high normalization and introduction of surrogate keys made some imports more complex. The data model implementing Routes business entity was significantly changed and now more tables are involved (example of resource requirements tables expansion). Microsoft Dynamics AX 2012 RTM/FPK there’s no standard Routes Web Service, however you can create custom Routes Web Service using AIF Document Service Wizard. In fact there’re also certain nuances related to number sequences, inventory dimensions, approval and activation processes, etc. when you consume custom Routes Web Service in Microsoft Dynamics AX 2012.
Solution: Microsoft Dynamics AX 2012 RTM/FPK doesn’t have standard Routes Web Service. In this walkthrough I will create custom Routes Web Service in order to import Routes data. Please also note that Microsoft Dynamics AX 2012 R2 ships with standard Routes Web Service which you can back-port to Microsoft Dynamics AX 2012 RTM/FPK and use it for import of Routes.
Assumption: Route operations and other required reference data is already created.
Data Model:
Table Name
|
Table Description
|
RouteVersion
|
The RouteVersion table contains versions for the routes defined in the RouteTable table. A route version connects to a route and additionally has parameters that define whether the route version is valid from a certain date or for a certain quantity. A route version needs to be approved to be used. Therefore, each route version record is started and approved by the property that is stored.
|
RouteTable
|
The RouteTable table contains routes, which in turn contain information that is required to guide an item through production. Each route includes operations to perform, the sequence for the operations, resources involved in completing the operations, and standard times for the setup and run of the production.
|
Route
|
The Route table contains the operations that represent the routes. Each record contains information about an operation that belongs to a route. Each operation connects to an operation stored in the RouteOprTable table.
|
InventDim
|
The InventDim table contains values for inventory dimensions.
|
RouteOpr
|
The RouteOpr table contains route operation relations. Each relation describes the parameters that will be used for a specific entity.
|
Data Model Diagram:
Walkthrough:
AxdRoutes Query
Please note that AxdRoutes Query will be used by RoutesService custom Web Service. I’ll use AIF Document Service Wizard to generate all required infrastructure objects for custom Routes Web Service
AIF Document Service Wizard
Welcome
Select document parameters
Select code generation parameters
Generate code
Completed
Project AxdRoutes
Please note that AxdRoutes Project was generated by AIF Document Service Wizard. AxdRoutes Project contains all required AIF infrastructure objects.
Compiler output 1
Please note that you will have to do some additional clean-up work after AIF Document Service Wizard generates number of classes
Resolution: Delete cacheObject, cacheRecordRecord methods in AxRouteVersion, AxRouteTable, AxRoute classes (this will take care of 6 of 7 errors)
Compiler output 2
Finally we’ll have to fix the last compilation error before we can generate CIL
Resolution: Implement setInventDim method in AxRouteVersion class (this will take care of 7th error)
AxRouteVersion.setInventDim and AxRouteVersion.updateFromInventDim Methods - Source code
protected void setInventDimId()
{
if (this.isMethodExecuted(funcname()))
{
return;
}
this.axInventDim().setInventDimId();
if (this.axInventDim().isFieldSet(fieldnum(InventDim, InventDimId)))
{
this.parmInventDimId(this.axInventDim().parmInventDimId());
}
}
|
public void updateFromInventDim()
{
//TODO delete this method if it only supports outbound actions in AIF
ttsbegin;
runState = AxBCRunState::Save;
fieldModified = new Set(Types::Integer);
this.initRecord();
this.inputStatus(InternalExternal::Internal);
this.setInventDimId();
inventDimIdDirtySaved = false;
this.validateFields();
this.validateWrite();
this.write();
routeVersion.InventDimId = InventDim::inventDimIdBlank();
this.resetInternalValues();
ttscommit;
}
|
RoutesService Web Service namespace
Generate incremental CIL
Now we can successfully generate CIL
Production Route Number number sequence
Please note that I checked 2 checkboxes in Allow user changes section (To a lower number [V] and To a higher number [V]). Doing so will allow to explicitly assign RouteId from integration client when consuming custom Routes Web Service and at the same time for business users using Microsoft Dynamics AX 2012 UI the system will still automatically assign RouteId from number sequence
Operation
Service group: AlexManufacturingServices
Please note that I created a new Service group “AlexManufacturingServices” and included custom Routes Web Service into this Service group
Inbound port: AlexManufacturingServices
Please note that Basic Inbound port will be automatically created and activated after you deploy Service group
Service reference: AlexManufacturingServices
In integration client (.NET Console application) I’ll add Service reference using WSDL URI
Integration client – Source code
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using ConsoleApplication1.ServiceReference1;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
try
{
const string RouteId = "Route_ALEXRTE1";
RoutesServiceClient client = new RoutesServiceClient();
CallContext context = new CallContext();
AxdRoutes Route = new AxdRoutes();
Route.RouteVersion = new AxdEntity_RouteVersion[1];
Route.RouteVersion[0] = new AxdEntity_RouteVersion();
Route.RouteVersion[0].RouteId = RouteId;
Route.RouteVersion[0].Name = "ALEXRTE";
Route.RouteVersion[0].ItemId = "ALEXPRODUCT";
//Route.RouteVersion[0].InventDimId = "Ax#1";
Route.RouteVersion[0].InventDim = new AxdEntity_InventDim1[1];
Route.RouteVersion[0].InventDim[0] = new AxdEntity_InventDim1();
Route.RouteVersion[0].InventDim[0].InventSiteId = "1";
Route.RouteVersion[0].RouteTable = new AxdEntity_RouteTable[1];
Route.RouteVersion[0].RouteTable[0] = new AxdEntity_RouteTable();
Route.RouteVersion[0].RouteTable[0].RouteId = RouteId;
Route.RouteVersion[0].RouteTable[0].Name = "ALEXRTE";
Route.RouteVersion[0].RouteTable[0].Route = new AxdEntity_Route[1];
Route.RouteVersion[0].RouteTable[0].Route[0] = new AxdEntity_Route();
Route.RouteVersion[0].RouteTable[0].Route[0].RouteId = RouteId;
Route.RouteVersion[0].RouteTable[0].Route[0].OprId = "ALEXOPR";
Route.RouteVersion[0].RouteTable[0].Route[0].OprNum = 10;
Route.RouteVersion[0].RouteTable[0].Route[0].OprNumNext = 0;
Route.RouteVersion[0].RouteTable[0].Route[0].OprNumNextSpecified = true;
EntityKey[] entityKey = client.create(context, Route);
Console.WriteLine("Done!");
Console.ReadLine();
}
catch (Exception ex)
{
Console.WriteLine("Error!" + ex.Message);
Console.ReadLine();
}
}
}
}
|
XML Request
<?xml version="1.0" encoding="UTF-8"?>
<RoutesServiceCreateRequest xmlns="http://schemas.microsoft.com/dynamics/2008/01/services">
<Routes xmlns="http://schemas.microsoft.com/dynamics/2008/01/documents/Routes">
<SenderId xsi:nil="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"></SenderId>
<RouteVersion class="entity">
<_DocumentHash xsi:nil="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"></_DocumentHash>
<Approver xsi:nil="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"></Approver>
<InventDimId xsi:nil="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"></InventDimId>
<ItemId>ALEXPRODUCT</ItemId>
<Name>ALEXRTE</Name>
<RouteId>Route_ALEXRTE1</RouteId>
<RouteTable class="entity">
<Approver xsi:nil="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"></Approver>
<ItemGroupId xsi:nil="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"></ItemGroupId>
<Name>ALEXRTE</Name>
<RouteId>Route_ALEXRTE1</RouteId>
<Route class="entity">
<OprId>ALEXOPR</OprId>
<OprNum>10</OprNum>
<OprNumNext>0</OprNumNext>
<RouteId>Route_ALEXRTE1</RouteId>
</Route>
</RouteTable>
<InventDim class="entity">
<ConfigId xsi:nil="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"></ConfigId>
<InventBatchId xsi:nil="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"></InventBatchId>
<InventColorId xsi:nil="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"></InventColorId>
<InventDimId xsi:nil="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"></InventDimId>
<InventLocationId xsi:nil="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"></InventLocationId>
<InventSerialId xsi:nil="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"></InventSerialId>
<InventSiteId>1</InventSiteId>
<InventSizeId xsi:nil="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"></InventSizeId>
<InventStyleId xsi:nil="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"></InventStyleId>
<WMSLocationId xsi:nil="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"></WMSLocationId>
<WMSPalletId xsi:nil="true" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"></WMSPalletId>
</InventDim>
</RouteVersion>
</Routes>
</RoutesServiceCreateRequest>
|
XML Response
<?xml version="1.0" encoding="UTF-8"?>
<RoutesServiceCreateResponse xmlns="http://schemas.microsoft.com/dynamics/2008/01/services">
<EntityKeyList xmlns="http://schemas.microsoft.com/dynamics/2006/02/documents/EntityKeyList">
<EntityKey xmlns="http://schemas.microsoft.com/dynamics/2006/02/documents/EntityKey">
<KeyData>
<KeyField>
<Field>RecId</Field>
<Value>5747147055</Value>
</KeyField>
</KeyData>
</EntityKey>
</EntityKeyList>
</RoutesServiceCreateResponse>
|
AxdRoutes.prepareForSaveExtended Method – Source code
public boolean prepareForSaveExtended(
AxdStack _axBcStack,
str _dataSourceName,
AxdRecordProcessingContext _recordProcessingContext,
AxInternalBase _childRecord)
{
AxRouteVersion axbc_RouteVersion;
AxRouteTable axbc_RouteTable;
AxRoute axbc_Route;
AxInventDim axbc_InventDim;
//TODO: Add code here to ensure that required fields specified in the initMandatoryFieldsMap method are sent in by the service caller.
switch (_dataSourceName)
{
// ----------------------------------------------------------------------
// Process RouteVersion records
// ----------------------------------------------------------------------
case #RouteVersion_DataSourceName:
axbc_RouteVersion = _axBcStack.top();
//TODO Put validating code for RouteVersion here
switch (_recordProcessingContext)
{
// Propagate children's keys
case AxdRecordProcessingContext::AfterChildRecordProcessed:
switch (_childRecord.dataSourceName())
{
case #InventDim_DataSourceName:
//TODO Validate relations
axbc_InventDim = _childRecord;
axbc_RouteVersion.parmInventDimId(axbc_InventDim.parminventDimId());
break;
case #RouteTable_DataSourceName:
//TODO Validate relations
axbc_RouteTable = _childRecord;
axbc_RouteVersion.parmRouteId(axbc_RouteTable.parmRouteId());
break;
}
return false;
// Ensure RouteVersion record is saved
case AxdRecordProcessingContext::AfterAllChildRecordsProcessed:
if (!axbc_RouteVersion.isProcessed())
{
return true;
}
return false;
}
return false;
// ----------------------------------------------------------------------
// Process RouteTable records
// ----------------------------------------------------------------------
case #RouteTable_DataSourceName:
axbc_RouteTable = _axBcStack.top();
//TODO Put validating code for RouteTable here
switch (_recordProcessingContext)
{
// Ensure RouteTable record is saved before any of its dependent child records
case AxdRecordProcessingContext::BeforeChildRecordProcessed:
switch (_childRecord.dataSourceName())
{
case #Route_DataSourceName:
if (!axbc_RouteTable.isProcessed())
{
return true;
}
break;
}
return false;
// Ensure RouteTable record is saved
case AxdRecordProcessingContext::AfterAllChildRecordsProcessed:
if (!axbc_RouteTable.isProcessed())
{
return true;
}
return false;
}
return false;
// ----------------------------------------------------------------------
// Process Route records
// ----------------------------------------------------------------------
case #Route_DataSourceName:
axbc_Route = _axBcStack.top();
//TODO Put validating code for Route here
switch (_recordProcessingContext)
{
// Propagate parent's key
case AxdRecordProcessingContext::BeforeAnyChildRecordsProcessed:
//TODO Validate relations
axbc_RouteTable = axbc_Route.parentAxBC();
axbc_Route.parmRouteId(axbc_RouteTable.parmRouteId());
return false;
// Ensure Route record is saved
case AxdRecordProcessingContext::AfterAllChildRecordsProcessed:
if (!axbc_Route.isProcessed())
{
return true;
}
return false;
}
return false;
// ----------------------------------------------------------------------
// Process InventDim records
// ----------------------------------------------------------------------
case #InventDim_DataSourceName:
axbc_InventDim = _axBcStack.top();
//TODO Put validating code for InventDim here
switch (_recordProcessingContext)
{
// Ensure InventDim record is saved
case AxdRecordProcessingContext::AfterAllChildRecordsProcessed:
if (!axbc_InventDim.isProcessed())
{
return true;
}
return false;
}
return false;
// ----------------------------------------------------------------------
// Unsupported data sources
// ----------------------------------------------------------------------
default:
error(strfmt("@SYS88979",classId2Name(classidget(_axBcStack.top()))));
return false;
}
return false;
}
|
Remark: The prepareForSaveExtended method is called immediately before saving the top node of the stack
Please note that prepareForSaveExtended method usually contains validation business logic or additional business logic which must be executed before the record is saved
Microsoft Dynamics AX 2012 Warning
Please note with AIF infrastructure classes automatically generated by AIF Document Service Wizard in case you try to provide RouteVersion.InventDimId field value explicitly you will see the warning above. In order to overcome this warning you can specify the contents of InventDim buffer instead.
Result
Microsoft Dynamics AX 2012 – Routes (Versions)
Microsoft Dynamics AX 2012 – Routes (Lines)
Infolog
Please note that after import of Routes some of fields are still not specified. This is because we didn’t include all required data sources into the query. For example, Route group field belongs to RouteOpr table. In order to be able to specify all necessary values the query supporting custom Routes Web Service will have to be extended. Every time you change Web Service query you will have to run AIF Document Update Wizard to regenerate AIF infrastructure objects supporting Web Service. Please refer to walkthrough about standard Routes Web Service in Microsoft Dynamics AX 2012 R2 for more details about complete query required for Routes import. In this walkthrough I’ll keep the query as it is for the sake of simplicity and we can assign missing values in AxdRoutes.updateNow method.
Aside from Route group setting please note that the following pieces of information are also missing for Routes because of incomplete query: Times settings, Cost categories settings and Resource requirements settings
This is how required minimum setup should look like
General tab
Setup tab
Times tab
Resource requirements tab
Microsoft Dynamics AX 2012 – Routes (Approve/Activate)
Please note that Routes Approval and Activation processes are implemented in Microsoft Dynamics AX 2012 UI as dedicated functions which also allow to specify who is performing an action (Approver). That’s why you are not supposed to approve or activate Route Version, or approve Route when consuming Routes Web Service. Please note that I didn’t include any related business logic in AxdRoutes.prepareForSaveExtended method yet
However you can implement automatic Approval and Activation without modifying AxdRoutes.prepareForSaveExtended method. This can be done by overriding AxdRoutes.updateNow method where you can use Routes Approval and Activation API
public void updateNow()
{
RouteApprove routeApprove;
BOMRouteVersionApprove versionApprove;
BOMRouteVersionActivate versionActivate;
super();
routeApprove = RouteApprove::newRouteTable(RouteTable));
routeApprove.parmRemove(NoYes::No);
routeApprove.parmApprover(HcmWorker::findByPersonnelNumber("000131").RecId);
routeApprove.run();
versionApprove = BOMRouteVersionApprove::newRouteVersion(RouteVersion);
versionApprove.parmRemove(NoYes::No);
versionApprove.parmApprover(HcmWorker::findByPersonnelNumber("000131").RecId);
versionApprove.run();
versionActivate = BOMRouteVersionActivate::newRouteVersion(RouteVersion);
versionActivate.run();
}
|
Please note that Approver field in RouteVersion and RouteTable tables is RecID reference to HCMWorker table. This is how you can easily convert current AX user into appropriate RecID reference in HCMWorker table
HcmWorker::userId2Worker(curuserid());
|
In order to assign missing values for Routes we can further extend AxdRoutes.updateNow method
Summary: In this walkthrough I demonstrated how to consume custom Routes Web Service for import of Routes. We also discussed how AIF Document Wizard can be used to generate AIF infrastructure objects supporting Web Service and the fact that if query is changed you have to run AIF Document Update Wizard to update AIF infrastructure objects appropriately. Please consider back-porting of standard Routes Web Service available in Microsoft Dynamics AX 2012 R2 for import of Routes in Microsoft Dynamics AX 2012 RTM/FPK.
Author: Alex Anikiiev, PhD, MCP
Tags: Microsoft Dynamics ERP, Microsoft Dynamics AX 2012, Data Import, Data Conversion, Data Migration, Application Integration Framework, Routes.
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 concepts and describe the examples.
No comments:
Post a Comment