Saturday, April 9, 2022

Multiple company selection in an SSRS report (LedgerLegalEntityLookup)

If you need to provide an SSRS report with a multiple company selection, you can opt for a cross-company query. In this case, such an option will be maintained by the system automatically. (You can try this [SrsReportQuery(queryStr(LogisticsEntityLocationUnion))])

But what if you need to do that without the former? In this case you'll need to use LedgerLegalEntityLookup class as follows. Say we deal with some mySalesBySegment report, which is meant to return some data for a given selection of legal entities.




I hid some not relevant code; so that you can get the gist.

Data contract mySalesBySegmentContract: we keep the user selection of companies as a string.

[DataContract]
[SysOperationContractProcessing(classstr(mySalesBySegmentUIBuilder), SysOperationDataContractProcessingMode::CreateUIBuilderForRootContractOnly)]

class mySalesBySegmentContract implements SysOperationValidatable
{
    ...
    str                                 legalEntityOptionsStr;
    ...
 
    [
        DataMember('legalEntityOptions')
        ,
        SysOperationGroupMember('Grouping'),
        SysOperationDisplayOrder('5')
    ]
    public str parmLegalEntityOptions(str _legalEntityOptions = legalEntityOptionsStr)
    {
        legalEntityOptionsStr = _legalEntityOptions;

        return legalEntityOptionsStr;
    }

}

Report controller mySalesBySegmentController: if no companies selected, let's set it to the user's context.

public class mySalesBySegmentController extends SrsReportRunController
{
 
    protected void prePromptModifyContract()
    {
        mySalesBySegmentContract   dc = this.parmReportContract().parmRdpContract() as mySalesBySegmentContract;

       ...
        if (!dc.parmLegalEntityOptions())
        {
            // Set the default value for the legal entity selection
            dc.parmLegalEntityOptions(con2str([curExt()]));
        }
    }

   
    protected void preRunModifyContract()
    {
        mySalesBySegmentContract   dc;
        container                   legalEntityOptions;

        dc                  = this.parmReportContract().parmRdpContract() as mySalesBySegmentContract;
        legalEntityOptions  = str2con(dc.parmLegalEntityOptions());

        // Default current company if there were no company specifications provided to the API.
        if (legalEntityOptions == conNull())
        {
            legalEntityOptions = [curExt()];
            dc.parmLegalEntityOptions(con2str(legalEntityOptions));
        }
  
    }

    public static void main(Args _args)
    {
        mySalesBySegmentController controller = new mySalesBySegmentController();

        controller.parmReportName(ssrsReportStr(mySalesBySegment, Report));
        controller.parmArgs(_args);
         controller.startOperation();
    }

}

User interface builder mySalesBySegmentUIBuilder: when an SSRS report runs, it shows its dialog twice: the second time in the report viewer, when the report is rendered. Thus we have to override dialog methods in the UIBuilder class to avoid the lovely 'Object reference not set to an instance of an object' error.

public class mySalesBySegmentUIBuilder extends SrsReportDataContractUIBuilder
{
    mySalesBySegmentContract   dc;
    // Legal entity lookup controls
    FormStringControl           dialogLegalEntitySelection;
    LedgerLegalEntityLookup     legalEntityLookup;
    int                         dialogLegalEntityLookupId;
    str                         userLegalEntityRange;
    container                   legalEntityOptions;

    /// <summary>
    /// Override this method in order to initialize the dialog fields after the fields are built.
    /// </summary>
    public void postBuild()
    {
        DialogField dialogField;

        super();
        // parmCompanySelection
        dialogField = this.bindInfo().getDialogField(this.dataContractObject(), methodStr(mySalesBySegmentContract, parmLegalEntityOptions));
        this.setInVisible(dialogField);

        this.constructLegalEntityControl(dialog);
        
    }

    /// <summary>
    /// post runs
    /// </summary>
    public void postRun()
    {
        super();
        
        this.constructLegalEntityLookup(dialog);
        Set userLegalEntitySet = LedgerSecurityHelper::ledgersWithMinimumSecurityAccess(menuItemActionStr(LedgerExchAdj), AccessRight::Edit, MenuItemType::Action);
        userLegalEntityRange = LedgerLegalEntityLookup::getLegalEntityRangeFromLegalEntitySet(userLegalEntitySet);
    }

    /// <summary>
    /// Contstruct
    /// </summary>
    /// <param name = "_dialog">Dialog</param>
    private void constructLegalEntityControl(Dialog _dialog)
    {
        FormBuildGroupControl currentGroup = _dialog.form().design().control(_dialog.curFormGroup().name());
        FormBuildStringControl dialogLegalEntityLookup = currentGroup.addControl(FormControlType::String, 'LegalEntityLookup');
        dialogLegalEntityLookup.extendedDataType(extendedTypeNum(LedgerLegalEntitySelection));
        dialogLegalEntityLookup.lookupOnly(true);
        dialogLegalEntityLookupId = dialogLegalEntityLookup.id();
    }

    /// <summary>
    /// Constructs the lookup for the legal entity selection.
    /// </summary>
    /// <param name = "_control">The <c>FormStringControl</c> object.</param>
    private void legalEntityLookup(FormStringControl _control)
    {
        legalEntityLookup.lookup(_control.text(), userLegalEntityRange);
    }

    /// <summary>
    /// Lookup override
    /// </summary>
    /// <param name = "_dialog">dialog</param>
    private void constructLegalEntityLookup(Dialog _dialog)
    {
        dialoglegalEntitySelection = _dialog.formRun().design().control(dialogLegalEntityLookupId);
        legalEntityLookup = LedgerLegalEntityLookup::construct(_dialog.formRun(), dialoglegalEntitySelection);
        // populates it from the packed paramater
        legalEntityLookup.setSelection(str2con(dc.parmLegalEntityOptions()));
        // let's have our own lookup
        dialoglegalEntitySelection.registerOverrideMethod(methodstr(FormStringControl, lookup), methodstr(mySalesBySegmentUIBuilder, legalEntityLookup), this);
    }

    /// <summary>
    /// prebuilds
    /// </summary>
    public void preBuild()
    {
        dc = this.dataContractObject() as mySalesBySegmentContract;
        super();
    }

    /// <summary>
    /// Gets it back from the dialog
    /// </summary>
    public void getFromDialog()
    {
        super();
        dc.parmLegalEntityOptions(con2Str(legalEntityLookup.getLegalEntitySelection()));
    }

}

Report data provider mySalesBySegmentDP: we need just to convert the saved string back to a container, then we can loop through it as required by the report logic.

[SRSReportParameterAttribute(classStr(mySalesBySegmentContract))] 
public class mySalesBySegmentDP extends SRSReportDataProviderPreProcessTempDB
{
    container                       legalEntityOptions;
    
    public void processReport()
    {
        mySalesBySegmentContract        dc;
        List                            legalEntityList;
        ListEnumerator                  legalEntityListEnumerator;
        SelectableDataArea              companyId;
        str                             companyName;

        dc                          = this.parmDataContract() as mySalesBySegmentContract;
     
        this.setUserConnection(tmp);
        
        // getting all selected companies from the report query
        legalEntityList             = con2List(str2con(dc.parmLegalEntityOptions()));
        legalEntityListEnumerator   = legalEntityList.getEnumerator();

        while (legalEntityListEnumerator.moveNext())
        {
            companyId   = legalEntityListEnumerator.current();
            companyName = CompanyInfo::findDataArea(companyId).name();
            changecompany(companyId)
            {
                // Populate the base processing table with data from the appropriate source table
                ...
            }
        }
    }
}

Monday, April 4, 2022

How to resolve reference with a form control value

If you need not just to maintain lookup for a refence group but also to validate a manually input value, you need to implement resolveReference method for the field of the form data source. Say, we want to validate a custom financial dimension value.





You can check resolveReference* methods in EcoResCategory table as a good example.


The most interesting detail for me is the way how the related form control value is found inside of the given reference group.

            /// <summary>
            /// Resolve reference
            /// </summary>
            /// <param name = "_formReferenceControl"></param>
            /// <returns></returns>
            public Common resolveReference(FormReferenceControl _formReferenceControl)
            {
                Common ret;
            
                ret = myAssignedBankAccountDimension.dimensionResolveReference(_formReferenceControl);
            
                return ret;
            }

    public Common dimensionResolveReference(FormReferenceControl _formReferenceControl)
    {
        DimensionAttribute                          dimensionAttribute;
        DimensionAttributeDirCategory               dimAttributeDirCategory;
        DimensionFinancialTag                       dimensionFinancialTag;
        myFinancialDimensionValueFinancialTagView  view;
        myAssignedBankAccountDimension             myAssignedBankAccountDimension;
        DimensionDisplayValue                       dimensionDisplayValue;

        if (!_formReferenceControl || _formReferenceControl.handle() != classNum(FormReferenceGroupControl) )
        {
            throw(error(strFmt("@SYS137393", Error::wrongUseOfFunction(funcName())) ));
        }

        dimensionDisplayValue = _formReferenceControl.filterValue(AbsoluteFieldBinding::construct(fieldStr(DimensionFinancialTag, Value), tableStr(DimensionFinancialTag))).value();
        dimensionDisplayValue = strLRTrim(dimensionDisplayValue);
        
        <..  implement your logic with the display value ...>

Sunday, April 3, 2022

How to change currency symbol in a given number format

Say, we need to print a Vendor payment advice in the vendor's language, whic is es (Spanish) in the example below. Once the report parameter AX_RenderingCulture is set to 'es', all related number formatting will be applied to amounts cells. 

However, the payment may be made in different currencies; thus its currency symbol $ must be used instead of Euro.




Basically, such parameters, like currency symbol etc, can be changed through System.Globalization.CultureInfo class created for the rendering culture. I did not find a way how to achieve it for a particular textbox in SSRS design. So, I formatted the amount directly in X++.


I used Global::strFmtByLanguage method as a basis for my method to replace the culture number format currency symbol to a given one. There are a few other interesting methods you can check to see how to deal with formatting dates and numbers.

    public str myChangeCurSymbolForAmountStr(LanguageId _languageId, System.Double _amountCur, CurrencySymbol _currencySymbol)
    {
        System.Globalization.CultureInfo    culture;
        str                                 res;
        System.Exception                    e;
        str                                 curSymbol;

        culture = new System.Globalization.CultureInfo(_languageId);

        try
        {
            res         = _amountCur.ToString("C", culture);
            curSymbol   = culture.NumberFormat.CurrencySymbol;
            res         = strReplace(res, curSymbol, _currencySymbol);
        }
        catch(Exception::CLRError)
        {
            e = CLRInterop::getLastException();
            while( e )
            {
                error( e.get_Message() );
                e = e.get_InnerException();
            }
            throw Exception::Error;
        }
        return res;
    }

    public myAmountStringWithCurrencySymbol myAmountStringWithCurrencySymbol(AmountCur _amountCur, CurrencyCode _currency, LanguageId _languageId)
    {
        Currency                            currency = Currency::find(_currency);
        
        return this.myChangeCurSymbolForAmountStr(_languageId, _amountCur, currency.Symbol);;
    }

    protected void insertBankPaymAdviceTmp()
    {
        BankPaymAdviceVendTmp bankPaymAdviceVendTmp;
        str                   email;

        next insertBankPaymAdviceTmp();

        bankPaymAdviceVendTmp = this.bankPaymAdviceTmp as BankPaymAdviceVendTmp;
                
        if (bankPaymAdviceVendTmp.RecId)
        {          
            ttsbegin;
            bankPaymAdviceVendTmp.selectForUpdate(true);
            bankPaymAdviceVendTmp.myBalance01Total+=bankPaymAdviceVendTmp.Balance01;
            bankPaymAdviceVendTmp.myAmountStringWithCurrencySymbol  = this.myAmountStringWithCurrencySymbol(bankPaymAdviceVendTmp.EOGBalance01Total, bankPaymAdviceVendTmp.CurrencyCode, VendTable::find(bankPaymAdviceVendTmp.AccountNum).languageId());          
            bankPaymAdviceVendTmp.update();
            ttscommit;
        }
    }

The final string can be referenced in Total textbox as Last(bankPaymAdviceVendTmp.myAmountStringWithCurrencySymbol) and with default format.








Thursday, March 17, 2022

Lookup, JumpRef, Modified for a form data source field through CoC

 As  explained in his old article https://community.dynamics.com/365/financeandoperations/b/ievgensaxblog/posts/d365foe-how-to-override-form-data-source-field-lookup-method, it is much better to override methods directly on a data source field than on its linked form controls.

I just want to re-iterate it and place here code snippets.


  • User can add new control using form personalization and this control won’t support overridden logic. It could be critical if you are restricting field lookup values or adding validations.
  • One form could have several controls that refers to one data source field so you have to duplicate your code.
  • Number of delegates are limited as well.
So, say we need to implement Lookup, JumpRef, and Modified methods for a custom field on CustInvoiceTable data source of CustFreeInvoice form. Implement these three aforementioned methods, e.g., directly in an extension to the form class.

 
[ExtensionOf(formStr(CustFreeInvoice))]
final class myCustFreeInvoice_Form_Extension
{
    public void myAssignedBankAccountIdModified(FormDataObject _targetField)
    {
       <logic>
    }
    public void myAssignedBankAccountIdJumpRef(FormDataObject _targetField)
    {
       <logic>
}
    // Different parameter here!
    public void myAssignedBankAccountIdLookup(FormStringControl _callingControl)
    {
       <logic>
    }

Now simply override them for the field when the data source is initialized.
 
public class myCustFreeInvoice_Form_EventHandler
{
    [FormDataSourceEventHandler(formDataSourceStr(CustFreeInvoice, CustInvoiceTable), FormDataSourceEventType::Initialized)]
    public static void myCustInvoiceTable_OnInitialized(FormDataSource _sender, FormDataSourceEventArgs _e)
    {
        FormRun         eogFormRun                  = _sender.formRun();
        FormDataObject  eogCustomField              = _sender.object(fieldNum(CustInvoiceTable, eogCustomField));
fdoEOGAssignedBankAccountId.registerOverrideMethod(methodStr(FormDataObject, jumpRef), formMethodStr(CustFreeInvoice, myAssignedBankAccountIdJumpRef), myFormRun); fdoEOGAssignedBankAccountId.registerOverrideMethod(methodStr(FormDataObject, lookup), formMethodStr(CustFreeInvoice, myAssignedBankAccountIdLookup), myFormRun); fdoEOGAssignedBankAccountId.registerOverrideMethod(methodStr(FormDataObject, modified), formMethodStr(CustFreeInvoice, myAssignedBankAccountIdModified), myFormRun); }

Monday, March 7, 2022

How to populate custom fields in GeneralJournalAccountEntry from LedgerJournalTrans for Ledger, Customer, Vendor, and Bank account type

 Say, we need to add a new field UniqueId to LedgerJournalTrans and then have it populated in GeneralJournalAccountEntry, once a General journal posted.



In other words the field value must be transferred from a general journal line to a related voucher transaction.


Generally speaking there are two different ways how GL transactions created in D365FO: via Source document framework and via LedgerVoucherObject. Moreover, one transaction may be a result of summarization of multiple documents. So, this approach works for this particular scenario, when GL transactions come from a general journal. The proposed solution covers Ledger, Customer, Vendor, and Bank types. You can elaborate it for Project, Fixed Asset, etc. Check their appropriate classes.

LedgerJournalCheckPost class creates one transaction per a line of Ledger type, two if the latter has an offset info.



When it comes to other transaction type, first, a transaction in CustTransVendTransBankTrans etc is created, and then based on the latter a new transaction is added.




So, we creates the following extensions.

Tables.





Classes.




Below, you can find code snippets for each of them.

BankVoucher_Extension

[ExtensionOf(classStr(BankVoucher))]
final class BankVoucher_Extension
{
    public UniqueId UniqueId;


    public UniqueId parmUniqueId(UniqueId _parm = uniqueId)
    {
        uniqueId = _parm;

        return uniqueId;
    }


    protected LedgerVoucherTransObject initializeLedgerVoucherTransObjectForPosting(LedgerVoucherObject _ledgerVoucherObject, CurrencyExchangeHelper _exchangeRateHelper)
    {
        LedgerVoucherTransObject ledgerVoucherTransObject = next initializeLedgerVoucherTransObjectForPosting(_ledgerVoucherObject, _exchangeRateHelper);
        if(ledgerVoucherTransObject)
{ ledgerVoucherTransObject.parmUniqueId(_ledgerVoucherObject.parmUniqueId()); } return ledgerVoucherTransObject; } }

CustVendVoucher_Extension

[ExtensionOf(classStr(CustVendVoucher))]
final class CustVendVoucher_Extension
{
    public UniqueId uniqueId;
    public UniqueId parmUniqueId(UniqueId _parm = uniqueId)
    {
        uniqueId = _parm;

        return uniqueId;
    }

   protected LedgerVoucherTransObject createLedgerVoucherTransObject(boolean _useSubLedger,
                                                                     LedgerDimensionAccount _ledgerDimensionMerged,
                                                                     LedgerJournalTrans _ledgerJournalTrans,
                                                                     LedgerVoucher _ledgerPostingJournal,
                                                                     CustVendTrans _custVendTrans)
    {
        LedgerVoucherTransObject ledgerVoucherTransObject = next createLedgerVoucherTransObject( _useSubLedger, _ledgerDimensionMerged, _ledgerJournalTrans, _ledgerPostingJournal, _custVendTrans);
        if(ledgerVoucherTransObject)
{ ledgerVoucherTransObject.parmUniqueId(uniqueId); } return ledgerVoucherTransObject; } }

LedgerJournalCheckPost_Extension

[ExtensionOf(classStr(LedgerJournalCheckPost))]
final class LedgerJournalCheckPost_Extension
{

    protected LedgerVoucherObject createPostingReference(LedgerJournalTrans _ledgerJournalTrans, SysModule _sysModule)
    {
        LedgerVoucherObject newVoucher = next  createPostingReference(_ledgerJournalTrans, _sysModule);
        if(newVoucher)
{ newVoucher.parmUniqueId(_ledgerJournalTrans.uniqueId); } return newVoucher; } protected LedgerVoucherObject updatePostingReference(LedgerVoucherObject _postingReference, LedgerJournalTrans _ledgerJournalTrans, SysModule _sysModule) { next updatePostingReference(_postingReference, _ledgerJournalTrans, _sysModule); if(_postingReference)
        {     _postingReference.parmUniqueId(_ledgerJournalTrans.uniqueId);         } return _postingReference; } }

LedgerJournalTransUpdateBank_Extension

[ExtensionOf(classStr(LedgerJournalTransUpdateBank))]
final class LedgerJournalTransUpdateBank_Extension
{

    protected BankVoucher initBankVoucher(  LedgerJournalTrans _ledgerJournalTrans,
                                            TaxAmount _taxAmount,
                                            real _taxWithholdAmount,
                                            DimensionDefault _defaultDimension,
                                            LedgerJournalType _ledgerJournalType,
                                            boolean _skipDimensionValidation)
    {
        BankVoucher bankVoucher = next initBankVoucher(_ledgerJournalTrans, _taxAmount, _taxWithholdAmount, _defaultDimension, _ledgerJournalType, _skipDimensionValidation);
        if(bankVoucher)
        {     bankVoucher.parmUniqueId(_ledgerJournalTrans.uniqueId);         } return bankVoucher; } }

LedgerVoucherObject_Extension

[ExtensionOf(classStr(LedgerVoucherObject))]
final class LedgerVoucherObject_Extension
{
    public UniqueId uniqueId;

    public UniqueId parmUniqueId(UniqueId _parm = uniqueId)
    {
        uniqueId = _parm;

        return uniqueId;
    }

}

LedgerVoucherTransObject_Extension

[ExtensionOf(classStr(LedgerVoucherTransObject))]
final class LedgerVoucherTransObject_Extension
{
    public UniqueId uniqueId;

    public UniqueId parmUniqueId(UniqueId _parm = uniqueId)
    {
        generalJournalAccountEntry.UniqueId  = _parm;
        return generalJournalAccountEntry.UniqueId;
    }

   
    public static LedgerVoucherTransObject newTransLedgerJournal(
                                                                    LedgerJournalTrans  _ledgerJournalTrans,
                                                                    TaxAmount           _taxAmount,
                                                                    boolean             _bridging,
                                                                    container           _intercompanyRecIds,
                                                                    boolean             _reversalsMayExist,
                                                                    boolean             _forcedExchangeRate)
    {
        LedgerVoucherTransObject ledgerVoucherTransObject = next newTransLedgerJournal(_ledgerJournalTrans, _taxAmount, _bridging, _intercompanyRecIds, _reversalsMayExist, _forcedExchangeRate);
        if(ledgerVoucherTransObject)
        {     ledgerVoucherTransObject.parmUniqueId(_ledgerJournalTrans.UniqueId);         } return ledgerVoucherTransObject; } public LedgerPostingTransactionTmp getLedgerPostingTransaction() { LedgerPostingTransactionTmp ledgerPostingTransaction = next getLedgerPostingTransaction(); ledgerPostingTransaction.UniqueId = generalJournalAccountEntry.UniqueId; return ledgerPostingTransaction; } public void initFromLedgerPostingTransaction(LedgerPostingTransactionTmp _ledgerPostingTransaction,LedgerPostingTransactionProjectTmp _projectPostingTransaction) { next initFromLedgerPostingTransaction(_ledgerPostingTransaction,_projectPostingTransaction); generalJournalAccountEntry.UniqueId = _ledgerPostingTransaction.UniqueId; } public static LedgerVoucherTransObject newTransactionAccountingAmountsDefault( LedgerVoucherObject _defaultLedgerPostingReference, LedgerPostingType _postingType, RecId _ledgerDimensionId, CurrencyCode _transactionCurrencyCode, Money _transactionCurrencyAmount, MoneyMST _accountingCurrencyAmount, CurrencyExchangeHelper _currencyExchangeHelper) { LedgerVoucherTransObject postingTrans; postingTrans = next newTransactionAccountingAmountsDefault(_defaultLedgerPostingReference, _postingType, _ledgerDimensionId, _transactionCurrencyCode, _transactionCurrencyAmount, _accountingCurrencyAmount, _currencyExchangeHelper);         if(postingTrans)
        {     postingTrans.parmUniqueId(_defaultLedgerPostingReference.parmUniqueId());         } return postingTrans; } }

I wish to credit the following articles I used:

http://axforum.info/forums/showthread.php?t=74038

https://allaboutdynamic.com/2018/06/25/d365-ax7-update-custom-fields-in-custtrans-vendtrans-from-ledgerjournaltrans-during-the-posting-of-journal/

http://axwiki.blogspot.com/2017/01/customize-field-in-ledgerjournaltabletr.html

Friday, February 11, 2022

Additional parameters in RunBaseBatch class extension

 Old good pack/unpack patterns with SysPackExtension call. Say, we need to augment RunBaseBatch based class <ClassName>.


/// <summary>
/// We are going to use a new additional parameter
/// </summary>
[ExtensionOf(classStr(<ClassName>))]
public final class My<ClassName>_Extension
{
    private boolean     myNewParm;   
    #define.CurrentVersion(1)
    #localmacro.CurrentList
        myNewParm
    #endmacro

    
    /// <summary>
    /// myNewParm access
    /// </summary>
    /// <param name = "_parm">boolean</param>
    /// <returns>boolean</returns>
    public boolean parmMyNewParm(boolean _parm = myNewParm)
{ myNewParm= _parm;
return myNewParm;
} /// <summary> /// Extends Pack /// </summary> /// <returns>container</returns> public container pack() { container packedClass = next pack(); return SysPackExtensions::appendExtension(packedClass, classStr(My<ClassName>_Extension), this.myPack());
} /// <summary> /// Extends Unpack /// </summary> /// <param name = "packedClass">container</param> /// <returns>boolean</returns> private boolean myUnpack(container packedClass) { Integer version = RunBase::getVersion(packedClass); switch (version) { case #CurrentVersion: [version, #currentList] = packedClass; break; default: return false; } return true; } /// <summary> /// Packs my locals /// </summary> /// <returns>container</returns> private container myPack() { return [#CurrentVersion, #CurrentList]; } /// <summary> /// Extends unpack /// </summary> /// <param name = "_packedClass">container</param> /// <returns>boolean</returns> public boolean unpack(container _packedClass) { boolean result = next unpack(_packedClass); if (result) { container myState = SysPackExtensions::findExtension(_packedClass, classStr(My<ClassName>_Extension));
//Also unpack the extension if (!this.myUnpack(myState)) { result = false; } } return result; } }