3. Overriding the Catalogue to allow for Bespoke Pricing

Often for different Business Models, Salespeople want to add a set of Products to an Opportunity to define what we are selling to a Customer but want to override the default pricing to give a bespoke price for each line or Opportunity Product.

Such that selling a Widget normally costs £500 per Unit, but in this incidence we wish to override this and sell the Widget for £400 per Unit.

The key point here is that the MSCRM model for Opportunities and Products does not allow this at the Opportunity stage – a Quote must be raised to begin overriding prices from the Product Catalogue.

This does limit the salesperson in pricing up an Opportunity and similarly forces them into a separate screen for raising the Quote – but also does force best practise in keeping both prices visible to see the difference between the normal/recommended price of the products specified, and the revised prices offered to the Customer.

NOTE: There is a Override Pricing field on the Opportunity Product entity, however this does not allow the Product Catalogue price to be overridden.
Custom Development via Plugin’s and Custom fields would be required to implement this in a fashion which allowed completely bespoke Product Pricing at the Opportunity stage – this may form a subsequent posting for this blog.

Raising the Quote and editing the Quote Lines

Quotes are an aspect of MSCRM that apply heavily to certain business models or have no place at all – in this example however; we think of a Quote as the price offered by a Salesperson for an Opportunity based on their judgement for line item discounts or bespoke pricing.

Raising a Quote in MSCRM will automatically bring across the Product Set and Pricing supplied in the Opportunity into the Quote – at which point the pricing can be overridden and Products added or removed.

Here we can add the same script as above to the Quote Product entity with one crucial difference:

//FUNCTIONS
function htmlEncode(source, display, tabs) {
    function special(source) {
        var result = '';
        for (var i = 0; i < source.length; i++) {
            var c = source.charAt(i);
            if (c < ' ' || c > '~') {
                c = '&#' + c.charCodeAt() + ';';
            }
            result += c;
        }
        return result;
    }

    function format(source) {
        // Use only integer part of tabs, and default to 4
        tabs = (tabs >= 0) ? Math.floor(tabs) : 4;

        // split along line breaks
        var lines = source.split(/\r\n|\r|\n/);

        // expand tabs
        for (var i = 0; i < lines.length; i++) {
            var line = lines[i];
            var newLine = '';
            for (var p = 0; p < line.length; p++) {
                var c = line.charAt(p);
                if (c === '\t') {
                    var spaces = tabs - (newLine.length % tabs);
                    for (var s = 0; s < spaces; s++) {
                        newLine += ' ';
                    }
                }
                else {
                    newLine += c;
                }
            }
            // Leading or ending spaces will be removed from a HTML Request, unless flagged as a nbsp type character
            newLine = newLine.replace(/(^ )|( $)/g, '&nbsp;');
            lines[i] = newLine;
        }
        // re-join lines
        var result = lines.join('<br />');
        // break up contiguous blocks of spaces with non-breaking spaces
        result = result.replace(/  /g, ' &nbsp;');

        return result;
    }
    var result = source;

    // ampersands (&)
    result = result.replace(/\&/g, '&amp;');
    // less-thans (<)
    result = result.replace(/\</g, '&lt;');
    // greater-thans (>)
    result = result.replace(/\>/g, '&gt;');

    if (display) {
        // format for display
        result = format(result);
    }
    else {
        // Replace quotes if it isn't for display,
        // since it's probably going in an html attribute.
        result = result.replace(new RegExp('"', 'g'), '&quot;');
    }

    result = special(result);
    return result;
}

FetchXmlResponse = function(fetchXml) {
    var xml =
        "<?xml version=\"1.0\" encoding=\"utf-8\" ?>" +
        "<soap:Envelope xmlns:soap=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xmlns:xsd=\"http://www.w3.org/2001/XMLSchema\">" +
            GenerateAuthenticationHeader() +
            "<soap:Body>" +
                "<Fetch xmlns=\"http://schemas.microsoft.com/crm/2007/WebServices\">" +
                    "<fetchXml>" + htmlEncode(fetchXml) + "</fetchXml>" +
                "</Fetch>" +
            "</soap:Body>" +
        "</soap:Envelope>";

    var xmlHttpRequest = new ActiveXObject("Msxml2.XMLHTTP");
    xmlHttpRequest.Open("POST", "/mscrmservices/2007/CrmService.asmx", false);
    xmlHttpRequest.setRequestHeader("SOAPAction", "http://schemas.microsoft.com/crm/2007/WebServices/Fetch");
    xmlHttpRequest.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
    xmlHttpRequest.setRequestHeader("Content-Length", xml.length);

    xmlHttpRequest.send(xml);

    return xmlHttpRequest.responseXML;
}

FetchDefaultUnit = function() {
    var productId;
    var assume = true;

    if (crmForm.all.new_assumedefaultunit != null) {
        if (crmForm.all.new_assumedefaultunit.DataValue == false) {
            assume = false;
        }
    }
    if (crmForm.all.productid.DataValue != null) {
        productId = crmForm.all.productid.DataValue[0].id;
    }
    else {
        assume = false
    }

    //                     "<attribute name=\"new_assumeoverridepricing\" />" +

    var fetchXml =
            "<fetch mapping=\"logical\">" +
                "<entity name=\"product\">" +
                    "<attribute name=\"defaultuomid\" />" +
                    "<attribute name=\"new_assumeoverridepricing\" />" +
                    "<attribute name=\"productnumber\" />" +
                    "<filter>" +
                        "<condition attribute=\"productid\" operator=\"eq\" value=\"" + productId + "\" />" +
                    "</filter>" +
                "</entity>" +
            "</fetch>";

    if (assume == true) {
        var resultXml = FetchXmlResponse(fetchXml);

        var oXmlDoc = new ActiveXObject("Microsoft.XMLDOM");
        oXmlDoc.async = false;
        oXmlDoc.loadXML(resultXml.text);

        // alert(oXmlDoc.xml);

        var defaultuomid = oXmlDoc.getElementsByTagName('defaultuomid');
        var new_assumeoveridepricing = oXmlDoc.getElementsByTagName('new_assumeoverridepricing');
        var productnumber = oXmlDoc.getElementsByTagName('productnumber');

        if (defaultuomid != null) {
            var lookupData = new Array();
            var lookupItem = new Object();

            // Set the id, typename, and name properties to the object.
            lookupItem.id = defaultuomid[0].text;
            lookupItem.typename = 'uom';
            lookupItem.name = defaultuomid[0].getAttribute("name");
            lookupData[0] = lookupItem;
            crmForm.all.uomid.DataValue = lookupData;
        }

        if (new_assumeoveridepricing != null) {
            if (new_assumeoveridepricing[0].text == "1") {
                crmForm.all.ispriceoverridden.DataValue = true;
                crmForm.all.priceperunit.Disabled = false;
            }
            else {
                crmForm.all.ispriceoverridden.DataValue = false;
                crmForm.all.priceperunit.Disabled = true;
            }
        }

        if (productnumber != null) {
            if (crmForm.all.new_productnumber != null) {
                crmForm.all.new_productnumber.DataValue = productnumber[0].text;
                crmForm.all.new_productnumber.ForceSubmit = true;
            }
        }
    }
}

crmForm.all.productid.onchange = function() { FetchDefaultUnit(); };

if (crmForm.FormType == 1) // i.e. create
{
    crmForm.all.quantity.DataValue = 1;
}

if (crmForm.all.ispriceoverridden.DataValue == true) {
    crmForm.all.priceperunit.Disabled = false;
}
else {
    crmForm.all.priceperunit.Disabled = true;
}

This script does much the same as the script added to the Opportunity Product entity in pulling the Default Unit of Sale into the Quote Product whenever the user specifies the Product for this line in the Quote – however here, this script also pulls down an additional custom field from the selected Product which determines whether to automatically override the price in the Quote Line for the Salesperson to specify this.

This again saves time for the Salesperson entering the product against the Quote – and gives a steer on whether a Product’s default price in the Product Catalogue should or could be overridden by the Salesperson.

From here, a Salesperson can define their Opportunity in terms of what Products or Services are to be sold to the Potential Customer and then offer a Quote with customised Prices for these Products or Services.

However there is one problem with this current process, in customising the Mapping between the Opportunity Product and Quote Product entities to ensure that any custom fields (such as the Product Number example) are mapped across – the next part of this article will show how this can be addressed.

Leave a comment