Category: Development

  • Comparison Framework Sample : BOM Version comparer

    I found this blog post Comparison Framework Sample: BOM Version comparer.

    Mkz created a BOM version comparer based on the comparison framework tutorial allowing you to see the line by line differences between BOM versions. I made a quick upgrade to AX 2012 and it now compiles without best practice errors.

    BOMComparer_AX2012.xpo

    Note: The performance is not very good, when the BOM is complex with many levels and many bom lines over the levels.

  • Debug alert processing

    The following simple job allows you to use breakpoints in alert processing classes.

    /// Run alert processing outside of batch job (for debugging purposes)
    static void testAlerts(Args _args)
    {
        ;
        EventJobCUD::expandCudRecords();
        EventJobCUDTask::newTask().run();
    }
    
  • Called by

    Not exactly best practice, but still a helpful way to make code dependent of the calling class.
    The specific calling method can also be identified with the _caller string: "ClassName\\methodName"

    boolean calledBy(str _caller)
    {
        container       stack = xSession::xppCallStack();
        for(int i=3;i<=conLen(Stack);i+=2)
        {
            if(strScan(conPeek(stack,i),_caller,1,9999))
            {
                return true;
            }
        }
        return false;
    }
    

  • Debug workflow processing

    The following simple job allows you to use breakpoints in workflow handler classes.

    /// Process workflow outside of batch job (for debugging purposes)
    /// Note: run multiple times for one single workflow
    static void processWorkflowQueue(Args _args)
    {
        SysWorkflowMessageQueueManager::runStatic(conNull());
    }
    
  • Insight into AX 2012 Product Dimensions

    Situation:  Configurable product masters (Constraint-based configuration) exist without the required standard configuration

    Probable cause: Setup was missing during import of product master.

    Solution: Requires a script as the default configuration should have been created automatically and the form required to add configurations manually is write protected for constraint-based configurations. The creation of product variants and activation as standard configuration would be possible manually, but is covered by the script as well.

    Script Overiew

    • Create product specific configuration  (Main tables involved: EcoResProductDimensionGroupProduct)
    • Create product variant (Main tables involved: EcoResDistinctProductVariant, EcoResProductVariantConfiguration; Helper Class: EcoResProductVariantManager)
    • Release product variant to company (Main tables involved: InventDimCombination, InventDim; Helper Class: EcoResProductVariantReleaseManager)
    • Set variant as default on item (Main tables involved: InventTable)
    static void ecoResProductMasterConfiguration_fix(Args _args)
    {
        EcoResConfiguration                 c = EcoResConfiguration::findByName(EcoResProductParameters::getConfigurationName());
        EcoResProduct                       p;
        EcoResProductMasterConfiguration    productMasterConfiguration, productMasterConfiguration_notExists;
        EcoResProductDimensionGroupProduct  productDimensionGroupProduct;
        EcoResProductDimensionGroup         productDimensionGroup = EcoResProductDimensionGroup::findByDimensionGroupName("CONFIG_GRP");
        EcoResDistinctProductVariant        distinctProductVariant, distinctProductVariant_notExists;
        RefRecId                            productVariant;
        RefRecId                            attribute = EcoResProductDimensionAttribute::inventDimFieldId2DimensionAttributeRecId(fieldNum(InventDim,configId));
        EcoResProductVariantReleaseManager  ecoResProductVariantReleaseManager;
        InventTable                         inventTable;
        InventDimCombination                inventDimCombination_notExists;
        int                                 i,u;
        ;
        //Add config to product master (this cannot be done manually - error during import)
        While select p join productDimensionGroupProduct where productDimensionGroupProduct.Product == p.RecId && productDimensionGroupProduct.ProductDimensionGroup == productDimensionGroup.RecId
            notExists join productMasterConfiguration_notExists where productMasterConfiguration_notExists.ConfigProductMaster == p.RecId && productMasterConfiguration_notExists.Configuration == c.RecId
        {
            info(p.productNumber());
            ttsBegin;
            productMasterConfiguration.initValue();
            productMasterConfiguration.ConfigProductMaster = p.RecId;
            productMasterConfiguration.Configuration = c.RecId;
            productMasterConfiguration.ConfigProductDimensionAttribute = EcoResProductDimensionAttribute::inventDimFieldId2DimensionAttributeRecId(fieldNum(InventDim,ConfigId));
            productMasterConfiguration.insert();
            ttsCommit;
            i++;
        }
        //create product variant
        While select p //debug: where p.RecId == EcoResProduct::findByProductNumber("0052999").RecId
            join productDimensionGroupProduct where productDimensionGroupProduct.Product == p.RecId && productDimensionGroupProduct.ProductDimensionGroup == productDimensionGroup.RecId
            notExists join distinctProductVariant_notExists where distinctProductVariant_notExists.ProductMaster == p.RecId //not precise, but sufficient to identify missing standard variant
        {
            info(p.productNumber());
            ttsBegin;
            productVariant = EcoResProductVariantManager::createProductVariant(p.RecId,p.SearchName,[[attribute,c.RecId]]);
            EcoResProductTranslation::createOrUpdateTranslation(productVariant, p.productName(), '');
            ttsCommit;
            i++;
        }
        //release product variant to current company
        While select forupdate inventTable
            join p where inventTable.Product == p.RecId //debug: && p.RecId == EcoResProduct::findByProductNumber("0052999").RecId
            join distinctProductVariant where distinctProductVariant.ProductMaster == p.RecId
            exists join productDimensionGroupProduct where productDimensionGroupProduct.Product == p.RecId && productDimensionGroupProduct.ProductDimensionGroup == productDimensionGroup.RecId
            notExists join inventDimCombination_notExists where inventDimCombination_notExists.ItemId == inventTable.itemId //not precise, but sufficient to identify not released standard variant
        {
            info(p.productNumber());
            ecoResProductVariantReleaseManager = EcoResProductVariantReleaseManager::construct();
            ecoResProductVariantReleaseManager.parmEcoResProduct(distinctProductVariant);
            ecoResProductVariantReleaseManager.parmLegalEntity(CompanyInfo::current());
            ecoResProductVariantReleaseManager.release();
            i++;
        }
        //set default configuration
        ttsBegin;
        While select forupdate inventTable where !inventTable.StandardConfigId
            join p where inventTable.Product == p.RecId
            exists join productDimensionGroupProduct where productDimensionGroupProduct.Product == p.RecId && productDimensionGroupProduct.ProductDimensionGroup == productDimensionGroup.RecId
        {
            info(p.productNumber());
            inventTable.StandardConfigId = c.Name;
            inventTable.update();
            u++;
        }
        ttsCommit;
        info(strFmt("%1 records inserted, %2 updated",i,u));
    }
    
  • Convert Product to Product master

    Changing the table inheritance of a record must be done on SQL level – the following code does this. Of course, related data must be handled as well.

    static void Job1(Args _args)
    {
        //change to product master
        class1::changeInheritance('EcoResProduct',tableNum(EcoResProductMaster),EcoResProduct::findByDisplayProductNumber('0119').RecId);
        //change back
        //class1::changeInheritance('EcoResProduct',tableNum(EcoResDistinctProduct),EcoResProduct::findByDisplayProductNumber('0119').RecId);
    }
    
    //Important: this code must be run on the server
    public static server void changeInheritance(str _tableName, tableId _newTableId, RefRecId    _refRecId)
    {
        Connection      connection;
        Statement       statement;
        str             query;
        ;
        // create connection object
        connection = new Connection();
        // create statement
        statement = connection.createStatement();
        // Set the SQL statement
        query = strfmt("update %1 set %1.InstanceRelationType ='%2' where %1.RecId = %3;", _tableName, _newTableId, _refRecId);
        info(query);
        // assert SQL statement execute permission
        new SqlStatementExecutePermission(query).assert();
        //BP Deviation documented
        statement.executeUpdate(query);
        // limit the scope of the assert call
        CodeAccessPermission::revertAssert();
    }
    
  • Better Default Number Sequences

    Update 22/02/2017: in Dynamics 365 for Operations jobs are a thing of the past. See Mass changes to Number Sequences in Dynamics 365 for Operations for an alternative.

    AX 2012 Wizard creates number sequences which contain the company and a dash e.g. “USMF-######”. This adds 5 characters to every number sequence, which is not always desirable.

    The following job removes the company and/or the dash from all number sequences and improves the naming of the number sequences.
    Use the constants to parameterize the script according to your requirements. If both REMOVECOMPANY and REMOVEDASH are false, then the script still fulfils the purpose of improving the naming of the number sequences.

    
    static void numberSequenceTable_Format(Args _args)
    {
        #define.REMOVECOMPANY(true) //parameter implemented as constant --> adjust accordingly
        #define.REMOVEDASH(true)    //parameter implemented as constant --> adjust accordingly
        str                     annotatedFormat;
        str                     format;
        container               segments;
        NumberSequenceTable     numberSequenceTable,numberSequenceTableSelect;
        NumberSequenceReference numberSequenceReference, firstReference, numberSequenceReferenceExists;
        NumberSequenceDatatype  numberSequenceDatatype;
        NumberSequenceScope     numberSequenceScope;
        boolean                 companyFound;
        int                     i, j;
        str                     company;
        str                     referencesStr;
        int                     starttime = timeNow();
        int                     pos;
        ;
        setPrefix('numberSequenceTable_Format');
        if(!(infolog.language() like 'en*'))
        {
            if(Box::okCancel('It is recommended to use an English login. Continue?',DialogButton::Cancel,'Login language')!=DialogButton::Ok)
            {
                throw error('It is recommended to use an English login.');
            }
        }
        while select RecId from numberSequenceTableSelect
        exists join numberSequenceReferenceExists where numberSequenceReferenceExists.NumberSequenceId == numberSequenceTableSelect.RecId   //avoid renaming of independent sequences (e.g. Serial numbers)
        {
            ttsBegin;
            numberSequenceTable = NumberSequenceTable::find(numberSequenceTableSelect.RecId,true);
            setPrefix(numberSequenceTable.NumberSequence);
            referencesStr = '';
            firstReference.clear();
    
            //0) get reference description (preparation needed for rename and description)
            while select numberSequenceReference where NumberSequenceReference.NumberSequenceId == numberSequenceTable.RecId
            {
                referencesStr += referencesStr?', ' : '' + numberSequenceReference.referenceLabel();
                if(!firstReference)
                {
                    firstReference = NumberSequenceReference::find(numberSequenceReference.RecId);
                }
            }
            if(!referencesStr)
            {
                referencesStr = numberSequenceTable.Txt;
            }
    
            //1) rename if numbering does not match convention (or language)
            if(firstReference)
            {
                numberSequenceDatatype = numberSequenceDatatype::find(firstReference.NumberSequenceDatatype);
                if(!(numberSequenceTable.NumberSequence like (NumberSequenceTable::autoId(numberSequenceDatatype) + '*')))
                {
                    pos=1;
                    while(strFind(numberSequenceTable.NumberSequence,'_',pos,999)>1)
                    {
                        pos = strFind(numberSequenceTable.NumberSequence,'_',pos,999)+1; //find position of last '_' in ID
                    }
                    numberSequenceTable.NumberSequence = NumberSequenceTable::autoId(numberSequenceDatatype) + subStr(numberSequenceTable.NumberSequence,pos,999); //new id with old index
                    if(NumberSequenceTable::existByNaturalKey(numberSequenceTable.NumberSequence, numberSequenceTable.NumberSequenceScope) || numberSequenceTable.NumberSequence like '*_')
                    {
                        numberSequenceTable.NumberSequence = NumberSequenceTable::findNextAutoId(numberSequenceDatatype.DatatypeId);
                    }
                    if(numberSequenceTable.NumberSequence != numberSequenceTable.orig().NumberSequence)
                    {
                        numberSequenceTable.update();
                        j++;
                    }
                }
            }
    
            //2) remove segments
            companyFound = false;
            company = '';
            segments = NumberSeq::parseAnnotatedFormat(numberSequenceTable.AnnotatedFormat);
            for(i=1;i<=conLen(segments);i++)
            {
                if(conPeek(conPeek(segments,i),1) == 0)
                {
                    company = conPeek(conPeek(segments,i),2);
                    if(#REMOVECOMPANY)
                    {
                        segments = conDel(segments,i,1); //remove company from number sequence
                        i--;
                    }
                    companyFound = true;
                }
                else if(companyFound && conPeek(conPeek(segments,i),1) == -1 && conPeek(conPeek(segments,i),2) == '-')
                {
                    if(#REMOVEDASH)
                    {
                    segments = conDel(segments,i,1); //remove '-' after company from number sequence
                    i--;
                    }
                    break;
                }
            }
            if(companyFound) //remove company id from segments
            {
                annotatedFormat = NumberSeq::createAnnotatedFormatFromSegments(segments);
                format = NumberSeq::createAnnotatedFormatFromSegments(segments, false);
                numberSequenceTable.AnnotatedFormat = annotatedFormat;
                numberSequenceTable.Format = format;
                numberSequenceTable.update();
                j++;
            }
    
            //3) adjust description to be more human readable
            numberSequenceScope = NumberSequenceScope::find(numberSequenceTable.NumberSequenceScope);
            if(numberSequenceScope.DataArea)
            {
                company = numberSequenceScope.DataArea;
            }
            else if(CompanyInfo::findRecId(numberSequenceScope.LegalEntity))
            {
                company = CompanyInfo::findRecId(numberSequenceScope.LegalEntity).DataArea;
            }
            else if(OMOperatingUnit::find(numberSequenceScope.OperatingUnit,numberSequenceScope.OperatingUnitType))
            {
                company = OMOperatingUnit::find(numberSequenceScope.OperatingUnit,numberSequenceScope.OperatingUnitType).NameAlias;
            }
            else
            {
                company = 'GLOBAL';
            }
            if(strScan(referencesStr,company,1,999)>0)
            {
                numberSequenceTable.Txt = strFmt('%1: %2',strUpr(company),numberSequenceTable.NumberSequence);
            }
            else
            {
                numberSequenceTable.Txt = strFmt('%1: %2',strUpr(company),referencesStr);
            }
            if(strCmp(numberSequenceTable.Txt, numberSequenceTable.orig().Txt)!=0)
            {
                numberSequenceTable.update();
                j++;
            }
            ttsCommit;
        }
        info(strFmt('@SYS74545',j,tableId2pname(tableNum(numberSequenceTable))));
        info(strFmt('@SYS316310',timeNow()-starttime));
    }
    
  • Enable Database Log on all Parameter- and Group-Tables

    I want to make sure that I know who changed what and when on all parameter- and group-tables.

    The following script will enable the database log on all those tables:

    static void enableDatabaselogOnParameterTables(Args _args)
    {
        #AOT
        DatabaseLog         databaseLog;
        TreeNode            treeNode;
        SysDictTable        sysDictTable;
        Name                name;
        ;
        ttsbegin;
        treeNode = TreeNode::findNode(#TablesPath);
        treeNode = treeNode.AOTfirstChild();
        while (treeNode)
        {
            name = treeNode.AOTname();
            sysDictTable = SysDictTable::newTableId(treeNode.applObjectId());
            if((sysDictTable.tableGroup() == TableGroup::Parameter || sysDictTable.tableGroup() == TableGroup::Group)
                && SysQueryForm::hasValidCountryCode(sysDictTable.id()))
            {
                select firstOnly databaseLog where databaseLog.logTable == sysDictTable.id(); //do not modify existing settings
                if(!databaseLog)
                {
                    databaseLog.LogTable = sysDictTable.id();
                    databaseLog.LogField = 0;
                    databaseLog.LogType = DatabaseLogType::Update;
                    databaseLog.insert();
                    databaseLog.LogType = DatabaseLogType::EventUpdate;
                    databaseLog.insert();
                    if(sysDictTable.tableGroup() != TableGroup::Parameter)
                    {
                        databaseLog.LogType = DatabaseLogType::Delete;
                        databaseLog.insert();
                        databaseLog.LogType = DatabaseLogType::EventDelete;
                        databaseLog.insert();
                        databaseLog.LogType = DatabaseLogType::RenameKey;
                        databaseLog.insert();
                        databaseLog.LogType = DatabaseLogType::EventRenameKey;
                        databaseLog.insert();
                        databaseLog.LogType = DatabaseLogType::Insert;
                        databaseLog.insert();
                        databaseLog.LogType = DatabaseLogType::EventInsert;
                        databaseLog.insert();
                    }
                }
            }
            treeNode = treeNode.AOTnextSibling();
        }
        ttscommit;
        SysFlushDatabaseLogSetup::main();
    }
    
  • How to get radio buttons on a dialog

    There is no dialog.addRadioButtonContol(…) function so how do you do it?

    static void Job1(Args _args)
    {
        Dialog                dialog;
        FormRadioControl      formRadioControl;
        FormBuildRadioControl formBuildRadioControl;
        FormBuildGroupControl formBuildGroupControl;
        int                   formBuildRadioControlId;
        ;
        //Build dialog with radio buttons
        dialog = new Dialog("Test Dialog");
        formBuildGroupControl = dialog.mainFormGroup();
        formBuildRadioControl = formBuildGroupControl.addControl(FormControlType::RadioButton,'radiobuttons');
        formBuildRadioControlId = formBuildRadioControl.id();
        //set number of buttons
        formBuildRadioControl.items(3);
        //add descriptions
        formBuildRadioControl.item(1);
        formBuildRadioControl.text("Radiobutton 1");
        formBuildRadioControl.item(2);
        formBuildRadioControl.text("Radiobutton 2");
        formBuildRadioControl.item(3);
        formBuildRadioControl.text("Radiobutton 3");
        
        if(dialog.run())
        {
            if(formBuildRadioControl)
            {
                //get control
                formRadioControl = dialog.formRun().control(formBuildRadioControlId);
                //get index
                info(strfmt("Radiobutton %1 selected",formRadioControl.selection()+1));
            }
        }
    }
    
  • Find Datatypes

    I like reusing/extending data types that already exist, however it is not always easy to find them. The following job lets you search the existing data types for a keyword:

    static void findEDT(Args _args)
    {
        #AOT
        Dialog              dialog = new Dialog('Find EDT');
        SearchString        searchString;
        DialogField         dialogField = dialog.addField('SearchString');
        boolean             stopAfterFound;
        void searchNodes(boolean _extendedDataTypes)
        {
            TreeNode            treeNode;
            TreeNodeIterator    treeNodeIterator;
            SysDictType         sysDictType;
            SysDictEnum         sysDictEnum;
            Boolean             found;
            ;
            if(_extendedDataTypes)
            {
                treeNode = TreeNode::findNode(#ExtendedDataTypesPath);
            }
            else
            {
                treeNode = TreeNode::findNode(#BaseEnumsPath);
            }
            treeNodeIterator = treenode.AOTiterator();
            treeNode = treeNode.AOTfirstChild();
            treeNode = treeNodeIterator.next();
            while(treeNode)
            {
                if(treeNode.treeNodeName() like "*" + searchString + "*")
                {
                    if(_extendedDataTypes)
                    {
                        sysDictType = sysDictType::newTreeNode(treeNode);
                    }
                    if(sysDictType)
                    {
                        if (sysDictType.extend()  && sysDictType.enumId())
                        {
                            info(strfmt("%1 (%2 - %3 -> %4)",treeNode.treeNodeName(), sysDictType.baseType(), extendedTypeId2name(sysDictType.extend()), enumId2Name(sysDictType.enumId()) ));
                        }
                        if(sysDictType.extend())
                        {
                            info(strfmt("%1 (%2 - %3)",treeNode.treeNodeName(), sysDictType.baseType(), extendedTypeId2name(sysDictType.extend()) ));
                        }
                        else if (sysDictType.enumId())
                        {
                            info(strfmt("%1 (%2 - %3)",treeNode.treeNodeName(), sysDictType.baseType(), enumId2Name(sysDictType.enumId()) ));
                        }
                        else
                        {
                            info(strfmt("%1 (%2)",treeNode.treeNodeName(), sysDictType.baseType()));
                        }
                    }
                    else
                    {
                        info(strfmt("%1",treeNode.treeNodeName()));
                    }
                    found = true;
                }
                else if(stopAfterFound  && found)
                {
                    return;
                }
                treeNode = treeNodeIterator.next();
            }
        }
        ;
        if(dialog.run())
        {
            searchString = dialogField.value();
            if(!strScan(searchString, "*", 1, 999)) //no "*"
            {
                searchString = "*" + searchString + "*";
            }
            else if(strKeep(searchString,"*") == "*"  && subStr(searchString,strLen(searchString),1)=="*") //just one final "*": beginns with
            {
                stopAfterFound = true;
            }
            info(strfmt("Find %1",searchString));
            if(searchString)
            {
                setPrefix('Extended Data Types');
                searchNodes(true);
                setPrefix('Base Enums');
                searchNodes(false);
            }
        }
    }
    

    Update: slight improvement “stopAfterFound”