Using booking rules in schedule board in field service in Dynamics 365

Posted on Posted in Dynamics 365, MS Dynamics CRM

It is always fun to work with Microsoft Dynamics 365 Field Service module and it gets more interesting when there are requirements to extend it.

In this example, I had a requirement to add a booking rule on Schedule Board in Field Service. When a user tries to move a resource from a AM slot (time group details) to PM or any other slot which has been set on a Booking then user should be warned that new slot is different to the one which has already been set on Booking (in case customer has been advised already that engineer will be arriving in the morning rather than evening etc.)

As it is a complex scenario where we have to get details from couple of related entities to check that newly selected time falls inside of existing slot or if it is outside and depending on other checks return a message to user.

Above is a simple case however we can have even handle more complex scenarios, which are not easy to handle in just JavaScript.

To achieve this I had to use following

  • Workflow Activity
  • Action
  • Web Resource
  1. Workflow Activity

Workflow activity is used to do all complex type of work and will return result back to Action. I will not write down whole code here however, it will take three parameters

    • Resource Requirement
    • New Start Time

It will return two parameters back

    • Result (some string message)
    • IsIDifferent (true or false)

  1. Action

Action is called inside our Web Resource and this will take same parameters as our Workflow activity. First thing we will check that it is an update and not create as on create of a new booking we are not required to run this check and then pass parameters to our Workflow activity. Once all work is complete in WFA, results are returned back to Action’s output parameters.

  1. Web Resource

Web Resource is core part of Booking Rule in Dynamics 365 Field Service. We can achieve a lot using just Web Resource and calling a method however, in complex scenarios it is better to split it and handle things on server side.

  1. Set up a booking rule

To set up a booking rule, login to Dynamics 365 and go to Field Service related app. Once in, select Resources at the left bottom (where it says change area) and go to booking rules.

Then create a new booking rule

Give it a name, select web resource you deployed, and add function name needs to be called. Save and close record.


Sample script (web resource) is below.


var Rokhri; //namespace to keep things clean
(function (Rokhri) {
Rokhri.IsTimeGroupDifferent = {
url: Xrm.Page.context.getClientUrl() + "/api/data/v9.1/",
actionName: "Rokhri_RokhriCheckifnewlyselectedTimeGroupissameastheonealreadyset",
actionInputParameters: function (context) {
let inputParameters = {};
if (context.isUpdate) {
console.log('Resource Requirement Id: ' + context.newValues.ResourceRequirementId);

//passing input parameters - we can add more if needed at action level
//we are only passing below as required at Action level
inputParameters = {
"StartTimeNew": context.newValues.StartTime,
"ResourceRequirementNew": {
"@odata.type": "Microsoft.Dynamics.CRM.msdyn_resourcerequirement",
"msdyn_resourcerequirementid": context.newValues.ResourceRequirementId,
"msdyn_name": ""
},
"IsCreate": context.isCreate //this comes as part of context being passed
};
}
else {
console.log('Resource Requirement Id: ' + context.newValues.ResourceRequirementId);
inputParameters = {
"IsCreate": context.isCreate
};
}
return JSON.stringify(inputParameters);
},
context: null,
ruleResult: {
IsValid: true,
Message: "",
Type: ""
},
outputParameters: {
isError: false,
isWarning: false,
errorMessage: "",
warningMessage: "",
WorkOrderId: ""
},
TimeGroupDetailsValidation: function (context) {
this.context = context;
FieldServiceBookingRuleAction.callBookingRuleAction(this);
return this.ruleResult;
},
errorCallback: function () {
//we can add logic here or call a new function;
},
warningCallback: function () {
//we can add logic here or call a new function;
},
successCallback: function () {
//we can add logic here or call a new function;
}
};

//this is our function to get WorkOrderId - this is a common in our namespace and can be used/called from other web resources too
Rokhri.GetWorkOrderIdViaResourceRequirement = function (resourceReqId) {
let workOrderId = null;
//lets get work order id first
if (resourceReqId !== null) {
let req = new XMLHttpRequest();
req.open("GET", Xrm.Page.context.getClientUrl() + "/api/data/v9.1/msdyn_resourcerequirements(" + resourceReqId + ")?$select=_msdyn_workorder_value", false);
req.setRequestHeader("OData-MaxVersion", "4.0");
req.setRequestHeader("OData-Version", "4.0");
req.setRequestHeader("Accept", "application/json");
req.setRequestHeader("Content-Type", "application/json; charset=utf-8");
req.setRequestHeader("Prefer", "odata.include-annotations=\"*\"");
req.onreadystatechange = function () {
if (this.readyState === 4) {
req.onreadystatechange = null;
if (this.status === 200) {
let result = JSON.parse(this.response);
workOrderId = result["_msdyn_workorder_value"];
} else {
Xrm.Navigation.openAlertDialog(alertStrings, alertOptions);
}
}
};
req.send();
}

return workOrderId;
};

//function to call our action (in this case this been created on Work Order but can be creation on a different entity too)
let FieldServiceBookingRuleAction = (function () {
function FieldServiceBookingRuleAction() {
}

FieldServiceBookingRuleAction.callBookingRuleAction = function (dummyparam) {
let resourceReqId = dummyparam.context.newValues.ResourceRequirementId.replace(/[{}]/g, "");
let workOrderId = Rokhri.GetWorkOrderIdViaResourceRequirement(resourceReqId);

//we have work order now, lets call action
let oDataEndpoint = dummyparam.url + "msdyn_workorders(" + workOrderId + ")/Microsoft.Dynamics.CRM." + dummyparam.actionName;
req = new XMLHttpRequest();
req.open("POST", oDataEndpoint, false);
req.setRequestHeader("Accept", "application/json");
req.setRequestHeader("Content-Type", "application/json; charset=utf-8");
req.setRequestHeader("OData-MaxVersion", "4.0");
req.setRequestHeader("OData-Version", "4.0");
req.onreadystatechange = function () {
if (req.readyState === 4) {
req.onreadystatechange = null;
if (req.status === 200) {
dummyparam.outputParameters = JSON.parse(req.response);
if (dummyparam.outputParameters.isError) {
dummyparam.ruleResult.IsValid = false;
dummyparam.ruleResult.Message = dummyparam.outputParameters.errorMessage;
dummyparam.ruleResult.Type = 'error';
return;
}
else if (dummyparam.outputParameters.isWarning) {
dummyparam.ruleResult.IsValid = false;
dummyparam.ruleResult.Message = dummyparam.outputParameters.warningMessage;
dummyparam.ruleResult.Type = 'warning';
return;
}
else {
dummyparam.ruleResult.IsValid = true;
dummyparam.ruleResult.Message = '';
dummyparam.ruleResult.Type = '';
if (dummyparam.outputParameters.IsDifferent) {
dummyparam.ruleResult.IsValid = false;
dummyparam.ruleResult.Type = 'warning';
dummyparam.ruleResult.Message = "You are attempting to move this Booking from its Appointed Slot";
}

dummyparam.WorkOrderId = workOrderId;
return;
}
}
else {
alert('Error calling Rule Action. Response = ' + req.response + ', Status = ' + req.statusText);
}
}
};
req.send(dummyparam.actionInputParameters(dummyparam.context));
};
return FieldServiceBookingRuleAction;
}());
})(Rokhri || (Rokhri = {}));

Leave a Reply