Business Event Development Video Tutorial

Please find the video tutorial how to develop a custom business event at my Youtube channel:

Develop Customer-Created Business Event in D365 SCM

Develop your own Business Events

Business Events in Dynamics 365 Finance and Supply Chain Management can be used to notify external systems in near-time when a certain event occurs in the ERP system. Dynamics 365 F/SCM comes with a set of predefined business events. You may want to develop you own specific business events to send data to another system. Three artifacts are needed for a custom Business Event. The contract that contains the data that is sent, the Business Event and at least one trigger. Here is an example for a Business Event that triggers when a new customer is created.

Required Model Dependencies:

  • Application Foundation
  • Application Suite
  • Directory
  • Contact Person

Contract

[DataContract]
public class ERPCustomerCreatedContract extends BusinessEventsContract
{
    protected Name name;

    [DataMember('Name'),BusinessEventsDataMember("Customer Name")]
    public Name parmName(Name _name = name)
    {
        name = _name;
        return name;
    }

    public static ERPCustomerCreatedContract newFromCustTable(CustTable _custTable)
    {
        ERPCustomerCreatedContract contract = new ERPCustomerCreatedContract();
        contract.parmName(_custTable.name());
        return contract;
    }
}

Business Event

[BusinessEvents(classStr(ERPCustomerCreatedContract),
                'Customer Created',
                'Customer Created',
                ModuleAxapta::Customer)]
public class ERPCustomerCreated extends BusinessEventsBase
{
    CustTable custTable;

    protected void new()
    {
    }

    public static ERPCustomerCreated newFromCustTable(CustTable _custTable)
    {
        ERPCustomerCreated event = new ERPCustomerCreated();
        event.parmCustTable(_custTable);
        return event;
    }

    public CustTable parmCustTable(CustTable _custTable = custTable)
    {
        custTable = _custTable;
        return custTable;
    }

    [Wrappable(false), Replaceable(false)]
    public BusinessEventsContract buildContract()
    {
        return ERPCustomerCreatedContract::newFromCustTable(custTable);
    }
}

Trigger

Make sure the trigger runs after inserted not on inserting 😉

class ERPCustTable_EventHandler
{
    [DataEventHandler(tableStr(CustTable), DataEventType::Inserted)]
    public static void CustTable_onInserted(Common sender, DataEventArgs e)
    {
        CustTable custTable = sender as CustTable;
        ERPCustomerCreated::newFromCustTable(custTable).send();
    }
}

Configuration

Make sure your code builds. In Dynamics 365 F/SCM open the Business Events Catalog. Rebuild the catalog to see your Business Event. Make sure you have an endpoint configured. Activate the Business Event. Create a new Customer. The Business Event will trigger and send the contract to your endpoint.

After creating a new customer, the Business Event triggers and sends the message to the configured endpoint. In my case it’s a Blob Storage in Azure. Here is the resulting JSON Message:

{
"BusinessEventId":"ERPCustomerCreated",
"BusinessEventLegalEntity":"DEMF",
"ContextRecordSubject":"",
"ControlNumber":5637166326,
"EventId":"13258F5D-9734-4EEF-8742-966C903E6896",
"EventTime":"/Date(1700741668000)/",
"EventTimeIso8601":"2023-11-23T12:14:28.5470919Z",
"InitiatingUserAADObjectId":"{2FDBF251-CB38-48ED-87CD-7515B9010431}",
"MajorVersion":0,
"MinorVersion":0,
"Name":"Test Customer",
"ParentContextRecordSubjects":[]
}

Business Events and PowerAutomate (aka. Flow)

I’ve made a video how to use Business Events in combination with Power Automate.

X++ Reflection: Tables and Fields

Reflection is used to dynamically retrieve metadata information from code artifacts dynamically at runtime. In older versions of Dynamics AX this was done using TreeNode framework which reflected the AOT structure. In Dynamics 365 Finance and Supply Chain Management you can use the MetadataSupport class.

Example: Table and its Fields

Create a temporary table that has a name field. The table will be dynamically populated with table names or field names. On the temporary table add 2 static methods to populate a table buffer with table names or field names.

public static TmpTableName populateTableName()
{
    TmpTableName _tmpTableName;
    var tables = Microsoft.Dynamics.Ax.Xpp.MetadataSupport::TableNames();
    while (tables.MoveNext())
    {
        _tmpTableName.clear();
        _tmpTableName.Name = tables.Current;
        _tmpTableName.insert();
    }
    return _tmpTableName;
}

public static TmpTableName populateFieldName(TableId _tableId)
{
     SysDictTable table = new SysDictTable(_tableId);
     Set fields = table.fields();
     SetEnumerator enum = fields.getEnumerator();
     TmpTableName _tmpTableName;

     while(enum.moveNext())
     {
         SysDictField field = enum.current();

         _tmpTableName.clear();
         _tmpTableName.Name = field.name();
         _tmpTableName.insert();
     }

     return _tmpTableName;
 }

Create a regular table that has 2 name fields, one for a table name and another for the field name. Overwrite the lookup method and provide the temporary table buffer as datasource.

public void lookupTableName(FormStringControl _control)
{
    SysTableLookup    lookup;
    QueryBuildDataSource qbds;
    Query q = new Query();
    qbds = q.addDataSource(tableNum(TmpTableName));
    qbds.addSortField(fieldNum(TmpTableName, Name), SortOrder::Ascending);
    lookup = SysTableLookup::newParameters(tableNum(TmpTableName),
          _control,
          true);
    lookup.addLookupField(fieldnum(TmpTableName, Name), true);
    lookup.parmQuery(q);
    lookup.parmTmpBuffer(TmpTableName::populateTableName());
    lookup.performFormLookup();
}

public void lookupFieldName(FormStringControl _control, TableId _tableId)
{
    SysTableLookup    lookup;
    QueryBuildDataSource qbds;
    Query q = new Query();
    qbds = q.addDataSource(tableNum(TmpTableName));
    qbds.addSortField(fieldNum(TmpTableName, Name), SortOrder::Ascending);
    lookup = SysTableLookup::newParameters(tableNum(TmpTableName),
          _control,
          true);
    lookup.addLookupField(fieldnum(TmpTableName, Name), true);
    lookup.parmQuery(q);
    lookup.parmTmpBuffer(TmpTableName::populateFieldName(_tableId));
    lookup.performFormLookup();                
}

Serialize and Deserialize JSON in X++

JSON strings can easily be handled using the FormJSONSerialized class in Dynamics 365 FO. Here is an example:

// JSON with string and number
str jsonString = @'{"Name":"Dynamics 365","RefRecId":123456789}';

Data Contract

Create an X++ class that matchtes the properties and add the DataContract and DataMember attributes. The attribute name has to match the JSON property name. You can stick to the parm*() naming schema for the method.

[DataContract]
class ERPDataContract
{
    Name name;
    RefRecId refRecId;

    [DataMember("Name")]
    public Name parmName(Name _name = name)
    {
        name = _name;
        return name;
    }

    [DataMember("RefRecId")]
    public RefRecId parmRefRecId(RefRecId _refRecId = RefRecId)
    {
        refRecId = _refRecId;
        return refRecId;
    }
}

Deserialize from JSON to X++ object

ERPDataContract xppObject = FormJsonSerializer::deserializeObject(
                                                        classNum(ERPDataContract),
                                                        jsonString);

info(strfmt("Name = %1, RefRecId = %2",xppObject.parmName(), 
                                       xppObject.parmRefRecId()));
JSON to X++ object

Serialize X++ object to JSON string

xppObject.parmRefRecId(1010101010);
jsonString = FormJsonSerializer::serializeClass(xppObject);
info(jsonString);
X++ object to JSON

Custom Scripts: Run X++ Code without downtime (Video)

Custom Scripts in Dynamics 365 Finance and Supply Chain Management enables you to upload and execute X++ Code without the need to deploy a new release. Custom Scripts serve the same purpose like X++ Jobs in AX 2012 and earlier versions e.g. data correction. This short video shows how to create such a custom script, upload and execute it in a Dynamics 365 FO instance:

Custom Scripts: Jobs are (almost) back with in 10.0.27

The Custom Scripts feature in Dynamics 365 Finance and Supply Chain Management allows you to upload and execute code snippets. Microsoft implemented some barriers because this feature is potential dangerous. You need two accounts, one for uploading the script and one for approval. The feature can be found in System Administration > Periodic Tasks > Database > Custom scripts.

Custom Scripts Feature in 10.0.27

If Sort-Field is empty sort by another field

At work we recently discussed a customer requirement regarding sorting of a SalesTable data set in Dynamics Ax. The requirement was to sort by ShippingDateConfirmed. If the order has no confirmation date yet, use the ShippingDateRequested instead.

If exists sort by Shipping Date Confirmed otherwise by Shipping Date Requested

There are several ways to implement this requirement. Depending on the technology you can use SQL code, computed columns in Dynamics Ax 2012+ or a union query in AX 2009.

SQL: Select CASE

The easiest way to achiev the goal is using pure SQL code where you can define a new column within the select statement and use it for sorting. Here is an example:

SELECT 
SalesId, SalesName, 
ShippingDateRequested, ShippingDateConfirmed, 
CASE 
WHEN ShippingDateConfirmed = '1900-01-01 00:00:00.000' 
THEN ShippingDateRequested 
ELSE ShippingDateConfirmed 
END 
AS ErpSortField
FROM SalesTable
WHERE DataAreaId = 'CEU'
ORDER BY ErpSortField

The result in SQL Server Management Studio for a Dynamics Ax 2009 database looks like this:

SELECT CASE WHEN .. THEN .. ELSE .. END in SQL
SELECT CASE WHEN .. THEN .. ELSE .. END in SQL

You may use such a SQL query as data source for an SSRS report

SSRS Report based on AX 2009 Sales Order
SSRS Report based on AX 2009 Sales Order

Dynamics 365 F/SCM: Computed Column

Since AX 2012 we can use computed columns in views. One way to address this requirement is to create a column that contains the same CASE – WHEN SQL Statement. To do so create a new view based on the SalesTable. Add a new static method:

private static server str compColShippingDate()
{
  #define.ViewName(MBSSalesTableView)
  #define.DataSourceName("SalesTable")
  #define.FieldConfirmed("ShippingDateConfirmed")
  #define.FieldRequested("ShippingDateRequested")
  str sReturn;
  str sRequested, sConfirmed;
  DictView dv = new DictView(tableNum(#ViewName));

  sRequested = dv.computedColumnString(
                   #DataSourceName,
                   #FieldRequested,
                   FieldNameGenerationMode::FieldList);
  sConfirmed = dv.computedColumnString(
                   #DataSourceName,
                   #FieldConfirmed,
                   FieldNameGenerationMode::FieldList);

  sReturn = "CASE WHEN " 
          + sConfirmed + " = '1900-01-01 00:00:00.000' THEN " 
          + sRequested + " ELSE " + sConfirmed + " END";

  return sReturn;
}

Add a computed column to the view and set the method as view method. Build and synchronize.

View with computed column in Dynamics 365 Finance
View with computed column in Dynamics 365 Finance

This will result in the following SQL definition in the AXDB:

Generated SQL view code in AxDB
Generated SQL view code in AxDB

Use the view as data source in form:

View in Dynamics 365 F/SCM form
View in Dynamics 365 F/SCM form

Dynamics AX 2009: Union Query

Older versions of Dynamics AX link 2009 computed columns were not supported. One workaround is to use a UNION Query.

First create a new view called ERPSalesTableConfirmed. Set the SalesTable as data source. Add a range based on the ShippingDateConfirmed field and set the range value to != ” (i.e. not empty). Add a view field based on the ShippingDateConfirmed and call it ERPSortField. This view will return all SalesTable records with a confirmed shipping date and a new field with the value in it.

SalesTable with confirmed shipping date
SalesTable with confirmed shipping date

Second, create a new view called ERPSalesTableRequested. Set the SalesTable as data source. Add a range based on the ShippingDateConfirmed and set the range value to = ” (i.e. empty). Add a view field based on the ShippingDateRequested and call it ERPSortField. This view will return all SalesTable records without a confirmed shipping data and use the ShippingDateRequested for the ERPSortField.

SalesTable with requested shipping date
SalesTable with requested shipping date

Next, create a query called ERPSalesTableSort. Set the query type to UNION. Add both views as data source. The execution of this query will return all SalesTable records. If the sales order was confirmed, the ERPSortField will contain the ShippingDateConfirmed value, otherwise the ERPSortField will contain the ShippingDateRequested.

UNION query in Dynamics AX 2009
UNION query in Dynamics AX 2009

Finally, create a new view called ERPSalesTableSort based on the query with the same name. Use all fields you like to see and the ERPSortField.

Dynamics AX 2009 view based on UNION query
Dynamics AX 2009 view based on UNION query

Open the view. The result is a SalesTable dataset that can be sorted on the confirmed shipping date, and if the confirmed date is not present sorted by the requested date.

Sort SalesTable in Dynamics AX 2009 by confirmed or requested shipping date
Sort SalesTable in Dynamics AX 2009 by confirmed or requested shipping date

Extend SalesTable2Line Framework (Dynamics 365 Finance / SCM)

This is an update to my older post how to extend the SalesTable 2 Line framework. The big difference is that in Dynamics 365 Finance and SCM overlaying is not supported and extensions and delegates need to be used. This post uses the same use case. A sales-notes field from the SalesTable needs to be updated in the SalesLines if it is configured so.

Download the sample source code: https://erpcoder.blog/source-code/

Extend the data model

Create a new string datatype and call it ERPSalesNote. Extend the SalesLine and add the ERPSalesNote datatype to the list of fields. Extend the SalesTable and add the ERPSalesNote to the fields. Also add the ERPSalesNote field to the field group HeaderToLineUpdate.

Extend the user interface

Extend the SalesTable form. Place the SalesTable.ERPSalesNote in the Delivery group of the HeaderView tab.

Add the SalesLine.ERPSalesNote field to the delivery group in the LineView tab.

Prepare the update-order-lines dialog

The dialog to choose if and which fields need to be updates at the lines is generated automatically based on the HeaderToLineUpdate field group. There is some code needed to show the fields name in the dialog. This is done by subscribing a custom method to the delegate SalesTable2LineField.lineUpdateDescriptionDelegate

Create a new class that returns a name for the ERPSalesNote field.

class ERPSalesTable2LineField
{
[SubscribesTo(classStr(SalesTable2LineField), delegateStr(SalesTable2LineField,lineUpdateDescriptionDelegate))]
public static void lineUpdateDescriptionDelegate(FieldId _fieldId, TableId _tableId, EventHandlerResult _result)
{
  if(_tableId == tableNum(SalesTable) &&
  _fieldId == fieldNum(SalesTable,ERPSalesNote))
  {
    _result.result("Sales Note");
  }
}
}

Open the Header to Line update dialog by clicking on Accounts receivable > Setup > Accounts receivable parameters > Tab update > update order lines

Extend the framework classes

Create an extension for the AxSalesTable class and create a parm method for the ERPSalesNote field

[ExtensionOf(classStr(AxSalesTable))]
final class AxSalesTable_Extension
{
public ERPSalesNote parmERPSalesNote(ERPSalesNote _salesNote = "")
{
  if (!prmisDefault(_salesNote))
  {
    this.setField(fieldNum(SalesTable, ERPSalesNote), _salesNote);
  }
  return salesTable.ERPSalesNote;
}
}

Create an extension for the AxSalesLine class. Implement a parm and set method. Use the chain of command pattern to extend the setTableFields method.

[ExtensionOf(classStr(AxSalesLine))]
final class AxSalesLine_Extension
{
public ERPSalesNote parmERPSalesNote(ERPSalesNote _salesNote = "")
{
  if (!prmisDefault(_salesNote))
  {
    this.setField(fieldNum(SalesLine, ERPSalesNote), _salesNote);
  }
  return salesLine.ERPSalesNote; 
} 
protected void setERPSalesNote() 
{ 
  if (this.isMethodExecuted(funcname(), fieldnum(SalesLine, ERPSalesNote))) 
  { 
  return; 
  } 
  this.setAxSalesTableFields(); 
  if (this.isAxSalesTableFieldsSet() || 
      this.axSalesTable().isFieldModified(fieldnum(SalesTable, ERPSalesNote))) 
  { 
  this.parmERPSalesNote(this.axSalesTable().parmERPSalesNote()); 
  }
} 
protected void setTableFields() 
{ 
  next setTableFields(); 
  this.setERPSalesNote(); 
}
}

Test your implementation

Make sure that the update method in the parameter is set to prompt. Open an existing sales order. Change to Header view and switch to edit mode. Change the notes in the delivery tab and save.

A dialog pops up and asks to update the lines. Click yes.

Check the sales note field in the sales line. The note field at the sales line should be updated with your text from the header.

Call REST Webservice with HTTP Basic Authentication from X++

Calling a web service with HTTP Basic Authentication is easy in C#. Here I’m using a REST service via HTTP GET which is secured via Basic Authentication.

C# Code

A HTTP GET webservice call using System.Net.WebRequest and System.Net.Webrespose in C#

string url = “http://yourhost.net/service”;
string user = “YourUserName”;
string pass = “YourPassWord”;

byte[] bytes = System.Text.Encoding.UTF8.GetBytes(user+”:”+ pass);
string base64  = System.Convert.ToBase64String(bytes);

WebRequest request = WebRequest.Create(url);
request.Headers.Add(“Authorization”, “Basic “+base64);
WebResponse response = request.GetResponse();
string wsResponse = new StreamReader(response.GetResponseStream())
.ReadToEnd();

Console.WriteLine(wsResponse);
Console.ReadKey();

X++ Code

Since Dynamics 365 FO does not support packages and some syntactic sugar from C# the code is more wordy.

str url = “http://yourhost.net/service”;
str user = “YourUserName”;
str pass = “YourPassword”;

System.Text.Encoding encoding = System.Text.Encoding::UTF8;
System.Byte[] bytes = encoding.GetBytes(user + “:” + pass);
System.String base64 = System.Convert::ToBase64String(bytes);

System.String headerKey = “Authorization”;
System.String headerValue = “Basic ” + base64;
System.Net.WebRequest request = System.Net.WebRequest::Create(url);       
System.Net.WebHeaderCollection headers = request.Headers;
headers.Add(headerKey,headerValue);

System.Net.WebResponse response = request.GetResponse();
System.IO.StreamReader reader = new System.IO.StreamReader(
response.GetResponseStream());
str wsResponse = reader.ReadToEnd();

info(wsResponse);

Extend PurchTable2Line Framework (AX 2012)

This is a follow-up to my initial blog post how to extend the SalesTable2Line Framework from 2011. However, this post is a walkthrough how to update PurchLine fields from the PurchTable header.

Fields and Field Groups

Create an extended datatype called ERPCarrier which extends the Name datatype. Provide a label called Carrier.On the PurchLine create two new fields called ERPCarrierRequested and ERPCarrierConfirmed based on the datatype ERPCarrier. Provide two meaningful labels, Requested Carrier and Confirmed Carrier. Create a field group called ERPCarrier and add both fields to the group.

On the PurchTable add two new fields called ERPCarrierRequested and ERPCarrierConfirmed based on the datatype ERPCarrier. Provide the same labels as on the PurchLine. Create a field group called ERPCarrier and add both fields to the group. Moreover, add both fields to the field group HeaderToLineUpdate!

image

On the PurchTable form, add the PurchTable field group ERPCarrier in the header view in the group delivery.

image

Add the PurchLine field group ERPCarrier in the line view in the tab delivery.

image

Code

On the AxPurchTable class add two parm Methods for the two new fields

public ERPCarrierId parmERPCarrierConfirmed(ERPCarrierId _carrierId = ”)
{
if (!prmisDefault(_carrierId))
{
this.setField(fieldNum(PurchTable, ERPCarrierConfirmed), _carrierId);
}

    return purchTable.ERPCarrierConfirmed;
}

public ERPCarrierId parmERPCarrierRequested(ERPCarrierId _carrierId = ”)
{
if (!prmisDefault(_carrierId))
{
this.setField(fieldNum(PurchTable, ERPCarrierRequested), _carrierId);
}

    return purchTable.ERPCarrierRequested;
}

On the AxPurchLine class add two parm methods for the two new fields

public ERPCarrierId parmERPCarrierConfirmed(ERPCarrierId _carrierId = ”)
{
if (!prmisDefault(_carrierId))
{
this.setField(fieldNum(PurchLine, ERPCarrierConfirmed), _carrierId);
}

    return purchLine.ERPCarrierConfirmed;
}

public ERPCarrierId parmERPCarrierRequested(ERPCarrierId _carrierId = ”)
{
if (!prmisDefault(_carrierId))
{
this.setField(fieldNum(PurchLine, ERPCarrierRequested), _carrierId);
}

    return purchLine.ERPCarrierRequested;
}

Next, on the AxPurchLine class add two set methods

protected void setERPCarrierConfirmed()
{
if (this.isMethodExecuted(funcName(),
fieldNum(PurchLine, ERPCarrierConfirmed)))
{
return;
}

    this.setAxPurchTableFields();

    if (!this.parmERPCarrierConfirmed() &&
this.axPurchTable().parmERPCarrierConfirmed())
{
this.parmERPCarrierConfirmed(
this.axPurchTable().parmERPCarrierConfirmed());
}
}

protected void setERPCarrierRequested()
{
if (this.isMethodExecuted(funcName(),
fieldNum(PurchLine, ERPCarrierRequested)))
{
return;
}

    this.setAxPurchTableFields();

    if (!this.parmERPCarrierRequested() &&
this.axPurchTable().parmERPCarrierRequested())
{
this.parmERPCarrierRequested(
this.axPurchTable().parmERPCarrierRequested());
}
}

On the AxPurchLine class add a new static method which is used to set the new fields.

public static void setTableFields_ERPCarrier(XppPrePostArgs _args)
{
AxPurchLine     thisAxPurchLine = _args.getThis();
thisAxPurchLine.setERPCarrierRequested();
thisAxPurchLine.setERPCarrierConfirmed();
}

On the AxPurchLine class, go to the setTableFields method and expand the event handler. Add a new Post X++ event handler. Provide the AxPurchLine as class for the event handler and the newly created method setTableFields_ERPCarrier as event handler method.

image

On the PurchTable2LineField class, open the getFieldDescription method and scoll down. Add  the following code to handle the two fields.

case fieldNum(PurchTable, ERPCarrierConfirmed):
description =  fieldid2pname(tablenum(PurchLine),
fieldnum(PurchLine, ERPCarrierConfirmed));
break;

case fieldNum(PurchTable, ERPCarrierRequested):
description =  fieldid2pname(tablenum(PurchLine),
fieldnum(PurchLine, ERPCarrierRequested));
break;

Test

Compile your code an build incremental IL. Open the table PurchTable2LineParameters and delete all records. Restart the AOS to make sure no cached version is used. In AX go to Accounts Payable > Settings > Parameter > Tab Updates and click the button “Update order lines”. Set the Update Requested Carrier and Confirmed Carrier to Always.

image

Open a purchase order in AX and edit the purchase header. Provide a requested carrier e.g. UPS and a confirmed carrier e.g. DHL. Save your changes. Check if the values from the header have been copied to the purchase lines.