Print

Zoho People: Reject an Application for Leave in Deluge based on Criteria

What?
This article documents how to stop an employee from applying for leave when already 80% of their team have already booked the same date off.  My brief was:


In other words, if there are 10 people in a department/team, and 8 people have booked tomorrow off, prevent a 9th team member booking tomorrow off.

Why?
I've written this article as it took a while to achieve. The Zoho People application and its API are documented but there are problems with the documentation at the time that this article was written. One is that there are 2 sets of documentation and the other is that the majority of the documentation doesn't refer to OAuth 2.0/json and instead uses the deprecated authtoken v1 and XML.

How?
The trick in this solution was more about the process and what that process was.

Reading the brief, and having written many custom functions for other Zoho apps, it would stand to reason that the process would be: allow a user to apply for leave, then on save/submit, auto approve/reject the application based on a custom function that could calculate the percentage of staff who already booked the requested dates as leave.

[Warning: red herring ahead] For this process, we setup connections choosing to make use of the Zoho People API and we could update any of the fields EXCEPT for "ApprovalStatus" and would get the following response when attempting:
copyraw
{
  "response": {
    "message": "Error occurred",
    "uri": "/api/forms/json/P_ApplyLeave/updateRecord",
    "errors": {
      "code": 7013,
      "message": "Field name 'ApprovalStatus' is invalid "
    },
    "status": 1
  }
}
  1.  { 
  2.    "response": { 
  3.      "message": "Error occurred", 
  4.      "uri": "/api/forms/json/P_ApplyLeave/updateRecord", 
  5.      "errors": { 
  6.        "code": 7013, 
  7.        "message": "Field name 'ApprovalStatus' is invalid " 
  8.      }, 
  9.      "status": 1 
  10.    } 
  11.  } 

[Warning: Another red herring ahead] We tried various other options such as adding a tickbox that would be used in an approval workflow but ran into issues when trying to update the record:
copyraw
{
  "response": {
    "message": "Error occurred",
    "uri": "/api/forms/json/P_ApplyLeave/updateRecord",
    "errors": {
      "code": 7115,
      "message": {
        "From": "Sorry, this record cannot be edited."
      }
    },
    "status": 1
  }
}
  1.  { 
  2.    "response": { 
  3.      "message": "Error occurred", 
  4.      "uri": "/api/forms/json/P_ApplyLeave/updateRecord", 
  5.      "errors": { 
  6.        "code": 7115, 
  7.        "message": { 
  8.          "From": "Sorry, this record cannot be edited." 
  9.        } 
  10.      }, 
  11.      "status": 1 
  12.    } 
  13.  } 

[The working solution] I'll cut to the quick solution, which was not to try to update the form at all but instead to prevent the user from applying for leave in the first place:
  1. Create a validation custom function:
    1. Login to ZohoPeople as an admin, Go to Settings > Customization > Forms > Leave
    2. Under "Form Properties" > Tick the box under "Validation" for "Custom Validation" to create a function:
      Zoho People - Form Properties - Validation - Custom Function
  2. Ensure you select parameters to input rather than retrieving the record data:
    1. I'm calling the Function Name "RejectLeave" then click on Edit Params
      Zoho People - Creating a Function
    2. I'm calling the Method Name "OnValidate" and I add the following parameters:
      • p_RequestID specified as "ID (Leave)" ... this is the ID of the Leave form record.
      • p_Dept as the name of the "Department" ... this is found on the Employee form not the leave form.
      • p_LeaveFrom this is the value of "From" on the "Leave" form.
      • p_LeaveTill this is the value of "To" on the "Leave" form.
      • Giving you something as per the following screenshot:
        Zoho People - Setting function parameters
      • Click on "Save" to accept this
      Note: It is important to add these parameters rather than trying to retrieve the details with a zoho.people.getRecordByID("P_ApplyLeave",v_RequestId) in the function because on application, the record doesn't properly exist in the system yet and the delay causes errors further down the line. Just add these parameters as per my above instructions for a more robust solution.
  3. [See below for full code] Enter the code to determine total number of employees in this department/team:
    1. To determine the total number of employees, we will use zoho.people.getRecords as a search:
      copyraw
      // get total "team members" in department
      l_SearchCriterias = List();
      m_SearchCriteria = Map();
      m_SearchCriteria.put("searchField","Department");
      m_SearchCriteria.put("searchOperator","Is");
      m_SearchCriteria.put("searchText",v_DeptName);
      l_SearchCriterias.add(m_SearchCriteria);
      l_DeptEmployees = zoho.people.getRecords("P_Employee",0,200,l_SearchCriterias);
      1.  // get total "team members" in department 
      2.  l_SearchCriterias = List()
      3.  m_SearchCriteria = Map()
      4.  m_SearchCriteria.put("searchField","Department")
      5.  m_SearchCriteria.put("searchOperator","Is")
      6.  m_SearchCriteria.put("searchText",v_DeptName)
      7.  l_SearchCriterias.add(m_SearchCriteria)
      8.  l_DeptEmployees = zoho.people.getRecords("P_Employee",0,200,l_SearchCriterias)
  4. Enter the code to search for and count holiday clashes:
    1. First we need the code to search for other requests with these dates, we're going to use a range of 1 month before and 1 month after the requested date (so this works as long as no one books longer than a month in one go - increase the submonth/addmonth parameter if an employee can book longer than a month):
      copyraw
      // set criteria to search for clashing annual leave dates (pre/post 1 month check)
      l_SearchCriterias = List();
      m_SearchCriteria = Map();
      m_SearchCriteria.put("searchField","From");
      m_SearchCriteria.put("searchOperator","After");
      m_SearchCriteria.put("searchText",v_GivenDateFrom.subMonth(1).toString("dd-MMM-yyyy"));
      l_SearchCriterias.add(m_SearchCriteria);
      m_SearchCriteria = Map();
      m_SearchCriteria.put("searchField","To");
      m_SearchCriteria.put("searchOperator","Before");
      m_SearchCriteria.put("searchText",v_GivenDateTill.addMonth(1).toString("dd-MMM-yyyy"));
      l_SearchCriterias.add(m_SearchCriteria);
      m_SearchCriteria = Map();
      m_SearchCriteria.put("searchField","Leavetype");
      m_SearchCriteria.put("searchOperator","Contains");
      m_SearchCriteria.put("searchText","Leave");
      l_SearchCriterias.add(m_SearchCriteria);
      l_AllTimeOffs = zoho.people.getRecords("P_ApplyLeave",0,200,l_SearchCriterias);
      1.  // set criteria to search for clashing annual leave dates (pre/post 1 month check) 
      2.  l_SearchCriterias = List()
      3.  m_SearchCriteria = Map()
      4.  m_SearchCriteria.put("searchField","From")
      5.  m_SearchCriteria.put("searchOperator","After")
      6.  m_SearchCriteria.put("searchText",v_GivenDateFrom.subMonth(1).toString("dd-MMM-yyyy"))
      7.  l_SearchCriterias.add(m_SearchCriteria)
      8.  m_SearchCriteria = Map()
      9.  m_SearchCriteria.put("searchField","To")
      10.  m_SearchCriteria.put("searchOperator","Before")
      11.  m_SearchCriteria.put("searchText",v_GivenDateTill.addMonth(1).toString("dd-MMM-yyyy"))
      12.  l_SearchCriterias.add(m_SearchCriteria)
      13.  m_SearchCriteria = Map()
      14.  m_SearchCriteria.put("searchField","Leavetype")
      15.  m_SearchCriteria.put("searchOperator","Contains")
      16.  m_SearchCriteria.put("searchText","Leave")
      17.  l_SearchCriterias.add(m_SearchCriteria)
      18.  l_AllTimeOffs = zoho.people.getRecords("P_ApplyLeave",0,200,l_SearchCriterias)
    2. Then we need the code to loop through all these matching records (and approved) and determine if it clashes with the requested dates:
      copyraw
      for each  r_TimeOff in l_AllTimeOffs
      {
          if(!isnull(r_TimeOff.get("Employee_ID")))
          {
              v_ThisStatus = r_TimeOff.get("ApprovalStatus");
              v_ThisFrom = r_TimeOff.get("From").toDate();
              v_ThisTill = r_TimeOff.get("To").toDate();
              v_ClashOrNot = "NO Clash with ";
              if(v_ThisStatus!="Cancelled")
              {
                  if(v_CompareDateFrom <= v_ThisFrom && v_CompareDateTill >= v_ThisFrom || v_CompareDateFrom <= v_ThisTill && v_CompareDateTill >= v_ThisTill)
                  {
                      v_TotalClashes = v_TotalClashes + 1;
                      v_ClashOrNot = "Clashes with ";
                  }
                  info v_ClashOrNot + r_TimeOff.get("Employee_ID") + ":" + v_ThisFrom + "-" + v_ThisTill;
              }
          }
      }
      1.  for each  r_TimeOff in l_AllTimeOffs 
      2.  { 
      3.      if(!isnull(r_TimeOff.get("Employee_ID"))) 
      4.      { 
      5.          v_ThisStatus = r_TimeOff.get("ApprovalStatus")
      6.          v_ThisFrom = r_TimeOff.get("From").toDate()
      7.          v_ThisTill = r_TimeOff.get("To").toDate()
      8.          v_ClashOrNot = "NO Clash with "
      9.          if(v_ThisStatus!="Cancelled") 
      10.          { 
      11.              if(v_CompareDateFrom <= v_ThisFrom && v_CompareDateTill >= v_ThisFrom || v_CompareDateFrom <= v_ThisTill && v_CompareDateTill >= v_ThisTill) 
      12.              { 
      13.                  v_TotalClashes = v_TotalClashes + 1
      14.                  v_ClashOrNot = "Clashes with "
      15.              } 
      16.              info v_ClashOrNot + r_TimeOff.get("Employee_ID") + ":" + v_ThisFrom + "-" + v_ThisTill; 
      17.          } 
      18.      } 
      19.  } 
      Note: Play around with the Status, there are "Pending", "Cancelled" (UK spelling), "Approval Not Enabled" and I think "Approved", "Rejected". We're using the !="Cancelled" and probably !="Rejected" because we want pending ones to also be taken into account (first come first serve basis).
  5. Calculate the percentage of people already booked off:
    1. A very quick percentage calculation snippet of code to work this out:
      copyraw
      // calculate percentage
      v_Percentage = (v_TotalClashes / v_TotalDeptEmployees * 100).round(0);
      1.  // calculate percentage 
      2.  v_Percentage = (v_TotalClashes / v_TotalDeptEmployees * 100).round(0)
  6. Return an error if overbooked, no error if not:
    1. To return an error (I know we set the function to return "Map" and are using a "Collection" but a Collection is either a list or a map):
      copyraw
      // return response as error popup
          v_OutputMessage = "Sorry! Leave has been rejected due to too many employees booking the same dates off.";
          c_Response = Collection();
          c_Response.insert("EmployeeID":v_OutputMessage);
          c_Error = Collection();
          c_Error.insert("errors":c_Response);
          return c_Error;
      1.  // return response as error popup 
      2.      v_OutputMessage = "Sorry! Leave has been rejected due to too many employees booking the same dates off."
      3.      c_Response = Collection()
      4.      c_Response.insert("EmployeeID":v_OutputMessage)
      5.      c_Error = Collection()
      6.      c_Error.insert("errors":c_Response)
      7.      return c_Error; 
      Displays to the user as per the following screenshot AND prevents the user from submitting this until they provide a date that doesn't clash: Zoho People - Return Error
    2. To return an ok response, as in application accepted, we simply don't return an error so this might be completely optional however the Zoho People system will require a return value for this function:
      copyraw
      // return response as ok popup (doesn't really do anything: just says "Record added successfully")
          v_OutputMessage = "Thank you for submitting your application for leave.";
          c_Response = Collection();
          c_Response.insert("EmployeeID":v_OutputMessage);
          c_Ok = Collection();
          c_Ok.insert("response":c_Response);
          return c_Ok;
      1.  // return response as ok popup (doesn't really do anything: just says "Record added successfully") 
      2.      v_OutputMessage = "Thank you for submitting your application for leave."
      3.      c_Response = Collection()
      4.      c_Response.insert("EmployeeID":v_OutputMessage)
      5.      c_Ok = Collection()
      6.      c_Ok.insert("response":c_Response)
      7.      return c_Ok; 
      Displays to the user as per the following screenshot: Zoho People - Record added successfully

The full code for this function
As mentioned, you may need to tweak the search for records with status as approved/pending but this is what we use:
copyraw
/* *******************************************************************************
Function:       OnValidate
Trigger:        Function executed on the application of a leave
Inputs:         string p_RequestID,string p_Dept,string p_LeaveFrom,string p_LeaveTill
Outputs:        map/collection

Date Created:   2020-10-27 (Joel Lipman)
                - Initial release
                - Workflow to calculate 80% dept workforce
                - Returns response as popup
		- Cancels submit with error popup if over 80%, else success
******************************************************************************* */
//
// declare
v_TotalClashes = 0;
v_TotalDeptEmployees = 0;
v_OutputMessage = "";
//
// get record details
v_RequestId = ifnull(p_RequestID,0).toLong();
v_DeptName = ifnull(p_Dept,"");
v_GivenDateFrom = p_LeaveFrom;
v_GivenDateTill = p_LeaveTill;
v_CompareDateFrom = v_GivenDateFrom.toDate();
v_CompareDateTill = v_GivenDateTill.toDate();
//
// get total "team members" in department
l_SearchCriterias = List();
m_SearchCriteria = Map();
m_SearchCriteria.put("searchField","Department");
m_SearchCriteria.put("searchOperator","Is");
m_SearchCriteria.put("searchText",v_DeptName);
l_SearchCriterias.add(m_SearchCriteria);
l_DeptEmployees = zoho.people.getRecords("P_Employee",0,200,l_SearchCriterias);
v_TotalDeptEmployees = l_DeptEmployees.size();
// ensure total is not zero as we need to divide by it (defaulting to 2 because if 1 then it's 100%, as 2 it's 50% might have it booked off)
v_TotalDeptEmployees = if(v_TotalDeptEmployees1)
{
    //
    // return response as error popup
    v_OutputMessage = "Sorry! Leave has been rejected due to too many employees booking the same dates off.";
    c_Response = Collection();
    c_Response.insert("EmployeeID":v_OutputMessage);
    c_Error = Collection();
    c_Error.insert("errors":c_Response);
    return c_Error;
}
else
{
    //
    // return response as ok popup (doesn't really do anything: just says "Record added successfully")
    v_OutputMessage = "Thank you for submitting your application for leave.";
    c_Response = Collection();
    c_Response.insert("EmployeeID":v_OutputMessage);
    c_Ok = Collection();
    c_Ok.insert("response":c_Response);
    return c_Ok;
}
  1.  /* ******************************************************************************* 
  2.  Function:       OnValidate 
  3.  Trigger:        Function executed on the application of a leave 
  4.  Inputs:         string p_RequestID,string p_Dept,string p_LeaveFrom,string p_LeaveTill 
  5.  Outputs:        map/collection 
  6.   
  7.  Date Created:   2020-10-27 (Joel Lipman) 
  8.                  - Initial release 
  9.                  - Workflow to calculate 80% dept workforce 
  10.                  - Returns response as popup 
  11.          - Cancels submit with error popup if over 80%, else success 
  12.  ******************************************************************************* */ 
  13.  // 
  14.  // declare 
  15.  v_TotalClashes = 0
  16.  v_TotalDeptEmployees = 0
  17.  v_OutputMessage = ""
  18.  // 
  19.  // get record details 
  20.  v_RequestId = ifnull(p_RequestID,0).toLong()
  21.  v_DeptName = ifnull(p_Dept,"")
  22.  v_GivenDateFrom = p_LeaveFrom; 
  23.  v_GivenDateTill = p_LeaveTill; 
  24.  v_CompareDateFrom = v_GivenDateFrom.toDate()
  25.  v_CompareDateTill = v_GivenDateTill.toDate()
  26.  // 
  27.  // get total "team members" in department 
  28.  l_SearchCriterias = List()
  29.  m_SearchCriteria = Map()
  30.  m_SearchCriteria.put("searchField","Department")
  31.  m_SearchCriteria.put("searchOperator","Is")
  32.  m_SearchCriteria.put("searchText",v_DeptName)
  33.  l_SearchCriterias.add(m_SearchCriteria)
  34.  l_DeptEmployees = zoho.people.getRecords("P_Employee",0,200,l_SearchCriterias)
  35.  v_TotalDeptEmployees = l_DeptEmployees.size()
  36.  // ensure total is not zero as we need to divide by it (defaulting to 2 because if 1 then it's 100%, as 2 it's 50% might have it booked off) 
  37.  v_TotalDeptEmployees = if(v_TotalDeptEmployees1) 
  38.  { 
  39.      // 
  40.      // return response as error popup 
  41.      v_OutputMessage = "Sorry! Leave has been rejected due to too many employees booking the same dates off."
  42.      c_Response = Collection()
  43.      c_Response.insert("EmployeeID":v_OutputMessage)
  44.      c_Error = Collection()
  45.      c_Error.insert("errors":c_Response)
  46.      return c_Error; 
  47.  } 
  48.  else 
  49.  { 
  50.      // 
  51.      // return response as ok popup (doesn't really do anything: just says "Record added successfully") 
  52.      v_OutputMessage = "Thank you for submitting your application for leave."
  53.      c_Response = Collection()
  54.      c_Response.insert("EmployeeID":v_OutputMessage)
  55.      c_Ok = Collection()
  56.      c_Ok.insert("response":c_Response)
  57.      return c_Ok; 
  58.  } 

Additional:
We asked Zoho for a solution that might work and were given the following snippet of code (not tested yet!). It looks like we have to fill in the blanks and it's on the same path as we were. The main revelation was the function getEmployeesOnLeave and perhaps this can be combined with a function that gets all employees of a department. The following allows only 1 user in a department to apply for leave in a day.
copyraw
c_Response = Collection();
for each  d in dd.keys()
{
    c_Params= Collection();
    c_Params.insert("date":d);
    c_Params.insert("type":"3");
    c_Params.insert("userId":empid);
    r_ApiResponse= invokeurl
    [
	url :"https://people.zoho.com/people/api/leave/getEmployeesOnLeave" 	
	type :POST 	
	parameters: c_Params.toMap() 	
	connection: “peoplecf”
    ];
    v_EmployeeCount = r_ApiResponse.get("response").get("result").get("employeeCount");
    if(v_EmployeeCount >= 1)
    {
	c_Response.insert("err1":"You cannot apply leave as another employee has already applied leave in this period");
    }
}
c_Return = Collection();
c_Return.insert("errors":c_Response);
return c_Return;
  1.  c_Response = Collection()
  2.  for each  d in dd.keys() 
  3.  { 
  4.      c_Params= Collection()
  5.      c_Params.insert("date":d)
  6.      c_Params.insert("type":"3")
  7.      c_Params.insert("userId":empid)
  8.      r_ApiResponse= invokeUrl 
  9.      [ 
  10.      url :"https://people.zoho.com/people/api/leave/getEmployeesOnLeave" 
  11.      type :POST 
  12.      parameters: c_Params.toMap() 
  13.      connection: "peoplecf" 
  14.      ]
  15.      v_EmployeeCount = r_ApiResponse.get("response").get("result").get("employeeCount")
  16.      if(v_EmployeeCount >= 1) 
  17.      { 
  18.      c_Response.insert("err1":"You cannot apply leave as another employee has already applied leave in this period")
  19.      } 
  20.  } 
  21.  c_Return = Collection()
  22.  c_Return.insert("errors":c_Response)
  23.  return c_Return; 

Source(s):
Category: Zoho :: Article: 736