On a recent client, we suggested replacing the Qualify button on the lead form. Every time a user qualified a lead, they always checked off account/contact/opportunity/Open newly created records. By replacing this window with a single button we would be saving the user 8 clicks!

I figured this was a common request and would find dozens of examples online. After an hour of searching I resigned myself to the fact that I would need to figure this out on my own. First things first, I needed to craft the Soap request that my Javascript would be calling. With that in mind, I whipped out the SoapLogger app from the sdk and got to work. I put together the following code:

var leadid = new Guid("BFE561EA-E7F4-E011-880C-1CC1DEE8CA50");

	QualifyLeadRequest req = new QualifyLeadRequest();
	req.CreateAccount = true;
	req.CreateContact = true;
	req.CreateOpportunity = true;
	req.LeadId = new EntityReference("lead", leadid);
	req.Status = new OptionSetValue(-1);               
	var res = (slos.Execute(req) as QualifyLeadResponse);

Looking good! Or so I thought until I ran my code. I kept getting the following not-so-helpful error message. Something is null, but no indication as to what:

System.NullReferenceException: Microsoft Dynamics CRM has experienced an error. Reference number for administrators or support: #ED27F8D6

After 30 minutes of banging my head against my keyboard I dug up the following post that points to the fact that campaign/customer/currency must have a value even though they aren’t required at compile time. I went back to the MSDN definition of the QualifyLeadrequest and sure enough. If I had one wish for CRM development, it’s that we actually get a useful error message. After filling out the “required” fields I was left with this snippet of code to run in SoapLogger:

			var leadid = new Guid("BFE561EA-E7F4-E011-880C-1CC1DEE8CA50");

			QualifyLeadRequest req = new QualifyLeadRequest();
			req.CreateAccount = true;
			req.CreateContact = true;
			req.CreateOpportunity = true;

			EntityReference currency = new EntityReference();
			currency.LogicalName = "transactioncurrency";
			currency.Id = new Guid("1FD001F2-E7F4-E011-880C-1CC1DEE8CA50");
			req.OpportunityCurrencyId = currency;
			req.SourceCampaignId = null;
			req.OpportunityCustomerId = null;

			req.LeadId = new EntityReference("lead", leadid);
			req.Status = new OptionSetValue(-1);               
			var res = (slos.Execute(req) as QualifyLeadResponse);  

Which gave me this beautiful response

HTTP REQUEST
--------------------------------------------------
POST https://customer.api.crm.dynamics.com/XRMServices/2011/Organization.svc/web
Content-Type: text/xml; charset=utf-8
SOAPAction: http://schemas.microsoft.com/xrm/2011/Contracts/Services/IOrganizationService/Execute

<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
  <s:Body>
    <Execute xmlns="http://schemas.microsoft.com/xrm/2011/Contracts/Services" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
      <request i:type="b:QualifyLeadRequest" xmlns:a="http://schemas.microsoft.com/xrm/2011/Contracts" xmlns:b="http://schemas.microsoft.com/crm/2011/Contracts">
        <a:Parameters xmlns:c="http://schemas.datacontract.org/2004/07/System.Collections.Generic">
          <a:KeyValuePairOfstringanyType>
            <c:key>LeadId</c:key>
            <c:value i:type="a:EntityReference">
              <a:Id>bfe561ea-e7f4-e011-880c-1cc1dee8ca50</a:Id>
              <a:LogicalName>lead</a:LogicalName>
              <a:Name i:nil="true" />
            </c:value>
          </a:KeyValuePairOfstringanyType>
          <a:KeyValuePairOfstringanyType>
            <c:key>CreateAccount</c:key>
            <c:value i:type="d:boolean" xmlns:d="http://www.w3.org/2001/XMLSchema">true</c:value>
          </a:KeyValuePairOfstringanyType>
          <a:KeyValuePairOfstringanyType>
            <c:key>CreateContact</c:key>
            <c:value i:type="d:boolean" xmlns:d="http://www.w3.org/2001/XMLSchema">true</c:value>
          </a:KeyValuePairOfstringanyType>
          <a:KeyValuePairOfstringanyType>
            <c:key>CreateOpportunity</c:key>
            <c:value i:type="d:boolean" xmlns:d="http://www.w3.org/2001/XMLSchema">true</c:value>
          </a:KeyValuePairOfstringanyType>
          <a:KeyValuePairOfstringanyType>
            <c:key>OpportunityCurrencyId</c:key>
            <c:value i:type="a:EntityReference">
              <a:Id>1fd001f2-e7f4-e011-880c-1cc1dee8ca50</a:Id>
              <a:LogicalName>transactioncurrency</a:LogicalName>
              <a:Name i:nil="true" />
            </c:value>
          </a:KeyValuePairOfstringanyType>
          <a:KeyValuePairOfstringanyType>
            <c:key>SourceCampaignId</c:key>
            <c:value i:nil="true" />
          </a:KeyValuePairOfstringanyType>
          <a:KeyValuePairOfstringanyType>
            <c:key>OpportunityCustomerId</c:key>
            <c:value i:nil="true" />
          </a:KeyValuePairOfstringanyType>
          <a:KeyValuePairOfstringanyType>
            <c:key>Status</c:key>
            <c:value i:type="a:OptionSetValue">
              <a:Value>-1</a:Value>
            </c:value>
          </a:KeyValuePairOfstringanyType>
        </a:Parameters>
        <a:RequestId i:nil="true" />
        <a:RequestName>QualifyLead</a:RequestName>
      </request>
    </Execute>
  </s:Body>
</s:Envelope>
--------------------------------------------------

HTTP RESPONSE
--------------------------------------------------
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
  <s:Body>
    <ExecuteResponse xmlns="http://schemas.microsoft.com/xrm/2011/Contracts/Services" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
      <ExecuteResult i:type="b:QualifyLeadResponse" xmlns:a="http://schemas.microsoft.com/xrm/2011/Contracts" xmlns:b="http://schemas.microsoft.com/crm/2011/Contracts">
        <a:ResponseName>QualifyLead</a:ResponseName>
        <a:Results xmlns:c="http://schemas.datacontract.org/2004/07/System.Collections.Generic">
          <a:KeyValuePairOfstringanyType>
            <c:key>CreatedEntities</c:key>
            <c:value i:type="a:EntityReferenceCollection">
              <a:EntityReference>
                <a:Id>e8a06600-fbf4-e011-8e26-1cc1deeae7d7</a:Id>
                <a:LogicalName>account</a:LogicalName>
                <a:Name i:nil="true" />
              </a:EntityReference>
              <a:EntityReference>
                <a:Id>e7a06600-fbf4-e011-8e26-1cc1deeae7d7</a:Id>
                <a:LogicalName>contact</a:LogicalName>
                <a:Name i:nil="true" />
              </a:EntityReference>
              <a:EntityReference>
                <a:Id>e6a06600-fbf4-e011-8e26-1cc1deeae7d7</a:Id>
                <a:LogicalName>opportunity</a:LogicalName>
                <a:Name i:nil="true" />
              </a:EntityReference>
            </c:value>
          </a:KeyValuePairOfstringanyType>
        </a:Results>
      </ExecuteResult>
    </ExecuteResponse>
  </s:Body>
</s:Envelope>
--------------------------------------------------

Now that the easy part is out of the way, it was time to craft the actual JScript that would be qualifying my lead on the fly. I won’t bore you with the details, the important bits are in qualifyRequest and qualifyResponse functions.

if (typeof (Slalom) == "undefined")
{ Slalom = { __namespace: true }; }
//This will establish a more unique namespace for functions in this library. This will reduce the 
// potential for functions to be overwritten due to a duplicate name when the library is loaded.
Slalom.Lead = {
 _getServerUrl: function () {
  ///<summary>
  /// Returns the URL for the SOAP endpoint using the context information available in the form
  /// or HTML Web resource.
  ///</summary
  var OrgServicePath = "/XRMServices/2011/Organization.svc/web";
  var serverUrl = "";
  if (typeof GetGlobalContext == "function") {
   var context = GetGlobalContext();
   serverUrl = context.getServerUrl();
  }
  else {
   if (typeof Xrm.Page.context == "object") {
    serverUrl = Xrm.Page.context.getServerUrl();
   }
   else
   { throw new Error("Unable to access the server URL"); }
  }
  if (serverUrl.match(//$/)) {
   serverUrl = serverUrl.substring(0, serverUrl.length - 1);
  }
  return serverUrl + OrgServicePath;
 },
 qualifyCurrentLead: function() {
	Slalom.Lead.qualifyRequest(Xrm.Page.data.entity.getId(),"1FD001F2-E7F4-E011-880C-1CC1DEE8CA50",-1);
 },
 qualifyRequest: function (leadId, currencyId, status) {

  //The request is simply the soap envelope captured by the SOAPLogger with variables added for the 
  // values passed. All quotations must be escaped to create valid JScript strings.
  var request = "<s:Envelope xmlns:s='http://schemas.xmlsoap.org/soap/envelope/'>" +
  "<s:Body>"+
  "  <Execute xmlns='http://schemas.microsoft.com/xrm/2011/Contracts/Services' xmlns:i='http://www.w3.org/2001/XMLSchema-instance'>"+
  "    <request i:type='b:QualifyLeadRequest' xmlns:a='http://schemas.microsoft.com/xrm/2011/Contracts' xmlns:b='http://schemas.microsoft.com/crm/2011/Contracts'>"+
  "      <a:Parameters xmlns:c='http://schemas.datacontract.org/2004/07/System.Collections.Generic'>"+
  "        <a:KeyValuePairOfstringanyType>"+
  "          <c:key>LeadId</c:key>"+
  "          <c:value i:type='a:EntityReference'>"+
  "            <a:Id>"+leadId+"</a:Id>"+
  "            <a:LogicalName>lead</a:LogicalName>"+
  "            <a:Name i:nil='true' />"+
  "          </c:value>"+
  "        </a:KeyValuePairOfstringanyType>"+
  "        <a:KeyValuePairOfstringanyType>"+
  "          <c:key>CreateAccount</c:key>"+
  "          <c:value i:type='d:boolean' xmlns:d='http://www.w3.org/2001/XMLSchema'>true</c:value>"+
  "        </a:KeyValuePairOfstringanyType>"+
  "        <a:KeyValuePairOfstringanyType>"+
  "          <c:key>CreateContact</c:key>"+
  "          <c:value i:type='d:boolean' xmlns:d='http://www.w3.org/2001/XMLSchema'>true</c:value>"+
  "        </a:KeyValuePairOfstringanyType>"+
  "        <a:KeyValuePairOfstringanyType>"+
  "          <c:key>CreateOpportunity</c:key>"+
  "          <c:value i:type='d:boolean' xmlns:d='http://www.w3.org/2001/XMLSchema'>true</c:value>"+
  "        </a:KeyValuePairOfstringanyType>"+
  "        <a:KeyValuePairOfstringanyType>"+
  "          <c:key>OpportunityCurrencyId</c:key>"+
  "          <c:value i:type='a:EntityReference'>"+
  "            <a:Id>"+currencyId+"</a:Id>"+
  "            <a:LogicalName>transactioncurrency</a:LogicalName>"+
  "            <a:Name i:nil='true' />"+
  "          </c:value>"+
  "        </a:KeyValuePairOfstringanyType>"+
  "        <a:KeyValuePairOfstringanyType>"+
  "          <c:key>SourceCampaignId</c:key>"+
  "          <c:value i:nil='true' />"+
  "        </a:KeyValuePairOfstringanyType>"+
  "        <a:KeyValuePairOfstringanyType>"+
  "          <c:key>OpportunityCustomerId</c:key>"+
  "          <c:value i:nil='true' />"+
  "        </a:KeyValuePairOfstringanyType>"+
  "        <a:KeyValuePairOfstringanyType>"+
  "          <c:key>Status</c:key>"+
  "          <c:value i:type='a:OptionSetValue'>"+
  "            <a:Value>"+status+"</a:Value>"+
  "          </c:value>"+
  "        </a:KeyValuePairOfstringanyType>"+
  "      </a:Parameters>"+
  "      <a:RequestId i:nil='true' />"+
  "      <a:RequestName>QualifyLead</a:RequestName>"+
  "    </request>"+
  "  </Execute>"+
  "</s:Body>"+
"</s:Envelope>";

  var req = new XMLHttpRequest();
  req.open("POST", Slalom.Lead._getServerUrl(), true)
  // Responses will return XML. It isn't possible to return JSON.
  req.setRequestHeader("Accept", "application/xml, text/xml, */*");
  req.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
  req.setRequestHeader("SOAPAction", "http://schemas.microsoft.com/xrm/2011/Contracts/Services/IOrganizationService/Execute");
  req.onreadystatechange = function () { Slalom.Lead.qualifyResponse(req); };
  req.send(request);

 },
 qualifyResponse: function (req) {
  ///<summary>
  /// Recieves the assign response
  ///</summary>
  ///<param name="req" Type="XMLHttpRequest">
  /// The XMLHttpRequest response
  ///</param>
  if (req.readyState == 4) {
   if (req.status == 200) {   
     Slalom.Lead._getSuccess(req.responseXML); 
   }
   else {
    Slalom.Lead._getError(req.responseXML);
   }
  }
 },
 _getSuccess: function (responseXml){	
	var xmlDoc = new ActiveXObject("Microsoft.XMLDOM"); 
	xmlDoc.async="false"; 
	xmlDoc.loadXML(responseXml.xml); 
	x=xmlDoc.getElementsByTagName("a:KeyValuePairOfstringanyType"); 
	for (i=0;i<x.length;i++) { 			
		var y=xmlDoc.getElementsByTagName("a:EntityReference"); 	
		for(j=0;j<y.length;j++)
		{
			if(y[j].getElementsByTagName("a:LogicalName")[0].text == "opportunity")
			{
				var oppId = y[j].getElementsByTagName("a:Id")[0].text;
				var serverUrl = Xrm.Page.context.getServerUrl(); 

				// Cater for URL differences between on premise and online
				if (serverUrl.match(//$/)) {
					serverUrl = serverUrl.substring(0, serverUrl.length - 1);
				}
				window.open (serverUrl + "/main.aspx?etc=3&extraqs=%3f_gridType%3d3%26etc%3d3%26id%3d%257b"+oppId+"%257d%26rskey%3d598263346&pagetype=entityrecord","new_opp");
				window.location.reload(true);

			}
		}
	}
 },
 _getError: function (faultXml) {
  ///<summary>
  /// Parses the WCF fault returned in the event of an error.
  ///</summary>
  ///<param name="faultXml" Type="XML">
  /// The responseXML property of the XMLHttpRequest response.
  ///</param>  
  var errorMessage = "Unknown Error (Unable to parse the fault)";  
  if (typeof faultXml == "object") {
   try {
    var bodyNode = faultXml.firstChild.firstChild;
    //Retrieve the fault node
    for (var i = 0; i < bodyNode.childNodes.length; i++) {
     var node = bodyNode.childNodes[i];

     //NOTE: This comparison does not handle the case where the XML namespace changes
     if ("s:Fault" == node.nodeName) {
      for (var j = 0; j < node.childNodes.length; j++) {
       var faultStringNode = node.childNodes[j];
       if ("faultstring" == faultStringNode.nodeName) {
        errorMessage = faultStringNode.text;
        break;
       }
      }
      break;
     }
    }
   }
   catch (e) { };
  }
  return new Error(errorMessage);
 },
 __namespace: true
};

So I took that, built out a library, replaced the button with my custom button and now I have a streamlined conversion process. The user clicks one button and they get a new opportunity that pops up that is already hooked up to an account which in turn is hooked up to a contact.

Thanks to Ben Hosk for the Response Parsing piece.