Settle payment journal against sales invoice in D365 FinOpsApps

Code used to settle posted payment journal transaction against the sales invoice

// <summary>
    /// Settle payment journal (which still has balance) against sales invoice. Each payment journal is make for one sales order only.
    /// No payment is made for multiple order. If balance is available after settle, leave the balance as there might be another invoice
    /// not comes in yet for the same order. This periodic job will settle against them when the invoice is in.
    /// </summary>
    /// <param name = "_contract"></param>
    public void processSettlement(AG_CustSalesPaymSettlementDataContract _contract)
    {
        RefRecId            paymLedgerJournalTransRefRecId = _contract.parmPaymLedgerJournalTransRecId();
        boolean             showInfoLog                    = _contract.parmShowInfoLog();
        LedgerJournalTable  paymLedgerJournalTable;
        LedgerJournalTrans  paymLedgerJournalTrans;
        CustTransOpen       paymCustTransOpen, custTransOpen;
        CustTrans           paymCustTrans;

        //Loop through all payment journal which still has open balance
        while select paymLedgerJournalTable
            where   paymLedgerJournalTable.Posted       == NoYes::Yes
            join    paymLedgerJournalTrans
            where   paymLedgerJournalTrans.JournalNum   == paymLedgerJournalTable.JournalNum
                &&  ((!paymLedgerJournalTransRefRecId)                                      //If specific payment journal line is provided, get that only,
                ||   (paymLedgerJournalTrans.RecId      == paymLedgerJournalTransRefRecId)) //else, get all lines (which satisfy other query criteria)
            join    paymCustTrans
            where   paymCustTrans.Voucher               == paymLedgerJournalTrans.Voucher
                &&  paymCustTrans.RecId                 == paymLedgerJournalTrans.CustTransId
                &&  paymCustTrans.TransType             == LedgerTransType::Payment
            join    paymCustTransOpen
            where   paymCustTransOpen.RefRecId          == paymCustTrans.RecId
        {
           AG_CustSalesPaymSettlementService::settleByPaymJourTrans(paymLedgerJournalTrans, showInfoLog);
        }

    }

    public static void settleByPaymJourTrans(LedgerJournalTrans _paymLedgerJournalTrans, boolean _showInfoLog)
    {
        boolean             useLegacyMethod = false;
        CustTable           custTable;
        LedgerJournalTrans  paymLedgerJournalTrans;
        CustTransOpen       invCustTransOpen, paymCustTransOpen, custTransOpen;
        CustTrans           invCustTrans, paymCustTrans;
        CustInvoiceJour     custInvoiceJour;
        SpecTransManager    manager;
        CustVendTransData   custVendTransData;

        //Given the payment LedgerJournalTrans, find the related CustTrans which has CustTransOpen
        select firstonly paymLedgerJournalTrans
            where   paymLedgerJournalTrans.RecId  == _paymLedgerJournalTrans.RecId
            join    paymCustTrans
            where   paymCustTrans.Voucher         == paymLedgerJournalTrans.Voucher
                &&  paymCustTrans.RecId           == paymLedgerJournalTrans.CustTransId
                &&  paymCustTrans.TransType       == LedgerTransType::Payment
            join    paymCustTransOpen
            where   paymCustTransOpen.RefRecId    == paymCustTrans.RecId;

        //Find the related invoice to be settled against the payment journal in the query above
        select firstonly invCustTrans
            where   invCustTrans.AccountNum      == paymCustTrans.AccountNum
                &&  invCustTrans.MCRPaymOrderID  == paymCustTrans.MCRPaymOrderID
                &&  invCustTrans.TransType       == LedgerTransType::Sales
            join    invCustTransOpen
            where   invCustTransOpen.RefRecId    == invCustTrans.RecId;

        custTable = CustTable::find(paymCustTrans.AccountNum);

        try
        {
            ttsbegin;

            if(paymCustTransOpen && invCustTransOpen)
            {
                //Legacy code for settlement --------------------------------------------------------------------------------------------------------
                if(useLegacyMethod)
                {
                    //Create an object of the CustVendTransData class with the invoice transaction as parameter and mark it for settlement
                    custVendTransData = CustVendTransData::construct(invCustTrans);
                    custVendTransData.markForSettlement(custTable);

                    //Create an object of the CustVendTransData class with the payment transaction as parameter and mark it for settlement
                    custVendTransData = CustVendTransData::construct(paymCustTrans);
                    custVendTransData.markForSettlement(custTable);

                    // Settle all transactions marked for settlement for this customer
                    CustTrans::settleTransact(custTable, null, true, SettleDatePrinc::DaysDate, systemdateget());
                }
                //New code for settlement -----------------------------------------------------------------------------------------------------------
                else
                {
                    //Mark for settlement
                    SpecTransExecutionContext specTransExecutionContext = SpecTransExecutionContext::newFromSource(custTable);
                    SpecTransManager          specTransManager          = SpecTransManager::construct(specTransExecutionContext.parmSpecContext());
                    Amount                    settleAmount              = invCustTransOpen.AmountCur;

                    //Prevent over settle
                    if(settleAmount > (-paymCustTransOpen.AmountCur))
                        settleAmount = (-paymCustTransOpen.AmountCur);

                    //Payment
                    specTransManager.insert(paymCustTransOpen.DataAreaId,
                                            paymCustTransOpen.TableId,
                                            paymCustTransOpen.RecId,
                                            -settleAmount,
                                            paymCustTrans.CurrencyCode);
            
                    //Invoice
                    specTransManager.insert(invCustTransOpen.DataAreaId,
                                            invCustTransOpen.TableId,
                                            invCustTransOpen.RecId,
                                            settleAmount,
                                            invCustTrans.CurrencyCode);
                
                    //Settle
                    if(CustTrans::settleTransaction(specTransExecutionContext, CustTransSettleTransactionParameters::construct()))
                    {
                        if(_showInfoLog)
                            info(strFmt("@AG:AG_SettlementCompletedFor", invCustTrans.MCRPaymOrderID, invCustTrans.Invoice, settleAmount)); //Label: Settlement completed for Sales order %1, Invoice %2, Amount %3
                    }
                }
            }

            ttscommit;
        }
        catch
        {
            error(strFmt("@AG:AG_SettlementForPaymentJournalLineFailed", _paymLedgerJournalTrans.RecId, _paymLedgerJournalTrans.accountDisplay())); //label: Settlement for payment journal line is unsuccessful (RecId: %1, Account: %2)
        }
    }

Get Last Selected Value on Form Control

Problem: Store last selection in the form by user

There was a requirement to store user’s last selection on the form – i.e. the site selected last time should be populated automatically when the user opens the same form again. So if the user selected siteInventory details inquiry Dynamics AX “E” the form should remember it the next time.

Solution: We can solve this problem by implementing standard Ax syslastvalue/Pack-Unpack methods. Here we store the value in variable for current user session and based on user, current extension etc. To develop this functionality add the following code on the form.

Code: Add these methods on the form and it will start working:

//class declaration

public class FormRun extends ObjectRun

{

InventSiteId                    site;

#define.CurrentVersion(1)

#localmacro.CurrentList

site

#endmacro

}

//pack

public container pack()

{

return [#CurrentVersion,#CurrentList];

}

// Unpack

public boolean unpack(container packedClass)

{

int version;

;

version = RunBase::getVersion(packedClass);

if(version)

{

[version,#CurrentList] = packedClass;

return true;

}

return false;

}

//Site sysLastValue: This method is use to initialize default value

Public void initParmDefault()

{

;

site = “SiteA”;

}

//Site sysLastValue: This method is use to store dataAreaId for current user

public dataAreaId lastValueDataAreaId()

{

return curext();

}

//Site sysLastValue: This method returns the name of caller

public identifiername lastValueDesignName()

{

return element.args().menuItemName();

}

//Site sysLastValue: This method returns the name of form/object

public identifiername lastValueElementName()

{

return this.name();

}

//Site sysLastValue: This method returns the type of caller

public UtilElementType lastValueType()

{

return UtilElementType::Form;

}

//Site sysLastValue: This method returns the name of current user

public userId lastValueUserId()

{

return curuserid();

}

//Site sysLastValue: on form close save site value

public void close()

{

site = StringEditSiteId.valueStr();

xSysLastValue::saveLast(this);

super();

}

//Site sysLastValue: on run assign value to form control

public void run()

{

super();

//get the last value stored in cache

//here unpack method used

xSysLastValue::getLast(this);

//set the last user selection on the field- this is form control for siteId

StringEditSiteId.text(site);

}

Adding custom filter on a list page

Imagine you want a filter on a list page say planned orders list page. Filter works on the order date and shows order due today or all. It has two values in the selection Today/All. Its not that straightforward as it may seem as it is a list page. On a normal AX form, its much easier.

A number of steps required to achieve this.

1. Add the custom filter to the list page. Note the filterExpression, its a call to method we create in next step. Also you need an enum and an EDT based off the enum, EDT is selected in the properties as well.

2. SysQueryRangeUtil
Add a method to the SysQueryRangeUtil.

3. Interaction class
List pages use the interaction classes for logic. We need to customize 2 methods in the ReqTransPOListPageInteraction.
In method initializing, after super(), add this
   customFilter = SysEPCustomFilter::construct(formStr(ReqTransPOListPage));  
   customFilter.load();  
   customFilter.setInitialFilterControlValue(formControlStr(ReqTransPOListPage, yourFilter), 0);  
Note: customFilter.load() will reload the last saved selection in your filter. In case, you want a specific enum selection everytime, comment out this line.
In method initializeQuery, before super(), add this
Note the call to method in SysQueryRangeUtil
   if (this.listPage().listPageArgs().parameters())  
   {  
     reqPOFilterEnum = customFilter.getFilterControlValue(formControlStr(ReqTransPOListPage, yourFilter));  
     SysQuery::findOrCreateRange(_query.dataSourceTable(tableNum(ReqPO)), fieldNum(ReqPO, ReqDateOrder)).value(SysQueryRangeUtil::xxxReqPOFilter(reqPOFilterEnum));  
   }  
4. Your list page

Capturing infolog messages

Retrieve the content of infolog and store it in log tables, made to track the progress of a batch job and take action on any errors. I used the below method in a catch statement handling all unexpected errors and at the same time updating error log tables. The while loop retrieves all the infolog errors using SysInfologEnumerator and SysInfologMessageStruct infolog classes and concatenating a string with the error messages.

 private str getErrorStr()  
 {  
   SysInfologEnumerator enumerator;  
   SysInfologMessageStruct msgStruct;  
   Exception exception;  
   str error;  
   enumerator = SysInfologEnumerator::newData(infolog.cut());  
   while (enumerator.moveNext())  
   {  
     msgStruct = new SysInfologMessageStruct(enumerator.currentMessage());  
     exception = enumerator.currentException();  
     error = strfmt("%1 %2", error, msgStruct.message());  
   }  
   return error;  
 }  

And the catch statement calling the above method.

   catch (Exception::Error)  
   {  
     ttsbegin;  
     mmsStagingPurchImport.selectForUpdate(true);  
     mmsStagingPurchImport.Error = true;  
     mmsStagingPurchImport.ErrorLog = this.getErrorStr();  
     mmsStagingPurchImport.update();  
     ttscommit;  
     retry;  
   }  

Execute business operations in CIL or not?

In AX 2012, steps are taken by the development team at Microsoft to align AX IDE with .NET. That’s why you regenerate the CIL after X++ code changes.
CIL obviously has its benefits like faster execution in cases of heavy logic processing.
But sometimes while testing or debugging, developers would prefer NOT to avail this option. Debugging can be cumbersome in AX 2012 as you will need to configure Visual Studio for code running in CIL. The solution is to uncheck a checkbox in Options at
Tools > Options > Development > General > Execute business operations in CIL
Now even the code which was running in CIL will now run in X++ interpreted mode, the old way.
Word of caution, this option should only be used by developers while debugging. It should ideally be checked again after work is finished.
Read more at http://msdn.microsoft.com/en-us/library/hh528509.aspx

Global findByRecId() function  

AX 2012 relies heavily on foreign key relations for its table relations. A consequence of this change is that now developers will be accessing tables using recIds rather than using any other primary key, which was the norm before. So far so good. So what’s the problem, you may ask.
One example I found in an excellent blog post.

http://www.doens.be/2009/07/select-a-record-from-a-table-when-you-only-have-the-tableid/

 Image
Now when you need to access a record buffer in a table MyTable using record id, instead of writing it as MyTable::findByRecId(_recId), you can use findByRecId(tableNum(MyTable), _recId)

Trade agreements Posting

Posting trade agreements is not a straightforward thing and i found it during a recent assignment. Trade agreements broadly revolve around three main tables, PriceDiscAdmTable, PriceDiscAdmTrans and PriceDiscTable and posting is handled via class PriceDiscAdmCheckPost.

So i create a journal header and fill the journal lines (details in another blog maybe) and call the below method to do the posting.

void postTradeAgreement(PriceDiscAdmTable _priceJourHeader) { PriceDiscAdmCheckPost jourPost; PriceDiscTable tradeTable; InfologData infologData; //Post jourPost = new PriceDiscAdmCheckPost(false); infologData = infolog.infologData(); // store infolog data infolog.clear(); // if you don’t clear the infolog, posting errors out. jourPost.initJournalNum(_priceJourHeader.JournalNum); jourPost.run(); infolog.import(infologData); // import infolog data }

See anything strange?
I do three things related to Infolog.
1. Store the contents. I am storing the contents of infolog (my logic needs to show info to user later) in a data type InfologData, which is a container.

  1. Clear the infolog. If i dont clear the infolog just before i hit run() method to post, i get an error which makes no sense.
  2. Import the infolog contents again.

Anybody knows a better way?

 

Controller Classes

SysOperation framework relies on SysOperationServiceController class for passing the args argument from the menu item to the framework. Overriding some of the methods on this class gives you more control over its functioning. Same way when you want to modify the user interface of a SysOperation service, UI Builder classes are the way to go. Two methods of particular interest to me are:
showQueryValues When a query is used and this method returns true, then the fields with ranges and a Select button will be shown on the dialog. Return false in this method to hide them.
showQuerySelectButton This method does the same as the showQueryValues method, but only affects the Select button.
Some cases we want to create a batchable job, which a user can use but don’t want to give him the option to ‘select’ any particular records or its meaningless to show the query fields on dialog. Overriding the above two methods to return false can solve the purpose.
You will also need to override main and construct methods in this case, like below.
 public static void main(args _args)  
 {  
   MMSPriceUpdatePublishDataController  controller = MMSPriceUpdatePublishDataController::construct();  
   controller.startOperation();  
 }  
 private static MMSPriceUpdatePublishDataController construct(SysOperationExecutionMode _mode = SysOperationExecutionMode::ReliableAsynchronous)  
 {  
   MMSPriceUpdatePublishDataController  controller = new MMSPriceUpdatePublishDataController(classStr(MMSPriceUpdatePublishData),methodStr(MMSPriceUpdatePublishData, run), _mode);  
   return controller;  
 }  
 public boolean showQuerySelectButton(str parameterName)  
 {  
   return false;  
 }  

Also set the Object property on the menu item as the name of your controller class.

Read Enum Elements

A quick job to fetch enum elements using a for loop. Much better than having to manually type the values.

 static void EnumValues(Args _args)  
 {  
   DictEnum      enum = new DictEnum(enumName2Id("ABC"));  
   int         i;  
   for (i=0; i < enum.values(); i++)  
   {      
     info(strFmt("%1-%2", enum.index2Label(i), enum.index2value(i)));   
   }   
 }