Wednesday, June 6, 2018

Extensions and Edit and Display methods declaration in D365

Just a short note for my current PU.

Display methods work well as instance methods from table extensions. As to edit-methods, we still need to declare them as static.


[ExtensionOf(tableStr(ProjProposalJour))]
final class avrProjProposalJourTable_ProjInvReport_Extension

 static public server edit avrProjInvReportFmtDescWithBr avrEditInvReportFormatWithBR(ProjProposalJour _this, boolean _set, PrintMgmtReportFormatDescription  _newReportFormat)
    {
        PrintMgmtReportFormatDescription    newReportFormat = _newReportFormat;
        PrintMgmtReportFormatDescription    reportFormat;

        reportFormat = ProjInvoicePrintMgmt::avrGetReportFormatWithBR(_this);
        if (_set)
        {
            if (_this.RecId && newReportFormat && newReportFormat != reportFormat)
            {
                ProjInvoicePrintMgmt::avrCreateOrUpdateInvoiceWithBRPrintSettings(_this, PrintMgmtNodeType::ProjProposalJour, newReportFormat);
                reportFormat = newReportFormat;
            }
        }
        return reportFormat;
    }


Do not forget to pass the table buffer as the first argument.

More detail can be found in Vania's article about news in PU11.


How to add a new report for a business document in D365

My colleague Rod showed it to me when I needed to add a new report to Project Invoice Proposal with billing rules to be present in Print management settings.


    /// <summary>
    /// Subscribes to print mgmt report format publisher to populate custom reports
    /// </summary>
    [SubscribesTo(classstr(PrintMgmtReportFormatPublisher), delegatestr(PrintMgmtReportFormatPublisher, notifyPopulate))]
    public static void notifyPopulate()
    {
        #PrintMgmtSetup

        void addFormat(PrintMgmtDocumentType _type, PrintMgmtReportFormatName _name, PrintMgmtReportFormatCountryRegionId _countryRegionId = #NoCountryRegionId)
        {
            avrPrintMgtDocType_ProjInvReport_Handler::addPrintMgmtReportFormat(_type, _name, _name, _countryRegionId);
        }

        addFormat(PrintMgmtDocumentType::SIProjInvoiceWithBR, ssrsReportStr(avrPSAContractLineInvoice, Report));
    }

    /// <summary>
    /// Adds a report format to the printMgtReportFormat table
    /// </summary>
    /// <param name = "_type">PrintMgmtDocumentType value</param>
    /// <param name = "_name">Name of the report (ie. reportname.Report)</param>
    /// <param name = "_description">Description of the report (ie. reportname.Report)</param>
    /// <param name = "_countryRegionId">Country or default (#NoCountryRegionId)</param>
    /// <param name = "_system">True if this is a system report</param>
    /// <param name = "_ssrs">SSRS report or another type</param>
    private static void addPrintMgmtReportFormat(
        PrintMgmtDocumentType _type,
        PrintMgmtReportFormatName _name,
        PrintMgmtReportFormatDescription _description,
        PrintMgmtReportFormatCountryRegionId _countryRegionId,
        PrintMgmtReportFormatSystem _system = false,
        PrintMgmtSSRS _ssrs = PrintMgmtSSRS::SSRS)
    {
        PrintMgmtReportFormat printMgmtReportFormat;

        select firstonly printMgmtReportFormat
            where printMgmtReportFormat.DocumentType == _type
                && printMgmtReportFormat.Description == _description
                && printMgmtReportFormat.CountryRegionId == _countryRegionId;

        if (!printMgmtReportFormat)
        {
            // Add the new format
            printMgmtReportFormat.clear();
            printMgmtReportFormat.DocumentType = _type;
            printMgmtReportFormat.Name = _name;
            printMgmtReportFormat.Description = _description;
            printMgmtReportFormat.CountryRegionId = _countryRegionId;
            printMgmtReportFormat.System = _system;
            printMgmtReportFormat.ssrs = _ssrs;
            printMgmtReportFormat.insert();
        }
    }

Then I can pick it up in Print management settings in Project module.



It may be a good idea to delete records from PrintMgmtReportFormat table; all its records will be recreated the next time you open Print management form.


Thursday, May 24, 2018

Simple form for Financial dimension value set lookup

Simple form for Financial dimension lookup.

On any form a consultant can add Fin dim field, which is actually just a rec id, then click on it to see the real value set. (Added FormRef to DimensionAttributeValueSet table.)

If opened as a separate window, the form allows to lookup any fin dim value.



Download AX 2012 XPO file.

Friday, March 9, 2018

TempDB table on a form with multiple updates

If you, like me, are still trying to understand how to use a TempDB table in a form and update it as many times as you need, or you are getting the error

"Cannot execute the required database operation.The method is only applicable to TempDB table variables that are not linked to existing physical table instance."

then you would better read the following short explanation.

Let's say your tempDB table is meant to be populated by request from a form on the server side.


In the form data source Init() we just initialize another tempDB buffer of the same type and link its physical instance to the current data source buffer.


public void init()
{
    super();
    wblTableTmpLocal.doInsert();
    delete_from wblTableTmpLocal;
    //wblTableTmp::populate(wblTableTmpLocal); // <-- no need at this step! 
    // if you need to populate it here by default, then comment the two previous lines
    wblTableTmp.linkPhysicalTableInstance(wblTableTmpLocal);
}

Any time you need to update its content, just re-populate it in a method by providing the linked temporary buffer from the form.


void clicked()
{
    super();
    element.rePopulate();
}

public void rePopulate()
{
    wblTableTmp::populate(wblTableTmpLocal);
    wblTableTmp.linkPhysicalTableInstance(wblTableTmpLocal);
    wblTableTmp_DS.research();
}

Populating method defined on the table, for example.


static server void populate(wblTableTmp _wblTableTmp)
{
    int k;
    
    delete_from _wblTableTmp; //<-- important to not have duplicates!
    
    for (k = 1; k<=4; k++)
    {
        _wblTableTmp.Field1 = int2str(k);
        _wblTableTmp.insert();
    }
}

All credits for this trick are for Iulian Cordobin.

Thursday, March 1, 2018

AX 2012 R3 CU13 Bug: DMF Bundle batch processing misses records

If you load many records via DMF, the most probable scenario is that you would divide them by setting a number of threads.

Unfortunately, there is a bug with it in the CU13.



When this batch job ends you will find 8*1041 records processed only.


As you can see the records ids are calculated correctly but the processing method gets wrong variables instead.


Down in the code, the batch task itself is set for a wrong number of records, too.

This is how to fix this bug.




There are two lessons we can take from this story:
- do not touch the code if it works;
- use really good names for your variables (it is not strong-typed environment!)

AX 2012 R3 CU13 Bug: DMF default value mapping for String type

There is bug in AX 2012 R3 CU13.

If you check Default type for your data entity staging mapping field of String type, then the system tries to find any first record matching to "Default" value as XML name to get the linked field and compare against its string length.






To fix it, we need to make the following changes in three methods of the form DMFStagingDefaultTable


public class FormRun extends ObjectRun
{
    SysDictField             dictField;
    DMFFieldType             dmfFieldType;
    DMFSourceXMLToEntityMap  dmfSourceXMLToEntityMap;
    Types                    valueType;
    // Begin: Alexey Voytsekhovskiy
    int                     wblMaxStrSize;
    // End: Alexey Voytsekhovskiy
}


public void init()
{
    Args                                args;
    DMFEntity                           entity;
    DMFDefinitionGroupEntityXMLFields   dmfDefinitionGroupEntityXMLFields;
    SysDictType                         dictType;

    #define.Integer('Integer')
    #define.String('String')
    #define.RealMacro('Real')
    #define.DateMacro('Date')
    #define.DateTimeMacro('UtcDateTime')
    #define.Time('Time')

    super();

    if (!element.args() || element.args().dataset() != tableNum(DMFSourceXMLToEntityMap))
    {
        throw error(strfmt("@SYS25516", element.name()));
    }

    args = element.args();

    dmfSourceXMLToEntityMap = args.record();
    // Begin: Alexey Voytsekhovskiy
    select firstOnly FieldType, FieldSize from dmfDefinitionGroupEntityXMLFields
        where dmfDefinitionGroupEntityXMLFields.FieldName == dmfSourceXMLToEntityMap.EntityField
        && dmfDefinitionGroupEntityXMLFields.Entity == dmfSourceXMLToEntityMap.Entity
        && dmfDefinitionGroupEntityXMLFields.DefinitionGroup == dmfSourceXMLToEntityMap.DefinitionGroup
        ;
    wblMaxStrSize = dmfDefinitionGroupEntityXMLFields.FieldSize ? dmfDefinitionGroupEntityXMLFields.FieldSize : 256;
    // End: Alexey Voytsekhovskiy
    if (dmfDefinitionGroupEntityXMLFields.FieldType && !dmfSourceXMLToEntityMap.IsAutoDefault)
    {
        dmfFieldType = dmfDefinitionGroupEntityXMLFields.FieldType;
        if (dmfsourceXMLToEntityMap.EntityField)
        {
            entity = DMFEntity::find(dmfsourceXMLToEntityMap.Entity);
            dictField = new SysDictField(tableName2id(entity.EntityTable), fieldName2id(tableName2Id(entity.EntityTable),dmfsourceXMLToEntityMap.EntityField));
        }
        switch(dmfDefinitionGroupEntityXMLFields.FieldType)
        {
            case #Integer:
                DMFStagingConversionTable_StagingIntValue.visible(true);
                break;
            case #String:
                DMFStagingConversionTable_StagingStrValue.visible(true);
                break;
            case #RealMacro:
                DMFStagingConversionTable_StagingRealValue.visible(true);
                break;
            case #DateMacro:
                DMFStagingConversionTable_StagingDateValue.visible(true);
                break;
            case #DateTimeMacro:
                DMFStagingConversionTable_StagingUtcDataTime.visible(true);
                break;
            case #Time:
                DMFStagingConversionTable_StagingTimeValue.visible(true);
                break;
            default:
                warning("@DMF694");
                element.close();
        }
    }
    else if(dmfSourceXMLToEntityMap.IsAutoDefault)
    {
        entity = DMFEntity::find(dmfsourceXMLToEntityMap.Entity);
        dictField = new SysDictField(tableName2id(entity.EntityTable),fieldName2id(tableName2Id(entity.EntityTable),dmfsourceXMLToEntityMap.EntityField));
        dictType = new SysDictType(dictField.typeId());
        if (dictType && dictType.isTime())
        {
            valueType = Types::Time;
        }
        else
        {
            valueType = dictField.baseType();
        }
        dmfFieldType = enum2str(dictField.baseType());
        switch(valueType)
        {
            case Types::Integer:
                DMFStagingConversionTable_StagingIntValue.visible(true);
                break;
            case Types::String:
                DMFStagingConversionTable_StagingStrValue.visible(true);
                break;
            case Types::Real:
                DMFStagingConversionTable_StagingRealValue.visible(true);
                break;
            case Types::Date:
                DMFStagingConversionTable_StagingDateValue.visible(true);
                break;
            case Types::UtcDateTime:
                DMFStagingConversionTable_StagingUtcDataTime.visible(true);
                break;
            case Types::Time:
                DMFStagingConversionTable_StagingTimeValue.visible(true);
                break;
            case Types::Guid:
                DMFStagingConversionTable_StagingGuidValue.visible(true);
                break;
            default:
                warning("@DMF694");
                element.close();
        }
    }

    else
    {
        element.close();
    }
}

and on the  sole form data source field StagingStrValue

public boolean validate()
{
    boolean                             ret;
    DMFDefinitionGroupEntityXMLFields   dmfDefinitionGroupEntityXMLFields;

    ret = super();

    if(ret)
    {
        // Begin: Alexey Voytsekhovskiy
        if(strLen(DMFStagingConversionTable.StagingStrValue) > wblMaxStrSize)
        {
            throw error("@DMF788");
        }

//        select firstOnly FieldSize from dmfDefinitionGroupEntityXMLFields
//            where dmfDefinitionGroupEntityXMLFields.FieldName == dmfSourceXMLToEntityMap.XMLField;
//
//        if(dmfDefinitionGroupEntityXMLFields.FieldSize)
//        {
//            if(strLen(DMFStagingConversionTable.StagingStrValue) > dmfDefinitionGroupEntityXMLFields.FieldSize)
//            {
//                throw error("@DMF788");
//            }
//        }
        // End: Alexey Voytsekhovskiy
    }

    return ret;
}

Saturday, February 10, 2018

How to update progress in DMF batch tasks

I do not  know why but standard DMF writer does not update its batch task progress. It is really dull to run DMF target step and see 0% progress for long time trying to guess only what ETA is.

Fortunately it is easy to fix. The whole idea is simple: init a server side progress and update it every time the next record is processed.


DMFEntityWriter class
private void wblProgressServerInit(RefRecId _startRecId, RefRecId _endRecId)
{
    if(xSession::isCLRSession())
    {
        // this progress just updates percentage in Batch task form
        wblProgressServer = RunbaseProgress::newServerProgress(1, newGuid(), -1, DateTimeUtil::minValue());
        wblProgressServer.setTotal(_endRecId - _startRecId);
    }
}





public container write(DMFDefinitionGroupExecution  _definitionGroupExecution,
                       DMFdefinationGroupName       _definitionGroup,
                       DMFExecutionID               _executionId,
                       DMFEntity                    _entity,
                       boolean                      _onlyErrored,
                       boolean                      _onlySelected,
                       RefRecId                     _startRefRecId = 0,
                       RefRecId                     _endRefRecId = 0,
                       boolean                      _isCompare = false,
                       DmfStagingBundleId           _bundleId = 0)
{
    Common                      target;
 ...

    if (_entity.TargetIsSetBased)
    {
        ...
    }
    else
    {
        ...

        while (nextStartRecId <= lastRecId)
        {
            ...

            try
            {
                
                this.wblProgressServerInit(startRefRecId, endRefRecId);
                
                ...
                while select staging where staging.(defGroupFieldId) == _definitionGroup && staging.(execFieldId) == _executionId
                            && ( (!_onlyErrored && !_onlySelected) ||
                                    (_onlyErrored && staging.(transferStatusFieldId) == DMFTransferStatus::Error) ||
                                    (_onlySelected && staging.(selectedFieldId) == NoYes::Yes)
                                )
                    join tmpStagingRecords where
                        tmpStagingRecords.RecId                 >= startRefRecId   &&
                        tmpStagingRecords.RecId                 <= endRefRecId     &&
                        tmpStagingRecords.StagingRecordRecId    == staging.RecId
                {
                    try
                    {
                        
                        this.wblProgressServerIncCount();
                        

                        ...
     }
     catch
     {
     ...
     }
            }
        }
        
        this.wblProgressServerKill();
        
    }
 ...
    return [newCount, updCount, stagingLogRecId,msgDisplayed];
}