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 mgcSalesBySegment report, which is meant to return some data for a given selection of legal entities.

the whole project, which you can get by this URL https://github.com/wojzeh/tmxProjectSalesPerSegment. Ping me, if you have any questions.


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

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

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

class mgcSalesBySegmentContract implements SysOperationValidatable
{
    ...
    str                                 legalEntityOptionsStr;
    ...
 
    /// <summary>
    /// Legal options
    /// </summary>
    /// <param name = "_legalEntityOptions">str</param>
    /// <returns>str</returns>
    [
        DataMember('legalEntityOptions')
        ,
        SysOperationGroupMember('Grouping'),
        SysOperationDisplayOrder('5')
    ]
    public str parmLegalEntityOptions(str _legalEntityOptions = legalEntityOptionsStr)
    {
        legalEntityOptionsStr = _legalEntityOptions;

        return legalEntityOptionsStr;
    }

}

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

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

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

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

        dc                  = this.parmReportContract().parmRdpContract() as mgcSalesBySegmentContract;
        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)
    {
        mgcSalesBySegmentController controller = new mgcSalesBySegmentController();

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

}

User interface builder mgcSalesBySegmentUIBuilder: 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 mgcSalesBySegmentUIBuilder extends SrsReportDataContractUIBuilder
{
    mgcSalesBySegmentContract   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(mgcSalesBySegmentContract, 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(mgcSalesBySegmentUIBuilder, legalEntityLookup), this);
    }

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

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

}

Report data provider mgcSalesBySegmentDP: 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(mgcSalesBySegmentContract))] 
public class mgcSalesBySegmentDP extends SRSReportDataProviderPreProcessTempDB
{
    container                       legalEntityOptions;
    
    public void processReport()
    {
        mgcSalesBySegmentContract       dc;
        List                            legalEntityList;
        ListEnumerator                  legalEntityListEnumerator;
        SelectableDataArea              companyId;
        str                             companyName;

        dc                          = this.parmDataContract() as mgcSalesBySegmentContract;
     
        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 = EOGAssignedBankAccountDimension.dimensionResolveReference(_formReferenceControl);
            
                return ret;
            }

    public Common dimensionResolveReference(FormReferenceControl _formReferenceControl)
    {
        DimensionAttribute                          dimensionAttribute;
        DimensionAttributeDirCategory               dimAttributeDirCategory;
        DimensionFinancialTag                       dimensionFinancialTag;
        EOGFinancialDimensionValueFinancialTagView  view;
        EOGAssignedBankAccountDimension             eogAssignedBankAccountDimension;
        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.

    /// <summary>
    /// Replaces language rendering culture currency symbol with the given one
    /// </summary>
    /// <param name = "_languageId">language rendering culture</param>
    /// <param name = "_amountCur">AmountCur</param>
    /// <param name = "_currencySymbol">New currency symbol</param>
    /// <returns>Formatted string</returns>
    public str eogChangeCurSymbolForAmountStr(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;
    }

    /// <summary>
    /// Gets amount with currency symbol
    /// </summary>
    /// <param name = "_amountCur">Amount in currency</param>
    /// <param name = "_currency">Currency to present symbol</param>
    /// <param name = "_languageId">Language for rendering culture</param>
    /// <returns>Formatted string for amunt cur with currency symbol</returns>
    public EOGAmountStringWithCurrencySymbol eogAmountStringWithCurrencySymbol(AmountCur _amountCur, CurrencyCode _currency, LanguageId _languageId)
    {
        Currency                            currency = Currency::find(_currency);
        
        return this.eogChangeCurSymbolForAmountStr(_languageId, _amountCur, currency.Symbol);;
    }

    /// <summary>
    /// Populates <c>BankPaymAdviceVendTmp</c> and inserts the report information for a single invoice and related payment.
    /// </summary>
    protected void insertBankPaymAdviceTmp()
    {
        BankPaymAdviceVendTmp bankPaymAdviceVendTmp;
        str                   email;

        next insertBankPaymAdviceTmp();

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

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