Friday, March 22, 2013

Copy, Set, Delete, Get Product Categories and Attributes in AX 2012 X++

Hi there,

I hope you had a great week and that you are ready for a great and restful weekend. In this post I would like to spotlight the work of one of my customers. His name is Colin Mitchell and he is a senior solutions architect for TURCK, a world leader cable manufacturer.


Colin and I worked on a very challenging project together for about 5 months. Colin created a super smart application that would allow TURCK sales and product managers to quickly configure a cable specification in real time. If we think about it, engineering approval processes are the key to a quality product and eventually happy customers. 


However, why do we need to involve “human” interaction if a new product could be analyzed automatically based on certain quality rules? Well, Colin and his team created a software solution that would decide, in real time, if the product that was being created needed to go through a required approval process or not.

So, what was the challenge? The challenge was implementing the same “intelligence” into Microsoft Dynamics AX 2012. For this, TURCK purchased ERPSolutionsTotal Engineering Change Management


My role was to modify the ERPSolutions Total Engineering Change Management software by implementing TURCK’s vision into it.

So, what was the outcome? It was a very successful project. And it was successful not only because capable people were working on it, but because Colin was involved each step of the way. On this thought, Colin created a solution to copy, set, delete and get Microsoft Dynamics AX product categories and attributes from one product to another. This might sound easy, but believe me is not. Colin and I spent countless hours working on his vision, but he was the one who came up with the final and working solution.


I need to add that despite Microsoft willingness to find a solution for us, they couldn’t.  This post is about sharing what he created. I got his permission to do so and I thought it would be a great idea and great benefit for all us to get this knowledge and high level of analysis.


The Process


Colin when about creating a table relationship diagram (depicted below) with how product categories and attributes are related to a product. 







Then, Colin wrote a class to achieve the following:


  • Copy product attributes and categories
  • Delete product attributes
  • Get product attributes
  • Set product attributes

The code sample is extensive and self-explanatory as Colin included really good comments in each step. 


Note: The following code is to be used at your own risk.


Just as a final note, I would like to thank Colin Mitchel for allowing me to share his work in my blog. In addition, I would like to point out that, successful software implementations still exist, and they are successful because of the customer willingness to learn, share, understand and succeed. TURCK was one of those customers, and Colin is a clear example of discipline, intelligence and willingness to go beyond his comfort zone and master a new language. He has really become an amazing X++ master.   
 

You can contact Colin in his Linked In profile here.

Code: 

        #//© 2013 TURCK, Inc.  All Rights Reserved.
        #/// <summary>
        #///    copies all category hierarchies, categories, attributes and attribute values from one product to another
       
        #server static void copyAttributesToProduct(itemID _sourceItem, itemID _destinationItem)
        #{
        #
        #   RefRecId  sourceProductRecId, destinationProductRecId,      productInstanceRecId_AfterInsert, ecoResTextValueRecId_AfterInsert;
        #
        #   EcoResProduct                      sourceProduct      = EcoResProduct::find(InventTable::find(_sourceItem).Product);
        #   EcoResProduct                      destinationProduct = EcoResProduct::find(InventTable::find(_destinationItem).Product);
        #
        #   EcoResProductCategory              ecoResProductCategory, ecoResProductCategory_ForInsert;
        #   EcoResCategory                     ecoResCategory;
        #   EcoResCategoryAttributeLookup      ecoResCategoryAttributeLookup;
        #
        #   EcoResAttribute                    ecoResAttribute;
        #   EcoResAttributeValue               ecoResAttributeValue, ecoResAttributeValue_ForInsert;
        #   EcoResProductInstanceValue         ecoResProductInstanceValue, ecoResProductInstanceValue_ForInsert;
        #   EcoResTextValue                    ecoResTextValue, ecoResTextValue_ForInsert;
        #
        #   if (sourceProduct && destinationProduct)
        #   {
        #
        #    // our source and destination products
        #    // both exist.
        #
        #    sourceProductRecId = sourceProduct.RecId;
        #    destinationProductRecId = destinationProduct.RecId;
        #
        #    if (sourceProductRecId != destinationProductRecId)
        #    {
        #
        #     // we know that the source and destination products
        #     // aren't the same product.
        #
        #     // the purpose of this code is to copy categories,
        #     // attributes and attribute value from one product to another,
        #     // so we have to delete any existing objects from our
        #     // destination product first. Let's go ahead and do that...
        #
        #     // note that we may use the delete_from statement here, too.
        #     // i used delete() for testing, to look at each value as
        #     // it's being deleted.
        #
        #     // first delete all existing categories, attribute, and values
        #     // if they exist
        #
        #     ProductAttributesManager::deleteCategoriesAndAttributeValues(destinationProductRecId);
        #
        #     // now we need to add our destination product to the ecoResProductInstanceValue
        #     // table. The important thing to know here is that the Product field is indexed
        #     // and doesn't allow duplicates -- that is to say, we add our product ONCE.
        #
        #     // note that ecoResProductInstanceValue extends the ecoResInstanceValue table
        #     // but no worries, we can write all of our values at once. The only trick
        #     // is that the "InstanceRelationType" field is a system field, which means
        #     // we can't call it normally. We need to use the overwriteSystemfields variable
        #     // and incorporate the fieldNum function, passing the ID of the EcoResProductInstanceValue
        #     // table to the field. When we're finished, we turn the overwriteSystemfields off again.
        #
        #     ttsBegin;
        #      select forUpdate ecoResProductInstanceValue_ForInsert;
        #       ecoResProductInstanceValue_ForInsert.Product = destinationProductRecId;
        #       new OverwriteSystemFieldsPermission().assert();
        #       ecoResProductInstanceValue_ForInsert.overwriteSystemfields(true);
        #       ecoResProductInstanceValue_ForInsert.(fieldNum(EcoResProductInstanceValue, InstanceRelationType)) = tableName2id("EcoResProductInstanceValue");
        #       ecoResProductInstanceValue_ForInsert.insert();
        #       ecoResProductInstanceValue_ForInsert.overwriteSystemfields(false);
        #       CodeAccessPermission::revertAssert();
        #     ttsCommit;
        #
        #     if (ecoResProductInstanceValue_ForInsert)
        #     {
        #      // IMPORTANT!
        #
        #      // we've now written our destination product into the ecoResProductInstanceValue table.
        #      // in doing so, we've created an important value which we need to keep
        #      // track of - the value of the RecID field for the new value we've written into
        #      // the ecoResProductInstanceValue table. This is the "InstanceValue" which we'll
        #      // write to the EcoResAttributeValue table. Let's put this value in a variable.
        #
        #      productInstanceRecId_AfterInsert = ecoResProductInstanceValue_ForInsert.RecId;
        #
        #      // the next step is to copy our source product's product categories over to our
        #      // destination product. we do that by writing into the ecoResProductCategory table.
        #
        #      // NOTE that products in AX 2012 can be associated with many category hierarchies,
        #      // so to make sure we copy of all them, we need to use a WHILE loop, which will find each
        #      // category and allow us to interact with them.
        #
        #      // we now need to move into discovering the relationships between categories and
        #      // and attributes. the relationship between products and categories is stored in the
        #      // ecoResProductCategory table. while it has a series of relationships
        #      // to other tables (for example, categories and hierarchies), the one we care about
        #      // is the relationship between the product and the category. additional information
        #      // about the category is located in the ecoResCategory table, so let's join
        #      // them as part of our WHILE loop.
        #
        #      while
        #       select ecoResProductCategory where ecoResProductCategory.Product == sourceProductRecId   // <--- we are asking for the data from our SOURCE product
        #       join ecoResCategory where ecoResCategory.RecId == ecoResProductCategory.Category
        #      {
        #       // now, while we're looping, write each product category from
        #       // our source product  to our destination product. The ecoResProductCategory
        #       // table is indexed on the Product and Category fields, allowing
        #       // us to have multiple categories for each product.
        #
        #       ttsBegin;
        #        select forUpdate ecoResProductCategory_ForInsert;
        #         ecoResProductCategory_ForInsert.Product           = destinationProductRecId;                    // <--- use the RecID of our DESTINATION product
        #         ecoResProductCategory_ForInsert.Category          = ecoResProductCategory.Category;
        #         ecoResProductCategory_ForInsert.CategoryHierarchy = ecoResProductCategory.CategoryHierarchy;
        #         ecoResProductCategory_ForInsert.insert();
        #       ttsCommit;
        #
        #       if (ecoResProductCategory_ForInsert)
        #       {
        #        // we've "copied" the product category information for category X
        #        // from our source product to our destination product. the actual
        #        // structure of the hierarchy isn't something we need to write, but
        #        // in order to write attribute values we need to know more about
        #        // categories and their relationship to the attributes.
        #
        #        // that relationship is maintained in a series of three tables:
        #
        #        //  ecoResCategory                  ->    ecoResCategoryAttributeLookup
        #        //  ecoResCategoryAttributeLookup   ->    ecoResAttribute
        #
        #        // we join these tables together while we're looping through
        #        // each category and create a second loop inside our
        #        // main "category" loop...
        #
        #        while
        #         select ecoResCategoryAttributeLookup where ecoResCategoryAttributeLookup.Category == ecoResCategory.RecId   // <--- ecoResCategory.RecId comes from our main "category" loop
        #         join ecoResAttribute where ecoResAttribute.RecId == ecoResCategoryAttributeLookup.Attribute
        #        {
        #
        #         // we're looping through our second WHILE statement, which will give us every attribute
        #         // for every category for every category hierarchy. now, in reality, AX allows only
        #         // one procurement category - where attributes are defined - so in daily use we will
        #         // be dealing with multiple category hierarchies, categorys, but only one of them
        #         // (the procurement category) will have attributes.
        #
        #         // example: if the product has two category hierarchies, with two categories within
        #         // each category hierarchy, and one attribute within each category,
        #         // the result would be:
        #
        #         // product      categoryhierarchy 1        category 1          attribute 1
        #         // product      categoryhierarchy 1        category 1          attribute 2
        #         // product      categoryhierarchy 1        category 2          attribute 1
        #         // product      categoryhierarchy 1        category 2          attribute 2
        #         // product      categoryhierarchy 2        category 1
        #         // product      categoryhierarchy 2        category 1
        #         // product      categoryhierarchy 2        category 2
        #         // product      categoryhierarchy 2        category 2
        #
        #         // so, at this point we know everything except the attribute values.
        #         // to get that, we need to check the ecoResAttributeValue table. That
        #         // table has three fields:
        #
        #         // Attribute       -- join to the ecoResAttribute.RecID field
        #         // InstanceValue   -- join to the ecoResProductInstanceValue.RecID field where the ecoResProductInstanceValue.Product field is our source product's RecID
        #         // Value           -- join to the ecoResTextValue.RecID field, but this is not a field value we're going to copy (see below)
        #
        #         select ecoResAttributeValue where ecoResAttributeValue.Attribute == ecoResAttribute.RecId
        #          join ecoResProductInstanceValue where ecoResProductInstanceValue.RecId == ecoResAttributeValue.InstanceValue && ecoResProductInstanceValue.Product == sourceProductRecId
        #          join ecoResTextValue where ecoResTextValue.RecId == ecoResAttributeValue.Value;
        #
        #         // the join to the ecoResTextValue table gives us access to the TextValue
        #         // field in that table. We're going to need that.
        #
        #         // So. now we need to copy the attribute values from our source product to our
        #         // destination product. to do that, we do perform the following operations
        #         // in the order listed:
        #
        #         // 1. write a new record into the EcoResTextValue table, and save the resulting RecID into a variable
        #         // 2. write a new record into the EcoResAttributeValue table using the EcoResTextValue.RecID variable as the Value field
        #
        #         // we have the same system field situation that we had above in the ecoResProductInstanceValue
        #         // table, but fortunately we know how to handle it below.
        #
        #         if (ecoResAttributeValue.Attribute > 0)   // <--- make sure that we have a valid attribute
        #         {
        #          ttsBegin;
        #           select forUpdate ecoResTextValue_ForInsert;
        #            ecoResTextValue_ForInsert.TextValue = ecoResTextValue.TextValue;
        #            new OverwriteSystemFieldsPermission().assert();
        #            ecoResTextValue_ForInsert.overwriteSystemfields(true);
        #            ecoResTextValue_ForInsert.(fieldNum(EcoResTextValue, InstanceRelationType)) = tableName2id("EcoResTextValue");
        #            ecoResTextValue_ForInsert.insert();
        #            ecoResTextValue_ForInsert.overwriteSystemfields(false);
        #            CodeAccessPermission::revertAssert();
        #          ttsCommit;
        #         }
        #
        #         if (ecoResTextValue_ForInsert)
        #         {
        #          ecoResTextValueRecId_AfterInsert = ecoResTextValue_ForInsert.RecId;   // <--- REALLY important value here! The returned RecID from our write to ecoResTextValue
        #
        #          // our last step is to wrap up all these values and
        #          // write them to the ecoResAttributeValue table.
        #
        #          ttsBegin;
        #           select forUpdate ecoResAttributeValue_ForInsert;
        #            ecoResAttributeValue_ForInsert.Attribute     = ecoResAttributeValue.Attribute;          // <--- the RecID of the attribute we're dealing with in the loop
        #            ecoResAttributeValue_ForInsert.InstanceValue = productInstanceRecId_AfterInsert;        // <--- the resulting RecID from writing into ecoResProductInstanceValue
        #            ecoResAttributeValue_ForInsert.Value         = ecoResTextValueRecId_AfterInsert;        // <--- the resulting RecID from writing into ecoResTextValue
        #            ecoResAttributeValue_ForInsert.insert();
        #          ttsCommit;
        #
        #          if (!ecoResAttributeValue_ForInsert)
        #          {
        #           throw error('An exception was raised - could not write attribute data to table ecoResAttributeValue. (ProductAttributesCopy/copyAttributesToProduct)');
        #          }
        #         }
        #         else
        #         {
        #          throw error('An exception was raised - could not write destination product attribute text to table ecoResTextValue. (ProductAttributesCopy/copyAttributesToProduct)');
        #         }
        #
        #        } // while
        #
        #       }
        #       else
        #       {
        #        throw error('An exception was raised - could not write destination product category to table ecoResProductCategory. (ProductAttributesCopy/copyAttributesToProduct)');
        #       }
        #
        #      } // while
        #
        #     }
        #     else
        #     {
        #      throw error('An exception was raised - could not write destination product to table EcoResProductInstanceValue. (ProductAttributesCopy/copyAttributesToProduct)');
        #     }
        #
        #    }
        #    else
        #    {
        #     throw error('An exception was raised - the source and destination products are the same. (ProductAttributesCopy/copyAttributesToProduct)');
        #    }
        #   }
        #   else
        #   {
        #    throw error('An exception was raised - the source or destination product does not exist. (ProductAttributesCopy/copyAttributesToProduct)');
        #   }
        #
        #}
     


        #//© 2013 TURCK, Inc.  All Rights Reserved.
        #/// <summary>
        #///  deletes all attribute values from a product. The attributes themselves remain, but the values are cleared
      
        #static void deleteAttributeValues(RefRecId _productRecID)
        #{
        #
        #    EcoResProductInstanceValue    ecoResProductInstanceValue;
        #
        #    ttsBegin;
        #
        #     while select forUpdate ecoResProductInstanceValue where ecoResProductInstanceValue.Product == _productRecID && _productRecID > 0
        #     {
        #      // ecoResProductInstanceValue is not a source of delete actions, but it extends table
        #      // EcoResInstanceValue which DOES have a cascade delete action on EcoResAttributeValue.
        #
        #      // So, we will delete our product out of table ecoResProductInstanceValue (if it exists),
        #      // and our attribute values will be deleted out of EcoResAttributeValue, too. Further,
        #      // EcoResAttributeValue is the source of a cascade delete on table EcoResValue, which is
        #      // extended by table EcoResTextValue. EcoResTextValue is the source of yet another
        #      // cascade delete action on table EcoResTextValueTranslation. So to sum up, when we
        #      // delete our product from table ecoResProductInstanceValue, it cleans up all attribute
        #      // values out there.
        #
        #       ecoResProductInstanceValue.delete();
        #     }
        #
        #    ttsCommit;
        #
        #}
     


        #//© 2013 TURCK, Inc.  All Rights Reserved.
        #/// <summary>
        #///  deletes the product <-> category relationship from a product. Also delete all attribute values.
        #/// </summary>
      
        #static void deleteCategoriesAndAttributeValues(RefRecId _productRecID)
        #{
        #
        #    EcoResProductCategory    ecoResProductCategory;
        #
        #    //first delete all attribute values so we don't leave any broken data
        #    //hanging out there
        #
        #    ProductAttributesManager::deleteAttributeValues(_productRecID);
        #
        #    ttsBegin;
        #     while select forUpdate ecoResProductCategory where ecoResProductCategory.Product == _productRecID && _productRecID > 0
        #     {
        #      // ecoResProductCategory is not a source of delete actions, which allows us
        #      // to remove categories from products without destroying the
        #      // categories and hierarchies within them. We delete the
        #      // product <-> category relationship.
        #       ecoResProductCategory.delete();
        #     }
        #    ttsCommit;
        #
        #}
   


        #//© 2013 TURCK, Inc.  All Rights Reserved.
        #/// <summary>
        #///  returns the value of the desired item's product attribute
        #/// </summary>
       
        #static AttributeValueText getProductAttributeValue(itemID _itemId, str _attributeName)
        #{
        #
        #   RefRecId                           itemRecID;
        #
        #   EcoResProduct                      product = EcoResProduct::find(InventTable::find(_itemId).Product);
        #
        #   EcoResProductCategory              ecoResProductCategory;
        #   EcoResCategory                     ecoResCategory;
        #   EcoResCategoryAttributeLookup      ecoResCategoryAttributeLookup;
        #
        #   EcoResAttribute                    ecoResAttribute;
        #   EcoResAttributeValue               ecoResAttributeValue;
        #   EcoResProductInstanceValue         ecoResProductInstanceValue;
        #   EcoResTextValue                    ecoResTextValue;
        #
        #   if (product)
        #   {
        #
        #    itemRecID = product.RecId;
        #
        #    while
        #     select ecoResProductCategory where ecoResProductCategory.Product == itemRecID
        #     join ecoResCategory where ecoResCategory.RecId == ecoResProductCategory.Category
        #    {
        #      while
        #       select ecoResCategoryAttributeLookup where ecoResCategoryAttributeLookup.Category == ecoResCategory.RecId
        #       join ecoResAttribute where ecoResAttribute.RecId == ecoResCategoryAttributeLookup.Attribute
        #      {
        #       select ecoResAttributeValue where ecoResAttributeValue.Attribute == ecoResAttribute.RecId
        #        join ecoResProductInstanceValue where ecoResProductInstanceValue.RecId == ecoResAttributeValue.InstanceValue && ecoResProductInstanceValue.Product == itemRecID
        #        join ecoResTextValue where ecoResTextValue.RecId == ecoResAttributeValue.Value;
        #
        #       if (strLwr(strLRTrim(ecoResAttribute.Name)) == strLwr(strLRTrim(_attributeName)))
        #       {
        #        return ecoResTextValue.TextValue;
        #       }
        #
        #      }
        #    }
        #
        #   }
        #   else
        #   {
        #    throw error('An exception was raised - the product does not exist. (ProductAttributesCopy/getProductAttributeValue)');
        #   }
        #
        #   return '';
        #
        #}
     
        #//© 2013 TURCK, Inc.  All Rights Reserved.
        #static void setProductAttributeValue(itemID _itemId, str _attributeName, str 1999 _attributeValue)
        #{
        #
        #   RefRecId                           itemRecID;
        #
        #   EcoResProduct                      product = EcoResProduct::find(InventTable::find(_itemId).Product);
        #
        #   EcoResProductCategory              ecoResProductCategory;
        #   EcoResCategory                     ecoResCategory;
        #   EcoResCategoryAttributeLookup      ecoResCategoryAttributeLookup;
        #
        #   EcoResAttribute                    ecoResAttribute;
        #   EcoResAttributeValue               ecoResAttributeValue;
        #   EcoResProductInstanceValue         ecoResProductInstanceValue;
        #   EcoResTextValue                    ecoResTextValue, ecoResTextValue_ForUpdate, ecoResTextValue_ForValidation;
        #
        #   if (product)
        #   {
        #
        #    itemRecID = product.RecId;
        #
        #    while
        #     select ecoResProductCategory where ecoResProductCategory.Product == itemRecID
        #     join ecoResCategory where ecoResCategory.RecId == ecoResProductCategory.Category
        #    {
        #      while
        #       select ecoResCategoryAttributeLookup where ecoResCategoryAttributeLookup.Category == ecoResCategory.RecId
        #       join ecoResAttribute where ecoResAttribute.RecId == ecoResCategoryAttributeLookup.Attribute
        #      {
        #       select ecoResAttributeValue where ecoResAttributeValue.Attribute == ecoResAttribute.RecId
        #        join ecoResProductInstanceValue where ecoResProductInstanceValue.RecId == ecoResAttributeValue.InstanceValue && ecoResProductInstanceValue.Product == itemRecID
        #        join ecoResTextValue where ecoResTextValue.RecId == ecoResAttributeValue.Value;
        #
        #       if (strLwr(strLRTrim(ecoResAttribute.Name)) == strLwr(strLRTrim(_attributeName)))
        #       {
        #        if (strLwr(strLRTrim(ecoResTextValue.TextValue)) != strLwr(strLRTrim(_attributeValue)))
        #        {
        #
        #         ttsBegin;
        #          while select forUpdate ecoResTextValue_ForUpdate
        #          where ecoResTextValue_ForUpdate.RecId == ecoResTextValue.RecId
        #          {
        #           ecoResTextValue_ForUpdate.TextValue = _attributeValue;
        #           ecoResTextValue_ForUpdate.update();
        #          }
        #         ttsCommit;
        #
        #         select ecoResTextValue_ForValidation where ecoResTextValue_ForValidation.RecId == ecoResTextValue.RecId;
        #
        #         if (ecoResTextValue_ForValidation.TextValue != _attributeValue)
        #          throw error('An exception was raised - could not update the attribute value. (ProductAttributesCopy/setProductAttributeValue)');
        #
        #        }
        #       }
        #
        #      }
        #    }
        #
        #   }
        #   else
        #   {
        #    throw error('An exception was raised - the product does not exist. (ProductAttributesCopy/setProductAttributeValue)');
        #   }
        #
        #}