Hi on a few posts on the MS CRM dynamics forum I commented on a solution for working out due dates taking into account weekends and holidays etc.
This is the solution I came up with which has been working for the last x months without any issues (as far as i'm aware). This is based on the task entity creation event and basically picks up the Creation Date and the Due date and calculates the difference between them and then changes the due date (assuming a user inputs the number of days till the task should be completed which will generate a due date automatically - could be tweaked I know) to be a working date taking into account any company / official holidays (if rules have been entered into the Business Closures entity Settings > Business Closures). The code should be transferable between many implementations but is used very specifically at the moment as above.
As far as I am aware it has been tested quite thouroughly with exception to the Business Closures which our company don't use at the moment.
These belong in my case within a workflow assembly (maybe called WorkflowAssembly.DLL like mine) and are used within the workflow manager.
/// <summary>
/// Change Due Date used by workflow required by task.
/// </summary>
/// <param name="caller">string</param>
/// <param name="creationDate">DateTime</param>
/// <param name="dueDate">DateTime</param>
/// <returns>DateTime</returns>
public DateTime ChangeDueDate(string caller, DateTime creationDate, DateTime dueDate)
{
XmlDocument xDoc = new XmlDocument();
xDoc.LoadXml(caller);
XmlNodeList nodes = xDoc.SelectNodes("caller/userid");
if (nodes.Count > 0)
{
// setup service
using (CrmService service = CrmSdkProxy.InstantiateService(new Guid(nodes[0].InnerXml.ToString())))
{
// get difference between duedate and creation date
TimeSpan ts = dueDate - creationDate;
return ReturnWorkingDate(creationDate, ts.Days, service);
}
}
// if theres any problems return standard due date.
return dueDate;
}
/// <summary>
/// Accepts a date,number of days to skip and a crmservice instance
/// </summary>
/// <param name="date">DateTime</param>
/// <param name="numberOfWorkingDays">Integer</param>
/// <param name="service">CRMService</param>
/// <returns>DateTime</returns>
public DateTime ReturnWorkingDate(DateTime date, int numberOfWorkingDays, CrmService service)
{
while (numberOfWorkingDays > 0)
{
date = date.AddDays(1);
// if today is not a day included in the business closure calendar or else move on a day
if (IsBusinessWorkingDay(date, service))
{
// if today is not saturday or sunday else move on a day
if (!IsWeekend(date))
{
// this is a working day
numberOfWorkingDays--;
}
}
}
// returns the date advanced by x number of working days
return date;
}
private static bool IsBusinessWorkingDay(DateTime dueDate, CrmService service)
{
// setup working variables
bool workingDay = true;
string guidString = string.Empty;
// create fetchxml
StringBuilder fetchXML = new StringBuilder();
fetchXML.Append("<fetch mapping='logical'>");
fetchXML.Append("<entity name='calendar'>");
fetchXML.Append("<attribute name='calendarid'/>");
fetchXML.Append("<filter type='and'>");
fetchXML.Append(" <condition attribute='isshared' operator='eq' value='1'/>");
fetchXML.Append("</filter>");
fetchXML.Append("<filter type='and'>");
fetchXML.Append(" <condition attribute='name' operator='eq' value='Business Closure Calendar'/>");
fetchXML.Append("</filter>");
fetchXML.Append("</entity>");
fetchXML.Append("</fetch>");
// perform fetch
string returnXML = service.Fetch(fetchXML.ToString());
// setup xml document for return xml
XmlDocument xDoc = new XmlDocument();
xDoc.LoadXml(returnXML);
XmlNodeList nodes = xDoc.SelectNodes("resultset/result/calendarid");
// if theres a calendar returned from the fetch then handle it
if (nodes.Count > 0)
{
// get Business closures calendar Guid
guidString = nodes[0].InnerXml.ToString();
// create calendar Guid
Guid calGuid = new Guid(guidString);
// return all calendar columns for this calendar (just need calendarrules)
calendar cal = (calendar)service.Retrieve(EntityName.calendar.ToString(), calGuid, new AllColumns());
// for each rule within the calendarrules collection
foreach (calendarrule cl in cal.calendarrules)
{
// Convert start and end date to DateTime
DateTime holStartDate = Convert.ToDateTime(cl.effectiveintervalstart.Value.ToString());
DateTime holEndDate = Convert.ToDateTime(cl.effectiveintervalend.Value.ToString());
// check if the date passed in is within the startdate / enddate of holidays via rules
if (DateTime.Compare(dueDate, holStartDate) > 0 && DateTime.Compare(dueDate, holEndDate) < 0)
{
workingDay = false;
}
}
}
return workingDay;
}
private static bool IsBusinessWorkingDay(DateTime dueDate, CrmService service)
{
// setup working variables
bool workingDay = true;
string guidString = string.Empty;
// create fetchxml
StringBuilder fetchXML = new StringBuilder();
fetchXML.Append("<fetch mapping='logical'>");
fetchXML.Append("<entity name='calendar'>");
fetchXML.Append("<attribute name='calendarid'/>");
fetchXML.Append("<filter type='and'>");
fetchXML.Append(" <condition attribute='isshared' operator='eq' value='1'/>");
fetchXML.Append("</filter>");
fetchXML.Append("<filter type='and'>");
fetchXML.Append(" <condition attribute='name' operator='eq' value='Business Closure Calendar'/>");
fetchXML.Append("</filter>");
fetchXML.Append("</entity>");
fetchXML.Append("</fetch>");
// perform fetch
string returnXML = service.Fetch(fetchXML.ToString());
// setup xml document for return xml
XmlDocument xDoc = new XmlDocument();
xDoc.LoadXml(returnXML);
XmlNodeList nodes = xDoc.SelectNodes("resultset/result/calendarid");
// if theres a calendar returned from the fetch then handle it
if (nodes.Count > 0)
{
// get Business closures calendar Guid
guidString = nodes[0].InnerXml.ToString();
// create calendar Guid
Guid calGuid = new Guid(guidString);
// return all calendar columns for this calendar (just need calendarrules)
calendar cal = (calendar)service.Retrieve(EntityName.calendar.ToString(), calGuid, new AllColumns());
// for each rule within the calendarrules collection
foreach (calendarrule cl in cal.calendarrules)
{
// Convert start and end date to DateTime
DateTime holStartDate = Convert.ToDateTime(cl.effectiveintervalstart.Value.ToString());
DateTime holEndDate = Convert.ToDateTime(cl.effectiveintervalend.Value.ToString());
// check if the date passed in is within the startdate / enddate of holidays via rules
if (DateTime.Compare(dueDate, holStartDate) > 0 && DateTime.Compare(dueDate, holEndDate) < 0)
{
workingDay = false;
}
}
}
return workingDay;
}
now that the above code has been compiled into a your workflow assembly to utilise the assembly you also need to add the function to the workflow config file which is located at C:\Program Files\Microsoft CRM\Server\bin\assembly
<method name="Change Due Date"
assembly="WorkflowAssembly.dll" typename="WorkflowAssembly.DateTimeFunctions"
methodname="ChangeDueDate" timeout="7200"
group="Date and time functions">
<parameter name="Caller" datatype="caller"/>
<parameter name="Creation Date" datatype="datetime" />
<parameter name="DueDate" datatype="datetime" />
<result datatype="datetime"/>
</method>
Open up the file and and paste the above into it. Basically its just a function definition which shows which class/methods within the dll are used and what parameters are expected.
Once you've copied over the DLL to C:\Program Files\Microsoft CRM\Server\bin\assembly and edited the workflow.config file within the same location reset IIS and the MSCRM Workflow Service.
Now if all is going well when you go into Workflow Manager and select the workflow you wish to use this functionality in. ( I used this in a task workflow which when the task was created automatically would change the due date to a working day x days in the future).
Goto insert action --> Call Assembly --> Date Time Functions --> Change Due Date and select the function

Figure 1
Click on the newly created function within the workspace and a new screen will popup (as per Figure 2). Double click the parameter called Creation Date and populate (as shown in figure 3), repeat for due date.

Figure 2
Figure 3
Well its my longest post yet and may well not be very clear (rushed it out in work) so if you have any questions or need anything clarifying give me a shout. Hopefully it will help somebody.