Saturday, July 18, 2015

Quickly Update Data Through X++ Scripts

, I want to update the Calculation Group on all of my released products – because I forgot to load it through DIEF.

  1. To create the script, just open up the AOT by pressing CTRL+D.
  2. And then press CTRL+SHIFT+P to open up the Projects explorer.
  3. Right-mouse-click on the Projects folder and select the Project option from the New sub-menu to create a new Project.
  4. This will create a new Project folder for you and if you click on it then you will be able to access the Properties window.
  5. Change the name on the Project so that it is a little more descriptive.
  6. Then right-mouse-click on the project and select the Open option to open up the project itself.
  7. When the Project is displayed, right-mouse-click on the header and select the New menu, and then the Job option.
  8. This will open up an X++ scripting pane for you to write your code within.
  9. Now is the dirty part. Create a variable that points to the table you want to update – in this case it’s the InventTable by adding this line of code: InventTable item;
  1. Then create a loop that will step through every record by typing this:

    while
    select forUpdate item
    {
    }
  1. Then add your code to see if there is a record in the table to update:
    if (item)
    {
    }
  1. Finally, add your code to update the record:
    ttsBegin;
    item.BOMCalcGroupId = “DEFAULT”;
    item.update();
    ttscommit;
  1. The full code will look like this.

    static
    void Job11(Args _args)

    {

    InventTable item;


while select forUpdate item
{
if (item)
{
ttsBegin;
item.BOMCalcGroupId = “DEFAULT”;
item.update();
ttscommit;
}
}
}

  1. To run the job, just click on the green play button in the menu bar.

Now all of the records will be updated for you.


That wasn’t as hard as you probably thought was it.

Microsoft Dynamics AX 2012 - Basic Security Concepts

In AX 2012, role based security provides an extensible framework for defining access to the Microsoft Dynamics AX application and data. It changes a little bit from what we know in ax 2009.

In AX 2009, administrators had to, literally, create their own user groups and manually assign users to those groups.  Also, in order give permission to a user group to execute a particular operation, the administrator had to identify objects, such as tables, fields, and menu items that were required for the operation, which was a pain in AX 2009 as identifying these elements was time consuming.

However, in AX 2012, security is role-based, and many security roles are predefined by the application to make the setup portion of it easy for us. Now, it is important to really comprehend this positive change as in role-based security, users are assigned to roles based on their responsibilities in the organization and their participation in business processes.  In fact, AX 2012 introduces duties, which are a group of privileges (See a short definition below). In addition, an administrator no longer has to identify application objects and grant access to those objects. Instead, the administrator grants access to the duties that users in a role perform and that's it.

In addition, in AX 2012 all permissions for all application objects have been grouped into task-based privileges and duties.  This means that an administrator only has grant access to the Maintain sales order duty, which includes all of the permissions that are required to view, create, modify, and delete sales orders.

Another interesting change has been the concept of reusable permissions.  I don’t know if you had the chance to work with multiple companies in AX 2009, but you couldn’t allow user groups could not span multiple companies. As a matter of fact, if the same functional role was required in two companies, an administrator had to create two user groups and so on and so forth. You know where I’m going with this, right?

In terms of record-level-security, this function is still available in AX 2012, but it will be removed in future versions. In ax 2012, the data security framework I used to help secure the data and take security privileges into account. For example, an administrator can grant View access to one subset of Purchase Orders and Edit access to another subset of Purchase Orders.

There are a few definitions about the AX 2012 security terminology I would like to share with you:




A security role represents a behavior pattern that a person in the organization can play. A security role includes one or more duties.


A duty is a responsibility to perform one or more tasks.  A duty includes one or more privileges.

Privileges specify the access that is required to perform a duty.  A privilege includes one or more permissions.

Permissions include the access level to one or more securable objects that are required to perform the function associated with an entry point.

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.