Friday, June 12, 2020

General journals log update bugs

I found a couple questionable things in General journal log updating.

First, Log field limit of 255 characters. It is evidently not enough for relatively long information to be stored for eventual analysis.

Second, the way to update journal logs by copying lines from Infolog afterwards.

Anyway the standard methods meant to shorten the log content do not work properly. Let's see why.


And the method to find a voucher number can speak one language only.



So, we get a poorly built journal log.


It can be fixed by two extensions.

First, let's gather all lines in a set so that they would be present once only.


/// <summary>
/// Extension of LedgerJournalCheckPost to fix a bug
/// </summary>
[ExtensionOf(classStr(LedgerJournalCheckPost))]
final class CDPLedgerJournalCheckPost_Extension
{    

    /// <summary>
    /// Updates the infolog for the given transaction.
    /// </summary>
    /// <param name = "_ledgerJournalTrans">The transaction.</param>
    public void updateTransInfoLog(LedgerJournalTrans _ledgerJournalTrans)
    {
        this.cdpUpdateTransInfoLog(_ledgerJournalTrans);
        // transLogPoint is already moved forward; therefore the next call will do no change to the log
        next updateTransInfoLog(_ledgerJournalTrans);
    }

    /// <summary>
    /// Updates the infolog for the given transaction.
    /// </summary>
    /// <param name = "_ledgerJournalTrans">The transaction.</param>
    private void cdpUpdateTransInfoLog(LedgerJournalTrans _ledgerJournalTrans)
    {
        #Define.UserTab('\t')

        Log             logTxt;
        Integer         x = transLogPoint;
        Integer         y = infolog.num(0);
        str             currentLine;
        str             strLine;
        Voucher         voucher;
        List            list;
        ListEnumerator  listEnumerator;
        Set             setAllLines = new Set(Types::String);
        // <GEERU>
        Log             tableLogTxt;
        #ISOCountryRegionCodes
        boolean         countryRegion_RU = SysCountryRegionCode::isLegalEntityInCountryRegion([#isoRU]);
        // </GEERU>

        if (postingResults)
        {
            postingResults.parmLedgerPostingMessageLog(ledgerPostingMessageCollection);
        }

        while (x < y)
        {
            x++;
            // parse all the lines if there are some prefixes
            currentLine     = infolog.text(x);
            list            = strSplit(currentLine, #UserTab);
            listEnumerator  = list.getEnumerator();
            while (listEnumerator.moveNext())
            {
                currentLine = listEnumerator.current();
                if(setAllLines.in(currentLine))
                {
                    continue;
                }
                setAllLines.add(currentLine);
                // <GEERU>
                if (countryRegion_RU)
                {
                    logTxt      =  currentLine + '\r\n';
                    tableLogTxt += logTxt;
                }
                else
                {
                    // </GEERU>
                    logTxt += currentLine + '\r\n';
                    // <GEERU>
                }
                // </GEERU>

                if (logTxt && postingResults != null)
                {
                    if (_ledgerJournalTrans.Voucher == '')
                    {
                        voucher = LedgerJournalCheckPostResults::getVoucherFromLogText(currentLine);
                        if (voucher == '')
                        {
                            // continue because calling the LedgerJournalCheckPostResults.updateErrorLog
                            // method with a blank voucher has terrible performance and will not change the results
                            continue;
                        }
                    }
                    else
                    {
                        voucher = _ledgerJournalTrans.Voucher;
                    }

                    postingResults.updateErrorLog(voucher, logTxt);
                }
            }
        }

        // <GEERU>
        if (countryRegion_RU)
        {
            logTxt = tableLogTxt;
        }
        // </GEERU>

        if (logTxt)
        {
            tableErrorLog += logTxt;
        }

        transLogPoint = y;
     }

}

Second, teach the voucher searching method to speak any language and do not return rubbish instead of a real voucher number.


/// <summary>
/// Extension of LedgerJournalCheckPostResults to fix a bug
/// </summary>
[ExtensionOf(classStr(LedgerJournalCheckPostResults))]
final class CDPLedgerJournalCheckPostResults_Extension
{    
    /// <summary>
    /// See the description for the standard method
    /// </summary>
    /// <param name = "_logText">log text</param>
    /// <returns>Voucher text</returns>
    public static Voucher getVoucherFromLogText(Log _logText)
    {
        // this standard function does not works correctly
        next getVoucherFromLogText(_logText);
        return LedgerJournalCheckPostResults::CDPGetVoucherFromLogText(_logText);
    }

    /// <summary>
    /// Finds voucher numbers as it should be in all languages
    /// </summary>
    /// <param name = "_logText">Log text</param>
    /// <returns>Voucher text</returns>
    private static Voucher cdpGetVoucherFromLogText(Log _logText)
    {
        List            list;
        ListEnumerator  listEnumerator;
        Voucher         voucher;
        str             strLine;
        const str       voucherText = '@GeneralLedger:Voucher';
        int             voucherLen  = strLen(voucherText);

        list = strSplit(_logText, ',');

        listEnumerator= list.getEnumerator();
        while (listEnumerator.moveNext())
        {
            strLine = listEnumerator.current();
            if (strContains(strLine, voucherText))
            {
                // delete the word VOUCHER in any language as well as all the spaces around
                voucher = strLRTrim(strDel(strLine, 1, voucherLen));
                break;
            }
        }
        return voucher;
    }

}

Now it looks much better and contains condensed information.


For storing more information, a new memo field can be a solution.

Hope, Microsoft will eventually come up with an optimized approach.

Good logging!


Wednesday, May 6, 2020

If you do not see your new Number sequence in Segments

You created a new number sequence, say BNR close - extension, for example, as decribed here . But neither does it appear in Segment configuration form nor in the list of Generate Wizard.

To overcome this issue, you can simply reset number sequences by the following menu item.
Go to Organization administration > Number sequences > Number sequences.



Then it appears in Segment configuration.


Now you can generate your new number sequence in Wizard.


Wednesday, April 22, 2020

New Image in FormGroupControl with BusinessCard Extended style

Developing and customizing forms in D365 are limited by predefined patterns and styles.

We can however overcome these limitations to some extent by placing new form controls and changing properties of existing ones and, of course, a bit of coding.

As an example, let's add a new Workflow image similar to Expense category to be shown in BusinessCard form group control, once an expense is assigned to the current user.



There are multiple ways to achieve the required change. To mention a few, playing with Style and ExtendedStyle properies in design, changing form controls placement with Top, Bottom, Left, Right properties, playing with DisplayOptions at run time, combining both images into one, replacing the standard images to customized ones: one for Assigned-to-me Category and standard Category, and so one.



Here we consider adding a new image of Workflow icon next to the standard Category one.We have a display method returning the required image. The key point here is to set its ExtendedStyle property to card_imageSquare, so that it would be shown properly.



Now it looks almost perfect, but the new form control pushed a bit the currency amount out of the card frame. 



Let's fix it by hiding the standard form control and placing its duplicate with ExtendedStyle = None.



The last thing is to make the text bold in order to emphasize it.


[ExtensionOf(tableStr(TrvExpTrans))]
final class TrvExpTrans_Extension
{
    boolean isCurrentUserWorkflow()
    {
        ...
    }

    display container currentUserWorkflowIndicator()
    {
        ImageReference imgRef;
        if (this.ApprovalStatus == TrvAppStatus::Pending && this.isCurrentUserWorkflow())
        {
            imgRef = ImageReference::constructForSymbol(ImageReferenceSymbol::Workflow);
            return imgRef.pack();
        }

        return conNull();
    }

    [FormDataSourceEventHandler(formDataSourceStr(TrvExpenses, TrvExpTrans), FormDataSourceEventType::DisplayOptionInitialize)]
    public static void ds_OnDisplayOptionInitialize(FormDataSource sender, FormDataSourceEventArgs e)
    {
        FormDataSourceDisplayOptionInitializeEventArgs  eventArgs   = e as FormDataSourceDisplayOptionInitializeEventArgs;

        FormDesign                                      fd          = sender.formRun().design(0);
        FormRowDisplayOption                            fo          = eventArgs.displayOption();
        FormControl                                     fc          = fd.controlName("newAmountCurrWithCurrencyCode");
        // if we can find our new form control for the expense amount
        if(fo && fc)
        {
            // let's make it bold to emphasize
            fo.affectedElementsByControl(fc.id());
            fo.fontBold(true);
        }
    }

}

The final view.


Friday, March 13, 2020

Visual Studio 2015 crash with "The supplied SnapshotPoint is on an incorrect snapshot" error

This error is really annoying bug in VS2015 when you barely touch your code, and the VS crashes all the time.

Thanks Joris, there is a workaround in VS Options:


Tuesday, March 10, 2020

Data validation by ValidateField and ValidateWrite (old but good)

The most time data are meant to be changed by a user via different kinds of forms or even without it, and sometimes we need to implement additional business logic there.

Say, we want that a discount never be more than 10%, while a user creates lines in a sales order. For such a scenario the best way is to implement ValidateField and check the value for the field of Discount. After that the form does not allow the user to leave the field of discount until its value is less or equal to 10%.


public boolean validateField(FieldId _fieldIdToCheck)
{
    boolean ret;

    ret = super(_fieldIdToCheck);
    switch(_fieldIdToCheck)
    {
        case fieldNum(MySalesLine, Discount):
            if(this.Discount <= 10)
            {
               ret = true;
            }
            else
            {
                ret = checkFailed("Discount cannot be more than 10%");
            }
            break;
    }

    return ret;


But we should remember that ValidateField method is called automatically for a form data source field only.



In other words, if the table record is inserted or updated by code, this logic will be NOT triggered.



[Form]
[DataSource]
     [DataField]
            public boolean validate()
    [Table]
     public boolean validateField(FieldId _fieldIdToCheck)


So, if we need to implement validation logic for the whole record, which is supposed to be triggered every time Insert or Update is called (not for form data sources only), we go with ValidateWrite.






Thanks Mohamed for this brilliant article Microsoft dynamics ax2012 : forms and tables methods call sequences, How To? Watch this presentation! It contains much more very useful information.



Thursday, February 20, 2020

How to replace a financial dimension value in D365

Thanks to Denis Trunin's article, we can easily implement something like this in D365FO.


public static DimensionDefault setValueToDefaultDimension(DimensionDefault _dimensionDefault, RefRecId _dimensionAttributeRecId, DimensionValue  _newDimensionValue)
    {
        DimensionAttributeValueSetStorage   dimStorage;
        DimensionDefault                    newDimensionDefault = _dimensionDefault;
        DimensionAttributeValue             dimensionAttributeValue;
        if (_dimensionAttributeRecId)
        {
            dimStorage = DimensionAttributeValueSetStorage::find(_dimensionDefault);
            if (_newDimensionValue)
            {
                dimensionAttributeValue = DimensionAttributeValue::findByDimensionAttributeAndValue(DimensionAttribute::find(_dimensionAttributeRecId), _newDimensionValue, false, true);
                dimStorage.addItem(dimensionAttributeValue);
            }
            else
            {
                dimStorage.removeDimensionAttribute(_dimensionAttributeRecId);
            }
            newDimensionDefault = dimStorage.save();
        }
        return newDimensionDefault;
    }


Then, say, we need to replace Department from Operations to Client Services for Ahmed Barnett in USMF.


static void setNewFinDimValueForEmployee(HcmWorkerRecId _worker, CompanyInfoRecId _legalEntity, Name _dimensionName, DimensionValue _dimensionValue)
    {
        HcmEmployment hcmEmployment = HcmEmployment::findByWorkerLegalEntity(_worker, _legalEntity);
        ttsbegin;
        hcmEmployment.selectForUpdate(true);
        hcmEmployment.validTimeStateUpdateMode(ValidTimeStateUpdate::Correction);
        DimensionDefault newDim = DimensionHelper::setValueToDefaultDimension(hcmEmployment.DefaultDimension, DimensionAttribute::findByName(_dimensionName).RecId, _dimensionValue);
        hcmEmployment.DefaultDimension = newDim;
        if(hcmEmployment.validateWrite())
        {
            hcmEmployment.update();
        }
        ttscommit;
    }
Here you go.




public static void main(Args _args)
    {
        HcmWorkerRecId              _worker         = 22565420995; //Ahmed Barnett
        CompanyInfoRecId            _legalEntity    = 22565422580; //USMF
        Name                        _dimensionName  = 'Department';
        DimensionValue              _dimensionValue = '028'; //Currently 026
        DimensionHelper::setNewFinDimValueForEmployee(_worker, _legalEntity, _dimensionName, _dimensionValue);
        Info(strFmt("Employee fin dim value changed!"));
    }


Saturday, January 4, 2020

How a Dev can be provisioned as admin in D365

If you don't have Windows administrator privileges, you can nevertheless open visual studio and run a debugging session. So you can simply import your Windows user and assign admin role to it.