top of page

Migrate Existing Products to ECM Products-Limitations and Restrictions of the OTB migration feature

For the below, I installed the feature called “Enable change management on existing products” what essentially is designed to help to change/migrate products to engineering products. There are a few limitations that the OTB features has. When you convert a product to an engineering product, it remains a product itself and it doesn't become a product master accordingly. Additionally, when you try to convert a product master that has a specific set of product dimensions enabled, those dimensions are maintained after the change. E.g., if you convert a product master that has the size dimension enabled, it will keep the size dimension moving forward. Short, if you have a distinct product, you can change it only to an engineering product that doesn't track the product dimension in transactions (means, the version dimension isn't used in transactions). Let's review the step by step instructions below and discuss workaround if the above limitations do not work for you (e.g. in case you can't change the item number of the legacy product as of operational reasons and essentially need to work with custom scripts instead of the OTB migration tool).


Requirements as well as limitations for ECM migration feature are:

  • Engineering product categories must exist for all relevant standard products before you can convert those products. (assignment will be done later when running the "Convert to engineering product" wizard) Note: The product type and dimension group must match both the product and its engineering product category, otherwise it will error.

  • If D365FO ECM acceptance policy (set in the ECM Parms) is set to manual, products need to be accepted in the receiving LE before they are final released to the respective LEs and ready for transacting. You can do such using the Open product releases page in the appropriate receiving companies. For the migration process below, it makes sense to set it to automatic.

  • Be aware that once a product is converted into an engineering product, this change cannot be reverted. Once converted, the product will have versions and will be managed through change management moving forward.

  • The engineering bill of materials (BOM) and routes won't automatically be released to those companies. Be sure to review each of the converted products and decide whether you need to release the BOMs and/or routes. Select the release product structure button for those products that have a BOM and/or route that should be released.


Configurations and Process flow:

1. Enable ECM:

ECM config license key as well as variant version config license key need to be enabled.

Note: The "Manage changes to formulas and their ingredients" feature needs to be enabled prior. Otherwise you won't be able to flag and activate the below ECM license keys.

Note: This requires the environment to be in Maintenance mode. For Tier 2 boxes you can facilitate such via LCS and maintain/enable Maintenance mode. For Tier 1 boxes, you would need to RDP in the box and enable maintenance mode manually via the below script.

  1. Open Sql Server Management Studio

  2. Point to the AXDB database

  3. Raise the following command, update SQLSYSTEMVARIABLES SET VALUE = 1 where PARM = 'CONFIGURATIONMODE'

  4. Restart the Service Fabric service against all AOSs Node

  5. When you've completed your maintenance mode activities, repeat steps 3 and 4 but set the value to 0 in step 3.

Lastly,, you would need to enable the Engineering change management feature via feature management.


2. Enable ECM Migration feature:

I enabled the feature required for the migration process: ”Enable change management on existing products".


3. Configurations:

Create LE: (later used as engineering LE).

Reference the above LE as engineering organizations.

Set up version number rule.

Set up Product lifecycle state.

Set up product release policy.

Set up ECM Parameters (acceptance set to "Manual").

Set up engineering product category details for products/released products.

Set up engineering product category details for product masters/variants with Color product dimension group assigned. The "Track version in transaction" flag needs to be set to "No". (See details on limitations of the migration feature explained further above)


4. Demo for product/released product as well as product maser/variant:

See below Product/Released Product prior migration process:

See below Product master (with color enabled product dimension assigned)/Released Variants prior migration process:

Under Released Products form for standard products and under Products masters for product master/variant products, navigate to the engineer tab and under ECM find the "Convert to engineering product: option: (Note: Execute the below process in the engineering LE - here: DEMF)

Run the Migration process/wizard - specify the correct engineering category accordingly, the product life cycle state and the version to be created.

See below the results - Engineering products for the above post migration.

See the result for the above Product/Released product.

Under engineering version see now an active version.

See the result for the above Product master/Variant. See below also the engineering versions for the product master and its variants.

You can now follow the standard ECM release process and process the new product structure via the below function from the DEMF engineering LE.

Note: Make sure that you accept the product design changes under ECM/Open product releases in the receiving LE if you have the policy set to manual. If it is set to automatic you can neglect this step and products are immediately ready for use.


Let's say the OTB ECM migration feature does not work for your use case as you need to migrate your existing item master and you need to be able to track version history at the transactional level. (This is one limitation that the OTB feature does not allow post migration) We can run the below script to migrate the existing products to engineering products. Please note, the below is not a MSFT recommended way to go and requires heavy smoke testing as we are using backdoors via code. Short, there is great operational risk doing so (as with all code updates/scripts that are not directly released by MSFT). Also, the below needs to be extended based on your use case to clean up possible legacy/existing custom dimension if you were having such pre migration.(e.g. EcoResProductMasterFlavor, EcoResFlavor, etc…, if you maintained a custom flavor dimension and want to get rid of such).


<?xml version="1.0" encoding="utf-8"?>
<AxClass xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
	<Name>EngChgInventVersionDimensionMigrationService</Name>
	<SourceCode>
		<Declaration><![CDATA[
/// <summary>
///    The <c>EngChgInventVersionDimensionMigrationService</c> class migrates the values from a selected field in the InventDim table to the <c>InventVersionId</c> dimension.
/// </summary>
internal class EngChgInventVersionDimensionMigrationService 
{
    public static const str InventDimUpdatePreventionTriggerName = 'INVENTDIMUPDATEPREVENTIONTRIGGER';
    private FieldId oldDimensionFieldId;
    private FieldName fieldName;
    private InventDim inventDim;
    private EngChgInventVersionDimensionMigrationUpgradeHashCode inventDimUpgradeHashCode;

}
]]></Declaration>
		<Methods>
			<Method>
				<Name>migrateInventDim</Name>
				<Source><![CDATA[
    /// <summary>
    ///    Executes the migration of all the values of a field in the <c>Inventdim</c> to the version dimension.
    /// </summary>
    /// <param name = "_contract">An instance of the <c>EngChgInventVersionDimensionMigrationContract</c> contract.</param>
    public void migrateInventDim(EngChgInventVersionDimensionMigrationContract _contract)
    {
        oldDimensionFieldId = _contract.parmFromInventDimFieldId();
        fieldName = _contract.parmFromInventDimFieldName();

        if (this.canConvert())
        {
            this.prepareConversion();

            DataArea companies;
            while select companies
            {
                changecompany(companies.Id)
                {
                    this.convertDimension();
                }
            }
            this.restoreDBChanges();
        }
    }

]]></Source>
			</Method>
			<Method>
				<Name>copyComputedHashCodesToInventDim</Name>
				<Source><![CDATA[
    private void copyComputedHashCodesToInventDim()
    {
        #OCCRetryCount

        this.callInsertUpdateRelatedSkipMethods(inventDim);

        try
        {
            ttsbegin;

            update_recordset inventDim
                setting SHA1HashHex = inventDimUpgradeHashCode.SHA1HashHex
                join inventDimUpgradeHashCode
                    where inventDimUpgradeHashCode.InventDimId == inventDim.InventDimId;

            ttscommit;
        }
        catch (Exception::Deadlock)
        {
            if (xSession::currentRetryCount() >= #RetryNum)
            {
                throw Exception::Deadlock;
            }
            else
            {
                retry;
            }
        }
    }

]]></Source>
			</Method>
			<Method>
				<Name>callInsertUpdateRelatedSkipMethods</Name>
				<Source><![CDATA[
    /// <summary>
    ///    Skips calling server methods which could influence the migration logic.
    /// </summary>
    /// <param name = "_inventDim">An instance of the <c>InventDim</c> table.</param>
    protected void callInsertUpdateRelatedSkipMethods(Inventdim _inventDim)
    {
        _inventDim.skipAosValidation(true);
        _inventDim.skipDatabaseLog(true);
        _inventDim.skipDataMethods(true);
        //needed in case of crossCompany
        _inventDim.skipEvents(true);
    }

]]></Source>
			</Method>
			<Method>
				<Name>canConvert</Name>
				<Source><![CDATA[
    private boolean canConvert()
    {
        SysDictField dictField = new SysDictField(tableNum(InventDim), oldDimensionFieldId);

        if (dictField.configurationKeyId() && !isConfigurationkeyEnabled(dictField.configurationKeyId())) //The dimension was not used in the previous DB
        {
            return false;
        }

        if(!isConfigurationkeyEnabled(configurationKeyNum(EcoResProductVersion)))
        {
            return false;
        }

        return true;
    }

]]></Source>
			</Method>
			<Method>
				<Name>prepareConversion</Name>
				<Source><![CDATA[
    private void prepareConversion()
    {
        EngChgInventVersionDimensionMigrationSynchronizationHandler::construct().runDropTrigger(EngChgInventVersionDimensionMigrationService::InventDimUpdatePreventionTriggerName);

        this.callInsertUpdateRelatedSkipMethods(inventDim);

        ttsbegin;
        delete_from inventDimUpgradeHashCode;
        ttscommit;
    }

]]></Source>
			</Method>
			<Method>
				<Name>convertDimension</Name>
				<Source><![CDATA[
    private void convertDimension()
    {
        boolean hashUpdateNeeded;

        ttsbegin;

        this.updateProductDimensionGroupSetup();

        RefRecId versionDimensionAttributeRecId = EcoResProductDimensionAttribute::inventDimFieldId2DimensionAttributeRecId(fieldNum(InventDim, InventVersionId));
        while select forupdate inventDim
            where inventDim.(oldDimensionFieldId) != ''
        {
            EcoResVersionName versionName = inventDim.(oldDimensionFieldId);
            EcoResVersion ecoResVersion = this.findOrCreateEcoResVersion(versionName);

            this.updateInventDim(inventDim, versionName);
            hashUpdateNeeded = true;

            this.updateInventDistinctProduct(inventDim, versionName);

            this.addProductDimensionValuesAndUpdateMaster(inventDim, ecoResVersion, versionDimensionAttributeRecId);
        }

        this.cleanupLeftoverCustomDimensionData();

        ttscommit;

        if(hashUpdateNeeded)
        {
            this.copyComputedHashCodesToInventDim();
        }
    }

]]></Source>
			</Method>
			<Method>
				<Name>addProductDimensionValuesAndUpdateMaster</Name>
				<Source><![CDATA[
    private void addProductDimensionValuesAndUpdateMaster(InventDim _inventDim, EcoResVersion _version, RefRecId _versionDimensionAttributeRecId)
    {
        InventDimCombination inventDimCombination;
        EcoResProductMasterVersion productMasterVersion;
        EcoResDistinctProductVariant ecoResDistinctProductVariant;

        container dimensions = [[_versionDimensionAttributeRecId, _version.RecId]];

        // invent dim combinations
        while select inventDimCombination
                where inventDimCombination.InventDimId == _inventDim.inventDimId
                join ecoResDistinctProductVariant
                    where ecoResDistinctProductVariant.RecId == inventDimCombination.DistinctProductVariant
                outer join productMasterVersion
                    where productMasterVersion.ProductVersionProductMaster == ecoResDistinctProductVariant.ProductMaster
                        && productMasterVersion.ProductVersion == _version.RecId
                        && productMasterVersion.ProductVersionProductDimensionAttribute == _versionDimensionAttributeRecId
        {
            if (!productMasterVersion)
            {
                EcoResProductMasterVersion productMasterVersionNew;
                productMasterVersionNew.ProductVersionProductMaster = ecoResDistinctProductVariant.ProductMaster;
                productMasterVersionNew.ProductVersion = _version.RecId;
                productMasterVersionNew.ProductVersionProductDimensionAttribute = _versionDimensionAttributeRecId;
                productMasterVersionNew.doInsert();
            }

            try
            {
                EcoResProductVariantManager::addProductDimensionsValues(inventDimCombination.DistinctProductVariant, dimensions);
            }
            catch (Exception::DuplicateKeyException)
            {
                exceptionTextFallThrough();
            }
        }
    }

]]></Source>
			</Method>
			<Method>
				<Name>updateInventDistinctProduct</Name>
				<Source><![CDATA[
    private void updateInventDistinctProduct(InventDim _inventDim, EcoResVersionName _versionName)
    {
        InventDistinctProduct inventDistinctProduct;
        while select forupdate inventDistinctProduct
                where inventDistinctProduct.InventDimId == _inventDim.inventDimId
        {
            inventDistinctProduct.InventVersionId = _versionName;

            // TODO: Blank the field with old dimension - need the field ID of the old dimension on InventDistinctProduct table
            // inventDistinctProduct.(inventDistinctProductInventDimension1FieldId) = '';

            inventDistinctProduct.doUpdate();
        }
    }

]]></Source>
			</Method>
			<Method>
				<Name>cleanupLeftoverCustomDimensionData</Name>
				<Source><![CDATA[
    private void cleanupLeftoverCustomDimensionData()
    {
        // TODO - Clean up data from EcoResProductMasterXXX table for the custom dimension (e.g. EcoResProductMasterFlavor)

        // TODO - Clean up data from EcoResProductVariantXXX table for the custom dimension (e.g. EcoResProductVariantFlavor)

        // TODO - Clean up data from EcoResXXX table for the custom dimension (e.g. EcoResFlavor)
    }

]]></Source>
			</Method>
			<Method>
				<Name>updateInventDim</Name>
				<Source><![CDATA[
    private void updateInventDim(InventDim _inventDim, EcoResVersionName _versionName)
    {
        _inventDim.InventVersionId = _versionName;
        _inventDim.(oldDimensionFieldId) = '';
        _inventDim.doUpdate();

        inventDimUpgradeHashCode.InventDimId = _inventDim.inventDimId;
        inventDimUpgradeHashCode.SHA1HashHex = _inventDim.hashValue();
        inventDimUpgradeHashCode.insert();
    }

]]></Source>
			</Method>
			<Method>
				<Name>updateProductDimensionGroupSetup</Name>
				<Source><![CDATA[
    private void updateProductDimensionGroupSetup()
    {
        EcoResProductDimensionGroupFldSetup ecoResProductDimensionGroupFldSetup;
        while select forupdate ecoResProductDimensionGroupFldSetup
                where ecoResProductDimensionGroupFldSetup.DimensionFieldId == oldDimensionFieldId
        {
            ecoResProductDimensionGroupFldSetup.DimensionFieldId = (fieldNum(InventDim, InventVersionId));
            ecoResProductDimensionGroupFldSetup.doUpdate();
        }
    }

]]></Source>
			</Method>
			<Method>
				<Name>findOrCreateEcoResVersion</Name>
				<Source><![CDATA[
    private EcoResVersion findOrCreateEcoResVersion(EcoResVersionName _versionName)
    {
        EcoResVersion ecoResVersion = EcoResVersion::findByName(_versionName);
        if (!ecoResVersion)
        {
            ecoResVersion.Name = _versionName;
            ecoResVersion.insert();
        }

        return ecoResVersion;
    }

]]></Source>
			</Method>
			<Method>
				<Name>restoreDBChanges</Name>
				<Source><![CDATA[
    private void restoreDBChanges()
    {
        new Application().RaiseOnDbSynchronize(tableNum(InventDim));
    }

]]></Source>
			</Method>
		</Methods>
	</SourceCode>

</AxClass>




Recent Posts

See All
bottom of page