Wednesday, July 26, 2023

How to create a Custom Business Event in D365 X++

 To create a custom business event, we would need following three artifacts.

·       BusinessEventsContract class

·       BusinessEventsBase class 

·       A Trigger Class to send the business Event

     


     BusinessEventContract Class :

    

[DataContract]

internal final class DaxCustomerBusinessEventContract extends BusinessEventsContract

{

   private CustAccount   custAccount;

   Private CustName      customerName;

   /// <summary>

   /// Initializes the field values.

   /// </summary>

   private void initialize(CustTable _custTable)

   {

       custAccount = _custTable.AccountNum;

   }

   /// <summary>

   /// Creates a <c>DaxCustomerBusinessEventContract</c> from a <c>CustTable</c> record.

   /// </summary>

   /// <param name = "_CustTable">A <c>CustTable</c> record.</param>

   /// <returns>A <c>CustomerCreatedBusinessEvent</c>.</returns>

   public static DaxCustomerBusinessEventContract newFromCustTable(CustTable _custTable)

   {

       var contract =  DaxCustomerBusinessEventContract::construct();

       contract.initialize(_custTable);

       contract.parmCustAccount(_custTable.AccountNum);

       contract.parmCustomerName(_custTable.name());

       return contract;

   }

   [DataMember('AccountNumber'), BusinessEventsDataMember("@Dev:AccountNumber")]

   public CustAccount parmCustAccount(CustAccount _custAccount = custAccount)

   {

       custAccount = _custAccount;

       return custAccount;

   }

   [DataMember(identifierStr(CustomerName)),

   BusinessEventsDataMember("Customer name")]

   public Name parmCustomerName(Name _customerName = customerName)

   {

       customerName = _customerName;

       return customerName;

   }

   private void new()

   {

   }

   public static DaxCustomerBusinessEventContract construct()

   {

       DaxCustomerBusinessEventContract retVal = new DaxCustomerBusinessEventContract();

       return retVal;

   }

}

BusinessEventsBase class :

[BusinessEvents(classStr(DaxCustomerBusinessEventContract),

"Dev:DaxCustomerCreatedEvent","Dev:DaxCustomerCreatedEventDescription",ModuleAxapta::Customer)]

public final class DaxCustomerBusinessEvent extends BusinessEventsBase

{

   private CustTable custTable;

   private CustTable parmCustTable(CustTable _custTable = custTable)

   {

       custTable = _custTable;

       return custTable;

   }

   private void new()

   {

       super();

   }

   public static DaxCustomerBusinessEvent construct()

   {

       DaxCustomerBusinessEvent retVal = new DaxCustomerBusinessEvent();

       return retVal;

   }

   [Wrappable(true), Replaceable(true)]

   public BusinessEventsContract buildContract()

   {

       return DaxCustomerBusinessEventContract::newFromCustTable(custTable);

   }

   static public DaxCustomerBusinessEvent newFromCustTable(CustTable _custTable)

   {

       DaxCustomerBusinessEvent businessEvent =  DaxCustomerBusinessEvent::construct();

       businessEvent.parmCustTable(_custTable);

       return businessEvent;

   } 

}

A Trigger Class to send the business Event :

[ExtensionOf(tableStr(CustTable))]

public final class DaxCustTable_Extension

{

   public void insert(DirPartyType _partyType, Name _name,boolean _updateCRM)

   {

       next insert(_partyType, _name, _updateCRM);

 

       if (BusinessEventsConfigurationReader::isBusinessEventEnabled(classStr(DaxCustomerBusinessEvent)))

       {

           DaxCustomerBusinessEvent::newFromCustTable(this).send();

       }

   }

}

Activate the custom Dynamics 365 FO business event

The Business Event catalog doesnt get automatically refreshed. To refresh the Business Event catalog, go to
System Administration -> Business Event Catalog -> Manage -> Rebuild business event catalog
Once rebuild is complete, the new business event would be added to the list
 .

To Activate the BE you need a endpoint. Before Activation create a end point.

System Administration >Setup>Business Event catalog>Endpoints


Activate the Business Event and assign the end point to the business event. Once it is activated, the business event should appear in the “Active events” tab.

Power Automate flow : 

·       Go to the Power Automate portal.

·       On the left pane click Create to start a new flow, select Automated cloud flow.

·       Name the flow and find the following trigger, click Create.



In the Instance setting provide a link to your Dynamics 365 F&O environment.

After the one is provided, Power Automate will connect to the environment via the link and gather available business event information. After the Category is selected, user may select the exact Business event with binding a company. In case a company is not set, the event is active for all companies.


Parse business event data from JSON :

 

Previous step makes customer data available in JSON format. To use it further it makes sense to parse it using "Parse JSON" step.

 

Click "New step" and find the appropriate step, select "body" in Content field (this is the customer information JSON string that we have received from Dynamics 365 F&O on the previous step).

 

Now we need to provide a JSON schema so the step is able to parse the data correctly. To generate a schema from sample, go to the System administration -> Setup -> Business event catalog form, find the created business event, go to details at the right of the form, click Download schema button.

 

The button will generate a JSON sample string for this particular business event. The string may be used for "Generate schema from sample" function while setting up Parse JSON step.




Send a message via Teams :

Finally, we were asked to send a message via Teams containing customer information.

Click New step to add "Post message in a chatbot or channel" step. Enter a channel or a user that should receive the message on new customer creation and save the flow.


Test Power Automate flow :

In Dynamics 365 F&O go to Accounts receivable -> Customers -> All customers and create a new customer. After the customer is created, the user or channel you specified at the Teams setup step should receive a message via Teams that looks like the following:



If we click the link, the system will open "All customers" form showing the newly created customer only.
























Export of vendor payment journal info message to JSON format in D365 X++

 My requirement is to export the vendor payment journal to JSON format through custom button.

And to generate the response in JSON file based on validations occured.

Custom button clicked Event Handler :

internal final class DaxLedgerJournalTable_EventHandler

{

   /// </summary>

   /// <param name="sender"></param>

   /// <param name="e"></param>

   [FormControlEventHandler(formControlStr(LedgerJournalTable, Json), FormControlEventType::Clicked)]

   public static void Json_OnClicked(FormControl sender, FormControlEventArgs e)

   {

       MenuFunction                    menuFunction;

       FormRun formRun                 = sender.formRun();

       FormDataSource                  LedgerJournalTable_ds = formRun.dataSource(1);

       LedgerJournalCheckpost          _ledgerJournalCheckpost;

       str                             message,msg,info,setvalue;

       container                       con;

       Set                             setOne;

       SetEnumerator                   enumerator;

       setOne                          = new Set(types::String);

       LedgerJournalTable              ledgerJournalTable;

       ledgerJournalTable              = LedgerJournalTable_ds.cursor();

       LedgerJournalTrans              ledgerJournalTrans;

       System.IO.StringWriter          stringWriter;

       Newtonsoft.Json.JsonTextWriter  jsonWriter;

       stringWriter                    = new System.IO.StringWriter();

//validation

       ledgerJournalTable              = LedgerJournalTable::find(ledgerjournaltable.JournalNum);

       _LedgerJournalCheckPost         = LedgerJournalCheckPost::newLedgerJournalTable(ledgerJournalTable,NoYes::No);

       _LedgerJournalCheckPost.run();

       message                         = _LedgerJournalCheckPost.parmErrorMsg();

       if(message)

       {

           msg             = Global::parmSysInfologStr();

       }

       info                = Global::parminfo();

       if(info != "Journal is OK.")

       {

           con = str2con(info);

       }

       setOne.add(message);

       setOne.add(msg);

       enumerator = setOne.getEnumerator();

       while (enumerator.moveNext())

       {

           setvalue += enumerator.current();

       }

       con = conIns(con,1,setvalue);

       jsonWriter                      = new Newtonsoft.Json.JsonTextWriter(stringWriter);

       jsonWriter.WriteStartObject();// add '{'

       jsonWriter.WritePropertyName("Journal Num");

       jsonWriter.WriteValue(ledgerJournalTable.JournalNum);

       jsonWriter.WritePropertyName("Journal Name");

       jsonWriter.WriteValue(ledgerJournalTable.JournalName);

       jsonWriter.WritePropertyName("Lines");

       jsonWriter.WriteStartArray();

       while select ledgerJournalTrans

           where ledgerJournalTrans.JournalNum == ledgerjournaltable.JournalNum

       {

           jsonWriter.WriteStartObject();// add '{'

           jsonWriter.WritePropertyName("Voucher");

           jsonWriter.WriteValue(ledgerJournalTrans.Voucher);

           jsonWriter.WritePropertyName("Vendor Name");

           jsonWriter.WriteValue(ledgerJournalTrans.LedgerDimensionName);

           jsonWriter.WriteEndObject(); // add '}'

       }

       jsonWriter.WriteEndArray(); //add ']'

       jsonWriter.WriteEndObject(); //add '}'

       if(info == "Journal is OK." && !msg && !message)

       {

           jsonWriter.WriteStartObject();

           jsonWriter.WritePropertyName("Response");

           jsonWriter.WriteStartArray();

           jsonWriter.WriteStartObject();// add '{'

           jsonWriter.WritePropertyName("Status");

           jsonWriter.WriteValue("Success");

           jsonWriter.WritePropertyName("Remark");

           jsonWriter.WriteValue(info);

       }

       else

       {

           jsonWriter.WriteStartObject();

           jsonWriter.WritePropertyName("Response");

           jsonWriter.WriteStartArray();

           jsonWriter.WriteStartObject();// add '{'

           jsonWriter.WritePropertyName("Status");

           jsonWriter.WriteValue("Failed");

           jsonWriter.WritePropertyName("Remark");

           jsonWriter.WriteValue(con2Str(con));

       }

       jsonWriter.WriteEndObject();

       jsonWriter.WriteEndArray();

       jsonWriter.WriteEndObject();

       info(strFmt("%1,",stringWriter.ToString()));

       //info(strFmt("my message %1", message)); //Warnings

       //info(strFmt("my msg %1", msg)); //warnings

       //info(strFmt("my info %1", info)); // info's

   }

}

Code to get warnings and info messages :

[ExtensionOf(classStr(Global))]

static final class DaxGlobal_Extension

{

   public static SysInfologStr  message;

   public static SysInfologStr  info;

   static boolean checkFailed(SysInfoLogStr txt, URL helpURL, SysInfoAction _sysInfoAction)

   {

       boolean     ret;

       ret = next checkFailed(txt,helpURL,_sysInfoAction);

       Global::parmSysInfologStr(txt);

       return ret;

   }

   static SysInfologStr parmSysInfologStr(SysInfologStr  _message = message)

   {

       message = getPrefix()+_message;

       return message;

   }

   static SysInfologStr parminfo(SysInfologStr  _info = info)

   {

       info = getPrefix()+_info;

       return info;

   }

   static Exception info(SysInfoLogStr txt, URL helpUrl, SysInfoAction _sysInfoAction)

   {

       boolean     ret;

       ret =next info(txt,helpUrl,_sysInfoAction);

       Global::parminfo(txt);

       return ret;

   }

}

Code to get the current record validation message :

[ExtensionOf(classStr(LedgerPostingMessageLog))]

internal final class DaxLedgerPostingMessageLog_Extension

{

   public SysInfologStr message;

   public boolean logCheckFailed(SysInfologStr _message, URL _helpURL)

   {

       boolean ret;

       ret= next logCheckFailed(_message,_helpURL);

       this.parmSysInfologStr(_message);

       return ret;

   }

   public SysInfologStr parmSysInfologStr(SysInfologStr  _message = message)

   {

       message = _message;

       return message;

   } 

}

Code to get the above class message in ledgerjournalcheckpost for use in custom button :

[ExtensionOf(classstr(LedgerJournalCheckPost))]

internal final class Dax_LedgerJournalCheckpost_Extension

{

   public SysInfologStr message;

   protected boolean checkJournalStatus()

   {

       boolean ret;

       ret = next checkJournalStatus();

       message = getPrefix() + ledgerPostingMessageCollection.parmSysInfologStr();

       return ret;

   }

   public SysInfologStr parmErrorMsg()

   {

       return message;

   }

}


Output :





Other Simplest way to get Info Message is :

Validation(info message)  is saved in Log field of Ledger Journal Table.











Tuesday, July 11, 2023

Code to enable/disable buttons for movement journals in D365 X++

 

[ExtensionOf(formStr(InventJournalMovement))]

internal final class DaxInventJournalMovementForm_Extension

{

   public void initDatasourceAllow()

   {

       InventJournalTable  inventJournalTableloc = inventJournalTable_ds.cursor();

       next initDatasourceAllow();

       if(inventJournalTableloc.Posted == NoYes::Yes)

       {

           inventJournalTable_ds.allowDelete(false);

       }

       else

       {

           inventJournalTable_ds.allowDelete(true);

       }

   }

}

Output :



Other Way :
https://daxingwithakhilpatel.blogspot.com/2023/07/disablestandredbutton.html




How to get Cursor record in COC of clicked method in D365 X++

         FormControl                 callerButton    = any2Object(this) as FormControl;

        FormRun                     formRun         = callerButton.formRun();

        FormDataSource              formDataSource  = formRun.datasource();

        CustTableChangeProposal     custTableChangeProposal = formDataSource.cursor();

How to get Cursor record in COC of FormDatasourceField methods in D365 X++

 FormDataObject formDataObject = any2Object(this) as FormDataObject;

FormDataSource formDataSource = formDataObject.datasource();

FormRun formRun = formDataSource.formRun();

CustTable   custTable = formDataSource.cursor();

How to open CustChangeProposal form for custom field in D365 X++

 My requirement is to add a custom field in Custtable form. 

Add a element in Enable Customer Approval Tab in AR Parameters.

If the element is checked. Then if we modify blocked field in Custtable form ,it should work like standard process i.e., a dialogue form(CustChangeProposal) should be opened containing the old and new values of field.

Field : DaxBlocked (Enum)

Add a element in CustTableChangeProposalField Base Enum :




Create a enum field :

Add Created enum in Following Tables:

1.CustTable
2.CustTableChangeProposal (Add Extra Field as like below)

Add A Group Control in CustChangeProposalForm :

Code :

[ExtensionOf(tableStr(CustTableChangeProposal))]
internal final class DaxCustableChangeProposal_Extension
{
    public void setFlagForChangedField(CustTableChangeProposalField _controlledField)
    {
        next setFlagForChangedField(_controlledField);
        switch (_controlledField)
        {
            case CustTableChangeProposalField::DaxBlocked :
                this.IsChangedDaxBlocked = NoYes::Yes;
                break;
        }
    }
}

..................................................................

[ExtensionOf(formControlStr(CustChangeProposal,DiscardDaxBlocked))]
internal final class DaxCustChangeProposal_Extension
{
    public void clicked()
    {
        FormControl                 callerButton    = any2Object(this) as FormControl;
        FormRun                     formRun         = callerButton.formRun();
        FormDataSource              formDataSource  = formRun.datasource();
        CustTableChangeProposal     custTableChangeProposal = formDataSource.cursor();
        ttsbegin;
        CustTableChangeProposal localRecord = CustTableChangeProposal::findByCustRecId(custTableChangeProposal.CustTable, true);
        localRecord.IsChangedDaxBlocked = NoYes::No;
        localRecord.update();
        ttscommit;
        next clicked();
    }
}

........................................................................

[ExtensionOf(formStr(CustChangeProposal))]
internal final class DaxCustChangeProposalForm_Extension
{
    protected void disableDiscardButtons()
    {
        FormRun formRun = this as FormRun;
        FormControl discardDaxBlocked = formRun.design().controlName(formControlStr(CustChangeProposal, DiscarddaxBlocked)) as FormControl;
        next disableDiscardButtons();
        discardDaxBlocked.enabled(false);
    }
      public void showHideGroups()
    {
        CustTableChangeProposal custTableChangeProposal;
        FormRun formRun = this as FormRun;
        FormDataSource  custTableChangeProposal_ds = formRun.dataSource(1);
        FormGroupControl  DaxBlocked = formRun.design().controlName(formControlStr(CustChangeProposal, DaxBlocked)) as FormGroupControl;
        custTableChangeProposal = custTableChangeProposal_ds.cursor();
        next showHideGroups();
        DaxBlocked.visible(custTableChangeProposal.IsChangedDaxBlocked);
    }
}

..........................................................................................

[ExtensionOf(tableStr(CustTableChangeProposalFieldEnablement))]
internal final class DaxCustTableChangeProposalFieldEnablement_Extension
{
    public static void initializeAll()
    {
        next initializeAll();
        CustTableChangeProposalFieldEnablement::findOrCreate(CustTableChangeProposalField::DaxBlocked);
    }
}

............................................................................

[ExtensionOf(formStr(CustTable))]
internal final class DaxCustTableForm_Extension
{
    public void markChangeProposalFields()
    {
        next markChangeProposalFields();
        if (CustTableChangeProposalFieldEnablement::isEnabledFor(CustTableChangeProposalField::DaxBlocked))
        {
            CustTable_DaxBlocked.label(strFmt(
                "@AccountsReceivable:FieldLabelMarkingInBrackets",
                CustTable_ds.object(fieldNum(CustTable, DaxBlocked)).labelText(),
                "@AccountsReceivable:FieldLabelMarkingRequiresApproval"));
        }
      }
}

................................................................................

[ExtensionOf(formDataFieldStr(CustTable,CustTable,DaxBlocked))]
internal final class DaxCustTableFormDataField_Extension
{
    public void modified()
    {
        FormDataObject formDataObject = any2Object(this) as FormDataObject;
        FormDataSource formDataSource = formDataObject.datasource();
        FormRun formRun = formDataSource.formRun();
        CustTable   custTable = formDataSource.cursor();
        formRun.showCustChangeProposalDialog();
        formRun.modifiedControlledField(
                    fieldNum(CustTable, DaxBlocked),
                    fieldNum(CustTableChangeProposal, DaxBlocked),
                    custTable,
                    CustTableChangeProposalField::DaxBlocked);
        next modified();
    }
}

.................................................................................

Output :

ARParameters:














Wednesday, July 5, 2023

Code to read CSV files from blob to D365 using X++

 My requirement is when payment vendor journal is posted, the file is being uploaded to Azure blob storage like in previous post.

When user modifies the file data from blob. It should also update in D365.

When journal is created and posted, the values are not allocated to DaxPaymentStatus,DaxRemarks,DaxUTRNumber fields.

After the file uploaded in blob storage. Manually i gave values to these fields in blob.

By using Batch Job i need to update the particular journal in D365.

Code :

using Microsoft.Azure;

using Microsoft.WindowsAzure.Storage;

using Microsoft.WindowsAzure.Storage.Blob;

using Microsoft.WindowsAzure.Storage.File;

internal final class DaxSysOperationServiceClass extends SysOperationServiceBase

{

    CloudStorageAccount                                 storageAccount;

    public void blobAccess(PersonnelIntegrationStorageAccountConnectionString   azureStorageKey)

    {

        storageAccount  = Microsoft.WindowsAzure.Storage.CloudStorageAccount::Parse(azureStorageKey);

    }

    public void getFileFromBlob(str containerName,str dirPath)

    {

        System.IO.MemoryStream  memoryStream;

        CloudBlobClient         cloudBlobClient;

        CloudBlobContainer      cloudBlobContainer;

        CloudBlobDirectory      cbDir;

        container               fileNameCon, memoryStreamCon;

        if (StorageAccount)

        {

            cloudBlobClient     = StorageAccount.CreateCloudBlobClient();

            cloudBlobContainer  = cloudBlobClient.GetContainerReference(containerName);

            cbDir = cloudBlobContainer.GetDirectoryReference(dirPath);

            System.Collections.IEnumerable lstbolbEnumarable = cbDir.ListBlobs(false,0,null,null);

            System.Collections.IEnumerator lstbolbEnumarator = lstbolbEnumarable.GetEnumerator();

            while(lstbolbEnumarator.MoveNext())

            {

                IListBlobItem item = lstbolbEnumarator.Current;

                if(item is CloudBlockBlob)

                {

                    CloudBlockBlob blob = item;

                     container   filecon = str2con(blob.Name, '/');

                    str         filName = conPeek(filecon, conLen(filecon));

                    memoryStream = new System.IO.MemoryStream();

                    blob.DownloadToStream(memoryStream, null, null, null);

                     this.csvFileRead( memoryStream);

                }

            }

        }

    }

    public void csvFileRead( System.IO.Stream memoryStream)

    {

    LedgerJournalTrans      ledgerJournalTrans;

    #define.delimiterField(',');

    CommaTextStreamIo       textStreamIo;

    counter                 counter;

    container               rec;

    str                     journalNum,Voucher;

    textStreamIo = CommaTextStreamIo::constructForRead(memoryStream);

    textStreamIo.inFieldDelimiter(#delimiterField);

    textStreamIo.inRecordDelimiter('\r\n');

    counter = 0;

     rec = textStreamIo.read();

    while (!textStreamIo.status())

    {

        rec = textStreamIo.read();

        journalNum = conPeek(rec, 1);

        Voucher = conPeek(rec, 2);

        if (rec)

        {

            select forupdate ledgerJournalTrans

                where ledgerJournalTrans.journalnum == journalNum

                && ledgerJournalTrans.Voucher == Voucher;

                ttsbegin;

                ledgerJournalTrans.DaxPaymentStatus =  conPeek(rec, 7);

                ledgerJournalTrans.DaxRemarks      =  conPeek(rec, 9);

                ledgerJournalTrans.DaxUTRNumber    =   conPeek(rec, 8);

                ledgerJournalTrans.update();

                ttscommit;

        }

    }

 }

      public void process()

    {

         this.blobAccess("DefaultEndpointsProtocol=https;AccountName=lohithstorageaccount123;AccountKey=BzXOh6xHERvdl1H6eZ43qMH/qlkCP+F0uKF2bZ3fOcyniSBiWrsYnVW4j7Bu3GEfnKcE8prXZK/2+AStrnbi5w==;EndpointSuffix=core.windows.net");

        this.getFileFromBlob("lohithcontainer","newfolder");

    }

  }