Microsoft Dynamics 365 Blog

Guest blogger and CRM MVP Aaron Elder provides a (in his words) ‘worthy’ blog today. Our technical reviewers give this post high marks and kudos.

The Microsoft Dynamics CRM Service Scheduling Engine is a powerful and mysterious system.  As a CRM developer you may be asking will this engine meet your needs?  It’s hard to say because right now there is insufficient documentation and we need more information on how this thing actually works.  In this post I hope to document a small portion of the system and provide some sample code to show how to programmatically define an availability schedule for a piece of equipment.

A good place to start to understand the basics of the Service Scheduling Engine is to read the following document (that I helped write):  Service Scheduling: Optimizations and Considerations (See Pages: 5,6,7)

The scope of this post is to explain the basics of Calendar Rules and how you can programmatically create a set of rules for a “resource”, which is a person or thing and in this case is a piece of equipment.  For this sample, you will need to understand the following CRM entities:

Entity

Description

Calendar

A set of ordered rules that define free/busy times for users, business units, and services, and for resources or resource groups. Examples of events are working and non-working hours, vacations, appointments and weekly meetings. Calendars support recurring events and can be nested. In addition to time blocks, a calendar also specifies capacity rules in terms of ‘effort required’ for a specific resource and appointment type and ‘effort available’ per duration by a resource in their respective calendars.

CalendarRule

A rule defining free and busy times for a service and for resources or resource groups (for example, working, non-working, vacation, blocked).

The mapping between a Calendar and a CalendarRule is essentially 1:1 or 1:N.  A CalendarRule can also link back to an “inner calendar”.  This is required because a CalendarRule cannot exist with a Calendar, so if you want to define another Rule you need another Calendar.  CalendarRules that have no inner calendar are referred to as leafs and the ones that do are roots.

Equipment

A type of resource representing either a piece of equipment, such as a tool, or a facility or location, such as a room.  Every Equipment has a single root CalendarRule.  You can determine the base rule by looking at the CalendarId on the Equipment entity.

For this post, I am going to break Calendar Rules into sub-types:

Entity

Description

Root

A CalendarRule that contains an Inner Calendar (IE has nested rules).

Leaf

A CalendarRule that does not contain an Inner Calendar and as such is the end of the “branch”.

For the purpose of this post, the relationships look something like this:

clip_image001

The following source sample creates a new schedule for a piece of equipment and says the equipment is available from 9AM to 5PM, every day and takes a 1 hour “lunch break” from 12-1pm.  The resulting calendar is displayed at the end.

Important Notes

  • This is a basic .NET 2.0 Console Application
  • Be sure to update the OrganizationName
  • Be sure to create a new Facility / Equipment in CRM and set its GUID on the testResource retrieve
  • Be sure to read the COMMENTS in this code, I have put a lot of useful descriptions of what is going in throughout.

class Program
{

                static CrmService CurrentCrmService 
                {

                                get 
                                {

                                                CrmService service = new CrmService(); 
                                                service.Credentials = System.Net.CredentialCache.DefaultCredentials; 
                                                service.CrmAuthenticationTokenValue = new CrmAuthenticationToken(); 
                                                service.CrmAuthenticationTokenValue.OrganizationName = “AscentiumCrmDev”;

                                    return service; 
                        } 
            }

 

                static void Main(string[] args)
                {

                                CrmService service = Program.CurrentCrmService;

                                // Retrieve a “known” piece of equipment – It is assumed this equipment has been created via the UI 
                                // When CRM’s UI creates a new equipment, it gets a default “All Day” Calendar Rule defined 
                                // This sample will override that calendar rule with most specific details

                                equipment testResource = (equipment)service.Retrieve(EntityName.equipment.ToString(), new Guid(“905B57D3-60DA-DC11-9EBF-0003FF0E509E”), new AllColumns());

                        #region Create Level 0 Rule

 

                        // ======================================================================
                        // ROOT – The “Level 0” Rule defines the OCCURANCE and the RANGE (start/end time) of the rule 
                        // ======================================================================

                                // Create an “empty” calendar, we will use this later to bind to the Level 1 rule 
                                Guid innerCalendarId = Program.CreateEmptyCalendar();

                                // Create rule to depict 8 am – 5pm  schedule 
                                calendarrule workingHoursRule = new calendarrule(); 

                                // This is a “root” rule and we want it to be the base on which we build 
                                workingHoursRule.rank = new CrmNumber(0);

                                // User’s timezone and description of the rule 
                                workingHoursRule.timezonecode = new CrmNumber(4); 

                                // Provide a user readable description 
                                workingHoursRule.description = “L0 – Root Rule”;

                        // One full day (1440 minutes) 
                        workingHoursRule.duration = new CrmNumber((int)TimeSpan.FromDays(1).TotalMinutes);

                        // Happens “daily, repeating every day” – AKA “all days” 
                        workingHoursRule.pattern = “FREQ=DAILY;INTERVAL=1;”;

                        // “Since the first day CRM is aware of” 
                        workingHoursRule.starttime = CrmDateTime.FromUniversal(CrmDateTime.MinValue);

                        // The inner “empty” calendar will be used later with a Level 1 Rule 
                        workingHoursRule.innercalendarid = new Lookup(EntityName.calendar.ToString(), innerCalendarId);

                        // Get the Root Calendar, so we can update it 
                        calendar cal = (calendar)service.Retrieve(EntityName.calendar.ToString(), testResource.calendarid.Value, new AllColumns());

                        // Append our Rule 
                        cal.calendarrules = Program.AddRuleToArray(cal.calendarrules, workingHoursRule);

                        // Update the Calendar so it adds our new rule 
                        service.Update(cal);

                        #endregion

#region Create Level 1 Rule – To hold the working hours

 

                                // ================================================================
                                // LEAF – The “Level 1” Rule defines the SCOPE and DETAILS (availability, etc) of the rule 
                                // =================================================================

                        calendarrule level1Rule = new calendarrule(); 
                        level1Rule.issimple = new CrmBoolean(true);

                        // This is a base rule that can be overridden 
                        level1Rule.rank = new CrmNumber(0); 
                        level1Rule.timezonecode = new CrmNumber(4);

 

                                // Day starts at midnight or Offset of “0”, we need to start at 9AM 
                                level1Rule.offset = new CrmNumber((int)TimeSpan.FromHours(9).TotalMinutes);

                        // The person will work for 8 hours so 8 * 60 
                        level1Rule.duration = new CrmNumber((int)TimeSpan.FromHours(8).TotalMinutes); 
                        level1Rule.description = “L1 – Daily Working Hours Rule”;

                        // The person is available and able to be scheduled during this time 
                        level1Rule.timecode = new CrmNumber((int)TimeCode.Available); 
                        level1Rule.subcode = new CrmNumber((int)SubCode.Schedulable);

                        calendar level0Cal = (calendar)service.Retrieve(EntityName.calendar.ToString(), innerCalendarId, new AllColumns()); 
                        level0Cal.calendarrules = Program.AddRuleToArray(level0Cal.calendarrules, level1Rule);

 

                                // Update the Calendar so it adds our new rule 
                                service.Update(level0Cal);

                        #endregion

#region Create another Level 1 Rule – To Define a “break” in the working hours

                        // ==================================================================
                        // LEAF – The “Level 1” Rule defines the SCOPE and DETAILS (availability, etc) of the rule 
                        // ===================================================================

                        calendarrule lunchBreak = new calendarrule(); 
                        lunchBreak.issimple = new CrmBoolean(true);

                        // Since the hours for the day are defined, we have to raise the importance of this to override 12-1 
                        lunchBreak.rank = new CrmNumber(1); 
                        lunchBreak.timezonecode = new CrmNumber(4);

 

                        // Break starts at 12 NOON 
                        lunchBreak.offset = new CrmNumber((int)TimeSpan.FromHours(12).TotalMinutes);

 

                        // Break lasts 1 hour 
                        lunchBreak.duration = new CrmNumber((int)TimeSpan.FromHours(1).TotalMinutes); 
                        lunchBreak.description = “L1 – Lunch Break Rule”;

 

                        // They are not available during lunch 
                        lunchBreak.timecode = new CrmNumber((int)TimeCode.Unavailable);

                        // Because it is a break 
                        lunchBreak.subcode = new CrmNumber((int)SubCode.Break);

                        level0Cal = (calendar)service.Retrieve(EntityName.calendar.ToString(), innerCalendarId, new AllColumns()); 
                        level0Cal.calendarrules = Program.AddRuleToArray(level0Cal.calendarrules, lunchBreak);

                        // Update the Calendar so it adds our new rule 
                        service.Update(level0Cal);

                        #endregion

                        Console.WriteLine(“Complete – Press any key to continue…”); 
                        Console.ReadLine(); 
            }

                /// <summary> 
                /// Extends the Calendar Rule Array with one additional item 
                /// </summary>

                static calendarrule[] AddRuleToArray(calendarrule[] currentRules, calendarrule ruleToAdd) 
                {

                                calendarrule[] newRules = new calendarrule[currentRules.Length + 1]; 
                                currentRules.CopyTo(newRules, 0); 
                                newRules[newRules.Length – 1] = ruleToAdd;

                        return newRules; 
            }

 

                /// <summary> 
                /// Creates a “empty” Calendar and returns its CRM ID 
                /// </summary>

                static Guid CreateEmptyCalendar() 
                {

                                CrmService service = Program.CurrentCrmService;

                        // Determine the current user 
                        WhoAmIResponse currentUser = (WhoAmIResponse)service.Execute(new WhoAmIRequest());

                                calendar newCal = new calendar(); 
                                newCal.name = “Empty Calendar”
                                newCal.description = “Holdering Calendar”
                                newCal.businessunitid = new Lookup(); 
                                newCal.businessunitid.Value = currentUser.BusinessUnitId;

                        // Create the Calendar 
                        Guid id = service.Create(newCal);

                        return id; 
            }
}

Here is the resulting schedule for this equipment, as seen from the UI:

clip_image002

Drilling into a particular day shows the “break”, since it’s rank is higher than the 9-5, it overrides as such…

clip_image003

Cheers,

Aaron Elder

CRM MVP and Chief CRM Architect at Ascentium

We're always looking for feedback and would like to hear from you. Please head to the Dynamics 365 Community to start a discussion, ask questions, and tell us what you think!