Monday, August 31, 2015

Avoid Hidden Mapping

Hello everyone !
I'm back from my holidays :)

For today's post I'll talk about hidden mappings. 
For those which doesn't know what it is, I explain :
When you convert a quote to an order the quote products are copied to the order as order products. If you have customized the Quote Product and the Order Product entity adding a new field, this new field won't be filled during the conversion to an order.
In order to copy the field there is a solution commonly named hidden mapping, which allow you to set how custom fields will be copied from a quote product to and order product. It's the same process for the other convert actions : Opportunity to Quote, Quote to Order, Order to Invoice.
I will not explain the technique because it is not supported by Microsoft and I don't like to share unsupported ways to customize the CRM ;)
BUT ! I will show you another way to achieve this.

In the examples below, I'll take the conversion of a Quote to an Order action but the process will be sensibly the same for the other Convert actions.
  1. First of all, create each field for the 2 entities with the same data type.
  2. Then, create a plugin on the Order Detail triggered on the CreateMessage.
    /// <summary>
    /// Initializes a new instance of the <see cref="SalesOrderDetailPlugin"/> class.
    /// </summary>
    public SalesOrderDetailPlugin()
        : base(typeof(SalesOrderDetailPlugin))
    {
        base.RegisteredEvents.Add(new Tuple<int, string, string, Action<LocalPluginContext>>(20, "Create", "salesorderdetail", new Action<LocalPluginContext>(ExecuteSalesOrderDetail)));
    
        // Note : you can register for more events here if this plugin is not specific to an individual entity and message combination.
        // You may also need to update your RegisterFile.crmregister plug-in registration file to reflect any change.
    }
    
  3. Check for the Parent Context Message to be ConvertQuoteToSalesOrder and replace the copy the data from the Quote Product
    protected void ExecuteSalesOrderDetail(LocalPluginContext localContext)
    {
        if (localContext == null)
        {
            throw new ArgumentNullException("localContext");
        }
    
        var context = localContext.PluginExecutionContext;
        Service = localContext.OrganizationService;
    
        // Check the parent message
        if (context.ParentContext != null && context.ParentContext.MessageName == "ConvertQuoteToSalesOrder")
        {
            // Get the source quote ID
            var quoteId = (Guid)context.ParentContext.InputParameters["QuoteId"];
    
            // Get the Sales Order Detail which will be created
            var currentSalesOrderDetail = context.InputParameters["Target"] as Entity;
            if (currentSalesOrderDetail == null) return;
    
            var currentLineItemNumber = currentSalesOrderDetail.GetAttributeValue<int>("lineitemnumber");
    
            // Retrieve the data from the quote detail
            var relativeQuoteDetail = getQuoteDetail(quoteId, currentLineItemNumber);
            if (relativeQuoteDetail == null) return;
    
            // Copy the data from the quote. Notice the data type.
            currentSalesOrderDetail["new_field1"] = relativeQuoteDetail.GetAttributeValue<string>("new_field1");
            currentSalesOrderDetail["new_field2"] = relativeQuoteDetail.GetAttributeValue<DateTime?>("new_field2");
        }
    }
    
The complete plugin will look like this :
// <copyright file="SalesOrderDetail.cs" company="">
// Copyright (c) 2015 All Rights Reserved
// </copyright>
// <author></author>
// <date>11/06/2015 09:43:20</date>
// <summary>Implements the SalesOrderDetail Plugin.</summary>
// <auto-generated>
//     This code was generated by a tool.
//     Runtime Version:4.0.30319.1
// </auto-generated>

using System.Collections.Generic;
using BackToTheCrm.Entities.Extension;

namespace BackToTheCrm.Plugins
{
    using System;
    using System.ServiceModel;
    using Microsoft.Xrm.Sdk;
    using System.Linq;

    /// <summary>
    /// SalesOrderDetail Plugin.
    /// </summary>    
    public class SalesOrderDetailPlugin : Plugin
    {
        #region attributes

        IOrganizationService Service = null;

        #endregion

    /// <summary>
    /// Initializes a new instance of the <see cref="SalesOrderDetailPlugin"/> class.
    /// </summary>
    public SalesOrderDetailPlugin()
        : base(typeof(SalesOrderDetailPlugin))
    {
        base.RegisteredEvents.Add(new Tuple<int, string, string, Action<LocalPluginContext>>(20, "Create", "salesorderdetail", new Action<LocalPluginContext>(ExecuteSalesOrderDetail)));

        // Note : you can register for more events here if this plugin is not specific to an individual entity and message combination.
        // You may also need to update your RegisterFile.crmregister plug-in registration file to reflect any change.
    }

        /// <summary>
        /// Executes the plug-in.
        /// </summary>
        /// <param name="localContext">The <see cref="LocalPluginContext"/> which contains the
        /// <see cref="IPluginExecutionContext"/>,
        /// <see cref="IOrganizationService"/>
        /// and <see cref="ITracingService"/>
        /// </param>
        /// <remarks>
        /// For improved performance, Microsoft Dynamics CRM caches plug-in instances.
        /// The plug-in's Execute method should be written to be stateless as the constructor
        /// is not called for every invocation of the plug-in. Also, multiple system threads
        /// could execute the plug-in at the same time. All per invocation state information
        /// is stored in the context. This means that you should not use global variables in plug-ins.
        /// </remarks>
        protected void ExecuteSalesOrderDetail(LocalPluginContext localContext)
        {
            if (localContext == null)
            {
                throw new ArgumentNullException("localContext");
            }

            var context = localContext.PluginExecutionContext;
            Service = localContext.OrganizationService;

            // Check the parent message
            if (context.ParentContext != null && context.ParentContext.MessageName == "ConvertQuoteToSalesOrder")
            {
                // Get the source quote ID
                var quoteId = (Guid)context.ParentContext.InputParameters["QuoteId"];

                // Get the Sales Order Detail which will be created
                var currentSalesOrderDetail = context.InputParameters["Target"] as Entity;
                if (currentSalesOrderDetail == null) return;

                var currentLineItemNumber = currentSalesOrderDetail.GetAttributeValue<int>("lineitemnumber");

                // Retrieve the data from the quote detail
                var relativeQuoteDetail = getQuoteDetail(quoteId, currentLineItemNumber);
                if (relativeQuoteDetail == null) return;

                // Copy the data from the quote. Notice the data type.
                currentSalesOrderDetail["new_field1"] = relativeQuoteDetail.GetAttributeValue<string>("new_field1");
                currentSalesOrderDetail["new_field2"] = relativeQuoteDetail.GetAttributeValue<DateTime?>("new_field2");
            }
        }

        /// <summary>
        /// Get the data of the quote product
        /// </summary>
        /// <param name="quoteId">ID of the parent Quote</param>
        /// <param name="currentLineItemNumber">Line number</param>
        /// <returns></returns>
        private Entity getQuoteDetail(Guid quoteId, int currentLineItemNumber)
        {
            string fetchXml =
                        string.Format(@"<fetch version='1.0' output-format='xml-platform' mapping='logical' distinct='false'>
                          <entity name='quotedetail'>
                            <attribute name='productid' />
                            <attribute name='new_field1' />
                            <attribute name='new_field2' />
                            <filter type='and'>
                              <condition attribute='quoteid' operator='eq' value='{0}' />
                              <condition attribute='lineitemnumber' operator='eq' value='{1}' />
                            </filter>
                          </entity>
                        </fetch>",
                        quoteId.ToString(),
                        currentLineItem
                    );
            List<Entity> results = Service.RetrieveMultiple(new FetchExpression(fetchXml));

            Entity quoteDetailToReturn = null;
            if (results != null)
                quoteDetailToReturn = results.FirstOrDefault();

            return quoteDetailToReturn;
        }
    }
}
Here is a table synthesizing the modification between the different actions :
Message Source entity Target entity
GenerateQuoteFromOpportunity OpportunityProduct QuoteDetail
GenerateSalesOrderFromOpportunity OpportunityProduct SalesOrderDetail
GenerateInvoiceFromOpportunity OpportunityProduct InvoiceDetail
ConvertQuoteToSalesOrder QuoteDetail SalesOrderDetail
ConvertSalesOrderToInvoice SalesOrderDetail InvoiceDetail