Normally, when we consume AIF Service, we use this code like below to handle Error messages

try
{
	client.register(ctx, contract);
	Console.WriteLine("items registed on Trans Id: " + contract.InventTransId + " with " + contract.Qty + " quantities.");
	Console.ReadLine();
}
catch (Exception ex)
{
	Console.WriteLine(string.Format("Ex: {0}", ex.Message));
	Console.ReadLine();
}

If it cause error, message would return like this

If you want to know more details, you have to go In Dynamics ax AIF Exceptions form then check

It’s quite hard for 3rd party developer, especially they don’t have right to access AX server.

Anyway, we can get meaningful error message by doing below steps

  • Check that box in AIF inbound ports

  • Use FaultException class to get message
try
{
	client.register(ctx, contract);
	Console.WriteLine("items registed on Trans Id: " + contract.InventTransId + " with " + contract.Qty + " quantities.");
	Console.ReadLine();

}
catch (System.ServiceModel.FaultException<ItemsRegistration.RegRef.AifFault> aifFault)
{
	//FaultMessageList[] list = aifFault.Detail.FaultMessageListArray[0];
	InfologMessage[] list = aifFault.Detail.InfologMessageList;

	foreach (InfologMessage message in list)
	{
		Console.WriteLine(message.Message);

	}
	Console.ReadLine();

}

what we got

Thank you for reading.

For this demonstration, I use AIF service to create Sales order with SalesSalesOrderService and I gonna consume AIF using C#.NET.

Normally, in consume service application we handle return value by using EntityKeyList, EntityKey, KeyData[0].Value. And for Sales Order It will return Created SalesID.

Let’s take a look on AxdSalesOrder class. More about Axd class please prefer this.

Then go to createList method, this method will handle response value

public AifEntityKeyList createList(
    AifDocumentXml                  _xml,
    AifEndpointActionPolicyInfo     _actionPolicyInfo,
    AifConstraintListCollection     _constraintListCollection)
{
    AifEntityKeyList aifEntityKeyList;

    aifEntityKeyList = super(_xml, _actionPolicyInfo, _constraintListCollection);

    // Sales orders are committed - master planning explosion can be executed and confirmed dates be set
    this.postSalesOrderCreation(aifEntityKeyList);

    return aifEntityKeyList;
}

So, how about customer wants another meaning value beside SalesID likes InventTransId information in SalesLine table or another tables base on your requirement. To do that, we need to customize this method.

How to do

I will use Map and MapEnumerator classes (Please refer MSDN to understand these class), this is steps:

  1. We get the SalesId from the original entityKeyList and add into Map.
  2. add Map to MapEnumerator.
  3. Use SalesId to find the sales line. (mapEnumerator.currentValue() can get current SalesID).
  4. Create a new entityKey and insert the sales line information to this entityKey.
  5. add back entityKey to entityKeyList

From here in entityKeyList will store information of SalesID and List fields of SalesLine table.

Here is the code for createList method, beside SalesId I will try to get InventTransId in SalesLine Table

public AifEntityKeyList createList(
    AifDocumentXml                  _xml,
    AifEndpointActionPolicyInfo     _actionPolicyInfo,
    AifConstraintListCollection     _constraintListCollection)
{
    AifEntityKeyList aifEntityKeyList;
    
    SalesId                         salesId;
    SalesLine                       salesLine;
    AifEntityKey                    entityKey;
    AifEntityKey                    salesEntityKey;
    Map                             keyDataMap;
    Map                             salesOrderMap;
    MapEnumerator                   mapEnumerator;

    aifEntityKeyList = super(_xml, _actionPolicyInfo, _constraintListCollection);

    // Sales orders are committed - master planning explosion can be executed and confirmed dates be set
    this.postSalesOrderCreation(aifEntityKeyList);

    entityKey       = aifEntityKeyList.getEntityKey(1);
    keyDataMap      = entityKey.parmKeyDataMap();
    mapEnumerator   = keyDataMap.getEnumerator();
    while (mapEnumerator.moveNext())
    {
        salesId = mapEnumerator.currentValue();
        if (salesId)
        {
            while select InventTransId, RecId from salesLine
                where salesLine.SalesId == salesId
            {
                salesEntityKey = new AifEntityKey();
                salesOrderMap  = new Map(Types::Integer, Types::Container);
                salesEntityKey.parmTableId(tableNum(SalesLine));
                salesEntityKey.parmRecId(salesLine.RecId);
                salesOrderMap.insert(fieldNum(SalesLine, InventTransId), [salesLine.InventTransId]);
                salesEntityKey.parmKeyDataMap(salesOrderMap);
                aifEntityKeyList.addEntityKey(salesEntityKey);
            }
        }
    }

    return aifEntityKeyList;
}

Code in C#.NET to get list key

SalesOrderServiceClient client = new SalesOrderServiceClient();
try
{
	EntityKey[] salesOrderCreatedEntity = client.create(callContext, salesOrder);
	//EntityKey salesOrderCreated = (EntityKey)salesOrderCreatedEntity.GetValue(0);

	System.Collections.IEnumerator enumerator = salesOrderCreatedEntity.GetEnumerator();

	while (enumerator.MoveNext())
	{
		EntityKey salesOrderCreated = (EntityKey)enumerator.Current;
		Console.WriteLine(salesOrderCreated.KeyData[0].Field);
		Console.WriteLine(salesOrderCreated.KeyData[0].Value);
	}
	//Console.WriteLine("The sales order created has a Sales ID of " + salesOrderCreated.KeyData[0].Value);
	Console.ReadLine();
}
catch (Exception e)
{
	Console.WriteLine(e.ToString());
	Console.ReadLine();
}

Here is what we got

Just remember this one just for AIF Document Standard, for AIF custom service we do another way I will so in next post.

Thank you for reading.

Main requirements is Using batch to find and block vendor base on last transaction condition and notify for them by emails.

Set up E-mail parameters

For set up email, we need Go to AX System administrator > Setup > E-mail parameters

Vendor emails

Vendor emails locate on LogisticsElectronicAddress.Locator, partyTable.PrimaryContactEmail, partyLocation.Location, please take a look on this job to find how to update Vendor emails and you also could see the relations more clearly.

static void UpdateVendorEmail(Args _args)
{
    VendTable                   vendTable;
    LogisticsElectronicAddress  electronicAddress;
    DirPartyTable               partyTable;
    DirPartyLocation            partyLocation;

    electronicAddress.initValue();
    electronicAddress.Type = LogisticsElectronicAddressMethodType::Email;
    electronicAddress.Description = "Max Nguyen";
    electronicAddress.Locator     = "luan52@outlook.com";
    electronicAddress.IsPrimary   = NoYes::Yes;
    electronicAddress.insert();

    while select forUpdate partyTable
        exists join vendTable
            where vendTable.Party == partyTable.RecId
    {
        ttsBegin;
        partyTable.PrimaryContactEmail = electronicAddress.RecId;
        partyTable.update();
        ttsCommit;

        select firstOnly forupdate partyLocation
            where partyLocation.Party == partyTable.RecId;

        if (partyLocation)
        {
            ttsBegin;
            partyLocation.Location = electronicAddress.Location;
            partyLocation.update();
            ttsCommit;
        }
        else
        {
            partyLocation.initValue();
            partyLocation.Location = electronicAddress.Location;
            partyLocation.Party     = partyTable.RecId;
            partyLocation.insert();
        }
    }
}

Batch class

Main logic here is find Vend accounts are not exist in VendTrans table with condition endTrans.TransDate >= beginDate, and beginDate count from today systemDateGet().

public class Max_VendorBlockedBatch extends RunBaseBatch
{
}

Get the date before 6 months from today

public TransDate getBeginDate()
{
    TransDate   beginDate;
    TransDate   currentDate;
    Months      month;
    YearBase    years;
    Day         day;

    currentDate = systemDateGet();
    day         = dayOfMth(currentDate);
    month       = mthOfYr(currentDate);
    years       = year(currentDate);
    if (month < 6)
    {
        beginDate = mkDate(day, 12 - (6 - month) + 1, years - 1);
    }
    else
    {
        beginDate = mkDate(day, month - 6 + 1, years);
    }

    return beginDate;
}

Send E-mail

public void sendEmail(AccountNum _vendor, str _recipient)
{
    str                                     sender  = 'sender@email.com';
    str                                     subject = 'Account blocked';
    str                                     body    = 'Your account is blocked due to last transaction';
    List                                    toList;
    ListEnumerator                          le;
    Set                                     permissionSet;
    System.Exception                        e;
    str                                     mailServer;
    int                                     mailServerPort;
    System.Net.Mail.SmtpClient              mailClient;
    System.Net.Mail.MailMessage             mailMessage;
    System.Net.Mail.MailAddress             mailFrom;
    System.Net.Mail.MailAddress             mailTo;
    System.Net.Mail.MailAddressCollection   mailToCollection;
 
    try
    {
        toList = strSplit(_recipient, ';');
         
        permissionSet = new Set(Types::Class);
        permissionSet.add(new InteropPermission(InteropKind::ClrInterop));
        CodeAccessPermission::assertMultiple(permissionSet);
 
        mailServer      = SysEmaiLParameters::find(false).SMTPRelayServerName;
        mailServerPort  = SysEmaiLParameters::find(false).SMTPPortNumber;
        mailClient      = new System.Net.Mail.SmtpClient(mailServer, mailServerPort);
 
        le = toList.getEnumerator();
        le.moveNext();
         
        mailFrom    = new System.Net.Mail.MailAddress(sender);
        mailTo      = new System.Net.Mail.MailAddress(strLTrim(strRTrim(le.current())));
        mailMessage = new System.Net.Mail.MailMessage(mailFrom, mailTo);     

        mailToCollection = mailMessage.get_To();
        while (le.moveNext())
        {
            mailToCollection.Add(strLTrim(strRTrim(le.current())));
        }
         
        mailMessage.set_Priority(System.Net.Mail.MailPriority::High);
        mailMessage.set_Subject(subject);
        mailMessage.set_Body(body);
 
        mailClient.Send(mailMessage);
        mailMessage.Dispose();
 
        CodeAccessPermission::revertAssert(); 
        info(strFmt('Email was sent to vendor %1.', _vendor));
    }
    catch (Exception::CLRError)
    {
        e = ClrInterop::getLastException();
        while (e)
        {
            info(e.get_Message());
            e = e.get_InnerException();
        }

        CodeAccessPermission::revertAssert();
    }
}

Initializes a new instance of the Batch class.

public static MAX_VendorBlockedBatch construct()
{
    return new MAX_VendorBlockedBatch();
}

Gets description of the dialog.

public static ClassDescription description()
{
    return 'Vendor blocked batch';
}

Find the vendor without transaction and disable, then send email to vendor

public void run()
{
    VendTrans   vendTrans;
    VendTable   vendTable;
    TransDate   beginDate;
    Email       email;
    int         i;

    try
    {
        beginDate = this.getBeginDate();
        while select forUpdate AccountNum, Party from vendTable
            Notexists join vendTrans
            where vendTrans.AccountNum == vendTable.AccountNum
                && vendTrans.TransDate >= beginDate
        {
            //Set the vendor blocked
            ttsBegin;
            vendTable.Blocked = CustVendorBlocked::All;
            vendTable.update();
            ttsCommit;

            //Send E-mail to vendor
            email = vendTable.email();
            if (email)
            {
                this.sendEmail(vendTable.AccountNum, email);
            }
            else
            {
                warning(strFmt('The vendor %1 did not have E-mail address.', vendTable.AccountNum));
            }
        }
    }
    catch (Exception::Deadlock)
    {
        retry;
    }
}

Provides an enter point for the Batch class.

public static void main(Args _args)
{
    MAX_VendorBlockedBatch vendorBlockedBatch = MAX_VendorBlockedBatch::construct();

    if (vendorBlockedBatch.prompt())
    {
        vendorBlockedBatch.run();
    }
}

From here you can run class and set up recurrence for batch job.

Thank you for reading!

  • There is a maximum of one Primary Key per table, whereas a table can have several alternate keys. The primary key is usually the type of key that other tables, called child tables, refer to when a foreign key field in those other tables need a relational identifier.

  • For new tables the default is a primary key based on the RecId field , incremented number or a completely meaningless number that is generated by the system surrogate key.

  • Alternate key can be chosen as the Replacement Key of a table that can display on forms instead of a meaningless numeric primary key value. Each table can have a maximum of one replacement key.

  • Natural key has meaning to people. Most replacement keys are natural keys.

  • Relations represents a foreign key.

Thank you for reading!

In Dynamics AX, there is a class called SysQueryRangeUtil that can be utilized in both query ranges and table filters. Using methods from this class allow you to be very precise about what dates you want to use in reports or for filtering your data.

Let’s say you have a report that you always want to run to see orders with shipping dates of the next day. It is possible to do so by using one of the methods from the SysQueryRangeUtil. The use of the letter ‘t’ will work for today’s date, but when you try to add days to it, it doesn’t work in reports. Instead, I will use the currentdate() method and add 1 to it.

All methods & expressions must be surrounded by parentheses as shown below.

Figure 1 – Filtering the requested ship dates in an AX query for tomorrow

Filtering the requested ship dates in an AX query for tomorrow (current day() + 1)

On any form with a grid, you filter your data by pressing Ctrl+G.

If I were to want to see open customer invoices from the last 90 days, I would filter my open customer invoices form and use the method (dayRange(-90,0)). The first number represents how many months backward from this month, and the second represents how many months forward. The same sorts of things can be done for the monthRange(), yearRange(), and dateRange() methods. The best part about this is that you can of course save these filters to create views that you might use on a daily basis.

Figure 2 – Searching the grid for the past 90 days

If you are creating your query ranges in code, these methods can also be utilized whenever you are setting them. Definitely be sure to check out the SysQueryRangeUtil class as there are many more methods to use.

Here is some methods you can use:

currentCustomerAccount()

currentVendorAccount()

currentUserId()

currentDate()

dateRange()

day()

dayRange()

greaterThanDate()

greaterThanUtcDate()

greaterThanUtcNow()

lessthanDate()

lessthanUtcDate()

lessthanUtcNow()

monthRange()

yearRange()

for example:

(dayRange(-30,0)) – Results in a date range for the last 30 days: "26-01-2017".."25-02-2017"

(day(-1)) – Results in yesterday's date: 24-02-2017

(day(0)) – Results in today's date: 25-02-2017

(day(1)) – Results in tomorrow's date: 26-02-2017

(greaterThanDate(2)) – Results in every date after today plus 2: > 27-02-2017

(lessThanDate(-1)) – Results in every date of today minus 1: < 24-02-2017

(monthRange(0,2))  Results in first day till the last day of the month's choosen (0 = current month): "01-02-2017".."30-04-2017"

(yearRange(-1,-1))  Results in first day till the last day of the chosen year: "01-01-2017".."31-12-2017"

Thank you for reading!