Skip to content
Branch: master
Find file History

README.md

Implementing a Custom Pricing Engine within Dynamics 365 Customer Engagement

Abstract

For years, it has been possible to override the default behaviour for sales entity calculations within Dynamics CRM, and even, today this functionality is still present in Dynamics 365 for Customer Engagement (D365CE) and - arguably - more relevant than ever. For organisations who are looking to factor in custom fields, highly bespoke calculations or other non-standard business logic when producing sales quotations, orders or invoices, developing a plug-in for the CalculatePrice message remains the supported and cleanest way to achieve this requirement. This session is aimed to introduce the functionality that a custom pricing plug-in can bring to the table, and attendees should walk away from the session understanding how to develop a custom pricing solution from start to finish. Although existing C# developers with CRM/D365CE experience will gain the most from this session, no previous knowledge is assumed, and this session will be an excellent starting point for those looking to get into D365CE development within a specific and highly business relevant area.

What's Here

  • Implementing a Custom Pricing Engine within Dynamics 365 Customer Engagement.pdf - PDF of the slide decks from the session.
  • CustomPricing_Before_1_0_0_0.zip - Unmanaged solution containing all functional components needed to work through the sample.
  • CustomPricing_Before_1_0_0_0_managed.zip - Managed solution containing all functional components needed to work through the sample.
  • CustomPricing_After_1_0_0_1.zip - Unmanaged solution containing the finished sample.
  • CustomPricing_After_1_0_0_1_managed.zip - Managed solution containing the finished sample.
  • D365CE.CustomPricingTalk - Visual Studio 2019 solution, containing the starter and complete custom pricing plug-in solution demonstrated during the session.
  • FreightAmount.csv - Data file for import into D365CE, containing a list of Freight Amount values and cities, used as part of the example solution.
  • ExampleCode.vs - Includes example code snippets and methods used for the sample.

Using the Samples

Overview

The goal of the sample is to provide a complete, end-to-end solution that achieves the following objectives:

  • Allows for Product Line item discounts to be applied, pre tax, by expressing a percentage at both Sales document & line item level.
  • Calculates the freight amount for each product line item, based on the location of the parent sales document record.
  • Displays an error message to a user if attempting to sell a product line item below cost price.

The requirements are achieved using a mixture of functional customisation and, primarily, a C# plug-in assembly, to perform all pricing calculations at runtime.

Pre-Requisites

To work with this sample, you must have:

  • A D365CE Online tenant (v9.x), ideally with sample data installed and at least 1 active price list.
  • Visual Studio 2015/2017/2019, with .NET Framework v4.7.1
  • A basic familiarity in working with D365CE solutions, instance management and data import.

Instructions

  1. Within D365CE, navigate to the Systems Settings area within the classic interface and open the Sales tab. Verify that the Set pricing calculation preference setting is set to No. With this option enabled, any custom code uploaded will not execute.
  2. Deploy a copy of the unmanaged/managed CustomPricing_Before solution to your D365CE environment. After installation, you should be able to access a new app called Custom Pricing
  3. Import the FreighAmount.csv data file into the Freight Amount custom entity, using instructions outlined in this article
  4. Clone the repository and open the D365CE.CustomPricingTalk.Start solution. Build the project and verify that all NuGet dependencies are downloaded successfully.
  5. To deploy assemblies to Dynamics 365 Customer Engagement, they must be signed using a Strong Name Key (.snk) file. Follow the instructions in this article. A password is recommended, but not required, for this.
  6. With the PostOpCalculatePriceCustomPricingTalk.cs file open, remove lines 30-82 of the code, which should represent the following:
            // The InputParameters collection contains all the data passed in the message request.            
            if (context.InputParameters.Contains("Target")
                && context.InputParameters["Target"] is EntityReference)
            {
                // Obtain the target entity from the input parmameters.
                EntityReference entity = (EntityReference)context.InputParameters["Target"];

                // Verify that the target entity represents an appropriate entity.                
                if (CheckIfNotValidEntity(entity))
                    return;

                try
                {
                    //Add shared variable, used earlier to check for infinite loops
                    context.SharedVariables.Add("CustomPrice", true);
                    context.ParentContext.SharedVariables.Add("CustomPrice", true);

                    //Get a reference to the organization service - used later
                    IOrganizationServiceFactory serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
                    IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId);

                    //TODO: Insert custom pricing logic here
                    //As part of this, you can either: 
                    //Apply the same custom pricing logic to all sales entities (as this example will do)
                    //Use a switch statement to process custom logic for each entity individually e.g. for Opportunity:

                    //switch (entity.LogicalName)
                    //{
                    //case "opportunity":
                    //    DoSomethingHere();
                    //    return;
                    //}

                    //List of available entities as part of this:
                    //opportunity, opportunityproduct, quote, quotedetail, salesorder, salesorderdetail, invoice, invoicedetail

                    //For further details, please refer to the following articles:
                    //https://docs.microsoft.com/en-us/dynamics365/customer-engagement/developer/use-custom-pricing-products
                    //https://docs.microsoft.com/en-us/dynamics365/customer-engagement/developer/sample-calculate-price-plugin
                }

                catch (FaultException<OrganizationServiceFault> ex)
                {
                    tracing.Trace("CalculatePrice: {0}", ex.ToString());
                    throw new InvalidPluginExecutionException("An error occurred in the Calculate Price plug-in.", ex);
                }

                catch (Exception ex)
                {
                    tracing.Trace("CalculatePrice: {0}", ex.ToString());
                    throw;
                }
            }
  1. The sample relies on several helper methods to work successfully. On line 125, hit the Return key twice and then copy/paste in the following C# methods:
private static string GetProductEntityName(string entity)
{
	string pe = "";
		
	switch (entity)
	{
		//Parent -> Line Item
        
		case "opportunity":
			pe = "opportunityproduct";
            return pe;
        case "quote":
            pe = "quotedetail";
            return pe;
        case "salesorder":
            pe = "salesorderdetail";
            return pe;
        case "invoice":
            pe = "invoicedetail";
            return pe;

        //Line Item -> Parent

        case "opportunityproduct":
            pe = "opportunity";
            return pe;
        case "quotedetail":
            pe = "quote";
            return pe;
        case "salesorderdetail":
            pe = "salesorder";
            return pe;
        case "invoicedetail":
            pe = "invoice";
            return pe;

        default:
            pe = "";
            return pe;
    }
}

private static string GetProductEntityIDName(string entity)
{
	string pe = "";

	switch (entity)
    {
		case "opportunityproduct":
			pe = "opportunityid";
            return pe;
        case "quotedetail":
            pe = "quoteid";
            return pe;
        case "salesorderdetail":
            pe = "salesorderid";
			return pe;
        case "invoicedetail":
            pe = "invoiceid";
            return pe;
        default:
            pe = "";
            return pe;
    }
}

private static bool CheckLineItemCostPrice(Entity lineItem, IOrganizationService service, ITracingService tracing)
{
    bool underCostPrice = false;

	//Retrieve parent sales document record and its associated Price List ID value
	//Use helper methods to retrieve the entity and lookup names we need and obtain ID for this record.
	string peName = GetProductEntityName(lineItem.LogicalName);
	string peIDName = GetProductEntityIDName(lineItem.LogicalName);
	EntityReference peID = lineItem.GetAttributeValue<EntityReference>(peIDName);
	Entity psd = new Entity();
	psd = service.Retrieve(peName, peID.Id, new ColumnSet("pricelevelid"));

	//Get the Price List ID value - if NULL, then we throw an error, as this should be there

	EntityReference pl = psd.GetAttributeValue<EntityReference>("pricelevelid");

	if (pl != null)
	{
		//Query the Price List Item entity, using the Price List ID and Product ID field from the line item.
		//Have to use QueryExpression here, as we are filtering on multiple fields.

		QueryExpression qe = new QueryExpression("productpricelevel");
		qe.ColumnSet.AddColumn("amount");

		ConditionExpression condition1 = new ConditionExpression("pricelevelid", ConditionOperator.Equal, pl.Id);
		ConditionExpression condition2 = new ConditionExpression("productid", ConditionOperator.Equal, lineItem.GetAttributeValue<EntityReference>("productid").Id);

		FilterExpression filter1 = new FilterExpression();
		filter1.Conditions.Add(condition1);

		FilterExpression filter2 = new FilterExpression();
		filter2.Conditions.Add(condition2);

		qe.Criteria.AddFilter(filter1);
		qe.Criteria.AddFilter(filter2);

		qe.PageInfo.ReturnTotalRecordCount = true;

		EntityCollection pliEntity = service.RetrieveMultiple(qe);

		//Get the Amount value from the Price List Item record
		//Always grab the top record only - only 1 record should ever be present, due to application limits.

		if (pliEntity.TotalRecordCount == 1)
		{
			//Get the appropriate values from the line item and price list item record and perform the comparison.
			Money a = pliEntity[0].GetAttributeValue<Money>("amount");
			Money liUP = lineItem.GetAttributeValue<Money>("priceperunit");
			tracing.Trace("Price List Amount = " + a.Value.ToString() + ", Line Item Price Per Unit = " + liUP.Value.ToString());

			if (liUP.Value < a.Value)
			{
				tracing.Trace("Product is being sold under cost!");
				underCostPrice = true;
			}
			else
			{
				tracing.Trace("Product is being sold at or over cost!");
			}
		}

		//Throw an error if nothing is there - having come this far, having nothing here indicates something has gone wrong.

		else
		{
			throw new InvalidPluginExecutionException("A problem occurred when retrieving an original amount for an existing line item record.");

		}
	}

	else
	{
		throw new InvalidPluginExecutionException("Could not locate a price list ID when attempting to calculate whether this line item is being sold under cost.");
	}

	return underCostPrice;
}

private static bool CheckIfPricingLocked(Entity entity, IOrganizationService service, ITracingService tracing)
{
	tracing.Trace("Determining whether prices are locked or not...");
	bool pricingLocked = false;
	if (entity.LogicalName == "salesorderdetail" || entity.LogicalName == "invoicedetail")
	{
		tracing.Trace(string.Concat("Entity is ", entity.LogicalName, ", checking whether prices are locked..."));

		if (entity.LogicalName == "salesorderdetail")
		{
			Entity e2 = new Entity();
			e2 = service.Retrieve("salesorder", entity.GetAttributeValue<EntityReference>("salesorderid").Id, new ColumnSet("ispricelocked"));
			if (e2.GetAttributeValue<Boolean>("ispricelocked") == true)
			{
				tracing.Trace(string.Concat("salesorder record with ID ", e2.Id.ToString(), " has its pricing locked."));
				pricingLocked = true;
			}
			else
			{
				tracing.Trace(string.Concat("salesorder record with ID ", e2.Id.ToString(), " does NOT have its pricing locked."));
			}
		}
		else if (entity.LogicalName == "invoicedetail")
		{
			Entity e2 = new Entity();
			e2 = service.Retrieve("invoice", entity.GetAttributeValue<EntityReference>("invoiceid").Id, new ColumnSet("ispricelocked"));
			if (e2.GetAttributeValue<Boolean>("ispricelocked") == true)
			{
				tracing.Trace(string.Concat("invoice record with ID ", e2.Id.ToString(), " has its pricing locked."));
				pricingLocked = true;
			}
			else
			{
				tracing.Trace(string.Concat("invoice record with ID ", e2.Id.ToString(), " does NOT have its pricing locked."));
			}
		}
	}
	else
	{
		tracing.Trace("Entity is opportunityproduct or quotedetail, pricing will not be locked.");
	}
	return pricingLocked;
}
  1. An additional method is required to perform calculation of discount amounts for each product line item. Immediately after the last curly brace from the previous step, hit the Return key twice and then paste in the following code:
private static Money CalculateLineItemDiscount(Entity lineItem, IOrganizationService service, ITracingService tracing)
{
	tracing.Trace("Calculating discount for line item record...");

	//Initialise the values needed
	Money da = new Money(0);
	int? dp = lineItem.GetAttributeValue<int?>("jjg_discountpercent");
	Money ba = lineItem.GetAttributeValue<Money>("baseamount");

	//Determine if a discount percentage exists on the Line item - have to make integer nullable to check this correctly
	if (dp != null && ba != null)
	{
		//If so, then apply the decrease accordingly
		//Have to convert Discount Percentage to a decimal, so it can be used in calculations.
		decimal dpDec = new decimal((int)dp);
		//Then, we can obtain the discounted Base Amount value
		da = new Money(ba.Value * dpDec / 100);
		tracing.Trace("Discount Amount = " + da.Value.ToString());
	}
	//Otherwise, we need to check the parent sales entity instead and use whatever is listed there
	else if (dp == null && ba != null)
	{
		//Use helper methods to retrieve the entity and lookup names we need and obtain ID for this record.
		string peName = GetProductEntityName(lineItem.LogicalName);
		string peIDName = GetProductEntityIDName(lineItem.LogicalName);
		EntityReference peID = lineItem.GetAttributeValue<EntityReference>(peIDName);
		//Do a NULL check, if NULL, then we throw an error - as we want to prevent orphan line item records.
		if (peID != null)
		{
			Entity pe = service.Retrieve(peName, peID.Id, new ColumnSet("jjg_discountpercent"));
			dp = pe.GetAttributeValue<int?>("jjg_discountpercent");
			if (dp != null)
			{
				//Once retrieved, we then apply our calculation.
				//Have to convert Discount Percentage to a decimal, so it can be used in calculations.
				decimal dpDec = new decimal((int)dp);
				da = new Money(ba.Value * dpDec / 100);
				tracing.Trace("Discount Amount = " + da.Value.ToString());
			}
			else
			{
				tracing.Trace("WARNING: Could not retrieve a discount percentage value from line item's parent record. Continuing...");
			}
		}
		else
		{
			throw new InvalidPluginExecutionException("Could not calculate line item record as it is not associated to a parent sales record. Please associate a parent record to this line item and try saving again.");
		}
	}
	//If all else fails, we just return a 0
	else
	{
		tracing.Trace("WARNING: Could not retrieve a discount percentage value from line item record. Continuing...");
	}
	return da;
}
  1. Likewise, a method is also needed to perform any freight amount calculations required. Immediately after the last curly brace from the previous step, hit the Return key twice and then paste in the following code:
private static Money CalculateLineItemFreight(Entity lineItem, IOrganizationService service, ITracingService tracing)
{
	tracing.Trace("Calculating freight amount for line item record...");

	//Initialise the values needed
	//25 is the default amount we want to return if we cannot get the correct value
	Money fa = new Money(25);

	//Retrieve parent sales document record and its associated City value
	//Use helper methods to retrieve the entity and lookup names we need and obtain ID for this record.
	string peName = GetProductEntityName(lineItem.LogicalName);
	string peIDName = GetProductEntityIDName(lineItem.LogicalName);
	EntityReference peID = lineItem.GetAttributeValue<EntityReference>(peIDName);
	Entity psd = new Entity();
	psd = service.Retrieve(peName, peID.Id, new ColumnSet("shipto_city"));

	//Get the city value - if NULL, then we default to Freight amount of £25

	string city = psd.GetAttributeValue<string>("shipto_city");

	if (city != null)
	{
		//Query the Freight Amount entity, using the City name field.
		//Can use QueryByAttribute here, due to simple nature of the query

		QueryByAttribute qba = new QueryByAttribute("jjg_freightamount");
		qba.ColumnSet = new ColumnSet("jjg_freightamountvalue");
		qba.Attributes.AddRange("jjg_city");
		qba.Values.AddRange(city);

		EntityCollection faEntity = service.RetrieveMultiple(qba);

		//Get the Freight Amount value
		//Always grab the top record only - the instance in question should have duplicate detection rules in place, to prevent potential conflicts

		if (faEntity.TotalRecordCount == 1)
		{
			fa = faEntity[0].GetAttributeValue<Money>("jjg_freightamountvalue");
		}

		else
		{
			tracing.Trace("Could not obtain a Freight Amount Value, defaulting to £25");
		}
	}
	else
	{
		tracing.Trace("No Ship To City value supplied for " + psd.LogicalName + " record, defaulting to £25");
	}

	return fa;
}
  1. Finally, an additional 3 methods are required to perform all appropriate pricing calculations for each applicable entity. Immediately after the last curly brace from the previous step, hit the Return key twice and then paste in the following code:
private static void CalculateLineItem(EntityReference lineItem, IOrganizationService service, ITracingService tracing)
{
	//Retrieve all required fields, including custom fields, from the Product record
	//For this example, we return all fields; this is NOT recommended for Production environments
	tracing.Trace("Retrieving " + lineItem.LogicalName + " record...");
	Entity e1 = new Entity();
	e1 = service.Retrieve(lineItem.LogicalName, lineItem.Id, new ColumnSet(true));
	tracing.Trace("Retrieved " + lineItem.LogicalName + " record with ID " + e1.Id.ToString());

	//If Order Line or Invoice Line, we need to check the related Order/Invoice to determine whether pricing is locked.
	//If locked, then no calculation takes place, to prevent errors.

	bool pricingLocked = CheckIfPricingLocked(e1, service, tracing);
	if (pricingLocked == false)
	{

		//If Existing Product, then we need to check to ensure it is not being sold underneath the value specified in the price list

		bool isWriteIn = e1.GetAttributeValue<bool>("isproductoverridden");

		if (isWriteIn == false)
		{
			tracing.Trace("Existing product record detected, checking Price List cost price value...");
			bool isUnderCostPrice = CheckLineItemCostPrice(e1, service, tracing);
			if (isUnderCostPrice == true)
			{
				throw new InvalidPluginExecutionException("You are attempting to sell this product record below its listed cost price value. Please update the record to ensure that it is equal or higher than its price list value.");
			}
		}

		//Obtain the correct Discount and Freight Amounts for the product record.
		//As Opportunity entity does not have Ship To Address values, we default to £25 instead.

		Money da = CalculateLineItemDiscount(e1, service, tracing);
		Money fa = new Money(25);

		if(e1.LogicalName != "opportunityproduct")
		{
			fa = CalculateLineItemFreight(e1, service, tracing);
		}

		//Set all required amounts on the line item record.
		//First, obtain the values needed from the product record.

		decimal ppu = e1.GetAttributeValue<Money>("priceperunit")?.Value ?? 0;
		decimal q = e1.GetAttributeValue<decimal>("quantity");
		decimal t = e1.GetAttributeValue<Money>("tax")?.Value ?? 0;

		//The, it's time to calculate!

		//Amount = Price Per Unit * Quantity
		decimal a = ppu * q;
		e1["baseamount"] = new Money(a);
		tracing.Trace("Amount = " + a.ToString());
		//Manual Discount
		e1["manualdiscountamount"] = da;
		tracing.Trace("Discount Amount = " + da.ToString());
		//Freight Amount
		e1["jjg_freightamount"] = fa;
		tracing.Trace("Freight Amount = " + fa.ToString());
		//Extended Amount = (Amount - Manual Discount) + Freight Amount + Tax
		decimal ea = (a - da.Value) + fa.Value + t;
		e1["extendedamount"] = new Money(ea);
		tracing.Trace("Extended Amount = " + ea.ToString());

		//Actually update the record

		service.Update(e1);
		tracing.Trace(e1.LogicalName + " updated successfully!");

	}

}

private static void CalculateLineItem(Entity lineItem, IOrganizationService service, ITracingService tracing)
{
	//Retrieve all required fields, including custom fields, from the Product record
	//For this example, we return all fields; this is NOT recommended for Production environments
	tracing.Trace("Retrieving " + lineItem.LogicalName + " record...");
	Entity e1 = new Entity();
	e1 = service.Retrieve(lineItem.LogicalName, lineItem.Id, new ColumnSet(true));
	tracing.Trace("Retrieved " + lineItem.LogicalName + " record with ID " + e1.Id.ToString());

	//If Order Line or Invoice Line, we need to check the related Order/Invoice to determine whether pricing is locked.
	//If locked, then no calculation takes place, to prevent errors.

	bool pricingLocked = CheckIfPricingLocked(e1, service, tracing);
	if (pricingLocked == false)
	{

		//If Existing Product, then we need to check to ensure it is not being sold underneath the value specified in the price list

		bool isWriteIn = e1.GetAttributeValue<bool>("isproductoverridden");

		if (isWriteIn == false)
		{
			tracing.Trace("Existing product record detected, checking Price List cost price value...");
			bool isUnderCostPrice = CheckLineItemCostPrice(e1, service, tracing);
			if (isUnderCostPrice == true)
			{
				throw new InvalidPluginExecutionException("You are attempting to sell this product record below its listed cost price value. Please update the record to ensure that it is equal or higher than its price list value.");
			}
		}

		//Obtain the correct Discount and Freight Amounts for the product record.
		//As Opportunity entity does not have Ship To Address values, we default to £25 instead.

		Money da = CalculateLineItemDiscount(e1, service, tracing);
		Money fa = new Money(25);

		if (e1.LogicalName != "opportunityproduct")
		{
			fa = CalculateLineItemFreight(e1, service, tracing);
		}

		//Set all required amounts on the line item record.
		//First, obtain the values needed from the product record.

		decimal ppu = e1.GetAttributeValue<Money>("priceperunit")?.Value ?? 0;
		decimal q = e1.GetAttributeValue<decimal>("quantity");
		decimal t = e1.GetAttributeValue<Money>("tax")?.Value ?? 0;

		//The, it's time to calculate!

		//Amount = Price Per Unit * Quantity
		decimal a = ppu * q;
		e1["baseamount"] = new Money(a);
		tracing.Trace("Amount = " + a.ToString());
		//Manual Discount
		e1["manualdiscountamount"] = da;
		tracing.Trace("Discount Amount = " + da.ToString());
		//Freight Amount
		e1["jjg_freightamount"] = fa;
		tracing.Trace("Freight Amount = " + fa.ToString());
		//Extended Amount = (Amount - Manual Discount) + Freight Amount + Tax
		decimal ea = (a - da.Value) + fa.Value + t;
		e1["extendedamount"] = new Money(ea);
		tracing.Trace("Extended Amount = " + ea.ToString());

		//Actually update the record

		service.Update(e1);
		tracing.Trace(e1.LogicalName + " updated successfully!");

	}
}

private static void CalculateSalesDocument(EntityReference salesDoc, IOrganizationService service, ITracingService tracing)
{
	//Only calculate record if it is in an Active state

	Entity e = service.Retrieve(salesDoc.LogicalName, salesDoc.Id, new ColumnSet("statecode"));
	OptionSetValue statecode = (OptionSetValue)e["statecode"];
	if (statecode.Value == 0)
	{

		//Assign correct entity name for related Product records

		string pe = GetProductEntityName(salesDoc.LogicalName);
		string peID = salesDoc.LogicalName + "id";

		//Build the query to return all line items for the related Sales Document

		QueryExpression query = new QueryExpression(pe);
		query.ColumnSet.AddColumns("baseamount", "manualdiscountamount", "jjg_discountpercent", "jjg_freightamount", "tax", "extendedamount");
		query.Criteria.AddCondition(peID, ConditionOperator.Equal, salesDoc.Id);
		EntityCollection ec = service.RetrieveMultiple(query);

		//Iterate through and total up values for each line item
		//Also create list to store discount percent values (used later)

		decimal da = 0;
		decimal d = 0;
		decimal fa = 0;
		decimal t = 0;
		decimal ta = 0;

		List<List<int>> l = new List<List<int>>();

		for (int i = 0; i < ec.Entities.Count; i++)
		{
			Money totalAmount = ec.Entities[i].GetAttributeValue<Money>("baseamount");
			Money totalDiscount = ec.Entities[i].GetAttributeValue<Money>("manualdiscountamount");
			Money totalFreightAmount = ec.Entities[i].GetAttributeValue<Money>("jjg_freightamount");
			Money totalTax = ec.Entities[i].GetAttributeValue<Money>("tax");
			Money totalExtendedAmount = ec.Entities[i].GetAttributeValue<Money>("extendedamount");

			//If no value in the returned fields, default to 0

			da += totalAmount?.Value ?? 0;
			d += totalDiscount?.Value ?? 0;
			fa += totalFreightAmount?.Value ?? 0;
			t += totalTax?.Value ?? 0;
			ta += totalExtendedAmount?.Value ?? 0;

			l.Add(new List<int> { ec.Entities[i].GetAttributeValue<int>("jjg_discountpercent")});
		}

		//For discounts, we figure out an average (arithmetic mean) percentage discount, based on each line item returned.
		//We only calculate this if there are related line item records, otherwise, set to 0.

		double dp = 0;

		if (ec.Entities.Count != 0)
		{
			dp = Math.Round(l.Average(inner => inner[0]), 2, MidpointRounding.AwayFromZero);
		}

		//Update entity with required values

		e["totallineitemamount"] = new Money(da);
		tracing.Trace("Total Detail Amount = " + da.ToString());
		e["discountpercentage"] = dp;
		tracing.Trace("Invoice Discount (%) = " + dp.ToString());
		e["discountamount"] = new Money(d);
		tracing.Trace("Invoice Discount Amount = " + d.ToString());
		//Pre-Freight Amount = Detail Amount - Discount
		decimal pfa = da - d;
		e["totalamountlessfreight"] = new Money(pfa);
		tracing.Trace("Total Pre-Freight Amount = " + pfa.ToString());
		e["freightamount"] = new Money(fa);
		tracing.Trace("Total Freight Amount = " + fa.ToString());
		e["totaltax"] = new Money(t);
		tracing.Trace("Total Tax = " + t.ToString());
		e["totalamount"] = new Money(ta);
		tracing.Trace("Total Amount = " + ta.ToString());

		//Actually update the sales document

		service.Update(e);
		tracing.Trace(e.LogicalName + " updated successfully!");

	}
}
  1. With all required methods defined, it is now necessary to return to the Execute method from step 6 and add in the remaining code to trigger the appropriate logic. On line 30 of this file, hit the Return key and then paste in the following code:
// The InputParameters collection contains all the data passed in the message request.            
if (context.InputParameters.Contains("Target") 
	&& context.InputParameters["Target"] is EntityReference)
{
	// Obtain the target entity from the input parmameters.
	EntityReference entity = (EntityReference)context.InputParameters["Target"];

	// Verify that the target entity represents an appropriate entity.                
	if (CheckIfNotValidEntity(entity))
		return;

	try
	{
		//Add shared variable, used earlier to check for infinite loops
		context.SharedVariables.Add("CustomPrice", true);
		context.ParentContext.SharedVariables.Add("CustomPrice", true);

		//Get a reference to the organization service - used later
		IOrganizationServiceFactory serviceFactory = (IOrganizationServiceFactory)serviceProvider.GetService(typeof(IOrganizationServiceFactory));
		IOrganizationService service = serviceFactory.CreateOrganizationService(context.UserId);

		switch(entity.LogicalName)
		{
			case "opportunityproduct":
			case "invoicedetail":
			case "quotedetail":
			case "salesorderdetail":
				tracing.Trace("Calculating " + entity.LogicalName + " as a line item calculation.");
				CalculateLineItem(entity, service, tracing);
				return;
			case "opportunity":
			case "quote":
			case "salesorder":
			case "invoice":
				tracing.Trace("Calculating " + entity.LogicalName + " as a sales document calculation.");
				CalculateSalesDocument(entity, service, tracing);
				return;
			default:
				throw new InvalidPluginExecutionException("Entity with name " + entity.LogicalName + " is not valid for calculating price information.");
		}
	}

	catch (FaultException<OrganizationServiceFault> ex)
	{
		tracing.Trace("CalculatePrice: {0}", ex.ToString());
		throw new InvalidPluginExecutionException("An error occurred in the Calculate Price plug-in.", ex);
	}

	catch (Exception ex)
	{
		tracing.Trace("CalculatePrice: {0}", ex.ToString());
		throw;
	}
}
  1. Right click on your solution and select the Build option. Verify that the solution builds successfully. This will generate a .dll file within the \bin\Debug folder.
  2. Right click on your solution and select the Open Folder in File Explorer. Within the Windows explorer window opens, drill-down into the packages\Microsoft.CrmSdk.XrmTooling.PluginRegistrationTool.9.1.0.1\tools folder and run the PluginRegistration.exe file.
  3. When the Plugin Registration Tool opens, select the CREATE NEW CONNECTION option and login to the instance that was accessed earlier.
  4. Using the instructions in this article, register the plugin assembly generated in step 12 and then add on the following Plug-in Steps:
    • CalculatePrice on the invoice entity, on the Post-Operation event, executed synchronously.
    • CalculatePrice on the invoicedetail entity, on the Post-Operation event, executed synchronously.
    • CalculatePrice on the opportunity entity, on the Post-Operation event, executed synchronously.
    • CalculatePrice on the opportunityproduct entity, on the Post-Operation event, executed synchronously.
    • CalculatePrice on the quote entity, on the Post-Operation event, executed synchronously.
    • CalculatePrice on the quotedetail entity, on the Post-Operation event, executed synchronously.
    • CalculatePrice on the salesorder entity, on the Post-Operation event, executed synchronously.
    • CalculatePrice on the salesorderdetail entity, on the Post-Operation event, executed synchronously.
    • Create on the quotedetail entity, on the Post-Operation event, executed synchronously.

With the plug-in deployed out, custom pricing logic will now apply and the solution can be tested/experimented with further.

Disclaimer

The examples include in this repository are provided "as-is" with no warranty expressed or implied. Please feel free to raise an issue if you encounter any problems, and I will happily take a look, but I can't offer any guarantee of resolution.

You can’t perform that action at this time.