Dynamics CRM - Set default unit on product line

24 August 2010

When picking a product on a CRM entity like quote product or order product the user also has to pick a unit of measure for the product. Wouldn't it be useful if CRM filled in the unit of measure field for the user based on the default unit for the product? Of course it would, but it doesn't do this out of the box.

My solution for this involves using a generic handler to get the default unit for a specific product and some JavaScript to call the handler asynchronously.

The JavaScript that is placed in the onchange event of the productid field looks like this:

var lookupItem = new Array;
// Get the lookup for the primarycontactid attribute on the account form.
lookupItem = crmForm.all.productid.DataValue;
if (lookupItem[0] != null)
{
  InitXmlHttp();
  xmlhttp.onreadystatechange= XMLHttpRequestCompleted;
  xmlhttp.open("GET", "../../../TSG.CRM.GenericHandlers/QuoteProductSetDefaultUnits.ashx?
id="+lookupItem[0].id , true );
  xmlhttp.send(null);
}
 
function InitXmlHttp() {
  // Attempt to initialize xmlhttp object
    try
    {
        xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");
    }
    catch (e)
    {
        // Try to use different activex object
        try
        {
            xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
        }
        catch (E)
        {
            xmlhttp = false;
        }
    }
    
    // If not initialized, create XMLHttpRequest object
    if (!xmlhttp && typeof XMLHttpRequest!='undefined')
      {     
            xmlhttp = new XMLHttpRequest();
      }    
}
 
function XMLHttpRequestCompleted()
{
        if (xmlhttp.readyState==4)
    {
        try
        {
           var lookupData = new Array();
           var lookupItem = new Object();
           var response = new Array();
           response = xmlhttp.responseText.split(',');
                             
           lookupItem.id = response[0];
           lookupItem.typename = response[1];
           lookupItem.name = response[2];
           lookupData [0] = lookupItem;
           crmForm.all.uomid.DataValue = lookupData ;
         }
        catch (e)
        {
           alert('Something has gone wrong!');
        }
    }
}
 

The generic handler code looks like this:

using System;
using System.Collections;
using System.Data;
using System.Linq;
using System.Web;
using System.Web.Services;
using System.Web.Services.Protocols;
using System.Xml.Linq;
using System.Text;
using CRM.GenericHandlers.CRMService;
using System.Configuration;
 
namespace CRM.GenericHandlers
{
    /// <summary>
    /// Summary description for $codebehindclassname$
    /// </summary>
    [WebService(Namespace = "http://tempuri.org/")]
    [WebServiceBinding(ConformsTo = WsiProfiles.BasicProfile1_1)]
    public class QuoteProductSetDefaultUnits : IHttpHandler
    {
 
        public void ProcessRequest(HttpContext context)
        {
            string productid = context.Request.QueryString["id"];
 
            try
            {
                CrmAuthenticationToken token = new CrmAuthenticationToken();
                token.AuthenticationType = 0; 
                //add an <appsetting> to web.config for your organisation name
                token.OrganizationName = ConfigurationManager.AppSettings["OrgName"].ToString();
 
                CrmService service = new CrmService();
                service.CrmAuthenticationTokenValue = token;
                service.Credentials = System.Net.CredentialCache.DefaultCredentials;
 
                //create the ColumnSet that indicates properties to be retrieved
                ColumnSet productCols = new ColumnSet();
                ColumnSet uomCols = new ColumnSet();
               
                //set the properties of the ColumnSet
                productCols.Attributes = new string[] { "productid", "defaultuomid" };
                uomCols.Attributes = new string[] { "uomid", "name" };
                
                //retrieve quote
                Guid ProductID = new Guid(productid);
                product product = (product)service.Retrieve("product", ProductID, productCols);
 
                Guid uomID = product.defaultuomid.Value;
                uom productUom = (uom)service.Retrieve("uom", uomID, uomCols);
 
                string productUomName = productUom.name;
 
                string[] arrLookupItem = new string[3];
                arrLookupItem[0] = uomID.ToString();
                arrLookupItem[1] = "uom";
                arrLookupItem[2] = productUomName;
 
               context.Response.Write("{"+uomID+"},uom,"+productUomName); 
 
            }
            catch (SoapException ex)
            {
                context.Response.Write(ex.Message);
            }
 
        }
 
        public bool IsReusable
        {
            get
            {
                return false;
            }
        }
    }
}
 

XmlHttpRequest Cache Busters

04 August 2010

The call to a generic handler in the post Using generic handlers in Dynamics CRM seemed to only be firing once. The alert was being displayed to say that the code had run successfully but I could see that after the first call it wasn't copying the entity as it should have.

It turns out this was because the xmlhttprequest was being cached and the fix was fairly simple. By adding an extra parameter to the url called by the request we can trick it into thinking that a completely new call is being made which prevents the cached result from being used.

Simply add the following to the query string: "bustCache=" + Math.random();

Using generic handlers in Dynamics CRM

28 July 2010

Sometimes you need to add functionality to Microsoft Dynamics CRM that cannot be easily achieved by simply adding JavaScript to form and field events.

One way to add extra complex functionality to CRM is to use a generic handler .ashx code file.

For example if you want to copy a quote and it's quote products this is not easily achieved using JavaScript and is not possible using the standard CRM workflows.

To make use of a .ashx generic handler is a 3 step process:

  1. Create the .ashx handler that performs the required functions and deploy it to your CRM server
  2. Add a JavaScript function to a form that will call the .ashx and handle the returned result
  3. Update the ISV.config to add a button to the relevant entity so that the user can trigger the functionality

1. Create .ashx handler

To create the handler you will need a new Visual Studio project. Add a Generic Handler (.ashx) code file to the project and then add references to the CRM web services.

Add your code to provide the desired functionality.

Click this link to download the .ashx code files for copying a Dynamics CRM quote 

The application then needs to be deployed to IIS.

2. Add a JavaScript function to call the handler and display the returned result

Add the JavaScript to the onload event of the form where this functionality will be called. In this example of copying a quote add the function to the onload event of the Quote main form.

CopyQuote = function()
{
InitXmlHttp();
xmlhttp.onreadystatechange= XMLHttpRequestCompleted;
// use the url to your generic handler project here - in this case 
//I have deployed the project to the website that is running CRM
xmlhttp.open("GET", http://YourCRMServer/CRM.GenericHandlers/CopyQuote.ashx?id=
+crmFormSubmit.crmFormSubmitId.value , true );
xmlhttp.send(null);
function InitXmlHttp() {
  // Attempt to initialize xmlhttp object
    try
    {
        xmlhttp = new ActiveXObject("Msxml2.XMLHTTP");
    }
    catch (e)
    {
        // Try to use different activex object
        try
        {
            xmlhttp = new ActiveXObject("Microsoft.XMLHTTP");
        }
        catch (E)
        {
            xmlhttp = false;
        }
    }
    
    // If not initialized, create XMLHttpRequest object
    if (!xmlhttp && typeof XMLHttpRequest!='undefined')
      {     
            xmlhttp = new XMLHttpRequest();
      }
      // Define function call for when Request obj state has changed
      xmlhttp.onreadystatechange=XMLHttpRequestCompleted;
}
function XMLHttpRequestCompleted()
{
        if (xmlhttp.readyState==4)
        {
            try
            {
                alert(xmlhttp.responseText);
            }
            catch (e)
            {
           }
        }
    }
}
 

3. Update ISV.config to add a button to call the CopyQuote() JavaScript

Export the ISV.config file and add the following to the Quote section:

<Button JavaScript="CopyQuote();">
    <Titles>
            <Title LCID="1033" Text="Copy Quote" />
          </Titles>
              <ToolTips>
                <ToolTip LCID="1033" Text="Copy Quote" />
              </ToolTips>
        </Button>
 

Save the updated ISV.config file and import it back into CRM.

The result of this is that you now have a button on the Quote form called Copy Quote. When this button is clicked the JavaScript calls the .ashx file which copies the quote and then sends a message back to the JavaScript to let the user know if the operation has been successful.

Create Dynamics CRM entity mapping for lookup field

20 July 2010

When editing attributes on CRM entities it is often necessary to pass that attribute through to the entity in the next stage in the sales process.

For example you add a contact name to the quote entity and you want that contact to be passed through to the order that is created from the quote.

To do this for standard fields it is simply a case of opening the customisation screen for the entity, adding a new attribute and then adding a new mapping to the relevant relationship. In the case of quotes and orders this would be the N:1 quote to order relationship.

Where things become a little bit trickier is if you want to create a mapping between two entities for an attribute that is a lookup. This is because it is not possible to simply add a lookup field as an attribute.

To do this for the case of the contact lookup being passed from quote to order do the following:

  • Create a new N:1 relationship on the order entity. The Primary Entity for the realtionship will be Contact.
  • Give it a display name of Contact and leave all the other fields as default.
  • Click Save and Close - this will have created a new attribute called Contact which we can now use to map the Quote Contact attribute to.
  • Edit the Quote to Order 1:N relationship and add a new mapping between Contact in the source and target entities.
  • Click OK.
  • Now you can add Contact to the forms and views where it needs to be visible.

Getting started with JavaScript in Dynamics CRM 4.0

19 June 2010

This is a blog that is usually about SharePoint and SharePoint development but recently I have been starting to do some work with Microsoft Dynamics CRM 4.0. Initially this work was about integrating SharePoint document libraries with CRM but has now gone beyond that into actual customisation and development of CRM itself.

CRM 4.0 has been around for a while now and many companies who have CRM will also have a SharePoint deployment so although it is a bit off topic I'm sure there are plenty of SharePoint people who will find a bit of CRM information helpful.

As with SharePoint there are lots of options for customising CRM. The first option is customisation of existing entities and forms. This allows you to add attributes to the various objects in CRM such as customers or orders. It is also possible to add JavaScript to the forms to add logic, validation or additional calculations into the forms and this is what this post will take a look at. Finally it is possible to add aspx pages or use ashx handlers in conjunction with the CRM web services to create virtually any extra complex business logic or functionality that is required - later posts will deal with this.

When customising CRM forms it is possible to easily add JavaScript to the on load and on save events of each form or to the on change event of any field.

A few hints and tips:

  • To reference an attribute on a form use crmForm.all.attributename.
  • If the attribute is not a string use .DataValue to return the value held in the attribute. e.g. crmForm.all.attributename.DataValue.
  • DataValue should also be used when creating comparison statements. E.g. if(crmForm.all.isapproved.DataValue == true)
  • If you have a calculated field that is set to read only on the form then you will need to make sure that it is set to ForceSubmit = true otherwise it will not be saved. To do this use the following - crmForm.all.attributename.ForceSubmit = true;
  • To hide a field or menu item use crmForm.all.attributname.style.display = "none";
  • To disable a field use - crmForm.all.attributename.Disabled = false; or use true; to disable the field.

It is possible to set the default for a lookup field. The following example script sets a default price list for the price list field:
 

if(crmForm.all.pricelevelid.DataValue == null)
{
//Create an array to set as the DataValue for the price list lookup control. 
var lookupData = new Array(); 
//Create an Object add to the array. 
var lookupItem= new Object(); 
//Set the id, typename, and name properties to the object. 
lookupItem.id = '{0049EC3B-1257-DF11-8A46-00155D025108}'; 
lookupItem.typename = 'pricelevel'; 
lookupItem.name = 'Test Price List'; 
// Add the object to the array. 
lookupData[0] = lookupItem; 
// Set the value of the lookup field to the value of the array. 
crmForm.all.pricelevelid.DataValue = lookupData;
}

Use the following JavaScript to call CRM web services to return information on the current user - this is then used to set a user lookup field to be the current user:

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>" + 
" <RetrieveMultiple xmlns=\"http://schemas.microsoft.com/crm/2007/WebServices\">" + 
" <query xmlns:q1=\"http://schemas.microsoft.com/crm/2006/Query\" xsi:type=\
"q1:QueryExpression\">" + 
" <q1:EntityName>systemuser</q1:EntityName>" + 
" <q1:ColumnSet xsi:type=\"q1:ColumnSet\">" + 
" <q1:Attributes>" + 
" <q1:Attribute>businessunitid</q1:Attribute>" + 
" <q1:Attribute>firstname</q1:Attribute>" + 
" <q1:Attribute>fullname</q1:Attribute>" + 
" <q1:Attribute>lastname</q1:Attribute>" + 
" <q1:Attribute>organizationid</q1:Attribute>" + 
" <q1:Attribute>systemuserid</q1:Attribute>" + 
" </q1:Attributes>" + 
" </q1:ColumnSet>" + 
" <q1:Distinct>false</q1:Distinct>" + 
" <q1:Criteria>" + 
" <q1:FilterOperator>And</q1:FilterOperator>" + 
" <q1:Conditions>" + 
" <q1:Condition>" + 
" <q1:AttributeName>systemuserid</q1:AttributeName>" + 
" <q1:Operator>EqualUserId</q1:Operator>" + 
" </q1:Condition>" + 
" </q1:Conditions>" + 
" </q1:Criteria>" + 
" </query>" + 
" </RetrieveMultiple>" + 
" </soap:Body>" + 
"</soap:Envelope>" + 
"";

var xmlHttpRequest2 = new ActiveXObject("Msxml2.XMLHTTP");

xmlHttpRequest2.Open("POST", "/mscrmservices/2007/CrmService.asmx", false);
xmlHttpRequest2 .setRequestHeader("SOAPAction", "http://schemas.microsoft.com/
crm/2007/WebServices/RetrieveMultiple");
xmlHttpRequest2.setRequestHeader("Content-Type", "text/xml; charset=utf-8");
xmlHttpRequest2.setRequestHeader("Content-Length", xml.length);
xmlHttpRequest2.send(xml);

var resultXml = xmlHttpRequest2.responseXML;
var entityNode = resultXml.selectSingleNode("//RetrieveMultipleResult/BusinessEntities/
BusinessEntity");

var firstNameNode = entityNode.selectSingleNode("q1:firstname");
var lastNameNode = entityNode.selectSingleNode("q1:lastname");
var fullNameNode = entityNode.selectSingleNode("q1:fullname");
var systemUserIdNode = entityNode.selectSingleNode("q1:systemuserid");
var businessUnitIdNode = entityNode.selectSingleNode("q1:businessunitid");
var organizationIdNode = entityNode.selectSingleNode("q1:organizationid");

//Create an array to set as the DataValue for the the lookup control. 
var lookupData = new Array(); 
//Create an Object add to the array. 
var lookupItem= new Object(); 
//Set the id, typename, and name properties to the object. 
lookupItem.id = systemUserIdNode.text; 
lookupItem.typename = 'systemuser'; 
lookupItem.name = fullNameNode.text; 
// Add the object to the array. 
lookupData[0] = lookupItem; 
// Set the value of the lookup field to the value of the array. 
crmForm.all.approvedby.DataValue = lookupData;
crmForm.all.approvedby.ForceSubmit = true;


 

Using JavaScript is a great way to customise CRM as any script you add is saved as part of the form so when you come to deploy them they are included in the export customisations file. In my customisation of CRM I have tried to use this approach wherever possible but sometimes what you want to achieve cannot be done without cracking open Visual Studio and this is what I plan on taking a look at in my next post.