Sunday, September 8, 2024

Token Generation in On-Premises vs. Cloud D365 Finance and Operations with .NET Adapter

Today, I want to shed light on a common challenge we may encounter when generating tokens in .NET adapters for integrating Dynamics 365 Finance and Operations with third-party applications. 

In the code snippet below, the HttpRequestMessage class is used to pass essential client credentials for the app registered in the Azure portal. This approach works seamlessly in a cloud environment.

Token generation in .NET adapter using HttpRequestMessage

There are numerous blog posts available on registering an app, and I'll provide a helpful link for your reference here from Microsoft Learn Register an application with the Microsoft identity platform.

Unfortunately, the earlier approach doesn’t work for on-premises instances of Dynamics 365 Finance and Operations. After experimenting with several methods, I found an alternative solution. As shown below, I used a PowerShell script to generate the token.


PowerShell script to generate token

In my .NET adapter class, I then used the PowerShell script to retrieve the token, as demonstrated in the code snippet below.


I hope this post helps save you significant time and effort if you're facing a similar issue. I’d love to hear your feedback as well!
         


Tuesday, January 17, 2023

Fix: Issue in publishing changes in excel template

It was required to add custom financial dimension such as OFFICE, COSTCENTER in FINOPS D365 to excel add-in in D365. I used standard documentation for this type of customization that has already been provided by the Microsoft as following. Using that, custom financial dimensions were added and this worked quite well for us!

https://docs.microsoft.com/en-us/dynamics365/unified-operations/dev-itpro/financial/dimensions-overview

https://docs.microsoft.com/en-us/dynamics365/unified-operations/dev-itpro/financial/add-dimensions-excel-templates

Issue:

However, we started facing following issues while publishing dimensions using excel after renaming financial dimensions like A_Office, B_CostCenter etc.

A row update in data set LedgerJournalLines was not published. Error message: 'The dimension display value could not be generated. Internal error: Exception occurred while executing action getCombinationDisplayValue on Entity DimensionCombination: Error found when validating record.'

Resolution:

Configuration change:

There was a missing setup in "Financial dimension configuration for integrating applications", Previously it had only defined for type Default dimension format, but you need to also have definition for type: "Ledger dimension format"

Technical resolution:

All the dimensions which were added previously from back-end with names Office, CostCenter etc. should be removed and added again with names A_Office, B_CostCenter etc.


References:

https://community.dynamics.com/365/financeandoperations/f/dynamics-365-for-finance-and-operations-forum/296783/unable-to-publish-changes-excel-import


Wednesday, January 26, 2022

Modify expiry and delivery date in RFQ created via Purchase requisition : D365 Finance and Operations

The feature of processing requests for quotations in Dynamics 365 Finance and Operations enables visibility to the quoting process, traceability, and better collaboration among buyers and suppliers(vendors) but it becomes complex for certain new users. 

It is recommended to go through this blog which is also a sort of a training guide for new users. 

Today I would like to share with you a business requirement along with the solution while creating a request for quotation via purchase requisition.

Create the request for quotation from purchase requisition.

While creating a purchase requisition, it is mandatory to setup the workflow "Purchase requisition review"

Navigate to Procurement and sourcing -> Procurement and sourcing workflows.

You can have an overview of the workflow here in this blog. Workflows can be configured on both purchase requisition header and lines. Once the purchase requisition workflow is configured now it is time to create a purchase requisition and submit to workflow. 

Navigate to Procurement and sourcing -> All purchase requisition


Click the "New" Button and add required purchase requisition lines and initiate the workflow process by clicking submit. 

Now you will be able to see "Create request for quotation" button enabled to create the RFQ from purchase requisition. Click this button and now you can see the request for quotation created from purchase requisition.

Navigate to Procurement and sourcing -> Request for Quotations -> All request for quotations

Business Requirement:

Now as per the business requirement, it was required to add the days offset in expiration and delivery dates of request for quotation so that while creating request for quotations from purchase requisition, the days offset is added automatically.

Solution: 

In order to meet the first requirement of adding the offset in expiration date, you can do in "Procurement and sourcing parameters" by adding the days offset.

Navigate to Procurement and sourcing -> Setup -> Procurement and sourcing parameters

And once you define the days offset in above screen, the expiration date is found added with days offset.

However, it is required to do a little bit of coding to modify the delivery date. 
Create an extension class of "PurchRFQCaseAutoCreate_PurchReq" and override the method "setPurchRFQCaseTable"


 I hope this blog post will help you understand the process of creating request for quotation via purchase requisition. Please keep following my blogs. I appreciate your feedback and comments. Thank you! 

Tuesday, January 18, 2022

Capturing the event handler for validation before posting vendor payment journal: D365 classes

 This is my first blog post in 2022, I would like to share with you related to validation and payment control for payment journals and would share with you peace of code to capture the event handler for validating the payment journals - Just to give you a bit of overview of payment journal. Payment journal consists of method of payment where you can control payments and its basic validations. For instance given below we have method of payment set as "Check".

Going into the detail of this method of payment we can see we can also set the payment control, where few of the basic validations can be configured. for instance if check number is mandatory before posting the payment journal or not.


Also one thing to be noted that in order to add any "check number" to payment journal, check number setup is required and it is mandatory to generate the payments before it is posted. And "Generate payment" button  is disabled if any associated workflow is initiated and payment journal is not approved yet. Once it is approved, you can generate payments.  also you can refer this blog for configuring the workflow for payment journals.


Now let's come to little code to capture the event handler before posting the payment journals. Here you go.


I hope this blog will help you understand, how do we control payments and validations for payment journal. Would like to know about your feedback further.


Thursday, January 16, 2020

Catch weight item in D365

Introduction

You could have a scenario particularly in manufacturing industry where a finished good or a product can vary in size, weight or both. In this case, you would have two units of measures for these sort of products (i.e. Catch weight unit and Inventory Unit.). Catch weight unit is the unit in which inventory transactions are performed, for example sold, received, transferred, picked, and shipped etc. The inventory unit is the unit of measure when the product is weighed and invoiced. .  The nominal quantity is the conversion between the catch weight unit and the inventory unit. These items are called catch weight items

Table Schema 

There are many important tables used in product information and management module. Few of the important tables and classes are listed in this blog .
Except all these important tables such as EcoResProduct, InventTable we do have a separate table for catch weights items that is PDSCatchWeightItem.

How do we identify the Released Item marked as catch weight item?

You can search a release item that is also a catch weight item adding a new field named, “CW product” and filter the grid with marked checked.


Or using the database you can use the SQL query
SELECT PDSCATCHWEIGHTITEM.* FROM PDSCATCHWEIGHTITEM
JOIN INVENTTABLE
ON INVENTTABLE.ITEMID = PDSCATCHWEIGHTITEM.ITEMID
AND PDSCATCHWEIGHTITEM.DATAAREAID = 'usmf'

How to create a catch weight item in D365?

Since the catch weight item was introduced in AX in version 2012 and all the steps that are almost same as mentioned in the blog - Creating a Product using the Catch Weight functionality in Microsoft Dynamics AX 2012. Also one thing you have to make sure transfer the appropriate inventory quantity to newly created catch weight item and also make sure to assign the important accounts to item group of the item. Otherwise it will block you using that item in sales and purchase orders. I hope this blog post will give you a brief view of catch weight item, let me know your feedback and queries via comments.



References:

Catch weight functionality a critical requirement for food manufacturing.

Friday, January 10, 2020

How to send that attachments as an email in D365

Normally in D365, there is a common table where your attachments are saved that is DocuRef, and it also stores the reference of table in field "RefTableId" field. Suppose we want to retrieve all the attachments specific to a table. We can simple achieve this using the RefTableId. Next important this is using the buffer of DocuRef table, we have multiple available options using this buffer. for example we can simple retrieve the container using the function as below:

 container _data = DocumentManagement::getAttachmentAsContainer(docRef)"

This container can further be passed on to method as provided below

public static void sendPDFEamilAttachment( SysEmailId      _emailId,
        LanguageId      _language,
        SysEmailAddress             _emailAddr,
        Map             _mappings,container _data,str _fileName)
    {
        SysEmailItemId                   nextEmailItemId;
        SysEmailTable                    sysEmailTable;
        SysEmailMessageTable             sysEmailMessageTable;
        SysEmailContents                 sysEmailContents;
        SysOutgoingEmailTable            outgoingEmailTable;
        SysOutgoingEmailData             outgoingEmailData;
        Filename filename, FileExtension;
      
        FileExtension=".pdf";
       
        select sysEmailTable
               join    sysEmailMessageTable
                where sysEmailMessageTable.EmailId==sysEmailTable.EmailId
                    && sysEmailMessageTable.EmailId== _emailId
                    && sysEmailMessageTable.LanguageId==_language;
       
        if(sysEmailTable.RecId>0)
        {
            sysEmailContents=SysEmailMessage::stringExpand(sysEmailMessageTable.Mail, _mappings);
            nextEmailItemId = EventInbox::nextEventId();
      
            filename =strFmt("%1_%2.pdf",nextEmailItemId,_fileName);
    
    
            outgoingEmailTable.clear();
            outgoingEmailTable.Origin=sysEmailTable.Description;
            outgoingEmailTable.EmailItemId = nextEmailItemId;
            outgoingEmailTable.IsSystemEmail = NoYes::Yes;
            outgoingEmailTable.Sender = sysEmailTable.SenderAddr;
            outgoingEmailTable.SenderName = sysEmailTable.SenderName;
            outgoingEmailTable.Recipient = _emailAddr;
            outgoingEmailTable.Subject =SysEmailMessage::stringExpand(sysEmailMessageTable.Subject, _mappings);
            outgoingEmailTable.Priority = eMailPriority::High;
            outgoingEmailTable.WithRetries = NoYes::NO;
            outgoingEmailTable.RetryNum = 0;
            outgoingEmailTable.UserId = curUserId();
            outgoingEmailTable.Status = SysEmailStatus::Unsent;
            outgoingEmailTable.Message =  sysEmailContents;
            outgoingEmailTable.LatestStatusChangeDateTime =DateTimeUtil::getSystemDateTime();
            outgoingEmailTable.TemplateId= _emailId;
            outgoingEmailTable.insert();


            if(conLen(_data)>0)
            {
                outgoingEmailData.clear();

                outgoingEmailData.EmailItemId = nextEmailItemId;
                outgoingEmailData.DataId = 1;
                outgoingEmailData.EmailDataType = SysEmailDataType::Attachment;
                outgoingEmailData.Data = _data;
                outgoingEmailData.FileName = filename;
                outgoingEmailData.FileExtension =FileExtension;

                outgoingEmailData.insert();
            }
        }
    }

}

Tuesday, October 22, 2019

Deserializing Json Array in D365

I was experiencing an issue while creating a purchase requisition which has more than single line in D365  environment via service. The issue was actually specific to de-serializing the JSON array D365 (X++).  Here I am sharing with you the code which may be helpful for you.


   "purchaseRequisitionList":{ 
      "PurchaseRequisition":[ 
         { 
            "ItemId":"Item1",
            "ItemQty":3
  },
         { 
            "ItemId":"Item2",
            "ItemQty":2
         }
      ]
   }
}

First thing let's create data contracts similar to json above, where would define list for JSON  array.

[DataContract]
class PurchaseRequisitionList
{
    List purchaseRequistion;

    [DataMember("PurchaseRequisition")]

    public List parmLinesDetail(List _purchaseRequistion = purchaseRequistion)
    {
        purchaseRequistion = _purchaseRequistion;
        return purchaseRequistion;
    }

}


[DataContract]
class CreatePurchaseRequisitionContract

    ItemId                   itemId;
    Qty                      itemQty;


    [DataMember("ItemId")]

    public ItemId parmItemId(ItemId _itemId = itemId)
    {
        itemId = _itemId;
        return itemId;
    }

    [DataMember("ItemQty")]

    public Qty parmItemQty(Qty _itemQty = itemQty)
    {
        itemQty = _itemQty;
        return itemQty;
    }
}


It was the issue relating to de serializing of JSON array to our contract class. Add the following code to utilize the newtosoft for JObject.

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

Given below is the code for posting the purchase requisition. You can add exception handling and fields the way you like.

public EntityResponseContract PostPurchaseRequisitionNew(PurchaseRequisitionList purchaseRequisitionList)
{
  EntityResponseContract      response = new EntityResponseContract();
  str                         message;
  boolean                     success = false;
  PurchReqTable               purchReq;
  PurchReqLine                purchReqLine;


purchReq.clear();
purchReq.initValue();

purchReq.PurchReqId              = NumberSeq::newGetNum(PurchReqTable::numRefPurchReqId()).num();

purchReq.insert();

ListIterator linesDetailIterator = new ListIterator(purchaseRequisitionList.parmLinesDetail());


while(linesDetailIterator.more())

{
JObject headerJObject = linesDetailIterator.value();
purchaseRequisition = JsonParser::JsonToObject(classStr(CreatePurchaseRequisitionContract), headerJObject.ToString());

ttsbegin;

purchReqLine.clear();
purchReqLine.initValue();
purchReqLine.initFromPurchReqTable(purchReq);
purchReqLine.Name                   = purchaseRequisition.parmItemId();
purchReqLine.PurchQty               = purchaseRequisition.parmItemQty();
/*All your remaining fields you can place here*/
purchReqLine.insert();
ttscommit;

linesDetailIterator.next();

}
}

You can write a new class call it JsonParser, or simply type this method is class above.

public class JsonParser
{
//This method will deserialize the json to object
public static object JsontoObject(str _className,str _json)
    {
        Object      returnObject=null;
        try
        {
            returnObject= FormJsonSerializer::deserializeObject(className2Id(_className),_json);
        }
        catch
        {
            error("Unable to deserialize due to an error");
        }
        return returnObject;
    }
}

I hope that will also be a quick start for service end point.

Friday, July 26, 2019

Data access using OData - Filter

As many of you may already be familiar that D365 uses OData (Open data protocol). You can access the data entity let's for example you have developed a Data entity with public collection name "Students".
  1.  Query below will produce the data related to all the students. /data/Students
      2.  Modify The query further to give you only top 10 records. /data/Students?$top=10

     3.  Modify the query to get the students selecting only columns FirstName and the LastName.
              /data/Students?$top=10&$select=FirstName,LastName

     4.  Modify the query to get the students filtering the records specifying the Firstname.
             /data/Students?$top=10&$select=FirstName,LastName&$filter=FirstName%20eq%20%27Elsa%27


Hope this little blog post will help you in filtering the relevant data in Data entities.

Wednesday, July 24, 2019

Download a file after saving in AX 365

I required quite simple code to download an XML file on client machine after saving it to a local directory. This is the small code snippet that might be useful for you as well.


Also this blog post http://erconsult.eu/blog/exposing-dynamics-365-onebox-lan/ will help you configuring the download storage so that we could download file to client browser.

Tuesday, March 27, 2018

Fetch number of children associated with a worker

Little code snippet as provided below can help you get the number of children associated with a particular worker.


    public static void getEmployeeTotalChildren(Args _args)
    {       
        DirPartyRelationship            partyRelationship;
        DirRelationshipTypeTable        relationshipTypeTable;
        DirPerson                                                                               dirPerson;
        HcmWorker                                                                                          worker;
        DirPartyTable                                                                        partyTable;
      
        ///get the worker by personnel number
       worker         = HcmWorker::findByPersonnelNumber('NNNNN');

        select           firstonly dirPerson
        where          dirPerson.RecId == worker.Person
            join           partyTable
                where partyTable.PartyNumber == dirPerson.PartyNumber;

        select count(RecId) from partyRelationship
            join  RelationshipTypeId, SystemType from relationshipTypeTable
            where partyRelationship.ParentParty == partyTable.RecId
            &&    relationshipTypeTable.RelationshipTypeId == partyRelationship.RelationshipTypeId
            &&    relationshipTypeTable.SystemType ==  DirSystemRelationshipType::Child;

        info(strFmt("Total Children of Worker %1, are %2", worker.name(), partyRelationship.RecId));
    }

Friday, March 23, 2018

Event handlers and post handlers in D365



Let’s discuss today, how to get the table buffers, form control values, class parameters and method arguments etc., while writing our own event-handlers in D365. I’ll elaborate this using example code snippets in this post. There are already very useful posts on this topic you can look into. Here I required these event handlers while doing customization in Item master.

Form data source event handler


 [FormDataSourceEventHandler(formDataSourceStr(EcoResProductDetailsExtended, InventTable), FormDataSourceEventType::Written)]
public static void InventTable_OnWritten(FormDataSource sender, FormDataSourceEventArgs e){
    FormRun                 form           = sender.formRun();
    FormDataSource          InventTable_ds =       form.dataSource(formDataSourceStr(EcoResProductDetailsExtended,InventTable)) as FormDataSource;
   InventTable             inventTable    = InventTable_ds.cursor();
}

Form event handler

Table Buffer on form closing event


[FormEventHandler(formStr(EcoResAttributeValue), FormEventType::Closing)]
public static void EcoResAttributeValue_OnClosing(xFormRun sender, FormEventArgs e)
{
     FormDataSource ecoResProduct_ds   =          sender.dataSource(formDataSourceStr(EcoResAttributeValue, EcoResProductAttributeValue));
      EcoResProductAttributeValue      ecoResAttributeValue = ecoResProduct_ds.cursor();
}   


Control value and form event level for which auto declaration must be set true


[FormControlEventHandler(formControlStr(EcoResProductCreate, OKButton), FormControlEventType::Clicked)]
public static void OKButton_OnClicked(FormControl sender, FormControlEventArgs e)
{
       FormRun             element       = sender.formRun();
       //form control
       FormControl         modelGroupRef = element.design(0).controlName("ModelGroupId");
        Info(strfmt(“Model Group %1”, modelGroupRef.valueStr()));
       //form parameter
       ItemId              itemId        = element.parmItemId();
}

Post handler for class method


[PostHandlerFor(classStr(EcoResProductReleaseManager), methodStr(EcoResProductReleaseManager, release))]
public static void EcoResProductReleaseManager_Post_release(XppPrePostArgs args){
     EcoResProductReleaseManager releaseMgr;
    //Getting the class object
    releaseMgr     = args.getThis();
   //Getting the class parameter
   ItemId itemId  = releaseMgr.parmItemId();
   //Getting the method argument
    boolean itemCreation = args.getArg("_isCreation");
}

Post handler for overriding table methods modified field and validate Write


[PostHandlerFor(tableStr(InventTable), tableMethodStr(InventTable, validateWrite))]
public static void InventTable_Post_validateWrite(XppPrePostArgs args)
{
      InventTable inventTable = args.getThis() as InventTable
      boolean ret = true;
      // Override the validations here and set the return value accordingly.
       Args.setReturnValue(ret);
}

[PostHandlerFor(tableStr(InventTable), tableMethodStr(InventTable, modifiedField))]
public static void InventTable_Post_modifiedField(XppPrePostArgs args)
{
        //Getting the table buffer
        InventTable inventTable = args.getThis() as InventTable
       //Getting the field id method argument.
        FieldId fieldModified = args.getArg("_fieldId");
        switch (fieldModified)
        {
            //Here you can write your logic on modified field method
                break;
        }
}

Hope this post will help to give you quick start in customization and writing your code in event handlers and post handlers. More information can be found over here

Thursday, November 2, 2017

Models Projects and Packages in D365

Today we will highlight an important architectural change in Dynamics AX 365. In AX 2012, the only option we had was over-layering for any sort of customization/modification in the out of box functionality. We had models/projects as set of elements, as the part of a given layer. Each layer can have one or more models/projects. Models can be exported to files that have the .axmodel extension and projects can be exported to xpo file. In Ax 2012, models files and XPO's are used for deployment purpose.

How do we customize or modify any element in DAX365?

In DAX365, we have same elements reside in Application Explorer and these elements are objects such as tables, forms and classes, menu items etc. Customization of any object is done once it is added into a project and a project is linked or associated with a model.

Model:

Unlike AX2012, In DAX365, creating a model is mandatory thing for any sort of customization. A particular model can contain multiple Visual Studio projects. Therefore you can say it is a collection of projects and a single project can have all or subset of elements from originating model. However, association of a project is only with a single model. It is basically a unit of development/customization. Metadata for models is stored locally on an XML file called a descriptor XML.

Package:

As it is already mentioned that previously in AX 2012, we used XPO's and model as deployment unit. However in DAX365, we have packages for deployment purpose. A package may contain one or more models. In addition to elements of the model, this also includes model metadata which is the description data that define the properties and behavior of the model.  Also a package can be exported to a file which can then be deployed into a staging or production environment. In other words, you can say package is an independent set of layers and models.  It's also a set of folders that consists of XML files representing the elements in the system. In this way, a package can be viewed as a mini model store. Physically package translates directly to unit of compilation which is an assembly or DLL file. Packages can reference other packages that is similar to how .NET assemblies can reference each other.

Packages References:

Referencing a packages is useful, when it is required to reuse a functionality that exists in to another package. In this way, one or more packages can be combined to create a deployable package. And lastly, XML files are stored in the model directory that sit inside the package directory.

Layers:

Layers are the traditional AX concept but in D365, there is a new process where we extend layers as shown below.


Also when you create your model, you specify which layer the model is going to live in. However the importance of layer is quite minimal for instance, you are not required to provide a key for your model to live in that layer. Previously layers were single stack of code that over-layered upon each other. This new process uses layers but they are located in independent stacks.

This was only an architectural overview of Dynamics AX 365, I would further elaborate how do we customize the elements with some examples.

Go through links for further explanation and understanding and share your feedback on the post in comments section.



Friday, March 10, 2017

Refresh caller datasource in the list page AX 2012

Today I am going to share with you sample code for updating the caller datasource on list page action menu item. We normally refresh the caller data source on a normal form like this way.


FormRun callerForm;

callerForm = element.args().caller();

callerForm.dataSource().refresh();
callerForm.dataSource().reread();
callerForm.dataSource().research();

In case of a list page how would you get the grid to refresh since the list page does not allow any override methods, because it uses Interaction Class. However, if I am going to use an action menu item. Code sample provided bellow is useful for this purpose.



void main(Args _args)
{

FormDataSource   callerDataSource;

callerDataSource = _args.record().dataSource();
callerDataSource.refresh();


callerDataSource.reread();
callerDataSource.research(true);
}



Thanks to read the post and hope that will help you in refresh the caller data source both for list page and normal form.

Tuesday, November 15, 2016

Batch jobs in AX 2012

Today, I will demo about batch jobs in AX 2012. At first, I will let you explore the Batch Server Overview on msdn. You have to follow all the steps as given bellow:
1. Configure an AOS instance as the batch server. (Click System administration > Setup > System > Server configuration).

2. Create a new Batch Group, name it ProcessD (Click System administration > Setup > Batch group).
 3. Add available batch server for batch group that has been created.
Now we have to create and schedule a batch job. Let's say we are going to schedule a batch which will fetch the number of purchase orders that are invoiced and add the count in a table named PurchaseOrders. So let's add a custom table Purchase Orders adding two fields DateTill and TotalPurchaseOrder.
Now create a class "ProcessingDataBatch", set it run at server. Also make sure you extend it with RunBaseBatch to make it behave like a batch job.



class ProcessingDataBatch extends RunBaseBatch
{
}
private void processdata()
{
    PurchTable          purchTable;
    PurchaseOrders      purchaseOrder;
    ttsBegin;

    select count(PurchId) from purchTable
        where purchTable.PurchStatus == PurchStatus::Invoiced;

    if (purchTable.PurchId)
    {
        purchaseOrder.TotalPurchOrders = purchTable.PurchId;
        purchaseOrder.DateTill         = today();
        purchaseOrder.insert();
    }


    ttsCommit;
}
public void run()
{
   this.processdata();
}
public static ClassDescription description()
{
    return "Process Data";
}
public static void main(Args _args)
{
    ProcessingDataBatch scheduler = new ProcessingDataBatch();

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

Now next thing is to compile the code and Generate Increment CIL which is required to setup every time you update the code.

In order to deploy the above class as batch Process job, we can do it in two way. Either to create a ax job or a action menu item.

static void ProcessingData(Args _args)
{
    BatchHeader header;
    SysRecurrenceData sysRecurrenceData;
    Batch batch;
    BatchJob batchJob;
    ProcessingDataBatch _ProcessIncrement;
    BatchInfo processBatchInfo;
    BatchRetries noOfRetriesOnFailure = 4;
;

    select batch where batch.ClassNumber == classnum(ProcessingDataBatch);
    if(!batch)
    {
        header = BatchHeader::construct();
        _ProcessIncrement = new ProcessingDataBatch();
        processBatchInfo = _ProcessIncrement.batchInfo();
        processBatchInfo.parmRetriesOnFailure(noOfRetriesOnFailure);
        processBatchInfo.parmCaption("Process Data");
        header.addTask(_ProcessIncrement);
        // Set the recurrence data
        sysRecurrenceData = SysRecurrence::defaultRecurrence();
        SysRecurrence::setRecurrenceStartDateTime(sysRecurrenceData, DateTimeUtil::addSeconds(DateTimeUtil::utcNow(), 20));
        SysRecurrence::setRecurrenceNoEnd(sysRecurrenceData);
        SysRecurrence::setRecurrenceUnit(sysRecurrenceData, SysRecurrenceUnit::Minute);
        header.parmRecurrenceData(sysRecurrenceData);
        // Set the batch alert configurations
        header.parmAlerts(NoYes::No, NoYes::Yes, NoYes::No, NoYes::Yes, NoYes::Yes);
        header.save();
        // Update the frequency to run the job to every two minutes
        ttsbegin;
        select forupdate batchJob
            join batch
        where batchJob.RecId == batch.BatchJobId
        && batch.ClassNumber == classnum(ProcessingDataBatch);

        sysRecurrenceData = batchJob.RecurrenceData;
        sysRecurrenceData = conpoke(sysRecurrenceData, 8, [3]);
        batchJob.RecurrenceData = sysRecurrenceData;
        batchJob.update();
        ttscommit;
    }

}
When you execute this AX job, you would be able to see a new batch job created in waiting state at System Administration > Inquiries > Batch jobs > Batch jobs with caption Process Data.

 Another way is to add an action menu item ProcessingDataBatchSchedule
Open action menu item select the available batch group as created above and set the recurrence as needed.

See the batch job state here, you can specify alerts check logs and modify recurrence, remove or delete batch jobs etc. Hope this blog post is going to help you in start your work on batch jobs. You can explore the batch jobs in this post as well.