Tuesday, October 18, 2022

Copy attachments from one form to another in D365FO

There is a requirement that we need to copy the attachment from one form to another with the flow of data.

Here we are copying attachment from custom table  to CustTable.

pubic Satic void main(Args _args)
{

    DocuRef    docuRefFrom;
    //Current record    
    CustomTable   current = _args.record();
    CustTable   custTable = CustTable::find('Cust-00001');

    While select docuRefFrom
                  where docuRefFrom.RefRecId   ==  current.RecId
                     && docuRefFrom.RefTableId ==  current.TableId
                    && docuRefFrom.RefCompanyId == current.DataAreaId
              {
                      //Copy attachment into CustTable
                      DocuRef::createFromDocuRef(docuRefFrom, custTable.RecId , tableNum(CustTable));
                                                                        OR
                      DocuRef::createFromDocuRef(docuRefFrom, custTable.RecId , custTable.TableId);
               }

}

Saturday, August 27, 2022

Cannot insert duplicate key in object 'dbo.SECURITYROLE'.

 Sometimes we got this error during database synchronization.

Error: Cannot insert duplicate key in object 'dbo.SECURITYROLE'.

Fig#


Solution:

In order to solve this issue, we need to temporarily update all security role tables by SQL:

select * into securityrole_bk from securityrole
update securityrole set recid = recid * 1000

select * into securitysubrole_bk from securitysubrole
update securitysubrole set securitysubrole = securitysubrole * 1000
update securitysubrole set securityrole = securityrole * 1000

select * into SECURITYROLERUNTIME_bk from SECURITYROLERUNTIME
update SECURITYROLERUNTIME set securityrole = securityrole * 1000

select * into securityuserRole_bk from securityuserRole
update securityuserRole set securityrole = securityrole * 1000

select * into SECURITYROLEEXPLODEDGRAPH_bk from SECURITYROLEEXPLODEDGRAPH
update SECURITYROLEEXPLODEDGRAPH set securitysubrole = securitysubrole * 1000
update SECURITYROLEEXPLODEDGRAPH set securityrole = securityrole * 1000

select * into SECURITYROLEDUTYEXPLODEDGRAPH_bk from SECURITYROLEDUTYEXPLODEDGRAPH
update SECURITYROLEDUTYEXPLODEDGRAPH set securityrole = securityrole * 1000

select * into SECURITYROLEPRIVILEGEEXPLODEDGRAPH_bk from SECURITYROLEPRIVILEGEEXPLODEDGRAPH
update SECURITYROLEPRIVILEGEEXPLODEDGRAPH set securityrole = securityrole * 1000


Now, run database synchronization. After Database synchronization succeeded, we will update RecId of SECURITYROLE back to old value  by SQL. In order to do that perform the following TSQL to return back the index:


update securityrole set recid = recid / 1000 where recid >= 1000 and recid % 1000 = 0 and not exists (select recid from securityrole as securityrole2 where securityrole2.recid = (securityrole.recid/1000))

update securitysubrole set securitysubrole = securitysubrole / 1000 where securityrole >= 1000 and securityrole % 1000 = 0 and not exists (select securitysubrole from securitysubrole as securitysubrole2 where securitysubrole2.securitysubrole = (securitysubrole.securitysubrole/1000))

update securitysubrole set securityrole = securityrole / 1000  where securityrole >= 1000 and securityrole % 1000 = 0  and not exists (select securityrole from securitysubrole as securitysubrole2 where securitysubrole2.securityrole = (securitysubrole.securityrole/1000))

update SECURITYROLEEXPLODEDGRAPH set securitysubrole = securitysubrole / 1000  where securitysubrole >= 1000 and securitysubrole % 1000 = 0 and not exists (select securitysubrole from SECURITYROLEEXPLODEDGRAPH as SECURITYROLEEXPLODEDGRAPH2 where SECURITYROLEEXPLODEDGRAPH2.securitysubrole= (SECURITYROLEEXPLODEDGRAPH.securitysubrole/1000))

update SECURITYROLEEXPLODEDGRAPH set securityrole = securityrole / 1000 where securityrole >= 1000 and securityrole % 1000 = 0 and not exists (select securityrole from SECURITYROLEEXPLODEDGRAPH as SECURITYROLEEXPLODEDGRAPH2 where SECURITYROLEEXPLODEDGRAPH2.securityrole = (SECURITYROLEEXPLODEDGRAPH.securityrole/1000))

update SECURITYROLERUNTIME set securityrole = securityrole / 1000 where securityrole >= 1000 and securityrole % 1000 = 0 and not exists (select securityrole from SECURITYROLERUNTIME as SECURITYROLERUNTIME2 where SECURITYROLERUNTIME2.securityrole = (SECURITYROLERUNTIME.securityrole/1000))

update securityuserRole set securityrole = securityrole / 1000 where securityrole >= 1000 and securityrole % 1000 = 0 and not exists (select securityrole from securityuserRole as securityuserRole2 where securityuserRole2.securityrole = (securityuserRole.securityrole/1000))

update SECURITYROLEDUTYEXPLODEDGRAPH set securityrole = securityrole / 1000 where securityrole >= 1000 and securityrole % 1000 = 0 and not exists (select securityrole from SECURITYROLEDUTYEXPLODEDGRAPH as SECURITYROLEDUTYEXPLODEDGRAPH2 where SECURITYROLEDUTYEXPLODEDGRAPH2.securityrole = (SECURITYROLEDUTYEXPLODEDGRAPH.securityrole/1000))

update SECURITYROLEPRIVILEGEEXPLODEDGRAPH set securityrole = securityrole / 1000 where securityrole >= 1000 and securityrole % 1000 = 0 and not exists (select securityrole from SECURITYROLEPRIVILEGEEXPLODEDGRAPH as SECURITYROLEPRIVILEGEEXPLODEDGRAPH2 where SECURITYROLEPRIVILEGEEXPLODEDGRAPH2.securityrole = (SECURITYROLEPRIVILEGEEXPLODEDGRAPH.securityrole/1000))

Happy daxing :) 

Sunday, July 24, 2022

Calculate MarkupTrans Amount in D365FO using X++

 static void calculateMarkupTrans(Args _args)
{
    SalesTable              salesTable = SalesTable::find('000854');
    SalesLine               salesLine;
    AmountCur               markupAmount;
    MarkupTrans             markupTrans;
    CurrencyExchangeHelper  curCurrencyExchangeHelper;
     
    while select salesLine
        where salesLine.SalesId == salesTable.SalesId
    {
        while select markupTrans
            where markupTrans.TransTableId  == salesLine.TableId
                && markupTrans.TransRecId    == salesLine.RecId
        {
            markupAmount = Markup::calcTrans(markupTrans, salesLine.SalesQty, salesLine.LineAmount);
            if (markupTrans.CurrencyCode != salesTable.CurrencyCode)
            {
                // To automatically conver amount Markup::calcMarkupAmount can be used
                curCurrencyExchangeHelper = CurrencyExchangeHelper::newExchangeDate(Ledger::primaryLedger(CompanyInfo::findDataArea(markupTrans.company()).RecId), systemDateGet());
                markupAmount = curCurrencyExchangeHelper.calculateAccountingToTransaction(salesTable.CurrencyCode, markupAmount, true);
            }
        }
    }
}

Report name in report contract cannot be null or empty.

 I ran into this issue while running the invoice journal report.



After spending sometime, I'm able to find the solution. To fix this issue just run  the following code in the runnable class:

class RunnableClass
{
    /// <summary>
    /// Run the class with the specified arguments.
    /// </summary>
    /// <param name = "_args">The specified arguments.</param>
    public static void main(Args _args)
    {
        PrintMgmtReportFormat                     printMgmtReportFormat;
        
        ttsbegin;
        select forupdate printMgmtReportFormat
            where printMgmtReportFormat.Name == 'ReportName.Report';
        printMgmtReportFormat.System = NoYes::Yes;
        printMgmtReportFormat.doUpdate();
        ttscommit;
    }
}

Create Offset Ledger Dimension using String Value in D365FO

Create offset ledger dimension using string value in D365FO X++:

Str offsetAccountValue = XXXX-XXXXXX-XXXX.XXXX-XXXX-XXXX-XXXXXX-XXXXXX


public RefRecId createOffsetLedgerDimension(str _offsetAccountVal)
    {
        LedgerAccountContract                      ledgerAccountContract, ledgerAccountContractLoc;
        List                                                      valueContracts, valueContractsLoc;
        DimensionHierarchy                           dimHierarchy;
        int                                                        dimCounterLoc;
        DimensionAttribute                            dimAttributeLoc;
        str                                                        dimAttValueLoc;
        DimensionAttributeValue                   dimAttributeVal, dimAttributeValLoc;
        DimensionAttributeValueContract     dimAttributeValueContract, dimAttributeValueContractLoc;
        MainAccountNum                              mainAccountId, mainAccountIdLoc;
        DimensionStorage                              dimStorage, dimStorageLoc;
        
        valueContracts                      = new List(Types::Class);
        valueContractsLoc                = new List(Types::Class);
        ledgerAccountContract         = new LedgerAccountContract();
        ledgerAccountContractLoc   = new LedgerAccountContract();

        select firstonly * from dimHierarchy
                                    where dimHierarchy.StructureType == DimensionHierarchyType::DataEntityLedgerDimensionFormat
                                    && dimHierarchy.IsDraft          == NoYes::No;
        
        if(offsetAccountVal)
        {
            for(int counterLoc = 0; counterLoc <= strLen(offsetAccountVal); counterLoc++)
            {
                dimCounterLoc++;
                dimAttributeLoc = DimensionAttribute::find(DimensionHierarchyLevel::findByDimensionHierarchyAndLevel(dimHierarchy.RecId, dimCounterLoc).DimensionAttribute);
                                    
                if(strFind(offsetAccountVal, "-", counterLoc, strLen(offsetAccountVal)) > 0)
                {
                    dimAttValueLoc = subStr(offsetAccountVal, counterLoc, (strFind(offsetAccountVal, "-", counterLoc, strLen(offsetAccountVal)) - counterLoc));
                                        
                    if(strFind(dimAttValueLoc, "-",0, strLen(dimAttValueLoc)) > 0)
                    {
                        dimAttValueLoc = subStr(dimAttValueLoc, 0, (strFind(dimAttValueLoc, "-",0, strLen(dimAttValueLoc)) - 1));
                    }
                    counterLoc = strFind(offsetAccountVal, "-", counterLoc, strLen(offsetAccountVal));
                }
                else
                {
                    dimAttValueLoc = subStr(offsetAccountVal, counterLoc, strLen(offsetAccountVal));
                    counterLoc     = strLen(offsetAccountVal);
                }
                dimAttributeValLoc = DimensionAttributeValue::findByDimensionAttributeAndValue(dimAttributeLoc, dimAttValueLoc);
                if(dimAttributeLoc.Name == "MainAccount")
                {
                    if(!MainAccount::findByMainAccountId(dimAttValueLoc).RecId)
                    {
                        throw Error(strFmt("Invalid value of %1",dimAttributeLoc.Name));
                    }
                    mainAccountIdLoc = dimAttValueLoc;
                    continue;
                }
                if(dimAttributeValLoc.RecId)
                {
                    dimAttributeValueContractLoc = new DimensionAttributeValueContract();
                    dimAttributeValueContractLoc.parmName(dimAttributeLoc.Name);
                    dimAttributeValueContractLoc.parmValue(dimAttributeValLoc.CachedDisplayValue);
                    valueContractsLoc.addEnd(dimAttributeValueContractLoc);
                }
                else
                {
                    throw Error(strFmt("Invalid value of %1",dimAttributeLoc.Name));
                }
            }
            ledgerAccountContractLoc.parmMainAccount(mainAccountIdLoc);
            ledgerAccountContractLoc.parmValues(valueContractsLoc);
            dimStorageLoc = DimensionServiceProvider::buildDimensionStorageForLedgerAccount(ledgerAccountContractLoc);
            return DimensionAttributeValueCombination::find(dimStorageLoc.save()).RecId;
        }
        return 0;
        
    }

You can get format combination from the below line. Currently it's for ledger dimension, you can also get the same for other dimension by changing the type of enum (DimensionHierarchyType).

select firstonly * from dimHierarchy
                                    where dimHierarchy.StructureType == DimensionHierarchyType::DataEntityLedgerDimensionFormat
                                    && dimHierarchy.IsDraft          == NoYes::No;


Saturday, May 21, 2022

Using Extension Chain of Command (COC) and EventHandlers in D365FO

 1. DataSource Extension class:

[ExtensionOf(formDataSourceStr(InventQualityOrderTable, InventQualityOrderLine)) ]

final class AxInventQualityOrderTableFormDataSource_Extension

{

    public void initValue()

    {

        FormDataSource formDS = this;

        InventQualityOrderLine  qualityOrderLine = formDS.cursor();

        

        //OR you can directly do this like

        InventQualityOrderLine qualityOrderLine = this.cursor();

        next initValue();

        qualityOrderLine.VariableId     = 'Test';

    }

}


2. FormDataSourceField Extension Class

[ExtensionOf(formDataFieldStr(InventQualityOrderTable, InventQualityOrderLine, TestId)) ]

final class AxInventQualityOrderTableFormDataSourceField_Extension

{

    public void modified()

    {

        FormDataObject      formDataObject = any2Object(this) as FormDataObject;    

        FormDataSource      formDS = formDataObject.datasource();

        InventQualityOrderLine  qualityOrderLine = formDS.cursor();

        //OR you can merge above two lines into single line like this;

        InventQualityOrderLine qualityOrderLine = formDataObject.datasource().cursor();

        next modified();

        // do your logic here

        qualityOrderLine.VariableId  = 'Assign value';

    }

}


3. Form Button COC Class Extension

[ExtensionOf(formControlStr(LedgerJournalTransCustPaym, ButtonSettlement))]

final class AxLedgerJournalTransCustPaymFormButton_Extension

{

    public void clicked()

    {  

        FormButtonControl       btnCtrl = any2Object(this) as FormButtonControl;

        FormDataSource          ds = btnCtrl.formRun().dataSource(tableStr(LedgerJournalTrans));

        LedgerJournalTrans      ledgerJournalTrans = ds.cursor();

        

        //do your logic here

        info(strFmt("%1", ledgerJournalTrans.Voucher));

        next clicked();

        //Here 

    }

}


4. Form Datafield OnModified EventHandler

[FormDataFieldEventHandler(formDataFieldStr(InventQualityOrderTable, InventQualityOrderLine, IncludeResults), FormDataFieldEventType::Modified)]

public static void IncludeResults_OnModified(FormDataObject sender, FormDataFieldEventArgs e)

{

    InventQualityOrderLine      qualityOrderLine = sender.datasource().cursor();

    info(strFmt("%1", qualityOrderLine.TestId));

}


5. Form control modified eventHandler

[FormControlEventHandler(formControlStr(InventQualityOrderTableCreate, InventoryDimensions_InventSiteId), FormControlEventType::Modified)]

public static void InventoryDimensions_InventSiteId_OnModified(FormControl sender, FormControlEventArgs e)

{

    FormRun                     formRun = sender.formRun();

    InventQualityOrderTable     inventQualityOrderTable   = formRun.dataSource(tableStr(InventQualityOrderTable)).cursor();

    InventDim                   inventDim                 = formRun.dataSource(tableStr(InventDim)).cursor();

    info(StrFmt("%1", inventDim.InventSiteId));


}


6. Post Event Handler 

[PostHandlerFor(tableStr(PurchTable), tableMethodStr(PurchTable, initValue))]

public static void PurchTable_Post_initValue(XppPrePostArgs args)

{

    PurchTable purchTable = args.getThis() as PurchTable;

    purchTable.test_PrintNote=NoYes::Yes;

}


7. Form DataSource OnActivated EventHandler

[FormDataSourceEventHandler(formDataSourceStr(CustTable, CustTable), FormDataSourceEventType::Activated)]

public static void CustTable_OnActivated(FormDataSource sender, FormDataSourceEventArgs e)

{

     CustTable           custTable     = sender.cursor();

     FormDataSource      custTable_ds  = sender.formRun().dataSource("CustTable");

     FormRun             element       = sender.formRun();

     FormControl         myNewButton   = element.design(0).controlName("MyNewButton");

     myNewButton.enabled(false);

}


8. Table OnInserted EventHandler

[DataEventHandler(tableStr(CustInvoiceTrans), DataEventType::Inserted)]

public static void CustInvoiceTrans_onInserted(Common sender, DataEventArgs e)

{

    CustInvoiceTrans custInvoiceTrans = sender as CustInvoiceTrans;

    SalesTable salesTable = SalesTable::find(custInvoiceTrans.SalesId);

    if(salesTable.SalesStatus == SalesStatus::Invoiced )

    {  

     //code

    }

}


9. Pre EventHandler

[PreHandlerFor(tableStr(PurchLineHistory), tableMethodStr(PurchLineHistory, update))]

public static void PurchLineHistory_Pre_update(XppPrePostArgs args)

{

    PurchLine _purchline;

    PurchLineHistory _purchLineHistory= args.getThis();

    _purchline=PurchLine::find(_purchLineHistory.PurchId,_purchLineHistory.LineNumber );

    _purchLineHistory.PriceDiscTableRefRecId =_purchline.PriceDiscTableRefRecId;

}

10. Form init Method COC

/// <summary>
/// LedgerJournalTransDaily Form Extension
/// </summary>
[ExtensionOf(formStr(LedgerJournalTransDaily))]
public final class LedgerJournalTransDaily_Extension
{
    /// <summary>
    /// Performs the basic initialization of the form.
    /// </summary>
    public void init()
    {
        FormRun formRun = this as FormRun;

        next init();
        
        //write your logic here
    }
}

SSRS report error message: The number of defined parameters is not equal to the number of cell definitions in the parameter panel.

Sometimes when we make any changes to the SSRS dataset and deploy the report. We face this kind of issue which is very common:


There are multiple solutions in order to solve this issue. Some are mentioned below:

Solution 1:
a) Create new design (Shouldn't duplicate it from existing design).
b) Copy all objects from old design and paste it in new design.
c) Save, Rebuild and deploy the report. It will work.

Solution 2:
a) Go to xml file of that report (Path : K:\AosService\PackagesLocalDirectory\ModelName\ModelName\AxReport)
b) Take a backup of that report for a safe side (optional)
c) Open that xml file in notepad (Hint: open notepad as administrator)
d) Remove "ReportParametersLayout" tag completely. See figure 2.1
e) Replace (in case of copy) or save the xml file and redeploy the report. It will work.

Figure 2.1:

Remove all the highlighted text coming inside ReportParametersLaout tag.








Happy Daxing :)

Tuesday, February 22, 2022

Bring Your Own Database (BYOD) in D365FO

BYOD (Bring Your Own Database) is a feature that helps us export data from D365FO (in full or incremental mode) to external database hosted in the cloud or on-premises. Data transfer is done with the help of data entities and a copy of the staging table is created in the target database. You can use existing entities or create your own entities to structure the data the way you want for your external database.

In this post, I will walk you through the steps involved in setting up and configure BYOD in D365FO.

Step-1: Login in D365FO and  Go to Modules > System Administration > Data Management

           Click  on  > Configure Entity Export to database



Step-2: Click on New button to create a new BYOD data source




Step-3: Enter Source name, Description, Azure SQL / SQL connection string and click on validate.

Connection string should be in the format:
Data Source=xxxxxxxx.database.windows.net,1433; Initial Catalog=xxxxxxxx; Integrated Security=False; User ID=xxxxxxxx; Password=xxxxxxxx;


Once, you click on validate it will show infolog.

Step-4: Click on Publish.


 It will take you to a workspace titled "Target entities", where you can scroll through all entities or search for specific ones.



Step-5: Select the entities you want to export and click "Publish" again. A Job Will schedule and Message will appear once job complete and a table will be created to targeted DB. See Fig 1.1

Note: There is the option to set CHANGE TRACKING for each entity. If you do decide to set this feature, you will be able to export incrementally. If you do not set this, then you can only do a full export. This feature must be set here and for each entity




Fig 1.1: 


Step-6: Now Go back to Systems Administrations > Workspace > Data management 
 and Click on > Export Data 





Step-7: Enter Group name, Description. Click on add entity and fill the required fields like Entity name and select Target data format to your Data source, Default refresh type should be incremental then click on Add.



Note that you also have the option to choose a Default refresh type ("Incremental" or "Full push only")."Incremental" refresh type should only be chosen if we turned on Change Tracking in step 5. Otherwise, select "Full push only".

You are ready to export :) You can also add multiple entities for exporting data.

Step-8: Click on export now. 



You can also export the data in batch for better performance. After completion of data export, you can verify your targeted data source Like below screenshot.



Done!

Sunday, February 6, 2022

SysOperation Framework in D365FO

The SysOperation Framework, initially called Business Operation Framework, seem to be the new substitute of the RunBase framework. As such, it allows you to perform operations that require parameters from the user, it allows you to set the operations to be executed in batch, or in a new asynchronous way, or in a synchronous manner. The great thing is that it simplifies the pack / unpack of variables that was pretty nasty in the RunBase framework, taking advantage of the Attributes feature, introduced with AX 2012.

SysOperation framework allows application logic to be written in a way that supports running operations interactively or via the Microsoft Dynamics AX batch server. It implements the MVC (Model–View–Controller) design pattern, with the isolation of Parameters (Model), Dialog (View) and Service (Controller), for the code that’s executed.

The key objects of the framework are defined below:

  • DataContract
  • UIBuilder
  • Controller
  • Service


DataContract:

The data contract is the model class in which we define which attributes we need for our operation, commonly set as parameters by the user in a dialog. It's nothing more than a model class with a few attributes in it. We can define a SysOperation Data Contract class simply by adding the DataContractAttribute attribute to its declaraion. 

Additionally, if we want a set of methods to be available to us, we can also extend the SysOperationDataContractBase base class. With this class, we can define how our basic dialog will look like to the user. 

We can define labels, groups, sizes and types of the parameters.


UIBuilder:

The UI builder class is actually an optional class for the SysOperation framework, which kind of acts as the view part of the pattern. You should only use it if you want to add some extra behavior to the dialog that AX constructs dynamically for you. If you are perfectly happy with the dialog AX shows you when you run your operation, you shouldn't worry about this class. To create a UI Builder, you should extend the SysOperationAutomaticUIBuilder class. It will provide you a set of methods to work with the dialog's design, but we usually add extra behavior or customize a lookup inside the postBuild method.


Controller:

The controller class has greater responsibility than the others. As the name suggests, it is the class that orchestrates the whole operation. The controller class also holds information about the operation, such as if it should show a progress form, if it should show the dialog, and its execution mode - asynchronous or not. To create a controller class you should extend the SysOperationServiceController, which will give you all of the methods you need.


Service:

There are some who put the business logic on controller classes so that they also perform the operation itself. I'm particularly not a big fan of that, it's too much responsibility for a single class! The programmers who created the SysOperation framework probably think the same, and they have made a way to separate the operation. You can create a service class! The only thing you have to do is extend the SysOperationServiceBase class and you're good to go. This class is the one that should contain all the business logic. When constructing your controller, you will indicate which class holds the operation that the controller will trigger, I'll demonstrate it later.

So this was a brief explanation of SysOperation framework . For more information, you can also download the official Microsoft whitepaper here.

Now, Let's have a look on sysoperation framework with the help of an example:

DataContract Class:

UI Builder:

  


Controller:


Service:


In the last, create a new action menu item that will call the controller class. So in the properties of action menu item  set the 'Object type' to be class and the 'object' to be name of the controller class. In my example, the controller class is named "DemoController".

Here is a screenshot of dialog box from the above UI Builder class defined by single and multiselect parameters.






































It's Done ☺