2. Adding Opportunity Products

The second step in examining the MSCRM Product Catalogue involve looking at how MSCRM allows us to build an Opportunity by adding various Products to the Opportunity, as way of detailing the contents of a potential sale prior to formalising a Quote or Order. This is done by adding ‘lines’ or Opportunity Products to the Opportunity.

Opportunity Product Screen in MSCRM

Opportunity Product Screen in MSCRM

Here the user must perform the following steps:

1. First select the Product by typing the Name or Product Code.
2. Select the Unit of Sale for how the Product will be sold.
3. Specify the Quantity of Product that is to be sold.

For many clients, the Unit of Sale step here is not applicable to their business model and so becomes a slightly tedious step of selecting ‘Primary Unit’ each time. Often we are given a Usability requirement to auto-populate this field with a value.

However to auto-populate this value, we need to look-up the Product’s Default Unit of Sale as otherwise the Product Catalogue calculations that MSCRM does to look up the Price for the Product cannot be done.

Therefore the logic to auto-populate this field is not so simple as directly supplying a Title or GUID Id for the method of sale, and instead needs to be retrieved from the MSCRM Web Service.

This can be tricky as this needs to be done directly via Client-side Javascript, however the following script takes case of this for us:

//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,
        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;

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


    
var fetchXml =
            "<fetch mapping=\"logical\">" +
                "<entity name=\"product\">" +
                    "<attribute name=\"defaultuomid\" />" +
                    "<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 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 (productnumber != null) {
            if (crmForm.all.new_productnumber != null) {
                crmForm.all.new_productnumber.DataValue = productnumber[0].text;
            }
        }
    }
}

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

This script then fires a FetchDefefaultUnit() method each time the Product is selected for the Opportunity Product – this method then looks up to the Product record selected to pull back the Product’s Default Unit of Sale, which is then directly populated within the Opportunity Product.

For simpler business models where the Company using MSCRM may not have multiple Units of Sale for their Products, this saves the user time clicking into the Lookup screen for the Unit of Sale field.

The other point here, is that the Product Number field is being pulled down from the Product entity into this Opportunity Product entity – whilst this does not allow any additional functionality in the process, it is useful to include this so we can show this field in an Opportunity’s associated Products:

Opportunity Product Screen

Opportunity Product screen with auto-populated Product Code and Unit of Measure

The other quick time-saver here for certain Business Models, is to auto-populate the Quantity field to 1:

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

(obviously here, we only want to do this when creating a New Opportunity Product!)

This then saves the user time on steps 2 and 3 for adding a new Opportunity Product to an Opportunity.

With these two script additions in place, we can now offer the end-user a quicker method for adding Products to an Opportunity and having the relevant prices taken from the Product Catalogue to give an overall price for the Opportunity.

3 Responses to 2. Adding Opportunity Products

  1. Steve Fu says:

    Hi,

    One of the “must have” requirement from my customer is they need to add the multiple products at one go for the opportunity. Is it do-able in MSCRM? I am facing problem on this…

    After checking from SalesForce.com features, add multiple products under one opportunity at one go is actually standard out-of-box feature.

    • Hi Steve,

      Is a common requirement for Dynamics CRM Projects – the problem for building out-of-the-box functionality is that almost all businesses want to add multiple groups of products in a different way depending on their business model.

      Two solutions I have used in the past might be helpful to you:

      (1) Add a new dropdown field to the Opportunity Entity which allows the type of Opportunity to be selected, when Opportunity records are saved, a custom piece of Plugin logic fires to automatically create a series of Opportunity Product records linked to the Opportunity.

      (2) Similar solution but more flexible –
      – Add a 1st new entity titled ‘Product Set’ which has a N:N Relationship to the Product entity, you can then add Product Set records and associate a Product Set to any number of Product records.
      – Add a 2nd new entity titled ‘Opportunity Product Set’ which has a 1:N Relationship to the Opportunity entity and a 1:N Relationship to the Product Set entity – allowing you to associate Product Set records to an Opportunity Record via these ‘Opportunity Product Set’ records.
      – Add a custom piece of Plugin logic which fires whenever a Opportunity Product Set record is created that automatically creates a Opportunity Product record associated to the Opportunity for each Product associated to the selected Product Set.
      – This solution at first glance seems quite complex but is simple to implement in MSCRM, however is a small job for a MSCRM Developer to implement.

      For a more fully featured solution – Experlogix have a industry-standard Product Configurator which integrates with MSCRM to provide a full graphical wizard for selecting groups of Products and Product Features, http://experlogix.com/products/microsoft_dynamics_crm.php.

      MSCRM gives an amazing level of flexiability for extending the base functionality to suit particular client requirements but the Opportunity and Opportunity Product side can be a little clunky out of the box – the only point I would be careful about adopting out-of-the-box functionality is whether that functionality provides the correct user-experience for the client, or will still require further development later down the road; as building the list of Products for an Opportunity or Order can often be a tricky area in getting the User Experience right for the user.

      Sorry for the ramble! Hope some of that might be helpful.

  2. Ragheed says:

    I have a question please. Where should I put that javascript for auto populating the unit?

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