projectal.entities.staff

  1from copy import deepcopy
  2from datetime import datetime
  3
  4import projectal
  5from projectal.entity import Entity
  6from projectal.linkers import *
  7from projectal.errors import UsageException
  8from projectal.enums import DateLimit
  9
 10
 11class Staff(
 12    Entity,
 13    LocationLinker,
 14    ResourceLinker,
 15    SkillLinker,
 16    FileLinker,
 17    CompanyLinker,
 18    DepartmentLinker,
 19    TaskLinker,
 20    TaskTemplateLinker,
 21    NoteLinker,
 22    CalendarLinker,
 23    TagLinker,
 24    ContractLinker,
 25):
 26    """
 27    Implementation of the [Staff](https://projectal.com/docs/latest/#tag/Staff) API.
 28    """
 29
 30    _path = "staff"
 31    _name = "staff"
 32    _links = [
 33        LocationLinker,
 34        ResourceLinker,
 35        SkillLinker,
 36        FileLinker,
 37        NoteLinker,
 38        CalendarLinker,
 39        TagLinker,
 40        ContractLinker,
 41    ]
 42    _links_reverse = [CompanyLinker, DepartmentLinker, TaskLinker, TaskTemplateLinker]
 43
 44    @classmethod
 45    def calendar(cls, uuId, begin=None, until=None):
 46        """
 47        Returns the calendar of the staff with `uuId`.
 48
 49        `begin`: Start date in `yyyy-MM-dd`.
 50
 51        `until`: End date in `yyyy-MM-dd`.
 52
 53
 54        Optionally specify a date range. If no range specified, the
 55        minimum and maximum dates are used (see projectal.enums.DateLimit).
 56        """
 57        if begin:
 58            begin = datetime.strptime(begin, "%Y-%m-%d").date()
 59        if until:
 60            until = datetime.strptime(until, "%Y-%m-%d").date()
 61
 62        url = "/api/staff/{}/calendar?".format(uuId)
 63        params = []
 64        params.append("begin={}".format(begin)) if begin else None
 65        params.append("until={}".format(until)) if until else None
 66        url += "&".join(params)
 67
 68        cals = api.get(url)
 69        cals = [projectal.Calendar(c) for c in cals]
 70        return cals
 71
 72    @classmethod
 73    def calendar_availability(cls, uuId, begin=None, until=None):
 74        """
 75        Returns the availability (in hours) of the staff in `uuId`
 76        for each day within the specified date range.
 77
 78        `begin`: Start date in `yyyy-MM-dd`.
 79
 80        `until`: End date in `yyyy-MM-dd`.
 81
 82        If no range specified, the minimum and maximum dates are
 83        used (see projectal.enums.DateLimit).
 84        """
 85        if begin:
 86            begin = datetime.strptime(begin, "%Y-%m-%d").date()
 87        if until:
 88            until = datetime.strptime(until, "%Y-%m-%d").date()
 89
 90        url = "/api/staff/{}/calendar/availability?".format(uuId)
 91        params = []
 92        params.append("begin={}".format(begin)) if begin else None
 93        params.append("until={}".format(until)) if until else None
 94        url += "&".join(params)
 95
 96        return api.get(url)
 97
 98    @classmethod
 99    def usage(
100        cls,
101        begin,
102        until,
103        holder=None,
104        start=None,
105        limit=None,
106        span=None,
107        ksort=None,
108        order=None,
109        staff=None,
110    ):
111        """
112        Returns the staff-to-task allocations for all staff within the `holder`.
113
114        See [Usage API](https://projectal.com/docs/latest/#tag/Staff/paths/~1api~1staff~1usage/post)
115        for full details.
116        """
117        url = "/api/staff/usage?begin={}&until={}".format(begin, until)
118        params = []
119        params.append("holder={}".format(holder)) if holder else None
120        params.append("start={}".format(start)) if start else None
121        params.append("limit={}".format(limit)) if limit else None
122        params.append("span={}".format(span)) if span else None
123        params.append("ksort={}".format(ksort)) if ksort else None
124        params.append("order={}".format(order)) if order else None
125        if len(params) > 0:
126            url += "&" + "&".join(params)
127        payload = staff if staff and not holder else None
128        response = api.post(url, payload)
129        # Do some extra checks for empty list case
130        if "status" in response:
131            # We didn't have a 'jobCase' key and returned the outer dict.
132            return []
133        return response
134
135    @classmethod
136    def auto_assign(
137        cls,
138        type="Recommend",
139        over_allocate_staff=False,
140        include_assigned_task=False,
141        include_started_task=False,
142        skills=None,
143        tasks=None,
144        staffs=None,
145    ):
146        """
147        Automatically assign a set of staff (real or generic) to a set of tasks
148        using various skill and allocation criteria.
149
150        See [Staff Assign API](https://projectal.com/docs/latest/#tag/Staff-Assign/paths/~1api~1allocation~1staff/post)
151        for full details.
152        """
153        url = "/api/allocation/staff"
154        payload = {
155            "type": type,
156            "overAllocateStaff": over_allocate_staff,
157            "includeAssignedTask": include_assigned_task,
158            "includeStartedTask": include_started_task,
159            "skillMatchList": skills if skills else [],
160            "staffList": staffs if staffs else [],
161            "taskList": tasks if tasks else [],
162        }
163        return api.post(url, payload)
164
165    @classmethod
166    def create_contract(
167        cls,
168        UUID,
169        payload=None,
170        end_current_contract=False,
171        start_new_contract=False,
172        include_calendars=False,
173    ):
174        """
175        Creates a new Contract for a staff, with updated fields from the payload
176
177        end_current: Sets the end date of the current contract to today's date
178        start_new_date: Sets the start date of the new contract to today's date
179        include_calendars: If False (default), calendarList is set to [] so calendars are not cloned
180
181        See [Staff Clone API](https://projectal.com/docs/latest#tag/Staff/paths/~1api~1staff~1clone/post)
182        for full details.
183        """
184
185        payload = deepcopy(payload) if payload else {}
186        if not include_calendars:
187            payload["calendarList"] = []
188
189        url = "/api/staff/clone?reference={}&as_contract=true".format(UUID)
190        date_today = datetime.today().strftime("%Y-%m-%d")
191
192        current_staff = None
193        if end_current_contract:
194            # Check if setting end date to today is
195            # invalid for current Staff Contract
196            current_staff = cls.get(UUID)
197            if projectal.timestamp_from_date(
198                current_staff.get("startDate")
199            ) > projectal.timestamp_from_date(date_today):
200                raise UsageException(
201                    f"Cannot set endDate before startDate for current contract: {date_today}"
202                )
203
204        if start_new_contract:
205            payload["startDate"] = date_today
206            payload["endDate"] = DateLimit.Max
207
208        response = api.post(url, payload)
209
210        if end_current_contract and current_staff:
211            current_staff["endDate"] = date_today
212            current_staff.save()
213
214        return response["jobClue"]["uuId"]
 12class Staff(
 13    Entity,
 14    LocationLinker,
 15    ResourceLinker,
 16    SkillLinker,
 17    FileLinker,
 18    CompanyLinker,
 19    DepartmentLinker,
 20    TaskLinker,
 21    TaskTemplateLinker,
 22    NoteLinker,
 23    CalendarLinker,
 24    TagLinker,
 25    ContractLinker,
 26):
 27    """
 28    Implementation of the [Staff](https://projectal.com/docs/latest/#tag/Staff) API.
 29    """
 30
 31    _path = "staff"
 32    _name = "staff"
 33    _links = [
 34        LocationLinker,
 35        ResourceLinker,
 36        SkillLinker,
 37        FileLinker,
 38        NoteLinker,
 39        CalendarLinker,
 40        TagLinker,
 41        ContractLinker,
 42    ]
 43    _links_reverse = [CompanyLinker, DepartmentLinker, TaskLinker, TaskTemplateLinker]
 44
 45    @classmethod
 46    def calendar(cls, uuId, begin=None, until=None):
 47        """
 48        Returns the calendar of the staff with `uuId`.
 49
 50        `begin`: Start date in `yyyy-MM-dd`.
 51
 52        `until`: End date in `yyyy-MM-dd`.
 53
 54
 55        Optionally specify a date range. If no range specified, the
 56        minimum and maximum dates are used (see projectal.enums.DateLimit).
 57        """
 58        if begin:
 59            begin = datetime.strptime(begin, "%Y-%m-%d").date()
 60        if until:
 61            until = datetime.strptime(until, "%Y-%m-%d").date()
 62
 63        url = "/api/staff/{}/calendar?".format(uuId)
 64        params = []
 65        params.append("begin={}".format(begin)) if begin else None
 66        params.append("until={}".format(until)) if until else None
 67        url += "&".join(params)
 68
 69        cals = api.get(url)
 70        cals = [projectal.Calendar(c) for c in cals]
 71        return cals
 72
 73    @classmethod
 74    def calendar_availability(cls, uuId, begin=None, until=None):
 75        """
 76        Returns the availability (in hours) of the staff in `uuId`
 77        for each day within the specified date range.
 78
 79        `begin`: Start date in `yyyy-MM-dd`.
 80
 81        `until`: End date in `yyyy-MM-dd`.
 82
 83        If no range specified, the minimum and maximum dates are
 84        used (see projectal.enums.DateLimit).
 85        """
 86        if begin:
 87            begin = datetime.strptime(begin, "%Y-%m-%d").date()
 88        if until:
 89            until = datetime.strptime(until, "%Y-%m-%d").date()
 90
 91        url = "/api/staff/{}/calendar/availability?".format(uuId)
 92        params = []
 93        params.append("begin={}".format(begin)) if begin else None
 94        params.append("until={}".format(until)) if until else None
 95        url += "&".join(params)
 96
 97        return api.get(url)
 98
 99    @classmethod
100    def usage(
101        cls,
102        begin,
103        until,
104        holder=None,
105        start=None,
106        limit=None,
107        span=None,
108        ksort=None,
109        order=None,
110        staff=None,
111    ):
112        """
113        Returns the staff-to-task allocations for all staff within the `holder`.
114
115        See [Usage API](https://projectal.com/docs/latest/#tag/Staff/paths/~1api~1staff~1usage/post)
116        for full details.
117        """
118        url = "/api/staff/usage?begin={}&until={}".format(begin, until)
119        params = []
120        params.append("holder={}".format(holder)) if holder else None
121        params.append("start={}".format(start)) if start else None
122        params.append("limit={}".format(limit)) if limit else None
123        params.append("span={}".format(span)) if span else None
124        params.append("ksort={}".format(ksort)) if ksort else None
125        params.append("order={}".format(order)) if order else None
126        if len(params) > 0:
127            url += "&" + "&".join(params)
128        payload = staff if staff and not holder else None
129        response = api.post(url, payload)
130        # Do some extra checks for empty list case
131        if "status" in response:
132            # We didn't have a 'jobCase' key and returned the outer dict.
133            return []
134        return response
135
136    @classmethod
137    def auto_assign(
138        cls,
139        type="Recommend",
140        over_allocate_staff=False,
141        include_assigned_task=False,
142        include_started_task=False,
143        skills=None,
144        tasks=None,
145        staffs=None,
146    ):
147        """
148        Automatically assign a set of staff (real or generic) to a set of tasks
149        using various skill and allocation criteria.
150
151        See [Staff Assign API](https://projectal.com/docs/latest/#tag/Staff-Assign/paths/~1api~1allocation~1staff/post)
152        for full details.
153        """
154        url = "/api/allocation/staff"
155        payload = {
156            "type": type,
157            "overAllocateStaff": over_allocate_staff,
158            "includeAssignedTask": include_assigned_task,
159            "includeStartedTask": include_started_task,
160            "skillMatchList": skills if skills else [],
161            "staffList": staffs if staffs else [],
162            "taskList": tasks if tasks else [],
163        }
164        return api.post(url, payload)
165
166    @classmethod
167    def create_contract(
168        cls,
169        UUID,
170        payload=None,
171        end_current_contract=False,
172        start_new_contract=False,
173        include_calendars=False,
174    ):
175        """
176        Creates a new Contract for a staff, with updated fields from the payload
177
178        end_current: Sets the end date of the current contract to today's date
179        start_new_date: Sets the start date of the new contract to today's date
180        include_calendars: If False (default), calendarList is set to [] so calendars are not cloned
181
182        See [Staff Clone API](https://projectal.com/docs/latest#tag/Staff/paths/~1api~1staff~1clone/post)
183        for full details.
184        """
185
186        payload = deepcopy(payload) if payload else {}
187        if not include_calendars:
188            payload["calendarList"] = []
189
190        url = "/api/staff/clone?reference={}&as_contract=true".format(UUID)
191        date_today = datetime.today().strftime("%Y-%m-%d")
192
193        current_staff = None
194        if end_current_contract:
195            # Check if setting end date to today is
196            # invalid for current Staff Contract
197            current_staff = cls.get(UUID)
198            if projectal.timestamp_from_date(
199                current_staff.get("startDate")
200            ) > projectal.timestamp_from_date(date_today):
201                raise UsageException(
202                    f"Cannot set endDate before startDate for current contract: {date_today}"
203                )
204
205        if start_new_contract:
206            payload["startDate"] = date_today
207            payload["endDate"] = DateLimit.Max
208
209        response = api.post(url, payload)
210
211        if end_current_contract and current_staff:
212            current_staff["endDate"] = date_today
213            current_staff.save()
214
215        return response["jobClue"]["uuId"]

Implementation of the Staff API.

@classmethod
def calendar(cls, uuId, begin=None, until=None):
45    @classmethod
46    def calendar(cls, uuId, begin=None, until=None):
47        """
48        Returns the calendar of the staff with `uuId`.
49
50        `begin`: Start date in `yyyy-MM-dd`.
51
52        `until`: End date in `yyyy-MM-dd`.
53
54
55        Optionally specify a date range. If no range specified, the
56        minimum and maximum dates are used (see projectal.enums.DateLimit).
57        """
58        if begin:
59            begin = datetime.strptime(begin, "%Y-%m-%d").date()
60        if until:
61            until = datetime.strptime(until, "%Y-%m-%d").date()
62
63        url = "/api/staff/{}/calendar?".format(uuId)
64        params = []
65        params.append("begin={}".format(begin)) if begin else None
66        params.append("until={}".format(until)) if until else None
67        url += "&".join(params)
68
69        cals = api.get(url)
70        cals = [projectal.Calendar(c) for c in cals]
71        return cals

Returns the calendar of the staff with uuId.

begin: Start date in yyyy-MM-dd.

until: End date in yyyy-MM-dd.

Optionally specify a date range. If no range specified, the minimum and maximum dates are used (see projectal.enums.DateLimit).

@classmethod
def calendar_availability(cls, uuId, begin=None, until=None):
73    @classmethod
74    def calendar_availability(cls, uuId, begin=None, until=None):
75        """
76        Returns the availability (in hours) of the staff in `uuId`
77        for each day within the specified date range.
78
79        `begin`: Start date in `yyyy-MM-dd`.
80
81        `until`: End date in `yyyy-MM-dd`.
82
83        If no range specified, the minimum and maximum dates are
84        used (see projectal.enums.DateLimit).
85        """
86        if begin:
87            begin = datetime.strptime(begin, "%Y-%m-%d").date()
88        if until:
89            until = datetime.strptime(until, "%Y-%m-%d").date()
90
91        url = "/api/staff/{}/calendar/availability?".format(uuId)
92        params = []
93        params.append("begin={}".format(begin)) if begin else None
94        params.append("until={}".format(until)) if until else None
95        url += "&".join(params)
96
97        return api.get(url)

Returns the availability (in hours) of the staff in uuId for each day within the specified date range.

begin: Start date in yyyy-MM-dd.

until: End date in yyyy-MM-dd.

If no range specified, the minimum and maximum dates are used (see projectal.enums.DateLimit).

@classmethod
def usage( cls, begin, until, holder=None, start=None, limit=None, span=None, ksort=None, order=None, staff=None):
 99    @classmethod
100    def usage(
101        cls,
102        begin,
103        until,
104        holder=None,
105        start=None,
106        limit=None,
107        span=None,
108        ksort=None,
109        order=None,
110        staff=None,
111    ):
112        """
113        Returns the staff-to-task allocations for all staff within the `holder`.
114
115        See [Usage API](https://projectal.com/docs/latest/#tag/Staff/paths/~1api~1staff~1usage/post)
116        for full details.
117        """
118        url = "/api/staff/usage?begin={}&until={}".format(begin, until)
119        params = []
120        params.append("holder={}".format(holder)) if holder else None
121        params.append("start={}".format(start)) if start else None
122        params.append("limit={}".format(limit)) if limit else None
123        params.append("span={}".format(span)) if span else None
124        params.append("ksort={}".format(ksort)) if ksort else None
125        params.append("order={}".format(order)) if order else None
126        if len(params) > 0:
127            url += "&" + "&".join(params)
128        payload = staff if staff and not holder else None
129        response = api.post(url, payload)
130        # Do some extra checks for empty list case
131        if "status" in response:
132            # We didn't have a 'jobCase' key and returned the outer dict.
133            return []
134        return response

Returns the staff-to-task allocations for all staff within the holder.

See Usage API for full details.

@classmethod
def auto_assign( cls, type='Recommend', over_allocate_staff=False, include_assigned_task=False, include_started_task=False, skills=None, tasks=None, staffs=None):
136    @classmethod
137    def auto_assign(
138        cls,
139        type="Recommend",
140        over_allocate_staff=False,
141        include_assigned_task=False,
142        include_started_task=False,
143        skills=None,
144        tasks=None,
145        staffs=None,
146    ):
147        """
148        Automatically assign a set of staff (real or generic) to a set of tasks
149        using various skill and allocation criteria.
150
151        See [Staff Assign API](https://projectal.com/docs/latest/#tag/Staff-Assign/paths/~1api~1allocation~1staff/post)
152        for full details.
153        """
154        url = "/api/allocation/staff"
155        payload = {
156            "type": type,
157            "overAllocateStaff": over_allocate_staff,
158            "includeAssignedTask": include_assigned_task,
159            "includeStartedTask": include_started_task,
160            "skillMatchList": skills if skills else [],
161            "staffList": staffs if staffs else [],
162            "taskList": tasks if tasks else [],
163        }
164        return api.post(url, payload)

Automatically assign a set of staff (real or generic) to a set of tasks using various skill and allocation criteria.

See Staff Assign API for full details.

@classmethod
def create_contract( cls, UUID, payload=None, end_current_contract=False, start_new_contract=False, include_calendars=False):
166    @classmethod
167    def create_contract(
168        cls,
169        UUID,
170        payload=None,
171        end_current_contract=False,
172        start_new_contract=False,
173        include_calendars=False,
174    ):
175        """
176        Creates a new Contract for a staff, with updated fields from the payload
177
178        end_current: Sets the end date of the current contract to today's date
179        start_new_date: Sets the start date of the new contract to today's date
180        include_calendars: If False (default), calendarList is set to [] so calendars are not cloned
181
182        See [Staff Clone API](https://projectal.com/docs/latest#tag/Staff/paths/~1api~1staff~1clone/post)
183        for full details.
184        """
185
186        payload = deepcopy(payload) if payload else {}
187        if not include_calendars:
188            payload["calendarList"] = []
189
190        url = "/api/staff/clone?reference={}&as_contract=true".format(UUID)
191        date_today = datetime.today().strftime("%Y-%m-%d")
192
193        current_staff = None
194        if end_current_contract:
195            # Check if setting end date to today is
196            # invalid for current Staff Contract
197            current_staff = cls.get(UUID)
198            if projectal.timestamp_from_date(
199                current_staff.get("startDate")
200            ) > projectal.timestamp_from_date(date_today):
201                raise UsageException(
202                    f"Cannot set endDate before startDate for current contract: {date_today}"
203                )
204
205        if start_new_contract:
206            payload["startDate"] = date_today
207            payload["endDate"] = DateLimit.Max
208
209        response = api.post(url, payload)
210
211        if end_current_contract and current_staff:
212            current_staff["endDate"] = date_today
213            current_staff.save()
214
215        return response["jobClue"]["uuId"]

Creates a new Contract for a staff, with updated fields from the payload

end_current: Sets the end date of the current contract to today's date start_new_date: Sets the start date of the new contract to today's date include_calendars: If False (default), calendarList is set to [] so calendars are not cloned

See Staff Clone API for full details.