Skip to content
This repository has been archived by the owner on Feb 12, 2022. It is now read-only.

Time off request API methods #57

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
125 changes: 117 additions & 8 deletions PyBambooHR/PyBambooHR.py
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,6 @@ def __init__(self, subdomain='', api_key='', datatype='JSON', underscore_keys=Fa
"commisionDate": ("date", ""),
"commissionAmount": ("currency", ""),
"commissionComment": ("text", ""),
"commissionComment": ("text", ""),
"benefitClassDate": ("date", ""),
"benefitClassClass": ("list", ""),
"benefitClassChangeReason": ("list", ""),
Expand Down Expand Up @@ -245,6 +244,35 @@ def _format_report_xml(
xml = '''<report output="{0}">\n\t<title>{1}</title>\n\t{2}<fields>\n{3}\t</fields>\n</report>'''.format(report_format, title, xml_filters, xml_fields)
return xml

def _format_time_off_xml(self, request_data):
"""
Utility method for turning dictionary of time_off request data into valid xml.
https://www.bamboohr.com/api/documentation/time_off.php#addRequest
@param request_data: Dictionary containing incoming data for time off request
"""
fields = ['status', 'start', 'end', 'timeOffTypeId', 'amount']
xml = ''
for f in fields:
value = request_data.get(f)
if value:
xml += '\n\t<{0}>{1}</{0}>'.format(f, value)

if request_data.get('notes') and len(request_data.get('notes')) > 0:
notes = ''
for n in request_data['notes']:
f = n.get('type', 'employee')
t = n.get('text', '')
notes += '\n\t\t<note from="{0}">{1}</note>'.format(f, t)
xml += '\n\t<notes>{0}\n\t</notes>'.format(notes)
Comment on lines +260 to +266
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if request_data.get('notes') and len(request_data.get('notes')) > 0:
notes = ''
for n in request_data['notes']:
f = n.get('type', 'employee')
t = n.get('text', '')
notes += '\n\t\t<note from="{0}">{1}</note>'.format(f, t)
xml += '\n\t<notes>{0}\n\t</notes>'.format(notes)
notes = ''
for note in request_data.get('notes', []):
f = note.get('type', 'employee')
t = note.get('text', '')
notes += '\n\t\t<note from="{0}">{1}</note>'.format(f, t)
xml += '\n\t<notes>{0}\n\t</notes>'.format(notes)

Same pattern can be used for dates 😉


if request_data.get('dates') and len(request_data.get('dates')) > 0:
dates = ''
for d in request_data['dates']:
dates += '\n\t\t<date ymd="{0}" amount="{1}" />'.format(d['ymd'], d['amount'])
xml += '\n\t<dates>{0}\n\t</dates>'.format(dates)

return '<request>{0}\n</request>'.format(xml)

def add_employee(self, employee):
"""
API method for creating a new employee from a dictionary.
Expand Down Expand Up @@ -628,24 +656,105 @@ def get_whos_out(self, start_date=None, end_date=None):
# return utils.transform_whos_out(r.content)

def get_time_off_requests(self, start_date=None, end_date=None, status=None, type=None, employee_id=None):
"""
API method for returning a list of time off requests
https://www.bamboohr.com/api/documentation/time_off.php#getRequests
Success response: 200
@return: List containing time off request dictionaries
"""
start_date = utils.resolve_date_argument(start_date)
end_date = utils.resolve_date_argument(end_date)

params = {}
if start_date:
params['start'] = start_date
if end_date:
params['end'] = end_date
if status:
params['status'] = status
# Must set dates or request errors 400 Bad Request
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BambooHR's API says it is optional. 🤔
https://www.bamboohr.com/api/documentation/time_off.php#getRequest

start (optional) - Only show time off that occurs on/after the specified start date.
end (optional) - Only show time off that occurs on/before the specified end date.

params['start']= start_date if start_date else '0000-01-01'
params['end']= end_date if end_date else '9999-01-01'
if type:
params['type'] = type
if employee_id:
params['employeeId'] = employee_id

r = self._query('time_off/requests', params, raw=True)
return r.json()
# return utils.transform_time_off(r.content)

def get_time_off_policies(self):
"""
Api method for returning list of time off policies for a subdomain
https://www.bamboohr.com/api/documentation/time_off.php#getTimeOffPolicies
Success Response: 200
@return: List containing time off policies
"""
url = 'meta/time_off/policies'
r = self._query(url, {})
return r

def get_time_off_types(self):
"""
Api method for returning list of time off types for a subdomain
https://www.bamboohr.com/api/documentation/time_off.php#getTimeOffTypes
Success Response: 200
@return: List containing time off types
"""
url = 'meta/time_off/types'
r = self._query(url, {})
return r['timeOffTypes']

def create_time_off_request(self, data, raw=False):
"""
API method for creating a new time off request
https://www.bamboohr.com/api/documentation/time_off.php#addRequest
Success Response: 201
@return: A dictionary containing the created time off request
"""
data['status'] = 'requested'
return self.update_time_off_request(data, raw)

def update_time_off_request(self, data, raw=False):
"""
API method for creating or updating a new time off request
https://www.bamboohr.com/api/documentation/time_off.php#addRequest
@param data = {
'status': 'requested',
'employee_id': '113',
'start': '2040-02-01',
'end': '2040-02-02',
'timeOffTypeId': '78',
'amount': '2',
'dates': [
{ 'ymd': '2040-02-01', 'amount': '1' },
{ 'ymd': '2040-02-02', 'amount': '1' }
],
'notes': [
{ 'type': 'employee', 'text': 'Going overseas with family' },
{ 'type': 'manager', 'text': 'Enjoy!' }
]
}
Success Response: 201
@return: A dictionary containing the created/updated time off request
"""
url = self.base_url + 'employees/{0}/time_off/request'.format(data.get('employee_id'))
xml = self._format_time_off_xml(data)
r = requests.put(url, timeout=self.timeout, headers=self.headers, auth=(self.api_key, ''), data=xml)
r.raise_for_status()
if raw:
return r
else:
return r.json()

def update_time_off_request_status(self, request_id, data, raw=False):
"""
https://www.bamboohr.com/api/documentation/time_off.php#changeRequest
Success Response: 200
@param data = d = {
'status': 'denied',
'note': 'have fun!'
}
@return: Boolean of request success (Status Code == 200).
"""
url = self.base_url + 'time_off/requests/{0}/status'.format(request_id)
r = requests.put(url, timeout=self.timeout, headers=self.headers, auth=(self.api_key, ''), json=data)
r.raise_for_status()
return True

def get_meta_fields(self):
"""
Expand Down
34 changes: 34 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,3 +93,37 @@ bamboo = PyBambooHR(subdomain='yoursub', api_key='yourapikeyhere', only_current=

```
BambooHR has effective dates for when promotions are scheduled to happen or when new hires are going to join the organization. In order to see these events before they happen using the BambooHR API set `only_current` to `False`. As a note, this only works for pulling reports and getting employee information. This does not work on getting the employee directory.

Handle time off requests:
- create a time off request
```py
from PyBambooHR import PyBambooHR

bamboo = PyBambooHR(subdomain='yoursub', api_key='yourapikeyhere')
data = {
'status': 'requested',
'employee_id': '113',
'start': '2040-02-01',
'end': '2040-02-02',
'timeOffTypeId': '78', # check your companies valid timeOffTypeId values using bamboo.get_time_off_policies or bamboo.get_time_off_types
'amount': '2',
'dates': [
{ 'ymd': '2040-02-01', 'amount': '1' },
{ 'ymd': '2040-02-02', 'amount': '1' }
],
'notes': [
{ 'type': 'employee', 'text': 'Going overseas with family' },
{ 'type': 'manager', 'text': 'Enjoy!' }
]
}
time_off_request = bamboo.create_time_off_request(data)
```
- update a requests status
```py
data = {
'status': 'declined',
'note': 'have fun!'
}
request_id = 222
bamboo.update_time_off_request_status(request_id, data)
```
107 changes: 107 additions & 0 deletions tests/test_time_off.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
"""Unittests for time off api
"""

import httpretty
import os
import sys
import unittest

from json import dumps
from requests import HTTPError

# Force parent directory onto path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

from PyBambooHR import PyBambooHR

class test_time_off(unittest.TestCase):
# Used to store the cached instance of PyBambooHR
bamboo = None

def setUp(self):
if self.bamboo is None:
self.bamboo = PyBambooHR(subdomain='test', api_key='testingnotrealapikey')

@httpretty.activate
def test_get_time_off_requests(self):
body = [{"id": "1342", "employeeId": "4", "status": {"lastChanged": "2019-09-12", "lastChangedByUserId": "2369", "status": "approved"}, "name": "Charlotte Abbott", "start": "2019-05-30", "end": "2019-06-01", "created": "2019-09-11", "type": {"id": "78", "name": "Vacation", "icon": "palm-trees"}, "amount": {"unit": "hours", "amount": "24"}, "actions": {"view": True, "edit": True, "cancel": False, "approve": False, "deny": False, "bypass": False}, "dates": {"2019-05-30": "24"}, "notes": {"manager": "Home sick with the flu."}}]
httpretty.register_uri(httpretty.GET,
"https://api.bamboohr.com/api/gateway.php/test/v1/time_off/requests",
body=dumps(body),
content_type="application/json")

response = self.bamboo.get_time_off_requests()
self.assertIsNotNone(response)
self.assertTrue(len(response) > 0)
self.assertEquals(response[0]['id'], '1342')
return

@httpretty.activate
def test_get_time_off_policies(self):
body = [{'id': '70', 'timeOffTypeId': '77', 'name': 'Testing Manual Policy', 'effectiveDate': None, 'type': 'manual'}]
httpretty.register_uri(httpretty.GET,
"https://api.bamboohr.com/api/gateway.php/test/v1/meta/time_off/policies",
body=dumps(body),
content_type="application/json")
response = self.bamboo.get_time_off_policies()
self.assertIsNotNone(response)
self.assertTrue(len(response) > 0)
self.assertEquals(response[0]['id'], '70')
return

@httpretty.activate
def test_get_time_off_types(self):
body = {'timeOffTypes': [{'id': '78', 'name': 'Vacation', 'units': 'hours', 'color': None, 'icon': 'palm-trees'}]}
httpretty.register_uri(httpretty.GET,
"https://api.bamboohr.com/api/gateway.php/test/v1/meta/time_off/types",
body=dumps(body),
content_type="application/json")
response = self.bamboo.get_time_off_types()
self.assertIsNotNone(response)
self.assertTrue(len(response) > 0)
self.assertEquals(response[0]['id'], '78')
return

@httpretty.activate
def test_create_time_off_request(self):
body = {'id': '1675', 'employeeId': '111', 'start': '2040-02-01', 'end': '2040-02-02', 'created': '2019-12-24', 'status': {'status': 'requested', 'lastChanged': '2019-12-24 02:29:45', 'lastChangedByUserId': '2479'}, 'name': 'xdd xdd', 'type': {'id': '78', 'name': 'Vacation'}, 'amount': {'unit': 'hours', 'amount': '2'}, 'notes': {'employee': 'Going overseas with family', 'manager': 'Enjoy!'}, 'dates': {'2040-02-01': '1', '2040-02-02': '1'}, 'comments': [{'employeeId': '111', 'comment': 'Enjoy!', 'commentDate': '2019-12-24', 'commenterName': 'Test use'}], 'approvers': [{'userId': '2479', 'displayName': 'Test user', 'employeeId': '111', 'photoUrl': 'https://resources.bamboohr.com/employees/photos/initials.php?initials=testuser'}], 'actions': {'view': True, 'edit': True, 'cancel': True, 'approve': True, 'deny': True, 'bypass': True}, 'policyType': 'discretionary', 'usedYearToDate': 0, 'balanceOnDateOfRequest': 0}
httpretty.register_uri(httpretty.PUT,
"https://api.bamboohr.com/api/gateway.php/test/v1/employees/111/time_off/request",
body=dumps(body),
content_type="application/json")
data = {
'status': 'requested',
'employee_id': '111',
'start': '2040-02-01',
'end': '2040-02-02',
'timeOffTypeId': '78',
'amount': '2',
'dates': [
{ 'ymd': '2040-02-01', 'amount': '1' },
{ 'ymd': '2040-02-02', 'amount': '1' }
],
'notes': [
{ 'type': 'employee', 'text': 'Going overseas with family' },
{ 'type': 'manager', 'text': 'Enjoy!' }
]
}
response = self.bamboo.create_time_off_request(data)
self.assertIsNotNone(response)
self.assertEquals(response['id'], '1675')
return

@httpretty.activate
def test_update_time_off_request_status(self):
body = {'id': '1675', 'employeeId': '111', 'start': '2040-02-01', 'end': '2040-02-02', 'created': '2019-12-24', 'status': {'status': 'declined', 'lastChanged': '2019-12-24 02:29:45', 'lastChangedByUserId': '2479'}, 'name': 'xdd xdd', 'type': {'id': '78', 'name': 'Vacation'}, 'amount': {'unit': 'hours', 'amount': '2'}, 'notes': {'employee': 'Going overseas with family', 'manager': 'Enjoy!'}, 'dates': {'2040-02-01': '1', '2040-02-02': '1'}, 'comments': [{'employeeId': '111', 'comment': 'Enjoy!', 'commentDate': '2019-12-24', 'commenterName': 'Test use'}], 'approvers': [{'userId': '2479', 'displayName': 'Test user', 'employeeId': '111', 'photoUrl': 'https://resources.bamboohr.com/employees/photos/initials.php?initials=testuser'}], 'actions': {'view': True, 'edit': True, 'cancel': True, 'approve': True, 'deny': True, 'bypass': True}, 'policyType': 'discretionary', 'usedYearToDate': 0, 'balanceOnDateOfRequest': 0}
httpretty.register_uri(httpretty.PUT,
"https://api.bamboohr.com/api/gateway.php/test/v1/time_off/requests/1675/status",
body=dumps(body),
content_type="application/json")
data = {
'status': 'declined',
'note': 'Have fun!'
}
response = self.bamboo.update_time_off_request_status(body['id'] ,data)
self.assertIsNotNone(response)
self.assertTrue(response)
return