Tuesday, 15 April 2025

Filtering Company-Specific Product Templates - SysRecordTmpTemplate lookup

Hi Techies -

Recently I have come across a requirement where I needed to display product templates specific to a selected company for a given record.

So, as shown in below screenshot there are two records with companies - CHC and NES. Lookup is showing product templets only from NES.

Product templates use two main tables SysRecordTemplateTable and SysRecordTmpTemplate. First table stores the record templates data in such format which is not readable and directly usable to show the user.

There is a standard code how to fetch the values form SysRecordTemplateTable and insert them into SysRecordTmpTemplate to use them to show on UI.

Retrieve Legal Entity Information:

First, I obtained the legal entity associated with the current product release:


MultipleProdRelease prodReleaseLocal = MultipleProdRelease_ds.cursor();
int prodReleasePosition = 1;
container legalEntities;

if (prodReleaseLocal)
{
    legalEntities = conIns(legalEntities, prodReleasePosition, prodReleaseLocal.DataArea);
}

                      

Fetch Template Data Across Companies:

Here DataAreaId condition in where clause didn't work, so had to insert required dataAreaId into a container and use with crossCompany as below. This query will select all record templates from given legal entity and insert data into dataContainer.


 container dataContainer;

 SysRecordTemplateTable templateTable;

while select crossCompany :legalEntities templateTable

        index hint TableIdIdx

        where templateTable.Table == tableNum(InventTable)

{

    dataContainer = dataContainer + templateTable.Data;

}

Populate Temporary Template Table:

Once we have a data of all record templates, we need to iterate through dataContainer and fill in the SysRecordTempTemplate table so that can be used as a lookup table.

    for (templateDataPosition = conlen(dataContainer); templateDataPosition > 1; templateDataPosition--)

    {

        [sysRecordTemplateTmp.Description, sysRecordTemplateTmp.DefaultRecord, sysRecordTemplateTmp.Data, sysRecordTemplateTmp.Details] = conpeek(dataContainer, templateDataPosition);


        sysRecordTemplateTmp.OrgDescription = sysRecordTemplateTmp.Description;

        sysRecordTemplateTmp.insert();               

    }            


Configure the Lookup:

Next, create a new lookup query and set a filled temp table with parmTmpBuffer () method.


    Query query = new Query();

    QueryBuildDataSource queryBuildDataSource;


    SysTableLookup sysTableLookup = SysTableLookup::newParameters(tableNum(SysRecordTmpTemplate), this);

       

    sysTableLookup.addLookupField(fieldNum(SysRecordTmpTemplate, Description), true);


    queryBuildDataSource =         query.addDataSource(tableNum(SysRecordTmpTemplate));

    query.allowCrossCompany(true);

    MultipleProdRelease prodReleaseLE = MultipleProdRelease_ds.cursor();

    query.addCompanyRange(prodReleaseLE.DataArea);


    sysTableLookup.parmQuery(query);

    sysTableLookup.parmTmpBuffer(sysRecordTemplateTmp);

    sysTableLookup.performFormLookup();   


There are many examples available for filtering a lookup, but the product template lookup has unique challenges as it uses temp table to fill in the data and company filter works with crossCompany query.


Wednesday, 5 March 2025

Add a validation on workflow actions

In Dynamics 365FO, workflows can involve a variety of actions such as Approve, Reject, Delegate, and Request Change. There may be scenarios where you need to add validation when a user performs any of these actions.

We encountered a similar situation where we needed to implement a validation process whenever a user performs one of these workflow actions. After investigating, we found that the WorkflowWorkItemActionManager class is triggered when such actions are executed by a user.

Solution -

To add the validation, we can extend the WorkflowWorkItemActionManager class and modify the dispatchWorkItemAction() method. Here's an example of how to extend the class and apply the validation logic:


[ExtensionOf(classStr(WorkflowWorkItemActionManager))]

final class Custom_WorkflowWorkItemActionManagerCls_Extension

{

}


public static void dispatchWorkItemAction( WorkflowWorkItemTable _workItem,                                                                                               WorkflowComment _comment,                                                                                                       WorkflowUser _toUser,                                                                                                                  WorkflowWorkItemActionType                                                                                                      _workItemActionType,                                                                                                                  menuItemName _menuItemName,                                                                                                Name _queueName)

    {  

           WorkflowTypeName    workflowTypeName; 


          //Get the context RecId - for which table the workflow is being executed.     

            RefRecId contextRecId =                                                                                                               WorkflowTrackingStatusTable::findByCorrelation(_workItem.CorrelationId).ContextRecId;


             //For instance, it is a LedgerJournalTable then you can take a journalTable buffer                            using context RecId as below -

            LedgerJournalTable journalTable     = LedgerJournalTable::findByRecId(contextRecId);                        

            //Get the workflow template name, there may be specific 

               template to which validation needs to be applied.

            workflowTypeName = WorkflowTable::findSequenceNumber(WorkflowTrackingStatusTable::

            findByCorrelation(_workItem.CorrelationId).ConfigurationNumber).TemplateName;


           //Check the workflow Action Item type, for which you need to add a validation.

           if (_workItemActionType == WorkflowWorkItemActionType::RequestChange ||

                   _workItemActionType == WorkflowWorkItemActionType::Delegate ||

                    _workItemActionType == WorkflowWorkItemActionType::Return ||

                    _workItemActionType == WorkflowWorkItemActionType::Complete)

        {

            //Declare a set with menu item names for which the validations to be performed.

                     Set menuItemSet = new Set(Types::String);

                    menuItemSet.add(Custom_ConstantHelper::LedgerApprove);       

                    menuItemSet.add(Custom_ConstantHelper::LedgerReject);  

                    menuItemSet.add(Custom_ConstantHelper::LedgerRequestChange); 

                    menuItemSet.add(Custom_ConstantHelper::LedgerDelegate);    


                     if (menuItemSet.in(_menuItemName))

                    {

                        if (Add your validation method call)

                        {

                            throw error("Validation failed");

                        }

                    }

           }

}

        

Extending WorkflowWorkItemActionManager - By extending WorkflowWorkItemActionManager class, you can inject your custom logic for additional validation.
Context RecId: The context of the workflow action is determined using the CorrelationId of the work item, which helps you fetch the corresponding record (in this case, a LedgerJournalTable).
Validation Logic: The validation is added only for specific workflow actions such as Request Change, Delegate, Return, and Complete. You can customize the actions for which the validation should apply.
Menu Item Validation: A set of menu items is defined, and if the current menu item matches any item in the set, the validation method is invoked. If the validation fails, an error is thrown, preventing the action from being completed.

This approach ensures that the workflow actions are validated appropriately, adding an extra layer of control to your functionality.

Sunday, 2 March 2025

Arrange report parameters in one column - UI Builder

Sometimes customer has requirement to have the specific design changes on report/parameter dialog, we came across one of such requirements to have all report parameters in one column of the dialog. This can be achieved with code of couple of lines in build method of UI builder class.

Here’s a code sample demonstrating how this can be done:


/// <summary>

/// UI builder class for managing report control

/// </summary>

class Demo_ReportUIBuilder extends SrsReportDataContractUIBuilder

{

    DialogField dialogField1;

    DialogField dialogField2;


    /// <summary>

    /// Overriden method to handle the report dialog controls

    /// </summary>

    public void build()

    {

        super();


        dialogField1 =  this.bindInfo().getDialogField(this.dataContractObject(),

    methodStr(Demo_ReportContract, parmField1));

 

                //Set as mandatory

         dialogField1.fieldControl().mandatory(true);


        dialogField2 = this.bindInfo().getDialogField(this.dataContractObject(),methodStr(Demo_ReportContract, parmField2));

        

//Code to arrange fields in one column -

        SysOperationDialog dlg = dialog as SysOperationDialog;

        dlg.mainFormGroup().columns(1);

    }

}


Binding Parameters: We used the getDialogField() method to bind the report parameters to dialog fields. In this example, parmField1 and parmField2 are the parameters being used.
Mandatory Field: The mandatory(true) method is used to set parmField1 as a mandatory field for the report.
Single Column Layout: To arrange all the report parameters in a single column, we accessed the SysOperationDialog and used the mainFormGroup().columns(1) method to set the layout to one column.

If you want to display the parameters in multiple columns, you can modify the columns() value accordingly.

Thursday, 28 March 2024

Create a custom business event to send File URL from D365FO to external system.

 

The ideal use case of business event is when you need to take some action outside of D365FO from business event response, and it is useful in case of lightweight payload. In this example the downloadable file URL generated in D365FO will be used by external system for further business purpose.

Business use case – The FO data needs to be written into a text file, the file needs to be uploaded on blob storage and the generated URL should to be shared via business event as part of integration to external system.

This post shows how to implement the business event using X++ code.

We are taking a simple business case as an example, just to understand the implementation, the classes can be updated as per the requirements, specifically the contract class methods and the trigger points.

High level steps to implement business event are –

  • Build the business event contract class.
  • Build the event class.
  • Implement the logic to trigger the business event from FO. 

Implement contract class as below. It must be extended with base class ‘BusinessEventsContract’.

A class must be decorated with [DataContract] attribute. Implement the param method that will access our URL.

The param method is decorated with DataMember and BusinessEventsDataMember, those will be appeared on ‘Business events catalog’ form as Field Name and Field Label respectively.

Similarly, implement initialize() and constructor method.


Next is business event class. Same as contract class – this class must be extended with business event base class. (BusinessEventsBase)

_downloadableURL variable in new method is used to initialize business event contract.

You can decorate the class with Business event label and description that will be appeared on ‘Business event dialog’ form as ‘Name’ and ‘Description’.

Implement the buildContract methods and other methods as shown. buildContract method will be called when the business event is activated. We can activate it from ‘Business event catalog’ form or if you are testing it through power automate, it will activate automatically for you when you save the flow with business event details. You can verify the new endpoint has created on Business event catalog form.

Business event catalog form, highlighted details are the labels that we've created in above classes.

Now, as we have business event classes ready, we can write a class to trigger it.

Create one new class and add below method. createFile() will create a new file and return URL of temporary blob storage. I've used sample text here in fileData variable, whereas you can generate the text file string as per the required logic and set to it.

The business will be triggered only when the event is activated.


Below method is implemented to create a file, upload it to temporary blob storage and return the generated URL back to process() method.

Standard class FileUploadTemporaryStorageStrategy is helpful to create this.



We can test this using Power Automate. For activating business events the Microsoft documentation can be referred. The finance and operations connector has a When a Business Event occurs trigger, which can be used. Once the business event is activated we are set to test it. 
Since I don't have the environment to test it right now, I am just attaching the image which should show similar details in Power automate as below-



Now, once you execute the create file process from FO, this flow should execute and receive the URL of file that can be downloaded. In Power automate run history you can check the status of the execution.

Monday, 8 May 2023

Embedding Power BI reports into D365F&O

In order to view the custom Power BI reports in F&O, we need to embed the Power BI report file to specific desired location on form.

In this blog post, we will see the process of embedding a Power BI report file into D365F&O, enabling user to view and interact with custom Power BI reports within the application.

High level steps-

  1. Develop a Power BI report
  2. Create a resource file in Visual studio of Power BI report file (.pbix file)
  3. Embed the resource contents using controller class on form.
  4. Call a controller class from form control

We presume our custom Power BI report is developed and ready to embed. We need to create a resource object in F&O and add this .pbix file under that resource.



Once our resource file is ready, create a new controller class like below -
We need to use Power BI namespace (PBIPaaS) in our controller class, it provides the classes and utilities for working with Power BI reports in D365F&O.

using Microsoft.Dynamics.AX.Framework.Analytics.Deploy.PBIPaaS;

public class DemoPowerBIController extends PBIReportControllerBase

{

    // Name of the resource containing the content pack

    const str PowerBIContentPackName = 'PowerBIResource';

    // Name of content pack

    const str PowerBIReportName      = 'Demo';


    //Show or hide a filter pane on report

    protected boolean showFilterPane()

    {

        return true;

    }

    //Show or hide navigation pane on report

    protected boolean showNavContentPane()

    {

        return true;

    }

    public PBIReportRunParameters setupReportRunParams()

    {

        // populate and return the report run parameters for the session

        PBIReportRunParameters reportRunParams = new PBIReportRunParameters();

        reportRunParams.parmResourceName(PowerBIContentPackName);

        reportRunParams.parmReportName(PowerBIReportName);

        reportRunParams.parmShowFilterPane(this.showFilterPane());

        reportRunParams.parmShowNavContentPane(this.showNavContentPane());

        reportRunParams.parmPageName(this.pageName());

        reportRunParams.parmIsEmbedded(true);

        reportRunParams.parmApplyCompanyFilter(false);

        return reportRunParams;

    }

    public static void main(Args _args)

    {

        // initialize the controller object

        DemoPowerBIController controller = new DemoPowerBIController();

        controller.setupReportRunParams();

        controller.run(_args);

    }

    public void initializeReportControl(PBIReportRunParameters _reportParameters, FormGroupControl     _formGroupControl)

    {

        PBIReportHelper::initializeReportControlWithReportRunParams(_formGroupControl,                           _reportParameters);

    }

Now as a step number 4 - We need to call this controller class from Form control - Add a new group on form, under tab page a new group can be added. Call initializeReportControl() method from here.


[Control("TabPage")]
    class PowerBIDemoTabPage
    {
        public void pageActivated()
        {
            
            DemoPowerBIController controller = new DemoPowerBIController();
            controller.initializeReportControl(controller.setupReportRunParams(ResourceName,                                           ReportName), GroupNameUnderTheTabPage);
           
        }

    }

initializeReportControl() method is used to initialize the Power BI report control within the user interface. It takes the PBIReportRunParameters and a FormGroupControl as parameters, and calls the initializeReportControlWithReportRunParams() method from the PBIReportHelper class. 
So basically it will initialize the report control with the provided parameters.

Once you open a form in F&O, it will call the report and show the Power BI design on F&O UI.
Please note, in order to view your report you need to test it on higher environments than Tier 1.

basicFilters() is very useful method we can implement in the controller class which will open report file in F&O with initial filters like default company or specific user Id etc. 
It allow us to define and apply basic filters to the Power BI report before it is rendered to the D365F&O user interface. Which we may cover in separate blog.


Hope the current content is helpful!

Wednesday, 4 January 2023

Display max end time of the day using X++

 

In this post we will see how we can display a maximum end time of the day. For instance if I want to show the max end time of today which will be 11:59:59 PM. We can use newDateTime() method of DateTimeUtil class and will use the seconds to adjust our time.

 

MyTable myTable;
TimeOfDay secondsElapsed;
secondsElapsed = 86399;

myTable.ToDateTime = DateTimeUtil::newDateTime(DateTimeUtil::getSystemDate(DateTimeUtil::getUserPreferredTimeZone()),secondsElapsed);

 

This will result you the system date with end time 11:59:59PM.

Form data source modified field event handler in D365FO

 

Below code shows how to get buffer from form data source field event handler(modified field) and update values as per the requirement


/// <summary>
/// Logic that displays the end time of the day
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
[FormDataFieldEventHandler(formDataFieldStr(BankStatementTable,
BankStmtISOAccountStatement, ToDateTime),
FormDataFieldEventType::Modified)]
public static void ToDateTime_OnModified(FormDataObject sender,
FormDataFieldEventArgs e)
{
    FormDataSource BankStmtISOAccountStatement_ds =
    sender.datasource();
    BankStmtISOAccountStatement bankStmtISOAccountStatement =
    BankStmtISOAccountStatement_ds.cursor();
    TimeOfDay secondsElapsed;
    secondsElapsed = 86399;
    if(!bankStmtISOAccountStatement.ToDateTime)
    {
      bankStmtISOAccountStatement.ToDateTime =
      DateTimeUtil::newDateTime(DateTimeUtil::getSystemDate
     (DateTimeUtil::
      getUserPreferredTimeZone()),
      secondsElapsed);
    }
    else
    {
      bankStmtISOAccountStatement.ToDateTime = 
      DateTimeUtil::newDateTime(DateTimeUtil::date
      (bankStmtISOAccountStatement.ToDateTime),
      secondsElapsed);
    }
}

Filtering Company-Specific Product Templates - SysRecordTmpTemplate lookup

Hi Techies - Recently I have come across a requirement where I needed to display product templates specific to a selected company for a give...