web
You’re offline. This is a read only version of the page.
close
Skip to main content
Community site session details

Community site session details

Session Id :

Avoid Hidden Mapping

BackToTheCrm Profile Picture BackToTheCrm
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 :
MessageSource entityTarget entity
GenerateQuoteFromOpportunityOpportunityProductQuoteDetail
GenerateSalesOrderFromOpportunity OpportunityProductSalesOrderDetail
GenerateInvoiceFromOpportunityOpportunityProductInvoiceDetail
ConvertQuoteToSalesOrderQuoteDetailSalesOrderDetail
ConvertSalesOrderToInvoiceSalesOrderDetailInvoiceDetail

This was originally posted here.

Comments

*This post is locked for comments