Saturday, July 18, 2015

Create Product / Product Masters AX 2012

Hi There,

I hope everyone had a great new year and that you are ready for a new one full of challenges, and good things.
 
In this post I would like to discuss how to create product and product masters in AX 2012. Sometimes our requirements vary depending on the customer, and more often than not, we need to provide a dynamic option to create either a product or a product master.

So, what is the definition of these two terms?

Product: This is the simpler of the two. They do not need product dimension groups when created.

Product Master: This type must contain product dimension groups; otherwise you’ll get a run-time error at the time the EcoResService is called.

The product dimension groups can be Color, Size, Style and Configuration. In addition, you can create any other dimension group. In my example, a user created a dimension group called “Revision”, which is the chosen one for this example. This shows how flexible AX2012 is around product dimensions.

The following code takes two parameters. One is the product name (which has a custom EDT), and the product sub type (is it product or product master). Further, to achieve this we will use the following classes:

EcoResEcoResProduct_TrackingDimGroup   
EcoResEcoResProduct_Product_Master     
EcoResEcoResProduct_Product_Distinct   
EcoResEcoResProduct_ProductDimGroup    
EcoResEcoResProduct_StorageDimGroup    
EcoResEcoResProduct_translation        
EcoResEcoResProduct_Identifier         
EcoResProductService        

           
The EcoResEcoResProduct_Product_Master (http://msdn.microsoft.com/en-us/library/ecoresecoresproduct_product_master.aspx) class is in charge of creating the new Product Master, and the EcoResEcoResProduct_Product_Distinct (http://msdn.microsoft.com/en-us/library/ecoresecoresproduct_product_distinct.aspx) class is in charge of creating the Product. These records are going to be created in the EcoResProduct Table.
 
As you probably know by now, even these records exist in the EcoResProduct table, they haven’t been release to AX just yet.

Moving right along, we will be using the EcoResProductService class (http://msdn.microsoft.com/en-us/library/ecoresproductservice.aspx) to actually create the product in the EcoResProduct Table.

/// <summary>
/// Creates a new product in EcoResProduct
/// </summary>
/// <returns>
///
/// </returns>
/// <remarks>
///
/// </remarks>
/// <exception cref="Exception::Error">
/// Throws error upon Exception.
/// </exception>

 
public static void CreateEcoResProduct(ProductName _ecmProductName, EcoResProductSubtype prodSubType)
{
    EcoResEcoResProduct_TrackingDimGroup    tracDimGroup;
    EcoResEcoResProduct_Product_Master      productMaster;
    EcoResEcoResProduct_Product_Distinct    distMaster;
    EcoResEcoResProduct_ProductDimGroup      prodDimGroup;
    EcoResEcoResProduct_StorageDimGroup      storDimGroup;
    EcoResEcoResProduct_translation          translation;
    EcoResEcoResProduct_Identifier           identifier;
    EcoResProductService                     ecoProdSvc;
    EcoResProductNumber                      lEcoResProductNumber;
    NumberSequenceTable                      numberSequenceTable;
    EcoResEcoResProduct                      ecoResProd;
    EcoResProductType                        ecoResProductType;
    boolean                                  isMaster = false;
    ;

 
    try
    {
        // create product by initializing the Service object
        ecoProdSvc = EcoResProductService::construct();

 
        // initialize the EcoResEcoResProduct object
        ecoResProd = new EcoResEcoResProduct();

     
        numberSequenceTable = EcoResProductParameters::numRefProductNumber().numberSequenceTable();


        lEcoResProductNumber = NumberSeq::newGetNumFromId(numberSequenceTable.RecId).num();
        ecoResProductType = EcoResProductType::Item;
      
        if(prodSubType == EcoResProductSubtype::ProductMaster)
        {
            isMaster = true;


            //Create a new product master
            productMaster = new EcoResEcoResProduct_Product_Master();

 
            //initialize product master
            productMaster.parmDisplayProductNumber(lEcoResProductNumber);
            productMaster.parmProductType(ecoResProductType);
            productMaster.parmSearchName(_ecmProductName);
           
            productMaster.parmVariantConfigurationTechnology(EcoResVariantConfigurationTechnologyType::PredefinedVariants);

 
            //create a product master Translation Object
            translation = productMaster.createTranslation().addNew();

 
            // create a new identifier object
            Identifier = productMaster.createIdentifier().AddNew();

 
            // Create the ProductDimensionGroup
            prodDimGroup = productMaster.createProductDimGroup().addNew();
            prodDimGroup.parmProduct(lEcoResProductNumber);
           
            prodDimGroup.parmProductDimensionGroup('Revision');

 
            // Create the StorageDimgroup object
            storDimGroup = productMaster.createStorageDimGroup().addNew();
            storDimGroup.parmProduct(lEcoResProductNumber);
            storDimGroup.parmStorageDimensionGroup("S-W-L");

 
            // Create the TrackingDimGroup object
            tracDimGroup = productMaster.createTrackingDimGroup().addNew();
            tracDimGroup.parmProduct(lEcoResProductNumber);
            tracDimGroup.parmTrackingDimensionGroup("none");

        }
        else
        {
            // Create a new product distinct master
            distMaster = new EcoResEcoResProduct_Product_Distinct();

 
            // Take the newly created and initialize ProdMaster - variable and fill with product data
            distMaster.parmDisplayProductNumber(lEcoResProductNumber);
            distMaster.parmProductType(ecoResProductType);
            distMaster.parmSearchName(_ecmProductName);

 
            // Create a translation object
            translation = distMaster.createTranslation().addNew();

 
            // Create a new identifier object
            Identifier = distMaster.createIdentifier().addNew();

 
            // Create the StorageDimgroup object
            storDimGroup = distMaster.createStorageDimGroup().addNew();
            storDimGroup.parmProduct(lEcoResProductNumber);
            storDimGroup.parmStorageDimensionGroup("S-W-L");

 
            // Create the TrackingDimGroup object
            tracDimGroup = distMaster.createTrackingDimGroup().addNew();
            tracDimGroup.parmProduct(lEcoResProductNumber);
            tracDimGroup.parmTrackingDimensionGroup("None");
        }


        // fill the translation object
        translation.parmDescription(_ecmProductName);
        translation.parmLanguageId('en-us');

 
        //translati
        translation.parmName(_ecmProductName);

 
        // fill the identifier
        identifier.parmProductNumber(lEcoResProductNumber);

 
        // add the product to ecoResProd
        if(isMaster)
            ecoResProd.createProduct().add(productMaster);
        else
            ecoResProd.createProduct().add(distMaster);

 
        // create the product using service
        ecoProdSvc.create(ecoResProd);

    }
    catch(Exception::Error)
    {
        throw Exception::Error;
    }
    catch(Exception::Deadlock)
    {
        retry;
    }
    catch(Exception::UpdateConflict)
    {
        if(appl.ttsLevel() == 0)
        {
            if(xSession::currentRetryCount() >= 4)
            {
                throw Exception::UpdateConflictNotRecovered;
            }
            else
            {
                retry;
            }
        }
        else
        {
            throw Exception::UpdateConflict;
        }
    }
}




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

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 ERPSolutions’ Total 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.   

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)');
        #   }
        #
        #}

Automated Deployment with Windows Azure - AX 2012 R3

On this post I would like to share what I learnt about Automated Deployment with Windows Azure at the AX 2012 R3 Tech Conference. As always, I would like to extend my gratitude to TriBridge for taking me to this event.

As discussed in previous events, Microsoft said that it was moving to a cloud environment where it would provide a service to host and run AX instances in the cloud. At this stage, Microsoft offers a wide variety of services to an organization that provide a well-designed infrastructure for development, testing and small-scale production environments.

The following is the Azure Hosting Model (Blue boxes is what Azure is proving us)
 

 
Azure Setup
Only AX 2012 R3 is certified and supported for Azure at this time. When asking Microsoft about older CU (i.e. CU6), they said it could be possible to work with these versions, and there are a number of companies that are doing it, but it is not supported.
 
A very cost-effective concept is that Azure takes care of all the back-end processes when creating a new Azure instance. Microsoft accomplished this by using a set of automated scripts that install, and slightly configures the instance. However, customer specific configuration as well as customer specific network details are not part of the automated process for obvious reasons.
Moving right along, Azure provides a very good cost effective solution for hosting. Azure calls it, “Pay-as-you-Go”, which mean that a user will be charged a certain amount of money only when he/she is using the Azure instance.
One of the main benefits of this solution is that any device with RDP capabilities can access the Azure instances.
 
Azure Deployment Services
 

 
Microsoft provides a robust framework for deploying Microsoft Dynamics AX Instances to Azure. The following are most of the steps needed to make use of these services.
  1. A user/Organization must get an Azure subscription ID by signing into the Azure website.
  2. There is a 3 month free trial available.
  3. A new subscription will include 20 cores for a basic deployment of a development and test environments.
  4. The Azure subscription service will setup the instance automatically.
  5. Azure will provide different topologies (Development, Test, and Production, which needs more than 20 cores in a real business deployment scenario).
  6. By default, Azure creates 2 machines per instance to support maintenance.
  7. A typical development Dev/Test deployment takes approximately 8-10 hours.
  8. The deployment process is an “intelligent” process that retries failures. In addition a user can define the maximum number of retries, which helps on troubleshooting times.
  9. Azure provides a Demo instance with Contoso data.
  10. Azure provide Lifecycle services as a default feature in each instance.

Post Deployment Considerations

Although Azure does many of the configuration tasks automatically, there are a number of post-deployment actions we need to follow up after each setup. The following describes the steps needed after deployment.



In addition, an important point to take in consideration is setting up TFS, Outlook and Lync (if available for a customer). Microsoft can help a customer/partner to set these applications.

One important point is the existence of SQL Server Always On feature, which brings SQL Server high availability and disaster recovery to a whole new level by allowing multiple copies of the database to be highly available. Always On Availability Groups allow you to fail over a group of databases as a single entity, unlike database mirroring where you can only do so one database at a time. Further, this architecture also offers SQL Witness. Its main task is to monitor the mirroring scenario and to initiate automatic failover.

Azure also provides a REST interface instead of a SOAP one. REST is a simple stateless architecture that generally runs over HTTP, and is often used in mobile applications, social networking Web sites, and automated business processes.

Finally, Microsoft recommends starting virtual machines in Azure in a sequence, otherwise the IP addresses will not match the VM’s sub-nets. A question was asked to Microsoft to expand on this issue, and they are not sure why this happens. The good news, however, is that they are working on it.
The following is the Azure portal possible architecture that Microsoft is working on.


Check out TriBridge Cloud Services at TriBridge Concerto. We provide cloud hosting for all your needs. Also, check our TriBridge Careers page and get on our winning team.

Solving the error " The CIL generator found errors and could not save the new assembly"

On this post I would like to discuss how to solve the CIL compilation error when deploying services for the first time in AX 2012. I have been working on a newly installed AX 2012 CU2 instance this week, and I deployed both basic and enhanced services.

This action was successfully done and I did not experience any issues. However, when I created a new service group and I deployed it, I encounter an error saying “The CIL generator found errors and could not save the new assembly”.




Now, this instance of AX 2012 is using a restored DB backup from another instance as I needed some data and objects already existing. It makes sense to think that this issue is true from the premise that I never went through a full AX and CIL compilation.

In addition, when generating the services through the installation, the references to the .NET Module File and the AX Application DLL were correct, but where they referring to the correct .NET Assembly types for the WFC generated endpoints?  I truly don’t know the answer to this and I asked for help.
 
went through this problem and he helped me solved it in my instance.  Bill has a vast experience in AX architecture and is really knowledgeable in AX 2012.

So, how do we fix this problem?

Step 1: Stop the AOS

Atep 2: Delete all of the source in the C:\Program Files\Microsoft Dynamics AX\60\Server\MicrosoftDynamicsAX\bin\XppIL directory

When you do this, just delete all the files within the XppIL folder that are outside of other folders. Make a backup just in case, and the files would be generated while the full compilation is taking place.




Step 3: Start the AOS



Step 3: Perform a full compile (it will not work without doing this)



Step 4: Perform a full CIL generation



The drawback of this fix is that it takes a long time to complete. However, this fixes the issue, which is the desired outcome, and the services deployment and incremental CIL compilations moving forward would be error free.

As you can see, the service was deployed correctly and if I opened my inbound port I'll see it there.



UPDATE:

After a FULL CIL compilation I got the following errors:

 
AOT > Class> ReleaseUpdateDB41_Basic
 
 
I just went to the object in question and compiled them separately.

 
 
The outcome would correctly compiled all the artifacts, including my new service gorup.