Friday, January 22, 2021

D365FO data entities with financial dimension requires special field names

 While creating data entities using financial dimensions, do not forget that those are backed in background by special classes via SysDataEntityPersister class




Those latter help resolving selected values (similar to what we see in segmented entry in forms). 



Each class is meant to serve a particular EDT, for example


/// <summary>
/// The <c>LedgerAccountDimensionDataEntityResolver</c> class resolves data entity ledger account values.
/// </summary>
[
System.ComponentModel.Composition.ExportMetadataAttribute("DimensionSFKType", identifierstr(LedgerDimensionAccount)),
System.ComponentModel.Composition.ExportAttribute("Microsoft.Dynamics.AX.DimensionDataEntitySFKFieldResolver")
]
public class LedgerAccountDimensionDataEntityResolver extends DimensionDataEntitySFKFieldResolver
{
    Name accountStructure;
...


In order to let it work correctly, some fields in your data entity must have particular names.


public class DimensionDataEntityConstants
{
    public static const str HasDimensionDataSourcesCustomPropertyName = '__HasDimensionDataSources__';
    public static const str ProviderCacheName = 'DimensionDataEntitySFKProviderCacheName';
    public static const str AccountStructureSuffix = 'AccountStructure';
    public static const str DisplayValueSuffix = 'DisplayValue';
    public static const str DisplayValueTypeName = 'DimensionDisplayValue';
}




Friday, January 15, 2021

How to delete obsolete values in ENUMVALUETABLE table X++

Following up this old issue with extensions to enums, I propose to run this class, if you cannot delete added values (grr* in my case) directly in SQL Studio. I am not sure if it is going to work in PROD. Please use it at your own risk.


class ENUMVALUETABLEDelete
{
   
    public static void main(Args _args)
    {
        if(!Box::confirm("Would you like to delete all grr* values of SysPolicyRuleTypeEnum type in ENUMVALUETABLE? (If not, just print them"))
        {
            ENUMVALUETABLEDelete::printRecords();
            return;
        }
        ENUMVALUETABLEDelete::deleteRecords();
        info("all related records have been deleted. Synchonize DB now!");
    }

    private static void deleteRecords()
    {
        Connection                      conn;
        SqlStatementExecutePermission   permission;

        str sqlSelect = 
@"SELECT
t.RECID
 from ENUMVALUETABLE as t
 join ENUMIDTABLE
 on enumid = id
 and ENUMIDTABLE.NAME = 'SysPolicyRuleTypeEnum'
 and t.NAME like 'grr%'";
        str sqlDelete = strFmt("%1 (%2)", "DELETE FROM ENUMVALUETABLE WHERE RECID IN ", sqlSelect);

        permission  = new SqlStatementExecutePermission(sqlDelete);
        conn        = new Connection();
        permission.assert();
        //conn.transactionScopeBegin();
        Statement statement = conn.createStatement();
        
        int result  = statement.executeUpdate(sqlDelete);
        str errText = statement.getLastErrorText();
        if(errText)
        {
            Info(errText);
        }

        // the permissions needs to be reverted back to original condition.
        CodeAccessPermission::revertAssert();
    }

    private static void printRecords()
    {
        Connection                      conn;
        SqlStatementExecutePermission   permission;

        str sqlSelect =
@"SELECT
t.RECID,
t.ENUMID,
t.ENUMVALUE,
t.NAME
 from ENUMVALUETABLE as t
 join ENUMIDTABLE
 on enumid = id
 and ENUMIDTABLE.NAME = 'SysPolicyRuleTypeEnum'
 and t.NAME like 'grr%'";

        permission  = new SqlStatementExecutePermission(sqlSelect);
        conn        = new Connection();
        permission.assert();

        Statement statement = conn.createStatement();
        
        ResultSet results = statement.executeQuery(sqlSelect);
        str errText = statement.getLastErrorText();
        if(errText)
        {
            Info(errText);
        }

        int enumId;
        str name;
        int i = 1;
        while (results.next())
        {
            enumId  = results.getInt(3);
            name    = results.getString(4);
            Info(strFmt("Found %1) %2 : %3", i, enumId, name));
            i++;
        }
        // the permissions needs to be reverted back to original condition.
        CodeAccessPermission::revertAssert();
    }

}

Do not forget to build your models and full DB sync after!

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 LedgerJournalCheckPost_Extension
{    

    /// <summary>
    /// Updates the infolog for the given transaction.
    /// </summary>
    /// <param name = "_ledgerJournalTrans">The transaction.</param>
    public void updateTransInfoLog(LedgerJournalTrans _ledgerJournalTrans)
    {
        this.myUpdateTransInfoLog(_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 myUpdateTransInfoLog(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 LedgerJournalCheckPostResults_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::myGetVoucherFromLogText(_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 myGetVoucherFromLogText(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!