X++ code to import multiple file from a folder in AX 2012 R3

X++ code to import multiple files from a folder in AX 2012 R3


Create a Runbase class :

class RBReadFileFromDirectory extends RunBaseBatch
{
    Filename                       ItemFileName;
    Filename                       filename;
    DialogField                   dialogFilename;
    SalesOrderTmp             salesOrderTmp;
    FilePath                        filePath;
    CommaTextIo               fileIO;
    Set                                 fileSet;
    CommaIo                      commaIo;
    str                                   currentFile;
    container                       lineCon;
    #File
    #AviFiles

    #define.CurrentVersion(1)
        #define.Version1(1)
        #localmacro.CurrentList
            fileName
        #endmacro
}

public Object dialog()
{
    DialogRunbase       dialog = super();
    ;

    dialogFilename   = dialog.addField(extendedTypeStr(FilenameOpen));
    dialogFilename.value(filename);

    return dialog;
}

public boolean getFromDialog()
{
    ;

    fileName = dialogFileName.value();
    return super();
}

public container pack()
{
    return [#CurrentVersion,#CurrentList];
}

public boolean unpack(container packedClass)
{
    Version version = runbase::getVersion(packedClass);
    ;
    switch (version)
    {
        case #CurrentVersion:
        [version,#CurrentList] = packedClass;
        break;
        default:
        return false;
    }
    return true;
}

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

public static void main(Args _args)
{
    RBReadFileFromDirectory      readFileFromDirectory;

    ;
    readFileFromDirectory= RBReadFileFromDirectory::construct();

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

}

public void run()
{
    SetEnumerator           sE;
    int                              row = 1;


    if (!filename)     // check file exist from dialog
    {
        filePath =  @'C:\Test\';   // sepcify the folder path
        this.getFiles();
        sE = fileSet.getEnumerator();

        while (sE.moveNext()) // loop through all files
        {
            filename = sE.current();

            if (filename)
            {
                this.Process(filename);
            }

        }
    }
    else           // this code run for the file from dialog
    {
        this.Process(filename);
    }

}

private void getFiles()
{
    System.String[]                 files;
    System.Collections.IEnumerator  enumerator;
    str  file;

    try
    {
        fileSet = new  Set(Types::Container);
        files = System.IO.Directory::GetFiles(filePath, '*.csv');
        enumerator = files.GetEnumerator();

        while(enumerator.MoveNext())
        {
            file = enumerator.get_Current();
            fileSet.add([file]);
        }
    }
    catch (Exception::CLRError)
    {
        throw error ("Error");
    }

}


// Core Logic 

private void Process(Filename    _fileName)
{
    #File
    IO                                       iO;
    Container                           con;
    boolean                             first = true;
    SalesTable                          salesTable;
    SalesLine                           salesLine;
    FileIoPermission                    perm;

    ;

    perm = new FileIoPermission(_fileName, #IO_Read);
    perm.assert();

    iO = new CommaTextIo(_fileName,#IO_Read);

    if (! iO || iO.status() != IO_Status::Ok)
    {
        throw error("@SYS19358");
    }

    
    while (iO.status() == IO_Status::Ok)
    {
        con = iO.read();//  read file
        if (con)
        {
            if (first)  //skip header
            {
                first = false;
            }
            else
            {
                        salesOrderLogLinesIns.clear();
                        salesOrderTmp.SalesId                = conPeek(con, 1);
                        salesOrderTmp.CustAccount       = conPeek(con, 2);
                        salesOrderTmp.PaymMode         = conpeek(con, 3);
                        salesOrderTmp.insert();
               }
               
        }
    }


}
























AX 2012 R3 Invent Marking through X++ Code


X++ code for Invent Marking in AX 2012 R3


public static  void RBInventMarking(purchLine _purchLine)
{
    RecId               issueInventTransOriginId,receiptInventTransOriginId;
    InventQty           qtyToMark;
    InventTrans         issueInventTrans;
    TmpInventTransMark  tmpInventTransMask;
    Map                 mapMarkNow;
    container           con;
    real                qty;
    Map                 mapTmp;
    MapEnumerator       mapEnumerator;
    PurchLine           prePurchLine;


    select firstOnly InventTransId from prePurchLine
        where prePurchLine.PurchId == _purchLine.PurchId
        &&    prePurchLine.ItemId ==  _purchLine.ItemId;

    if (_purchLine.PurchQty < 0 && _purchLine.PurchPrice > prePurchLine.PurchPrice)
    {
        issueInventTransOriginId =
            InventTransOrigin::findByInventTransId(prePurchLine.InventTransId).RecId;

        receiptInventTransOriginId =
            InventTransOrigin::findByInventTransId(_purchLine.InventTransId).RecId;

        qtyToMark = -1;  // specify the qty as per your requirement

        ttsBegin;

        issueInventTrans = InventTrans::findByInventTransOrigin(
            issueInventTransOriginId);

        [con, qty] = TmpInventTransMark::packTmpMark(
            InventTransOrigin::find(issueInventTransOriginId),
            issueInventTrans.inventDim(),
            issueInventTrans.Qty);

        mapTmp = Map::create(con);
        mapEnumerator = mapTmp.getEnumerator();
        while (mapEnumerator.moveNext())
        {
            tmpInventTransMask = mapEnumerator.currentValue();

            if (tmpInventTransMask.InventTransOrigin == receiptInventTransOriginId)
            {
                tmpInventTransMask.QtyMarkNow = qtyToMark;
                tmpInventTransMask.QtyRemain -= tmpInventTransMask.QtyMarkNow;
                mapMarkNow = new Map(Types::Int64, Types::Record);
                mapMarkNow.insert(tmpInventTransMask.RecId, tmpInventTransMask);

                TmpInventTransMark::updateTmpMark(
                    issueInventTransOriginId,
                    issueInventTrans.inventDim(),
                    -qtyToMark,
                    mapMarkNow.pack());

                break;
            }
        }

        ttsCommit;
    }
}

X++ code to copy user roles from one user to another user in AX 2012 R3


X++ code to copy user roles from one user to another user


Create a class as well as the methods below:

class RBUserRoleMapping extends RunBaseBatch
{
 
    UserId            fromUser,toUser;
    NoYesId         roles,groups,options;

    SysQueryRun     queryrun;

    DialogField     dlgFromUserId,dlgToUserId,dlgRoles,dlgGroups,dlgOptions;

    #define.CurrentVersion(1)
    #define.Version1(1)
    #localmacro.CurrentList
        fromUser,
        toUser,
        roles,
        groups,
        options
    #endmacro
}

server static RBUserRoleMapping construct()
{
    return new RBUserRoleMapping ();
}

public server static void main(Args args)
{
    RBUserRoleMapping    userRoleMapping = RBUserRoleMapping ::construct();

    if (userRoleMapping.prompt())
        userRoleMapping.run();

}

protected void new()
{
    super();
}

public Object dialog()
{
    DialogRunbase   dialog = super();
;
    //dialog = this.dialogInternal(dialog);

    dlgFromUserId   = dialog.addField(extendedTypeStr(UserId),  "From UserId");
    dlgToUserId     = dialog.addField(extendedTypeStr(UserId),  "To UserId");
    dlgRoles        = dialog.addField(extendedTypeStr(NoYesId), "Roles");
    dlgGroups       = dialog.addField(extendedTypeStr(NoYesId), "Groups");
    dlgOptions      = dialog.addField(extendedTypeStr(NoYesId), "Options");

    dlgFromUserId.value(fromUser);
    dlgToUserId.value(toUser);
    dlgRoles.value(roles);
    dlgGroups.value(groups);
    dlgOptions.value(options);

    return dialog;

}

public boolean getFromDialog()
{
    boolean ret;

    ret = super();
 
    fromUser    =   dlgFromUserId.value();
    toUser      =   dlgToUserId.value();
    roles       =   dlgRoles.value();
    groups      =   dlgGroups.value();
    options     =   dlgOptions.value();

    return ret;
}


public container pack()
{
    return [#CurrentVersion,#CurrentList];
}

public boolean unpack(container packedClass)
{
    Version     version = RunBase::getVersion(packedClass);

    switch (version)
    {
        case #CurrentVersion:
            [version,#CurrentList] = packedClass;
            break;
        default:
            return false;
    }

    return true;
}


private boolean copyUserRoles()
{
    boolean                 ret = true;
    SecurityRole            securityRole;
    SecurityUserRole        securityUserRole;
    SecurityUserRole        securityUserRoleExist;
    SecurityUserRole        securityUserRoleInsert;
    OMUserRoleOrganization  userRoleOrganization, userRoleOrganization_Insert;

    List                    copiedUserRoles = new List(Types::String);

    ListEnumerator          lEnumerator;

    #define.SystemUser('SystemUser')

    setPrefix(strFmt("Copy User", fromUser, toUser));

    try
    {
        select firstonly securityRole
            where securityRole.AotName == #SystemUser;
        delete_from securityUserRole where securityUserRole.User == toUser && securityUserRole.SecurityRole == securityRole.RecId;

        while select securityUserRole
                where securityUserRole.User == fromUser
            notExists join * from securityUserRoleExist
                where securityUserRoleExist.SecurityRole    == securityUserRole.SecurityRole
                    && securityUserRoleExist.User           == toUser
        {
            select firstOnly securityRole
                where securityRole.RecId == securityUserRole.SecurityRole;

            copiedUserRoles.addStart(securityRole.Name);

            securityUserRoleInsert.initValue();
            securityUserRoleInsert.SecurityRole = securityUserRole.SecurityRole;
            securityUserRoleInsert.User         = toUser;
            securityUserRoleInsert.insert();
            securityUserRoleInsert.clear();

            while select userRoleOrganization
                    where userRoleOrganization.User == fromUser
                        && userRoleOrganization.SecurityRole == securityUserRole.SecurityRole
            {
                userRoleOrganization_Insert.initValue();

                userRoleOrganization_Insert.OMHierarchyType             = userRoleOrganization.OMHierarchyType;
                userRoleOrganization_Insert.OMInternalOrganization      = userRoleOrganization.OMInternalOrganization;
                userRoleOrganization_Insert.SecurityRole                = userRoleOrganization.SecurityRole;
                userRoleOrganization_Insert.SecurityRoleAssignmentRule  = userRoleOrganization.SecurityRoleAssignmentRule;
                userRoleOrganization_Insert.User                        = toUser;

                userRoleOrganization_Insert.insert();
                userRoleOrganization_Insert.clear();
            }
        }
    }
    catch
    {
        ret = false;
    }

    if (ret)
    {
        lEnumerator = copiedUserRoles.getEnumerator();

        if (copiedUserRoles.empty())
            info(strFmt("User %1 and %2 have already the same user role",fromUser, toUser));

        while (lEnumerator.moveNext())
        {
            info(strFmt('%1',lEnumerator.current()));
        }
    }
    else
        error(strFmt("User Roles aborted please review list"));

    return ret;
}

private boolean copyUserGroups()
{
    boolean                 ret = true;

    UserGroupList           userGroupList;
    UserGroupList           userGroupListExist;
    UserGroupList           userGroupListInsert;

    List                    copiedGroups = new List(Types::String);

    ListEnumerator          lEnumerator;

    setPrefix(strFmt("Copy user groups", fromUser, toUser));

    try
    {
        while select userGroupList
                where userGroupList.userId == fromUser
            notExists join * from userGroupListExist
                where userGroupListExist.groupId == userGroupList.groupId
                    && userGroupListExist.userId == toUser
        {
            copiedGroups.addStart(userGroupList.groupId);

            userGroupListInsert.initValue();
            userGroupListInsert.groupId = userGroupList.groupId;
            userGroupListInsert.userId  = toUser;
            userGroupListInsert.insert();
            userGroupListInsert.clear();
        }
    }
    catch
    {
        ret = false;
    }

    if (ret)
    {
        lEnumerator = copiedGroups.getEnumerator();

        if (copiedGroups.empty())
            info(strFmt("User %1 and %2 have already the same user group",fromUser, toUser));

        while (lEnumerator.moveNext())
        {
            info(strFmt('%1',lEnumerator.current()));
        }
    }
    else
        error(strFmt("User group aborted , please review it"));

    return ret;
}


private boolean copyUserOptions()
{
    boolean                 ret = true;

    UserInfo                userInfoSource;
    UserInfo                userInfoTarget;

    SysUserInfo             sysUserInfoSource;
    SysUserInfo             sysUserInfoTarget;

    setPrefix(strFmt("Copy user option", fromUser, toUser));

    try
    {
        select filterByGridOnByDefault,statuslineInfo,Id from userInfoSource
            where userInfoSource.id == fromUser
        join Id,DefaultCountryRegion from sysUserInfoSource
            where sysUserInfoSource.Id == userInfoSource.id;

        ttsBegin;

            select forUpdate userInfoTarget
                where userInfoTarget.id == toUser;
            userInfoTarget.filterByGridOnByDefault = userInfoSource.filterByGridOnByDefault;
            userInfoTarget.statuslineInfo = userInfoSource.statuslineInfo;
            userInfoTarget.update();


            select forUpdate sysUserInfoTarget
                where sysUserInfoTarget.Id == toUser;
            sysUserInfoTarget.DefaultCountryRegion = sysUserInfoSource.DefaultCountryRegion;
            sysUserInfoTarget.update();
        ttsCommit;

    }
    catch
    {
        ret = false;
    }

    if (ret)
    {
        info(strFmt("User  %1 and %2 has the same user option ", fromUser, toUser));
    }
    else
        error(strFmt("User option aborted, please review it"));

    return ret;
}


public void run()
{
    DialogButton diagBut;
    str strMessage = strFmt("Are you sure to copy the roles From UserId : %1 To User Id : %2",fromUser,toUser);
    str strTitle = "User Role Mapping";

    ;

    diagBut = Box::yesNoCancel(
        strMessage,
        DialogButton::No, // Initial focus is on the No button.
        strTitle);
    if (diagBut == DialogButton::Yes)
    {
         if (roles)
        {
            this.copyUserRoles();
        }
        if (groups)
        {
            this.copyUserGroups();
        }
        if (options)
        {
            this.copyUserOptions();
        }

        info(strfmt("Successfully copied the user roles from %1 to %2", fromUser,toUser));

    }
    else
    {
       print "You cancelled the operation";
    }

}

public boolean canGoBatch()
{
    return true;
}

AX 2012 R3 - X++ CODE TO RESET THE ACTUAL VAT AMOUNT FOR SALES ORDER

AX 2012 R3 - X++ CODE TO RESET THE  ACTUAL VAT AMOUNT FOR SALES ORDER



static void RB_ResetVATAmount1(Args _args)
{

    #File

    IO                             iO;
 
   FilenameOpen           filename = "c:\\VAT1.csv";//file name here
   Container                   con;
   boolean                      first = true;
 
   SalesTable                  salesTable;

   SalesLine                     salesLine;

   SalesId                         salesId;

   int                                 cnt;
//Manually adjust sales taxes
TaxRegulation                  taxRegulation;

SalesTotals                       salesTotals;

TaxAmount                        taxAmount;

;
  
iO = new CommaTextIo(filename,#IO_Read);
if (! iO || iO.status() != IO_Status::Ok)
{
            throw error("@SYS19358");
 }
 
while (iO.status() == IO_Status::Ok)
{

     con = iO.read();// read file
     if (con)
     {
            if (first) //skip header
            {
                     first = false;
             }
             else
             {
                     salesId = conpeek(con, 1);
                     salesTable = SalesTable::find(salesId);

                    if (salesTable)
                    {
                       salesTotals = SalesTotals::construct(salesTable, SalesUpdate::All);
                       salesTotals.calc();

                       taxRegulation = TaxRegulation::newTaxRegulation(salesTotals.tax(), null,salestable.TableId, salestable.RecId);
                       if (taxRegulation.taxLinesExist()) // * needs to be true
                       {
                              //taxRegulation.allocateAmount(20); // manually specify the amount you wish and comment the next line i.e. //taxRegulation.resetTaxRegulation();
 
                               taxRegulation.resetTaxRegulation();

                               taxRegulation.saveTaxRegulation();
                       }

                       cnt++;

                   }

            }
       }

  }

 
      info(strFmt("Total number of records updated :- %1",cnt));
 
 
}

AX 2012 R3 Line by line invoicing the sales order using X++ code

static void RB_PostSalesInvoice(Args _args)
{
    SalesTable salesTable;
    SalesFormLetter salesFormLetter;
    Query query;
    QueryRun queryRun;
    QueryBuildDataSource qbds;

    salesTable = SalesTable::find('20001');
    query = new Query(QueryStr(SalesUpdatePackingSlip));
    qbds = query.dataSourceTable(tableNum(SalesLine));

    // Build query range to find those lines which needs to be posted.

    qbds.addRange(fieldNum(SalesLine, SalesId)).value('20001');
    qbds.addRange(fieldNum(SalesLine, SalesStatus)).value(queryValue(SalesStatus::Backorder));
    qbds.addRange(fieldNum(SalesLine, itemId)).value('V12_IF');
    queryRun = new queryRun(query);

    salesFormLetter = SalesFormLetter::construct(DocumentStatus::Invoice);
    salesFormLetter.chooseLinesQuery(queryRun);
    salesFormLetter.update(salesTable);
}

AX 2012 R3 SSRS reports to multi select in parameter and functionality of multivalued filtration for SSRS report dialog

In this walk-through , I will give an overview of multi select lookups with filtration of lookup values based on another multi select lookup.  There was requirement of five parameters for a report including: from dateTo DateProject groupsProject names and customer of selected projects as shown below :
In following sections I will show you the implementation of the multi select lookups step by step:   
1.       Creating UI Builder class
Create a class and extend it with SysOperationAutomaticUIBuilder
class SL_InvoiceDetailsUIBuilder extends SysOperationAutomaticUIBuilder
{
    //Dialog fields declaration
    DialogField                             dfprojGroup, dfprojName, dfcustName, dfFromDate, dfToDate;
    container                               ProjGroupsContainer, ProjIdContainer;
    //Multiselect grid declaration
    SysLookupMultiSelectGrid     msCtrlProjGroups, msCtrlProjNames, msCtrlCustNames;
    SL_InvoiceDetailsContract      contract;
}
Override the build method to add dialog fields as follows:
public void build()
{
 contract               = this.dataContractObject();
dfFromDate          = this.addDialogField(methodStr(SL_InvoiceDetailsContract, parmFromDate), contract);
dfToDate              = this.addDialogField(methodStr(SL_InvoiceDetailsContract, parmToDate), contract);
dfprojGroup         = this.addDialogField(methodStr(SL_InvoiceDetailsContract, parmProjGroups), contract);
dfprojName         = this.addDialogField(methodStr(SL_InvoiceDetailsContract, parmProjNames), contract);
dfcustName         = this.addDialogField(methodStr(SL_InvoiceDetailsContract, parmCustNames), contract);
}
Override the PostBuild method to register the multi select lookups with lookup events as follows:
public void postBuild()
{
    super();
    //fromDate
dfFromDate =     this.bindInfo().getDialogField(this.dataContractObject(), methodStr(SL_InvoiceDetailsContract, parmfromdate));
    // to date
dfToDate  = this.bindInfo().getDialogField(this.dataContractObject(), methodStr(SL_InvoiceDetailsContract, parmtodate));
    //Project Group
 dfprojGroup   = this.bindInfo().getDialogField(contract, methodStr(SL_InvoiceDetailsContract,      parmProjGroups));
 dfprojGroup.registerOverrideMethod(methodStr(FormStringControl, lookup), methodStr(SL_InvoiceDetailsUIBuilder, ProjGroupLookup), this);
       if (dfprojGroup){
        dfprojGroup.lookupButton(2);
       }
    //Project Names
dfprojName   = this.bindInfo().getDialogField(contract, methodStr(SL_InvoiceDetailsContract, parmProjNames));
dfprojName.registerOverrideMethod(methodStr(FormStringControl, lookup), methodStr(SL_InvoiceDetailsUIBuilder, ProjNamesLookup), this);
      if (dfprojName){
        dfprojName.lookupButton(2);
      }
    //Customers Name
dfcustName   = this.bindInfo().getDialogField(contract, methodStr(SL_InvoiceDetailsContract, parmCustNames));
dfcustName.registerOverrideMethod(methodStr(FormStringControl, lookup), methodStr(SL_InvoiceDetailsUIBuilder, CustNamesLookup), this);
      if (dfcustName){
        dfcustName.lookupButton(2);
     }
}
There are three multi select lookups Project Groups, Project Names and Customer names, the implementation of Project Group lookup is as follows:
private void ProjGroupLookup(FormStringControl _control)
{
    msCtrlProjGroups = SysLookupMultiSelectGrid::construct(_control, _control);
    msCtrlProjGroups.parmQuery(this.ProjGroupQuery());
    msCtrlProjGroups.run();
}
The following method returns the query for ProjGroupLookup to fill in the data.
private Query ProjGroupQuery()
{
    Query       query;
    query = new query();
    query.addDataSource(tableNum(ProjGroup));
    query.dataSourceTable(tableNum(ProjGroup)).addSelectionField(fieldNum(ProjGroup, ProjGroupId));
    query.dataSourceTable(tableNum(ProjGroup)).addSelectionField(fieldNum(ProjGroup, Name));
    return query;
}
Implementation of Project names lookup:
private void ProjNamesLookup(FormStringControl _control)
{
    msCtrlProjNames = SysLookupMultiSelectGrid::construct(_control, _control);
    msCtrlProjNames.parmQuery(this.ProjNameQuery());
    msCtrlProjNames.run();
}
The following method returns the query for ProjNamesLookup to fill in the data; the project group range is also applied to the query that is selected on the parameter screen.
private Query ProjNameQuery()
{
    Query            query;
    int              counter;
    query = new query();
    query.addDataSource(tableNum(ProjTable));
    if(msCtrlProjGroups){
        //container is filled with selected project groups on the parameter screen
        ProjGroupsContainer = msCtrlProjGroups.getSelected();
    }
    //Loop over the container values and apply range to query
    for(counter = 1; counter <= conLen(ProjGroupsContainer); counter++)
    {
query.dataSourceTable(tableNum(ProjTable)).addRange(fieldNum(ProjTable,     ProjGroupId)).value(queryValue(ProjGroup::sl_findRecId(conPeek(ProjGroupsContainer , counter)).ProjGroupId));
    }
    query.dataSourceTable(tableNum(ProjTable)).addSelectionField(fieldNum(ProjTable, ProjId));
    query.dataSourceTable(tableNum(ProjTable)).addSelectionField(fieldNum(ProjTable, Name));
    return query;
}
Implementation of Customer names lookup:
private void CustNamesLookup(FormStringControl _control)
{
    msCtrlCustNames = SysLookupMultiSelectGrid::construct(_control, _control);
    msCtrlCustNames.parmQuery(this.CustNameQuery());
    msCtrlCustNames.run();
}
The following method returns the query for CustNamesLookup to fill in the data; only those customers are filled whose projects are selected in project names parameter.
private Query CustNameQuery()
{
    Query  query;
    int    counter;
    query = new query();
    query.addDataSource(tableNum(CustTable));
    query.dataSourceTable(tablenum(CustTable)).addDataSource(tableNum(DirPartyTable));
    if(msCtrlProjNames)
    {
        //container is filled with selected projects on the parameter screen
        ProjIdContainer = msCtrlProjNames.getSelected();
    }
    //Loop over the container values and apply range to query
    for(counter = 1; counter <= conLen(ProjIdContainer); counter++)
    {
        query.dataSourceTable(tableNum(CustTable)).addRange(fieldNum(CustTable, AccountNum)).
        value(queryValue(ProjTable::findRecId(conPeek(ProjIdContainer, counter)).CustAccount));
    }
    query.dataSourceTable(tableNum(CustTable)).addSelectionField(fieldNum(CustTable, AccountNum));
    //Below relation is applied to get customer name along with customer account
    query.dataSourceTable(tablenum(DirPartyTable)).joinMode(JoinMode::InnerJoin);
    query.dataSourceTable(tablenum(DirPartyTable)).relations(true);
    query.dataSourceTable(tablenum(DirPartyTable)).addSelectionField(fieldNum(DirPartyTable,  Name));
    return query;
}
This completes my implementation of multiselect lookups for SSRS report, remember to comment the super in postRun() method after overriding it, to avoid the exception of RegisterOverrideMethod was called twice for the same object for method ‘lookup’
2.       Contract class
The implementation of contract class for sl_InvoiceDetailsUIBuilder is given below:
Class Declaration:
[
    DataContractAttribute,
    SysOperationContractProcessingAttribute(classstr(SL_InvoiceDetailsUIBuilder)),
    SysOperationGroupAttribute('Date range', literalStr("@SYS329083"),  '1',FormArrangeMethod::HorizontalFlushRight),
    SysOperationGroupAttribute('Criteria', literalStr("Criteria"),'2')
]
class SL_InvoiceDetailsContract
{
    FromDate    fromDate;
    ToDate      toDate;
    List        projGroup, projName, Customers;
}
From Date data contract methods:
[
    DataMemberAttribute('FromDate'),
    SysOperationLabelAttribute(literalStr("@SYS329084")),
    SysOperationGroupMemberAttribute('Date range'),
    SysOperationDisplayOrderAttribute('1')
]
public FromDate parmFromDate(FromDate _fromDate = fromDate)
{
    fromDate = _fromDate;
    return fromDate;
}
To Date data contract methods:
[
    DataMemberAttribute('ToDate'),
    SysOperationLabelAttribute(literalStr("@SYS329086")),
    SysOperationGroupMemberAttribute('Date range'),
    SysOperationDisplayOrderAttribute('2')
]
public ToDate parmToDate(ToDate _toDate = toDate)
{
    toDate = _toDate;
    return toDate;
}
Project Group data contract methods:
[
    DataMemberAttribute("ProjGroupList"),
    AifCollectionTypeAttribute("ProjGroupList", Types::String),
    SysOperationLabelAttribute(literalStr("Project Groups")),
    SysOperationGroupMemberAttribute('Criteria'),
    SysOperationDisplayOrderAttribute('1')
]
public List parmProjGroups(List _projGroups = projGroup)
{
    projGroup = _projGroups;
    return projGroup;
}
Project Names data contract methods:
[
    DataMemberAttribute("ProjNameList"),
    AifCollectionTypeAttribute("ProjNameList", Types::String),
    SysOperationLabelAttribute(literalStr("Project Names")),
    SysOperationGroupMemberAttribute('Criteria'),
    SysOperationDisplayOrderAttribute('2')
]
public List parmProjNames(List _projNames = projName)
{
    projName = _projNames;
    return projName;
}
Customer Names data contract methods:
[
    DataMemberAttribute("CustNameList"),
    AifCollectionTypeAttribute("CustNameList", Types::String),
    SysOperationLabelAttribute(literalStr("Customers")),
    SysOperationGroupMemberAttribute('Criteria'),
    SysOperationDisplayOrderAttribute('3')
]
public List parmCustNames(List _custNames = Customers)
{
    Customers = _custNames;
    return Customers;
}
This completes my data contract class implementation, Below are the few snapshots for demonstration.  
3.       Demonstration
Figure 1 : Multi select Project group parameter
Figure 2: Multi select Project names parameter filtered according to project groups selected
Figure 3: Multi select customer parameter filtered according to project names
Note : This article assumes that you have prior basic knowledge of Data contract class, User Interface (UI) Builder Class and Report Data Provider Class.

Install and Configure Zpl Printer on D365 F&O

  Setup Zpl Printer On D365 F&O Posted on Recently, I have had an opportunity review the possibilities to print license plates within D3...