Wednesday, September 2, 2020

GUID based Data contract fields are not available for SysOperationAutomaticUIBuilder

In order to play with Data Contract fields before or after running dialog in terms of SysOperation framework, we have to implement a SysOperationAutomaticUIBuilder based class.

And it works well, until you try to get access to GUID based fields via BindInfo() method: they are not allowed to be there!

Therefore, we get an exception in this case, alas.




Wednesday, July 29, 2020

How to get a list of the Tables maintained by Change Tracking in SQL

Thanks to Brent Ozar and Dave Phillips who showed us how to get a list of the Tables maintained by Change Tracking directly in MS SQL Server Management Studio. It works for both AX2012 and D365 versions.


SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
GO
SELECT
   sct1.name AS CT_schema,
   sot1.name AS CT_table,
   ps1.row_count AS CT_rows,
   ps1.reserved_page_count*8./1024. AS CT_reserved_MB,
   sct2.name AS tracked_schema,
   sot2.name AS tracked_name,
   ps2.row_count AS tracked_rows,
   ps2.reserved_page_count*8./1024. AS tracked_base_table_MB,
   change_tracking_min_valid_version(sot2.object_id) AS min_valid_version
FROM sys.internal_tables it
JOIN sys.objects sot1 ON it.object_id=sot1.object_id
JOIN sys.schemas AS sct1 ON sot1.schema_id=sct1.schema_id
JOIN sys.dm_db_partition_stats ps1 ON it.object_id = ps1. object_id AND ps1.index_id in (0,1)
LEFT JOIN sys.objects sot2 ON it.parent_object_id=sot2.object_id
LEFT JOIN sys.schemas AS sct2 ON sot2.schema_id=sct2.schema_id
LEFT JOIN sys.dm_db_partition_stats ps2 ON sot2.object_id = ps2. object_id AND ps2.index_id in (0,1)
WHERE it.internal_type IN (209, 210)
order by tracked_name
;

GO


Thursday, July 9, 2020

How to activate a financial dimension

First, run the following script over your DB via SQL Management Studio.


update SQLSYSTEMVARIABLES SET VALUE = 1 where PARM = 'CONFIGURATIONMODE'

select value from SQLSYSTEMVARIABLES  where PARM = 'CONFIGURATIONMODE'



Then restart IIS from inside of Visual Studio.



Activate your financial dimension.



Now, run the same script but by setting the variable to zero, and restart IIS again.


update SQLSYSTEMVARIABLES SET VALUE = 0 where PARM = 'CONFIGURATIONMODE'

select value from SQLSYSTEMVARIABLES  where PARM = 'CONFIGURATIONMODE'

Wednesday, July 8, 2020

Event handler: Get access from FormDataSource argument to other data sources and form controls

Kind a code template to accelerate our job:


    [FormDataSourceEventHandler(formDataSourceStr(<FormName>, <FormDataSourceName>), FormDataSourceEventType::Activated)]
    public static void FormDataSourceName_OnActivated(FormDataSource _sender, FormDataSourceEventArgs _e)
    {
        <FormDataSourceTable>       formDataSourceTable                 = _sender.cursor();
        FormRun                     formRun                             = _sender.formRun();
        FormDataSource              anyFormDataSource_ds                = formRun.dataSource(formDataSourceStr(<FormName>, <AnyFormDataSourceName>)) as FormDataSource;
        <AnyFormDataSourceTable>    anyFormDataSourceTable              = anyFormDataSource_ds.cursor();
        FormControl                 anyFormControl                      = formRun.design(0).controlName('AnyFormControlName');
        
        // your logic goes here, for example
        if(formDataSourceTable.enabled())
        {
            anyFormControl.visible(false);
            anyFormControl.enabled(!anyFormDataSourceTable.RecId);
        }
    }

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.