8/14/2019

Build default dimension and ledger dimension in D365fo

It's pretty common to build default dimension or ledger dimension with dimension name and value, especially when developing transaction related functionalities. In D365fo, the dimension utility classes are different than the AX ones, so I created this post as a memo, and if it could help others that will be even better.

1. Build default dimension

public static RecId GetDefaultDimension(Map _dimNameValueMap)
{
MapEnumerator           mapEnum;
//Map                      dimNameValueMap = new Map(Types::String, Types::String);
RecId                    defaultDimension;        
DimensionAttribute       dimAttribute;
DimensionAttributeValue  dimAttributeValue;
DimensionAttributeValueSetStorage dimAttributeValueSetStorage = new DimensionAttributeValueSetStorage();
   
mapEnum = _dimNameValueMap.getEnumerator();

while(mapEnum.moveNext())
{
str     dimName, dimValue;
dimName      = mapEnum.currentKey();
dimValue     = mapEnum.currentValue();
// find and validate the dimension name.
dimAttribute = DimensionAttribute::findByName(dimName);
if (dimAttribute.RecId == 0)
{
error(strFmt('Dimension {0} doesn\'t exist.', dimName));
}

// find the dimension attribute value and append it to storage.
if (dimValue)
{
dimAttributeValue = DimensionAttributeValue::findByDimensionAttributeAndValue(dimAttribute, dimValue, false, true);
dimAttributeValueSetStorage.addItem(dimAttributeValue);
}
else
{
error(strFmt('Dimension {0} doesn\'t have value.', dimName));
}
}

defaultDimension = dimAttributeValueSetStorage.save();
return defaultDimension;
}


2. Build ledger dimension with main account and default dimension

public static RecId GetLedgerDimension(MainAccountNum _mainAccountId, RecId _defaultDimension)
{
MainAccount     mainaccount = MainAccount::findByMainAccountId(_mainAccountId);
RecId      ledgerddimension;

LedgerDefaultAccountHelper::getDefaultAccountFromMainAccountId(_mainAccountId);
ledgerddimension = LedgerDimensionFacade::serviceCreateLedgerDimension(LedgerDefaultAccountHelper::getDefaultAccountFromMainAccountId(_mainAccountId), _defaultDimension);

return ledgerddimension;
}



3. Get display value of a specific dimension name from ledger dimension

public staic str GetDisplayValue(RecId _ledgerDimension, str _dimensionName)
{
    DimensionAttributeLevelValueView dimAttributeLevelValueView;
    DimensionAttribute dimAttribute = DimensionAttribute::findByName(_dimensionName);
    select DisplayValue from dimAttributeLevelValueView  where dimAttributeLevelValueView.ValueCombinationRecId == _ledgerDimension && dimAttributeLevelValueView.DimensionAttribute == dimAttribute.RecId;
    return dimAttributeLevelValueView.DisplayValue;
}

5/16/2018

How to get error message

The error messages in AX are stored in the global object infolog. So when trying to print the message, we need to access infolog instance and fetch the information. I created a static method in my utility class so I can reuse it whenever I want to do that in my code.
The method is,

public static str getErrorMsg(int _infologPosition = 0)
{
    str msg;
    int currentLine;
    ;
    for (currentLine = _infologPosition + 1; currentLine <= infologline(); currentLine++)
    {
        msg += infolog.text(currentLine) + '\n';
    }

    return msg;
}


So in code, when I want to capture the message, I can just,

try
{
......
}
catch
{
errorMsg = UtilityClass::getErrorMsg();
......
}

8/24/2017

How to add .dll file into AX 2012

When I was trying to add a .dll into AX 2012, I could not find a document how to do that. After many testings I did, eventually, I figured out how it works. Basically, this post is for those developers who have not used .dll in AX 2012. Hope it could help them understand how to do it.
I found that to let AX use the .dll we need to,

1. Let AX know that .dll, so we can compile the code.

  • Add reference



















If the .dll is already added to GAC on the client machine, we can select it from the assembly list. Otherwise, we'll need to select the .dll file by browsing the file.

  • If .dll is not in GAC on the client machine, we additionally need to copy the .dll file to the client/bin folder and restart the client so we can compile the code which consumes the .dll. 


2. Let AX be able to find the .dll to consume it.

  • If the code is run at server side, we need to make sure the .dll could be correctly consumed. So we need to either add that .dll to GAC on the server machine or add it to server/[InstanceName]/bin folder and restart the AOS.
  • If the code is run at client side, we need to do the same to the client machine.


Once we've done above, the .dll should be able to be called by AX 2012.

4/25/2017

How to email report in AX 2012 without outlook?

In out of box AX 2012, the user has to have outlook to mail the report which is kind of annoying, cause not all of the AX user has outlook. We can find code snippet to bypass outlook to do that online, but you'll notice those codes are only for the old AX report instead of SSRS report. In AX 2012,  the reports are all SSRS reports which means we cannot modify the code in Info\reportSendMail as we did for AX 2009 to bypass outlook.
In fact, it's even easier to do that in AX 2012. In AX 2012, when emailing reports, the class 'SrsReportRunMailer' is executed. What we need to do is just change that class. If you open that class, you'll see the method 'initMailer' decides whether to use SMTP or outlook. When mailing in a batch, it uses SMTP, but when emailing at client, it uses outlook. To change the logic to always go with SMTP, we only need to comment out the 'if' clause and leave the code which takes SMTP. See below,

private void initMailer()
{
    fromAddress = SrsReportRunMailer::buildFromEmailAddress();
    // Always use SMTP, never outlook.
    //if (isRunningOnServer())
    //{
        // get the mailer object
        mailer = this.parmSysMailer();

        // validate the from email addresses
        if (!fromAddress ||
            !SysEmailDistributor::validateEmail(fromAddress))
        {
            error(strfmt("@SYS134596", "@SYS4083"));
            return;
        }
    //}
    //else
    //{
        //inetMailer = new SysINetMail();
    //}



2/24/2017

Access the private project created by other users

Sometimes we do want to access the private project created by other users, e.g. when importing a model we get a private project conflict, to resolve this we have to find the project and delete it. If the owner of those projects are gone, we'll not be able to access them as normal, we'll have to do something unusual to get it.
Even though there're many different approaches to get this resolved in others' blogs/forum, I found my way is probably easiest. Here it is,
1. Find the AX database on SQL server.
2. Open table ModelElement with the private project type only.The private project type is 38.
3. Change the prefix of the private project you are looking for from the original user Id to your user Id by a simple update like.
  update [AX1111_2].[dbo].[ModelElement]
  set Name = 'Admin_AOTExport2012_DynamicsPerfDirect'
  where Name = 'JasonPAdmin_AOTExport2012_DynamicsPerfDirect' and ElementType = 38
4. Reopen AX client with your account, and you'll see the project appears in your private project

11/03/2016

Programmatically settle a payment journal trans against a specific open transaction.

In the following example, I used a customer invoice as an example of open transaction, the logic applies to vendor open transaction as well.
To make sure the amount can be fully settled, I checked if the payment amount matches the open transaction amount. According to the specific requirement, we can ajust the logic.
The key point of this code snippet is the use of CustVendOpenTransManager.

static void SettleCustOpenTrans(Args _args)
{
    CustVendOpenTransManager manager;
    LedgerJournalTrans ljt;
    CustTransOpen cto;
    CustTrans ct;
    ;
    ct = CustTrans::findFromInvoice('Invoice Number', 'Customer Account');

    // Get cust trans open.
    if (ct.RecId)
    {
        cto = CustTransOpen::findRefId(ct.RecId);

        // Check amount to make sure the open trans was not partially settled before.
        if (cto.AmountCur != ct.AmountCur)
            throw error('invoice has been partially paid');
    }
    else
    {
        throw error('invoice could not be found');
    }

    // Fetch the added LedgerJournalTrans.
    ljt = LedgerJournalTrans::find('Journal number', 'Jounal trans voucher', true);

    // Check if the journal trans is paying the invoice wholly.
    if (ljt.AmountCurCredit - ljt.AmountCurDebit != cto.AmountCur)
        throw error('this payment journal line is not paying the invoice wholly');


    ttsBegin;
    // Mark the invoice.
    manager = CustVendOpenTransManager::construct(ljt);
    manager.updateTransMarked(cto, true);

    // Update journal trans.
    ljt.MarkedInvoice = ct.Invoice;
    ljt.SettleVoucher = SettlementType::SelectedTransact;
    ljt.update();
    ttsCommit;

    info('done');
}


10/28/2016

Observations in sysoperation framework Synchronous execution mode

Observations in Synchronous execution mode.

Called from: Client
Run on: Server (If the service class is registered in AXClient service group, it will run on client, cause the executeOperationWithRunAs returns false in that case.)
Session: CIL (If the service class is registered in AXClient service group, it will run in an interpreter session, cause the executeOperationWithRunAs returns false in that case.)

Possible errors:
  • As I said above, normally synchronous service will be executed on server and in CIL session, but if it's executed on client and interpreter session, we cannot set 'Runon' of the service class to server any longer. Otherwise, we'll see a data contract error like this,
  • If the method is published as an AIF service entry (with [SysEntrypointAttribute]), and the 'Runon' is 'Call from', we'll see this 'uncheced' error when executing,

Conclusion,
In AX, we cannot call a method in a servie class in both synch and asynch mode, because of the 2 errors above and asynch mode needs the service to be registered in AXClient service group.