lunes, 11 de julio de 2016

overwriteSystemfields, modifiedDateTime, and more!

Gosh.  Such a simple task that takes such a long time.
The simple requisit is this.  At the moment that a user updates a referenced field to a sales order we are required to update the modifiedDateTime field on the sales order and associated invoices.  Nothing more.  The reason we are doing this, in a similar scenario to one stated in this DUG forum, is that we have an integration with SalesForce that requires all sales orders and associated invoices to be updated and we are using the modifiedDateTime field as our integration control for these entities.
Activating the ModifiedDateTime field on the table properties
Update the table properties for the ModifiedDateTime and in theory it should work...  But today we found an example in CustInvoiceJour table where it wasn't updating and we weren't the only ones to notice this.  Our suspicion is something to do with the layer that the change has been executed on, CUS, and that we work from VAR.  It's the only reason we can think of.

Therefore we opted for the overwriteSystemfields method on the buffer to be able to overwrite the value we required...  But this only works if executed on the server, requires us to set permissions, and the killer is that it only works on insert operations.
static server void ZZZupdateSalesTableRefs(SalesExportReason _newReason, SalesExportReason _oldReason)
{
    SalesTable              salesTable;
    CustInvoiceJour         custInvoiceJour;
    CustInvoiceSalesLink    custInvoiceLink;
    ;
    if (_newReason != _oldReason)
    {
        select count(RecId) from salesTable where salesTable.ExportReason == _oldReason;
        if (salesTable.RecId)
        {
            ttsBegin;
            new OverwriteSystemfieldsPermission().assert();

            while select forUpdate salesTable where salesTable.ExportReason == _oldReason
            {
                salesTable.ExportReason = _newReason;
                salesTable.doUpdate();              // Automagically updates modifiedDateTime...

                while select forUpdate custInvoiceJour
                    exists join custInvoiceLink
                    where custInvoiceLink.origSalesId == salesTable.SalesId
                        && custInvoiceLink.salesId == custInvoiceJour.SalesId
                        && custInvoiceLink.invoiceId == custInvoiceJour.InvoiceId
                        && custInvoiceLink.invoiceDate == custInvoiceJour.InvoiceDate
                {
                    //custInvoiceJour.modifiedDateTime = DateTimeUtil::utcNow(); //Compiler error!
                    custInvoiceJour.overwriteSystemfields(true);
                    custInvoiceJour.(fieldNum(CustInvoiceJour,modifiedDateTime)) = DateTimeUtil::utcNow();
                    custInvoiceJour.doUpdate();     // Only works on Insert operations
                }
            }
            CodeAccessPermission::revertAssert();
            ttsCommit;
        }
    }
}

We tried creating a UserConnection but updating the CustInvoiceJour record still would not fire the update to the modifiedDateTime value.

It's time to jump back about 10 years and use direct SQL! This time we have added a new utcDateTime field to the CustInvoiceJour table, which should be sufficient for most use cases, and then we launch a SQL script overwriting the modifiedDateTime value. It's horrible!
static server void ZZZupdateSalesTableRefs(SalesExportReason _newReason, SalesExportReason _oldReason)
{
    SalesTable              salesTable;
    CustInvoiceJour         custInvoiceJour;
    CustInvoiceSalesLink    custInvoiceLink;
    Connection              connection;
    Statement               statement;
    str                     query;
    boolean                 updateInvoice;
    ;

    if (_newReason != _oldReason)
    {
        select count(RecId) from salesTable where salesTable.ExportReason == _oldReason;
        if (salesTable.RecId)
        {
            ttsBegin;

            while select forUpdate salesTable where salesTable.ExportReason == _oldReason
            {
                salesTable.ExportReason = _newReason;
                salesTable.doUpdate();

                updateInvoice = false;
                while select forUpdate custInvoiceJour
                    exists join custInvoiceLink
                    where custInvoiceLink.origSalesId == salesTable.SalesId
                        && custInvoiceLink.salesId == custInvoiceJour.SalesId
                        && custInvoiceLink.invoiceId == custInvoiceJour.InvoiceId
                        && custInvoiceLink.invoiceDate == custInvoiceJour.InvoiceDate
                {
                    custInvoiceJour.ZZZModifiedDateTime = DateTimeUtil::utcNow(); // New field
                    custInvoiceJour.doUpdate();     // Does not update modifiedDateTime field
                    updateInvoice = true;
                }

                if (updateInvoice)
                {
                    query = strFmt(@"
                    UPDATE CustInvoiceJour
                    SET CustInvoiceJour.ModifiedDateTime = CustInvoiceJour.ZZZModifiedDateTime
                    FROM CustInvoiceJour
                    INNER JOIN CustInvoiceSalesLink
                        ON CustInvoiceSalesLink.origSalesId = '%2'
                        AND CustInvoiceSalesLink.DataAreaId = '%1'
                        AND CustInvoiceSalesLink.salesId = CustInvoiceJour.SalesId
                        AND CustInvoiceSalesLink.invoiceId = CustInvoiceJour.InvoiceId
                        AND CustInvoiceSalesLink.invoiceDate = CustInvoiceJour.InvoiceDate
                        AND CustInvoiceSalesLink.DataAreaId = CustInvoiceJour.DataAreaId",
                        salesTable.dataAreaId, salesTable.SalesId);
                    new SqlStatementExecutePermission(query).assert();
                    connection = new Connection();
                    statement = connection.createStatement();
                    statement.executeUpdate(query);
                    CodeAccessPermission::revertAssert();
                }
            }
            ttsCommit;
        }
    }
}

Now all we need to do is to justify to the project manager the time spent with this simple operation...

viernes, 24 de junio de 2016

Calling the DocumentHandling service

Today we're obtaining files via WCF web services from Visual Studio, the DocumentHandling service to be exact.
Activate the standard AX DocumentHandling service!
Now remember that these files are all available via a shared directory, and so before launching into using this service do investigate if it's worth obtaining the directory from DocuType, file name and file type from the DocuValue entity

From Visual Studio register the service from the WSDL URI and add the service reference to your project.  Remember that we will need to change the server and port in our project when it's time to move our project reference to the production environment.

Now all you would need to do is find a RecId from the DocuRef entity.  In the example below we can see a document associated with an Item Lot number:
Document Management is activated for multiple entities from Lots to Sales Invoices

Pseudo code below.  I have a paranoia with the Client object where we could leave connections open and therefore no garbage collection. Adapt the below to your requirements.:
using XXXProj.DocumentHandlingServiceReference;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections;
using System.Configuration;
using System.IO;
using System.Text;

/// <summary>
/// Download PDF Quality Certificate associated with lot '140801-033711'.
/// RecId 5637152126 (DEVELOPMENT environment)
/// </summary>
[TestMethod]
public void TestCertificate_GetPDF1()
{
  // We have a doc associated with InventBatch, lot '140801-033711' - See table DocuRef
  Int64 CERT_RECID1 = 5637152126;
        
  DocumentFileDataContract docuContract = new DocumentFileDataContract();

  // Create a client only for as long as we need to.  Note the exception handling with client.
  using (DocumentHandlingServiceClient client = new DocumentHandlingServiceClient())
  {
    try
    {
      // *NO* company context for Document Management
      //CallContext context = this.setCompanyContext("CONT");

      //Execute as another user, and he's called 'Bob'
      RunAsBob(client.ClientCredentials);

      // Set the AX AIF service endpoint.
      client.Endpoint.Address = setAXEnvironment(client.Endpoint.Address);
      
      // Obtain file, String format, encoded in base 64
      docuContract = client.getFile(null, CERT_RECID1);
      client.Close();
    }
    catch (System.ServiceModel.CommunicationException e)
    {
      client.Abort();
      Assert.Fail(e.ToString());
    }
    catch (TimeoutException e)
    {
      client.Abort();
      Assert.Fail(e.ToString());
    }
    catch (Exception e)
    {
      client.Abort();
      Assert.Fail(e.ToString());
    }
  }

  Assert.IsNotNull(docuContract);
  Assert.IsTrue(docuContract.RecId > 0, "Document not found - " + CERT_RECID1.ToString());
  Assert.IsNotNull(docuContract.Attachment, "Document is empty");

  // Let's save the document in a temporary directory
  string documentAttachment = docuContract.Attachment;
  string file = "C:\\TEMP\\file.pdf";
  byte[] ba = System.Convert.FromBase64String(documentAttachment);
  System.IO.File.WriteAllBytes(file, ba);
}

Helper or shared methods:
using System.Configuration;
using System;
using System.ServiceModel;

/// <summary>
/// Execute as user 'Bob'.  Data saved in app.config xml file.
/// </summary>
/// <param name="clientCredentials"></param>
protected static void RunAsBob(System.ServiceModel.Description.ClientCredentials clientCredentials)
{
  clientCredentials.Windows.ClientCredential.Domain = ConfigurationManager.AppSettings["Domain"];
  clientCredentials.Windows.ClientCredential.UserName = ConfigurationManager.AppSettings["UserName"];
  clientCredentials.Windows.ClientCredential.Password = ConfigurationManager.AppSettings["Password"];
}

/// <summary>
/// Assign the environment's Host/Port.  Are we testing DEVELOPMENT or PRODUCTION?
/// e.h.: srvax2012:8202
/// </summary>
/// <param name="address">client.EndpointAddress</param>
/// <returns></returns>
protected static System.ServiceModel.EndpointAddress setAXEnvironment(System.ServiceModel.EndpointAddress address)
{
  var newUriBuilder = new UriBuilder(address.Uri);
  newUriBuilder.Host = ConfigurationManager.AppSettings["NEW_ENDPOINT_HOST"];
  newUriBuilder.Port = System.Int16.Parse(ConfigurationManager.AppSettings["NEW_ENDPOINT_PORT"]);
  address = new EndpointAddress(newUriBuilder.Uri, address.Identity, address.Headers);
  return address;
}

/// <summary>
/// Context - Select Company.  DataAreaId: CONT/CONZ/TEST/DAT/...
/// </summary>
/// <param name="dataAreaId"></param>
/// <returns></returns>
private CustPackingSlipServiceReference.CallContext setCompanyContext(String dataAreaId)
{
  CustPackingSlipServiceReference.CallContext context = new CustPackingSlipServiceReference.CallContext();
  context.Company = dataAreaId;
  context.MessageId = Guid.NewGuid().ToString();
  return context;
}

lunes, 13 de junio de 2016

Call an AIF service operation via Job

Simulate a call to an AIF service operation via a Job in AX2012.  Thus avoiding having to attach to the server process when debugging.
The OperationContext below was mostly 'ignored' when being called from the Job.
static void ZZZ_SimulateAIFServiceCall(Args _args)
{
    // Sales Invoice find() operation
    SalesSalesInvoiceService    salesInvSvc;
    AifQueryCriteria            qryCriteria;
    AifCriteriaElement          criteriaEle;
    AifOperationContext         opContext;
    SalesSalesInvoice           salesInvoice;

    // Filter to find a customer invoice, in company 'HAL'
    criteriaEle = AifCriteriaElement::newCriteriaElement(
                                    "CustInvoiceJour",
                                    "RecId",
                                    AifCriteriaOperator::Equal,
                                    "5637156576");
    qryCriteria = AifQueryCriteria::construct();
    qryCriteria.addCriteriaElement(criteriaEle);
    salesInvSvc = SalesSalesInvoiceService::construct();
    // Simulate operation context
    opContext = new AifOperationContext(
        "XXXX",                       // ActionId (?)
        "SalesSalesInvoice",          // AifDocument that we are simulating a call to
        14005,                        // ClassId - classes/SalesSalesInvoice(?)
        "find",                       // Method name
        "find",                       // Op. method name
        "AccountsReceivableServices", // AIFPort.name
        "HAL",                        // ***Company / DataAreaId
        AifMessageDirection::Inbound, // ***Inbound / Outbound
        null);                        // Map of parameters in Request?
    salesInvSvc.setOperationContext(opContext);
    // Simulate AIF operation call
    salesInvoice = salesInvSvc.find(qryCriteria);

    info(strFmt(@'Invoice exists: %1', salesInvoice.existsCustInvoiceJour()));
}
Don't forget to generate the CIL when making changes to the service classes and calling AIF from outside of AX!

lunes, 30 de mayo de 2016

Exam MB6-705 :: Microsoft Dynamics AX 2012 R3 CU8 Installation and Configuration


It's certification time!  Lets explore MB6-705, and learn something about installing, configuring and generally getting our hands dirty with an AX2012 R3 CU8 (or later) installation.  If you have access to the Microsoft Dynamics Learning Portal do try to pass through the '80672AE' training.  I highly recommend it although there is never enough content in the Security section for my needs.  Much of the information in MSDN is repeated in the 372 page AX 2012 Installation Guide (marked as IG below, version January 2015) and at the time of writing information such as Slipstreaming I could not find on MSDN at all.
Personally I found this exam difficult due to the wide range of competencies and knowledge required.


Plan a Microsoft Dynamics AX 2012 installation (15–20%)


Install, configure, and update Microsoft Dynamics AX 2012 R3 CU8 (15–20%)

Update Microsoft Dynamics AX 2012 R3 CU8

    • Describe Lifecycle Services, identify tools for updating the environment, understand and implement slipstream installations (1,IG pg. 43)

  • Manage users and security (15–20%)


    Implement services and manage workflows (10–15%)


    Manage reporting and analytics (10–15%)


    Manage the Enterprise Portal (10–15%)


    Manage Microsoft Dynamics AX 2012 R3 CU8 installations (15–20%)

    lunes, 22 de febrero de 2016

    DIXF\DMF Duplicate a Definition group

    With the data import/export framework we can easily and quickly import data from external data stores.  Here's an image I stole from the MSDN web site (it's at least 6 months old now so it's probably been moved, redesigned, relocated, deleted,..) indicating the basic steps:

    The migratory pattern of a database row in AX

    One of the more monotonous steps, should you be migrating data across multiple companies, is creating the data definition groups and adding the same entities to the group, but presumably with different parameters to import into the Staging table.  Example below where we use a stored procedure with a parameter to separate data across legal entities:
    With a DSN datasource calling a stored procedure, passing in a company data area identifier
    We can either launch the same definition group changing the parameters each time per company, or recreate the whole definition group with associated entities.

    There exists another option, however.  Why not get someone clever to write some X++ to just duplicate the whole definition group to another, empty group:
    Highlighted is our source group with the parameter that we are replacing
    Linked is the class (alt xpo download link) I've created, use at your own discretion.  The class will ask for a Processing group as it's source, and an optional parameter value that you will be replacing from the source.  It will then look for an empty destination Processing group that you will have created beforehand and recreate the entities present in the original replacing the parameter value. It'll finally also launch the assignments for you, automatically validating them.
    In the example above, the Asset table entity had a DSN datasource query as the following:
        EXEC HIEAssetTable 'HAL'
    Updated in the destination to:
        EXEC HIEAssetTable 'ZZZ'


    Finally, when copying DMF configuration data across AOS instances, we could also try to use the DMF framework itself to export then import the DMF data into a different installation?!  The concept makes my head hurt so I asked a colleague to set it up and it works.  Use at your own risk:
    Configure a DMF group, to set up your DMF data how you like it (AX 2012 R3 CU8 example).