7.2 Assigning Tax Codes via the Child Pipeline

MSCRM has the concept of creating a new Invoice from an Order to implement the final step in the Sales Ordering Process – and by creating a new Invoice from an Order prompts MSCRM to copy all the Order Lines associated to the Invoice as new Invoice Lines. This logic to create the Invoice Lines is managed by MSCRM’s default business logic which gives the Execution Flow outlined above a crucial difference, the Invoice Line Create Messages are sent to the MSCRM Child Pipeline as opposed to the Parent Pipeline.

This means the Tax Code Assignment Plugin registered above will not trigger and so no Tax Code would be assigned for any of the Invoice Lines resulting from the Order.

To alter our Tax Code logic to implement this, we must alter the Tax Code Assignment Plugin code (for both Invoices and Invoice Lines) to function within the MSCRM Child Pipeline. However the MSCRM Child Pipeline comes with some tight fixed restrictions for our use:

– The CRM Web Service must be invoked in a different fashion when running from the Child Pipeline, see the linked Microsoft SDK Article for a full description.
– Accessing the CRM Web Service when running a Plugin in the Child Pipeline is limited to Create, Delete, Update and RetrieveExchangeRate operations when running Synchronously.
– Execution via the Child Pipeline must then generally speaking run Asynchronously, which can give execution pattern problems.

The linked article from a blog by Mark Kovalcson gives a fuller breakdown of using the MSCRM Child Pipeline – MS CRM 4.0 Plug-in Stages, Pipelines and Execution Modes @ CRM Scape

Invoice Plugin to assign Tax Code on creation of a new Invoice – Child Pipeline

Looking at this in regard of handling Tax Codes – firstly looking at the Plugin Logic to assign a Tax Code to an Invoice, the code must be amended to also function as a Post-Create Plugin running Asynchronously from the Child Pipeline.

        public void Execute(IPluginExecutionContext cxt)
        {
            try
            {
                DynamicEntity targetMessage = null;
                DynamicEntity postImage = null;
                string typeName = string.Empty;

                #region Get Target
                // Check if the input parameters property bag contains a target
                // of the create operation and that target is of type DynamicEntity.
                if (cxt.InputParameters.Properties.Contains("Target") &&
                   cxt.InputParameters.Properties["Target"] is DynamicEntity)
                {
                    // Obtain the target business entity from the input parmameters.
                    targetMessage = (DynamicEntity)cxt.InputParameters.Properties["Target"];

                    typeName = targetMessage.Name;

                    if (typeName != "invoice")
                    {
                        // plugin is valid for this entity
                        throw new Exception("TaxCodeAssignmentInvoice Plugin is not valid for the '" + typeName + "' entity type");
                    }
                }
                else
                {
                    throw new Exception("No Target Entity in Plugin Context");
                }
                #endregion

                #region Get Post Image for Child Pipeline Execution
                // Check if the input parameters property bag contains a target
                // of the create operation and that target is of type DynamicEntity.
                if (cxt.PostEntityImages.Contains("PostImage") &&
                   cxt.PostEntityImages["PostImage"] is DynamicEntity)
                {
                    // Obtain the target business entity from the input parmameters.
                    postImage = (DynamicEntity)cxt.PostEntityImages["PostImage"];

                    if (postImage.Name != "invoice")
                    {
                        // plugin is valid for this entity
                        throw new Exception("TaxCodeAssignmentInvoice Plugin Post Image is not valid for the '" + postImage.Name + "' entity type");
                    }
                }
                #endregion

                #region Create instance of CRM Methods class to invoke any Data Access requests to/from MSCRM
                PluginCRMMethods crmMethods = null;
                if (cxt.InvocationSource == 0)
                {
                    // Parent Pipeline
                    crmMethods = new PluginCRMMethods(cxt.CreateCrmService(true));
                }
                else if (cxt.InvocationSource == 1)
                {
                    // Child Pipeline 
                    // (designed for Invoice Lines created internally to MSCRM in child pipeline by converting an Order to a Invoice)
                    crmMethods = new PluginCRMMethods(cxt, true);
                }
                else
                {
                    throw new Exception("Unknown Plugin Invocation Source " + cxt.InvocationSource.ToString() + "");
                }
                #endregion

                #region Business Logic
                Guid taxCodeId = Guid.Empty;
                Guid customerId = Guid.Empty;
                string customerType = string.Empty;
                bool updateTaxCode = true;

                // Get details from Post Image for Child Pipeline execution
                if (postImage != null)
                {
                    if (postImage.Properties.Contains("new_taxcodeid"))
                    {
                        Microsoft.Crm.Sdk.Lookup new_taxcodeid = (Microsoft.Crm.Sdk.Lookup)postImage.Properties["new_taxcodeid"];
                        taxCodeId = new_taxcodeid.Value;
                        updateTaxCode = false; // already exists                        
                    }
                    else if (postImage.Properties.Contains("customerid"))
                    {
                        Microsoft.Crm.Sdk.Customer customerid = (Microsoft.Crm.Sdk.Customer)postImage.Properties["customerid"];
                        customerId = customerid.Value;
                        customerType = customerid.type;
                    }
                }
                else
                {
                    // Get details primarily from Target Message
                    // TARGET- Invoice already has specified Tax Code Id
                    if (targetMessage.Properties.Contains("new_taxcodeid"))
                    {
                        Microsoft.Crm.Sdk.Lookup new_taxcodeid = (Microsoft.Crm.Sdk.Lookup)targetMessage.Properties["new_taxcodeid"];
                        taxCodeId = new_taxcodeid.Value;
                    }
                    // TARGET- determine whether a Tax Code can be assigned from the Customer selected for the Invoice
                    else if (targetMessage.Properties.Contains("customerid"))
                    {
                        Microsoft.Crm.Sdk.Customer customerid = (Microsoft.Crm.Sdk.Customer)targetMessage.Properties["customerid"];
                        customerId = customerid.Value;
                        customerType = customerid.type;
                    }
                }

                if (!(taxCodeId.Equals(Guid.Empty)))
                {
                    // got Tax Code from Post Image or Target - can proceed
                }
                else if (!(customerId.Equals(Guid.Empty)))
                {
                    Guid accountId = Guid.Empty;

                    if (customerType == "contact")
                    {
                        // read up from Contact to determine Account
                        accountId = crmMethods.GetAccountFromContact(customerId, true);
                    }
                    else if (customerType == "account")
                    {
                        accountId = customerId;
                    }

                    if (!(accountId.Equals(Guid.Empty)))
                    {
                        taxCodeId = crmMethods.GetDefaultTaxCodeFromAccount(accountId);
                    }
                }
                
                if (taxCodeId.Equals(Guid.Empty)) // alternatively use organisation's default Tax Code if no other could be determined
                {
                    taxCodeId = crmMethods.GetDefaultTaxCode();
                }

                if (taxCodeId.Equals(Guid.Empty))
                {
                    throw new Exception("No Tax Code specified for Invoice - must have a Tax Code");
                }
                else
                {
                    if ((postImage != null) && (updateTaxCode == true))
                    {
                        // running in child pipeline - need to update Invoice via Web Service as opposed
                        // to altering Target Message
                        Guid invoiceId = ((Key)postImage.Properties["invoiceid"]).Value;

                        crmMethods.Invoice_UpdateTaxCode(invoiceId, taxCodeId);
                    }
                    else
                    {
                        if (!(targetMessage.Properties.Contains("new_taxcodeid")))
                        {
                            LookupProperty property = new LookupProperty();
                            property.Name = "new_taxcodeid";
                            property.Value = new Lookup("new_taxcode", taxCodeId);
                            targetMessage.Properties.Add(property);
                        }
                        else
                        {
                            Lookup new_taxcodeid = (Lookup)targetMessage.Properties["new_taxcodeid"];
                            new_taxcodeid.Value = taxCodeId;
                        }
                    }
                }
                #endregion

            }
            catch (Exception ex)
            {
                string errMessage = "TaxCodeAssignmentInvoice.Execute (" + ex.Message + ")";
                throw new Exception(errMessage);
            }
        }

Crucially here – towards the end of the Plugin Logic the code must update the Invoice using the MSCRM Web Service rather than altering the Target Message, this being due to the Child Pipeline Plugin operating on the Post Create event as opposed to the Parent Pipeline Plugin operating on the Pre-Create event.

Invoice Line Plugin to assign Tax Code on create or update of an Invoice Line – Child Pipeline

Secondly, the earlier Plugin to assign a Tax Code for an Invoice Line must also be amended to support execution from a Post event in the Child Pipeline. This amendment shares many similarities to the code above for the Invoice assignement, however this gives a possible execution for this Plugin from either Pre-Create, Pre-Update or Post-Create and so possible drawing details from either the Target Message, PreImage or PostImage.

This does give the Plugin a number of possible executions paths and so some careful debugging and error-reporting would be required here, however this is often the case when making use of the Child Pipeline given the often complex executions paths this can pose. (particularly as Asynchronously Plugins will not show any error message upon error and so require logging or Event Log tracking to capture and report on errors encountered.)

        public void Execute(IPluginExecutionContext cxt)
        {
            try
            {
                DynamicEntity targetMessage = null;
                DynamicEntity preImage = null;
                DynamicEntity postImage = null;
                string typeName = string.Empty;
                decimal currentTaxRate = 0;

                #region Get Target
                // Check if the input parameters property bag contains a target
                // of the create operation and that target is of type DynamicEntity.
                if (cxt.InputParameters.Properties.Contains("Target") &&
                   cxt.InputParameters.Properties["Target"] is DynamicEntity)
                {
                    // Obtain the target business entity from the input parmameters.
                    targetMessage = (DynamicEntity)cxt.InputParameters.Properties["Target"];

                    typeName = targetMessage.Name;

                    if (typeName != "invoicedetail")
                    {
                        // plugin is valid for this entity
                        throw new Exception("TaxCodeHandling Plugin is not valid for the '" + typeName + "' entity type");
                    }
                }
                else
                {
                    throw new Exception("No Target Entity in Plugin Context");
                }
                #endregion

                #region Get PreImage (if preUpdate Message)
                if ( cxt.MessageName == "Update" )
                {
                    // Check if the input parameters property bag contains a target
                    // of the create operation and that target is of type DynamicEntity.
                    if (cxt.PreEntityImages.Contains("PreImage") &&
                       cxt.PreEntityImages["PreImage"] is DynamicEntity)
                    {
                        // Obtain the target business entity from the input parmameters.
                        preImage = (DynamicEntity)cxt.PreEntityImages["PreImage"];

                        if (preImage.Name != "invoicedetail")
                        {
                            // plugin is valid for this entity
                            throw new Exception("TaxCodeHandling Plugin is not valid where Pre Image is for the '" + preImage.Name + "' entity type");
                        }
                    }
                    else
                    {
                        throw new Exception("No Preimage in Plugin Context for Update Message");
                    }
                }
                #endregion

                #region Get Post Image for Child Pipeline Execution
                // Check if the input parameters property bag contains a target
                // of the create operation and that target is of type DynamicEntity.
                if (cxt.PostEntityImages.Contains("PostImage") &&
                   cxt.PostEntityImages["PostImage"] is DynamicEntity)
                {
                    // Obtain the target business entity from the input parmameters.
                    postImage = (DynamicEntity)cxt.PostEntityImages["PostImage"];

                    if (postImage.Name != "invoicedetail")
                    {
                        // plugin is valid for this entity
                        throw new Exception("TaxCodeAssignmentInvoice Plugin Post Image is not valid for the '" + postImage.Name + "' entity type");
                    }
                }
                #endregion

                #region Create instance of CRM Methods class to invoke any Data Access requests to/from MSCRM
                PluginCRMMethods crmMethods = null;                
                if (cxt.InvocationSource == 0)
                {
                    // Parent Pipeline
                    crmMethods = new PluginCRMMethods(cxt.CreateCrmService(true));
                }
                else if (cxt.InvocationSource == 1)
                {
                    // Child Pipeline 
                    // (designed for Invoice Lines created internally to MSCRM in child pipeline by converting an Order to a Invoice)
                    crmMethods = new PluginCRMMethods(cxt, true);
                }
                else
                {
                    throw new Exception("Unknown Plugin Invocation Source " + cxt.InvocationSource.ToString() + "");
                }
                #endregion

                #region Business Logic
                Guid taxCodeId = Guid.Empty;

                // Get details from Post Image for Child Pipeline execution
                if (postImage != null)
                {
                    // POSTIMAGE- Invoice Line already has specified Tax Code Id
                    if (postImage.Properties.Contains("new_taxcodeid"))
                    {
                        Microsoft.Crm.Sdk.Lookup new_taxcodeid = (Microsoft.Crm.Sdk.Lookup)postImage.Properties["new_taxcodeid"];
                        taxCodeId = new_taxcodeid.Value;
                    }
                    // POSTIMAGE- determine whether a Tax Code can be assigned from the Product selected for the Invoice Line
                    else if (postImage.Properties.Contains("productid"))
                    {
                        Microsoft.Crm.Sdk.Lookup productid = (Microsoft.Crm.Sdk.Lookup)postImage.Properties["productid"];
                        taxCodeId = crmMethods.GetTaxCodeFromProduct(productid.Value);
                    }

                    // POSTIMAGE- alternatively determine whether a Tax Code can be assigned from the Parent Invoice for the Invoice Line                    
                    if ( taxCodeId.Equals(Guid.Empty))
                    {
                        if (postImage.Properties.Contains("invoiceid"))
                        {
                            Microsoft.Crm.Sdk.Lookup invoiceid = (Microsoft.Crm.Sdk.Lookup)postImage.Properties["invoiceid"];
                            taxCodeId = crmMethods.GetTaxCodeFromInvoice(invoiceid.Value);
                        }
                        else
                        {
                            Microsoft.Crm.Sdk.Lookup invoiceid = (Microsoft.Crm.Sdk.Lookup)postImage.Properties["invoiceid"];
                            throw new Exception("Post Image for Invoice Detail has blank 'invoiceid', should never happen! '" + invoiceid.Value.ToString() + "'");
                        }
                    }
                }

                // Get details from Pre-Image where possible
                if (preImage != null)
                {
                    if (preImage.Properties.Contains("new_taxrate"))
                    {
                        CrmDecimal new_taxrate = (CrmDecimal)preImage.Properties["new_taxrate"];
                        currentTaxRate = new_taxrate.Value;
                    }

                    // PREIMAGE- Invoice Line already has specified Tax Code Id
                    if (preImage.Properties.Contains("new_taxcodeid"))
                    {
                        Microsoft.Crm.Sdk.Lookup new_taxcodeid = (Microsoft.Crm.Sdk.Lookup)preImage.Properties["new_taxcodeid"];
                        taxCodeId = new_taxcodeid.Value;
                    }
                    // PREIMAGE- determine whether a Tax Code can be assigned from the Product selected for the Invoice Line
                    else if (preImage.Properties.Contains("productid"))
                    {
                        Microsoft.Crm.Sdk.Lookup productid = (Microsoft.Crm.Sdk.Lookup)preImage.Properties["productid"];
                        taxCodeId = crmMethods.GetTaxCodeFromProduct(productid.Value);
                    }
                    // PREIMAGE- alternatively determine whether a Tax Code can be assigned from the Parent Invoice for the Invoice Line
                    if (taxCodeId.Equals(Guid.Empty))
                    {
                        if (preImage.Properties.Contains("invoiceid"))
                        {
                            Microsoft.Crm.Sdk.Lookup invoiceid = (Microsoft.Crm.Sdk.Lookup)preImage.Properties["invoiceid"];
                            taxCodeId = crmMethods.GetTaxCodeFromInvoice(invoiceid.Value);
                        }
                    }
                }

                // Get details primarily from Target Message
                // TARGET- Invoice Line already has specified Tax Code Id
                if (targetMessage.Properties.Contains("new_taxcodeid"))
                {
                    Microsoft.Crm.Sdk.Lookup new_taxcodeid = (Microsoft.Crm.Sdk.Lookup)targetMessage.Properties["new_taxcodeid"];
                    taxCodeId = new_taxcodeid.Value;
                }
                // TARGET- determine whether a Tax Code can be assigned from the Product selected for the Invoice Line
                else if (targetMessage.Properties.Contains("productid"))
                {
                    Microsoft.Crm.Sdk.Lookup productid = (Microsoft.Crm.Sdk.Lookup)targetMessage.Properties["productid"];
                    taxCodeId = crmMethods.GetTaxCodeFromProduct(productid.Value);
                }
                // TARGET- alternatively determine whether a Tax Code can be assigned from the Parent Invoice for the Invoice Line
                if (taxCodeId.Equals(Guid.Empty))
                {
                    if (targetMessage.Properties.Contains("invoiceid"))
                    {
                        Microsoft.Crm.Sdk.Lookup invoiceid = (Microsoft.Crm.Sdk.Lookup)targetMessage.Properties["invoiceid"];
                        taxCodeId = crmMethods.GetTaxCodeFromInvoice(invoiceid.Value);
                    }
                }

                if (taxCodeId.Equals(Guid.Empty))
                {
                    throw new Exception("No Tax Code specified for Invoice Line - must have a Tax Code");
                }
                else
                {
                    // get Tax Rate based on assigned Tax Code
                    decimal taxRate = crmMethods.GetTaxRateFromTaxCode(taxCodeId);

                    if (postImage != null)
                    {
                        // running in child pipeline - need to update Invoice Line via Web Service as opposed
                        // to altering Target Message
                        Guid invoiceDetailId = ((Key)postImage.Properties["invoicedetailid"]).Value;

                        crmMethods.InvoiceLine_UpdateTaxCode(invoiceDetailId, taxCodeId, taxRate);
                    }
                    else
                    {
                        if (!(targetMessage.Properties.Contains("new_taxcodeid")))
                        {
                            LookupProperty property = new LookupProperty();
                            property.Name = "new_taxcodeid";
                            property.Value = new Lookup("new_taxcode", taxCodeId);
                            targetMessage.Properties.Add(property);
                        }

                        if (taxRate != currentTaxRate)
                        {
                            if (targetMessage.Properties.Contains("new_taxrate"))
                            {
                                Microsoft.Crm.Sdk.CrmDecimal taxRateProperty = (Microsoft.Crm.Sdk.CrmDecimal)targetMessage.Properties["new_taxrate"];
                                taxRateProperty.Value = taxRate;
                            }
                            else
                            {
                                Microsoft.Crm.Sdk.CrmDecimal taxRateProperty = new CrmDecimal(taxRate);
                                Microsoft.Crm.Sdk.CrmDecimalProperty property = new CrmDecimalProperty();
                                property.Name = "new_taxrate";
                                property.Value = taxRateProperty;
                                targetMessage.Properties.Add(property);
                            }
                        }
                    }
                }
                #endregion

            }
            catch (Exception ex)
            {
                string errMessage = "TaxCodeAssignment.Execute (" + ex.Message + ")";
                throw new Exception(errMessage);
            }
        }

This Plugin would then be registered against the Post-Create event of the Invoice Detail entity working Asynchronously in the Child Pipeline.

Updating the two sets of plugin logic in this fashion will then allow us to update the Plugin Registrations accordingly:

Plugin Registration for Tax Code Handling Plugin logic working from both the Parent and Child Pipelines

Plugin Registration for Tax Code Handling Plugin logic working from both the Parent and Child Pipelines

This will have the affect of capturing the Invoice and Invoice Detail creation events for when a Invoice is raised against a Sales Order in MSCRM, such that when raising the Invoice, the Invoice itself and each of the Invoice Lines are assigned a Tax Code based from either the Company or Product involved:

Raising an Invoice against a Sales Order in MSCRM

Raising an Invoice against a Sales Order in MSCRM

Invoice raised in MSCRM from the Order following the use of the Tax Code Calculation Plugin in the MSCRM Child Pipeline

Invoice raised in MSCRM from the Order following the use of the Tax Code Calculation Plugin in the MSCRM Child Pipeline

This then gives MSCRM a full system for handling a number of Tax Codes derived from the Product involved with the Invoice Line and/or the Customer involved with the Invoice.

As described at the beginning of these posts, this is a curious example of bringing some financial logic into MSCRM which may or may not be the right step depending on the wider solution for CRM/ERP involved – however this does also give a good example of using the Child Pipeline in MSCRM and the various difficulties this can present.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s