Monday, March 18, 2013

Microsoft Dynamics AX 2012 AIF - Routes Web Service

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:

<![if !vml]><![endif]>
 
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