PACT/BECTU - Timesheet Model (H.E.T.V.)
Overview
Here we show how the the PACT/BECTU Timesheet rules and guidelines can be modelled with Python. (The same can be done in M.S. Excel!)
This is a difficult Timesheet to model, not only because of the number of rules, but also because of:
1. The potential for ‘knock on’ / ‘knock back’ affects that can happen between two consecutive timesheets. E.g.:
- if compensatory rest is retrospectively given on a Monday in one week, this would remove the need for Night Premium pay or Daily Broken Turnaround pay given in the previous week,
- if working on a Sunday in a prior week results in a 6th or 7th Day potentially being worked in the following week
2. Gaps in knowledge and sometimes disagreements or inconsistencies between PACT and BECTU. E.g.:
- on the definitions of SCWDs (is a SWCD10, 10 + 0.5 hours (BECTU), or 10.5 + 0.5 hours (PACT)?)
- how any Broken Turnaround should be rounded up (by 30mins, or by 60 mins after the first hour)
- whether travel days should be counted as a WORKING DAY or NON-WORKING DAY, and what reimbursements in cost and time should be given when returning from a Resident Location or Outside the U.K., on a scheduled rest day.
3. Lack of detailed written guidance from PACT, and BECTU. E.g.:
- Definitions that lack clarity and specificity, often leaving too much room for ambiguous interpretation on different work pattern scenarios (which can be the main source of confusion!).
- Currently, there is no Timesheet system (that I have seen) in the industry that properly considers the affect of potentially breaching 2nd meal time breaks
Python Script
Nonetheless, below is an attempt to try and model this atomically / programatically:
Much work still needs to be done, and I am in touch with the Head of Screen Skills, and am trying to contact some experts from BECTU to fill in the gaps!
Below is how things stand, so far:
from datetime import datetime, timedelta
import math
## Functions
#**************************************************************************************************************************#
def string_to_date(date, time):
return datetime(
date.year, date.month, date.day,
datetime.strptime(time, '%H:%M').hour,
datetime.strptime(time, '%H:%M').minute)
def camera_ot_roundup(camera_ot):
if timedelta(minutes = 0) < camera_ot <= timedelta(minutes = 60):
return timedelta(minutes = 30 * math.ceil(camera_ot / timedelta(hours = 0.5)))
elif camera_ot > timedelta(minutes = 60):
return timedelta(hours = math.ceil(camera_ot / timedelta(hours = 1)))
## Dates & Times
#**************************************************************************************************************************#
today = datetime(2000, 1, 1)
early_call_input = ('06:00')
early_call = string_to_date(today, early_call_input)
midnight_input = ('00:00')
midnight = string_to_date(today, midnight_input) + timedelta(days=1)
time_in_input = ('08:10')
time_in = string_to_date(today, time_in_input)
unit_call_input = ('08:00')
unit_call = string_to_date(today, unit_call_input)
# 1. Not sure about adjusting Unit Call if Time Out is before Unit Wrap? (E.g. Hair & Makeup, early starters but also early finishers)
first_meal_start_input = ('')
if first_meal_start_input:
first_meal_start = string_to_date(today, first_meal_start_input)
if first_meal_start < unit_call:
first_meal_start = first_meal_start + timedelta(days=1)
else:
first_meal_start = None
first_meal_end_input = ('')
if first_meal_end_input:
first_meal_end = string_to_date(today, first_meal_end_input)
if first_meal_end < first_meal_start:
first_meal_end = first_meal_end + timedelta(days=1)
else:
first_meal_end = None
second_meal_start_input = ('19:45')
if second_meal_start_input:
second_meal_start = string_to_date(today, second_meal_start_input)
if second_meal_start < unit_call:
second_meal_start = second_meal_start + timedelta(days=1)
else:
second_meal_start = None
second_meal_end_input = ('20:00')
if second_meal_end_input:
second_meal_end = string_to_date(today, second_meal_end_input)
if second_meal_end < second_meal_start:
second_meal_end = second_meal_end + timedelta(days=1)
else:
second_meal_end = None
unit_wrap_input = ('19:50')
unit_wrap = string_to_date(today, unit_wrap_input)
# 1. Adjust Unit Wrap if Time In is after Unit Call
if time_in > unit_call:
unit_wrap = unit_wrap + (time_in - unit_call)
if unit_wrap < unit_call:
unit_wrap = unit_wrap + timedelta(days=1)
time_out_input = ('19:50')
time_out = string_to_date(today, time_out_input)
if time_out < time_in:
time_out = time_out + timedelta(days=1)
## Permitted Pre-Call & Pre-Wrap Times
#**************************************************************************************************************************#
permitted_pre_call = timedelta(hours = 0, minutes = 0)
permitted_post_wrap = timedelta(hours = 0, minutes = 0)
## Working Day, and Day Type
#**************************************************************************************************************************#
# 1. Day types include: Working, Flat Day, T\'round, Trvl, Hol, Bank Hol, Sick, Sick Unpaid, Sick Paid, Isolating, Paid Leave, Unpaid Leave, Not Wkng
# 2. Working days include: CWD9, CWD10, SWD10, SWD11, and ERDD
# 3. Not sure about SCWD9 and SCWD10!?!? BECTU and PACT interpretations are different!
# 4. Where a Crew take time out from a rest day to travel (e.g. to catch a train on a Sunday afternoon, ready for Monday morning), appropriate compensation should be offered for interfering with their time off
# 5. A travel day is a NON-WORKING day, in accordance with EU law, and EU Working Time Directives. Travel Days do not form part of the working week. (BECTU disagree with this)
# 6. W.r.t. Travel on Rest days, all departments should be treated the same!
# 7. A 'Turnaround' day is a rest period between two periods of work. It could be either a weekend or rest day given when adjusting between night and daytime work
working_day = "SWD11"
day_type = "Working"
## Bank Holidays
#**************************************************************************************************************************#
# 1. Not sure about the affect of this!?!?
bank_holiday = "No"
## Shooting Crew
#**************************************************************************************************************************#
# 1. Cam OT should only given for shooting crew
shooting_crew = "Yes"
## Grace Periods
#**************************************************************************************************************************#
# 1. Grace should not be given on New Shoots!
# 2. Grace should only really be applied if Cam OT <= 15 minutes
# 3. The first two Grace Periods in a week are unpaid
# 4. Where a third Grace Period is used in a working week, one hour OT shall be paid, or one hour thirty minutes given as compensatory rest in the same working week
grace = "No"
grace_time = timedelta(minutes = 15)
## Meals
#**************************************************************************************************************************#
# 1. A meal break of 1 hour should be given for a SWD10 or SWD11 working day
# 2. A second meal break of 30 mins should also be due on a SWD10 and SWD11 working day (within 6 hours of the first meal break, excluding any Permitted Wrap Work and Permitted Grace Periods)
# 3. A meal break time of 30mins should be given for a SCWD11 or SCWD10 working day
# 4. CWD9 and CWD10 days do not have formal meal breaks, however a rest break of no less than 20 mins should be given, within 6 hours of starting work (or within 6 hours of completion of the previous break)
# 5. Where work continues for more than one hour following the end of a CWD (excluding any Permitted Wrap Work and Permitted Grace Periods), the Producer should provide food and refreshments
SWD_first_meal_break = timedelta(minutes = 60)
SWD_second_meal_break = timedelta(minutes = 30)
# 1. First meal delay - should begin no later than 6 hours after the Unit Call (on SWD10 and SWD11 working days)
# 2. Second meal delay - should begin no later than 6 hours after the end of the First Meal Break (on SWD10 and SWD11 working days)
first_meal_delay = timedelta(hours = 6)
second_meal_delay = timedelta(hours = 6)
## Calculate Scheduled Finish time
#**************************************************************************************************************************#
match working_day:
case "CWD9":
scheduled_finish_time = max(unit_call, time_in) + timedelta(hours = 9)
case "CWD10":
scheduled_finish_time = max(unit_call, time_in) + timedelta(hours = 10)
case "SWD10":
if first_meal_start and first_meal_end:
scheduled_finish_time = max(unit_call, time_in) + timedelta(hours = 10) + (first_meal_end - first_meal_start)
if second_meal_start and second_meal_end and scheduled_finish_time > second_meal_start:
scheduled_finish_time = scheduled_finish_time + (second_meal_end - second_meal_start)
else:
scheduled_finish_time = max(unit_call, time_in) + timedelta(hours = 10)
if second_meal_start and second_meal_end and scheduled_finish_time > second_meal_start:
scheduled_finish_time = scheduled_finish_time + (second_meal_end - second_meal_start)
case "SWD11":
if first_meal_start and first_meal_end:
scheduled_finish_time = max(unit_call, time_in) + timedelta(hours = 11) + (first_meal_end - first_meal_start)
if second_meal_start and second_meal_end and scheduled_finish_time > second_meal_start:
scheduled_finish_time = scheduled_finish_time + (second_meal_end - second_meal_start)
else:
scheduled_finish_time = max(unit_call, time_in) + timedelta(hours = 11)
if second_meal_start and second_meal_end and scheduled_finish_time > second_meal_start:
scheduled_finish_time = scheduled_finish_time + (second_meal_end - second_meal_start)
case _:
scheduled_finish_time = today
print("Working Day not found")
print(scheduled_finish_time)
## Overtime Functions
#**************************************************************************************************************************#
# Early Call OT
#----------------------#
# 1. Early Call OT is given for Work starting before 6:00am
# 2. Paid at the applicable Overtime rate, at £35/hr or 1.5T subject to a maximum of £45/hr
# 3. Should be rounded up to the nearest 30mins
def early_call_fn(time_in, unit_call, early_call, permitted_pre_call):
if time_in and time_in < unit_call and time_in < early_call:
if unit_call <= early_call and unit_call - time_in >= permitted_pre_call:
return (unit_call - time_in) - permitted_pre_call
if unit_call > early_call and unit_call - time_in >= permitted_pre_call:
return min(unit_call - permitted_pre_call, early_call) - time_in
# Pre-Call OT
#----------------------#
# 1. No Pre-Call OT given if the Unit Call is before 6:00am - it all becomes Early Call OT
# 2. Paid at the applicable Overtime rate, at £35/hr or 1.5T subject to a maximum of £45/hr
# 3. Should be rounded up to the nearest 30mins
def pre_call_fn(time_in, unit_call, early_call, permitted_pre_call):
if time_in and early_call < unit_call and time_in < unit_call:
if unit_call - max(time_in, early_call) >= permitted_pre_call:
return unit_call - max(time_in, early_call) - permitted_pre_call
# Camera OT
#----------------------#
# 1. Camera OT only paid for Shooting Crew
# 2. No Camera OT given: if Unit Wrap minus Scheduled Finish is less than 15 minutes AND if Grace is given
# 3. Paid at the applicable Overtime rate, at £35/hr or 1.5T subject to a maximum of £45/hr
# 4. Should be rounded up to the nearest 30mins, in the first hour, then the nearest 60mins, thereafter
def camera_ot_fn(scheduled_finish_time, unit_wrap, time_in, time_out, shooting_crew, grace):
if shooting_crew == "Yes" and time_in and unit_wrap > scheduled_finish_time and time_out > scheduled_finish_time:
# should you also consider the time out here, should it be min(wrap, timeout)?
if unit_wrap - scheduled_finish_time <= grace_time and grace == "Yes":
return
else:
return min(unit_wrap, time_out) - scheduled_finish_time
# Post-Wrap OT
#----------------------#
# 1. Post-Wrap OT calculated back from the Unit Wrap time, not Scheduled Finish time, if there is no Camera OT
# 2. Paid at the applicable Overtime rate, at £35/hr or 1.5T subject to a maximum of £45/hr
# 3. Should be rounded up to the nearest 30mins
def post_wrap_fn(scheduled_finish_time, camera_ot, unit_wrap, time_in, time_out, permitted_post_wrap, shooting_crew):
if camera_ot:
camera_ot = camera_ot_roundup(camera_ot)
unit_wrap = scheduled_finish_time + camera_ot
if time_in and time_out > scheduled_finish_time:
if shooting_crew == "No":
if time_out - scheduled_finish_time >= permitted_post_wrap:
return time_out - scheduled_finish_time - permitted_post_wrap
if shooting_crew == "Yes" and camera_ot and time_out > unit_wrap:
if time_out - unit_wrap >= permitted_post_wrap:
return time_out - unit_wrap - permitted_post_wrap
if shooting_crew == "Yes" and unit_wrap > scheduled_finish_time and camera_ot == None:
if time_out > unit_wrap:
if time_out - unit_wrap >= permitted_post_wrap:
#Is this line right, or should it go back to the scheduled finish time?
return time_out - unit_wrap - permitted_post_wrap
# Also do you get no Wrap if the Time Out < Unit Wrap
if shooting_crew == "Yes" and unit_wrap <= scheduled_finish_time:
if time_out - scheduled_finish_time >= permitted_post_wrap:
return time_out - scheduled_finish_time - permitted_post_wrap
# Broken Turnaround OT
#----------------------#
# 1. Workers should be given no less than 11 hours rest between the end of one work period and commencement of the next
# 2. Workers should also have at least one scheduled rest day per 7 day period
# 3. If one rest day is scheduled (e.g. in an "eleven day fortnight"), the total rest period should be 24 + 11 = 35 hours
# 4. If two rest days are scheduled, the total rest period should be 48 + 11 = 59 hours
# 5. If this scheduled rest period is broken, the worker should be given an equivalent period of compensatory rest.
# 6. If the broken rest time cannot be given back the worker, it should be paid at the applicable Overtime rate (at between £35/hr and £45/hr)
# 7. Should be rounded up to the nearest 30mins
# Meal Curtailment 1 OT
#----------------------#
# 1. Due if, for an SWD, the first meal break is less than 60 mins
# 2. Paid at the applicable Overtime rate, at £35/hr or 1.5T subject to a maximum of £45/hr
# 3. Should be rounded up to the nearest 30mins
def meal_curtailment1_fn(time_in, time_out, unit_call, unit_wrap, first_meal_start, first_meal_end, first_meal_delay, SWD_first_meal_break, working_day):
if time_in and first_meal_start and first_meal_end and working_day:
if working_day in ["SWD10", "SWD11"] and (first_meal_end - first_meal_start) < SWD_first_meal_break:
if min(time_out, unit_wrap) > first_meal_end:
return min(SWD_first_meal_break, min(time_out, unit_wrap) - first_meal_start) - (first_meal_end - first_meal_start)
elif time_in and not(first_meal_start) and not(first_meal_end) and working_day:
if working_day in ["SWD10", "SWD11"]:
# should this be from unit_call or max(time_in, and unit_call?)
if min(time_out, unit_wrap) > unit_call + first_meal_delay:
return min(SWD_first_meal_break, min(unit_wrap, time_out) - (unit_call + first_meal_delay))
# Meal Curtailment 2 OT
#----------------------#
# 1. Due if, for an SWD, the second meal break is less than 30 mins
# 2. Paid at the applicable Overtime rate, at £35/hr or 1.5T subject to a maximum of £45/hr
# 3. Should be rounded up to the nearest 30mins (or added to Meal Curtailment 1 OT, and then rounded up?)
def meal_curtailment2_fn(time_in, time_out, unit_call, unit_wrap, first_meal_start, first_meal_end, first_meal_delay, second_meal_start, second_meal_end, second_meal_delay, SWD_second_meal_break, working_day):
if time_in and second_meal_start and second_meal_end and working_day:
if working_day in ["SWD10", "SWD11"] and (second_meal_end - second_meal_start) < SWD_second_meal_break:
if min(time_out, unit_wrap) > second_meal_end:
return min(SWD_second_meal_break, min(time_out, unit_wrap) - second_meal_start) - (second_meal_end - second_meal_start)
elif time_in and not(second_meal_start) and not(second_meal_end) and working_day:
if working_day in ["SWD10", "SWD11"] and first_meal_start and first_meal_end:
if min(time_out, unit_wrap) > first_meal_end + second_meal_delay:
return min(SWD_second_meal_break, min(unit_wrap, time_out) - (first_meal_end + second_meal_delay))
elif working_day in ["SWD10", "SWD11"] and not(first_meal_start) and not(first_meal_end):
# should this be from unit_call or max(time_in, and unit_call?)
if min(time_out, unit_wrap) > unit_call + first_meal_delay + second_meal_delay:
return min(SWD_second_meal_break, min(unit_wrap, time_out) - (unit_call + first_meal_delay + second_meal_delay))
# Meal Delay 1 OT
#----------------------#
# 1. Due if, for an SWD, the first meal break is more than 6 hours after the Unit Call (or is it Time In?)
# 2. Paid at the applicable Overtime rate, at £35/hr or 1.5T subject to a maximum of £45/hr
# 3. Should be rounded up to the nearest 30mins
def meal_delay1_fn(time_in, time_out, unit_call, unit_wrap, first_meal_start, first_meal_end, first_meal_delay, second_meal_start, second_meal_end, working_day):
if time_in and first_meal_start and first_meal_end and working_day:
# should this be from unit_call or max(time_in, and unit_call?)
if working_day in ["SWD10", "SWD11"] and (first_meal_start - unit_call) > first_meal_delay:
if min(time_out, unit_wrap) > unit_call + first_meal_delay:
return min(first_meal_start, min(time_out, unit_wrap)) - unit_call - first_meal_delay
elif time_in and not(first_meal_start) and not(first_meal_end) and working_day:
if working_day in ["SWD10", "SWD11"] and second_meal_start and second_meal_end:
# should this be from unit_call or max(time_in, and unit_call?)
if min(time_out, unit_wrap) > unit_call + first_meal_delay:
return min(second_meal_start, min(time_out, unit_wrap)) - unit_call - first_meal_delay
elif working_day in ["SWD10", "SWD11"] and not(second_meal_start) and not(second_meal_end):
# should this be from unit_call or max(time_in, and unit_call?)
if min(time_out, unit_wrap) > unit_call + first_meal_delay:
return min(time_out, unit_wrap) - unit_call - first_meal_delay
# Meal Delay 2 OT
#----------------------#
# 1. Due if, for an SWD, the second meal break is more than 6 hours after the end of the previous meal break, excluding any Permitted Wrap Work and Permitted Grace Periods
# 2. Paid at the applicable Overtime rate, at £35/hr or 1.5T subject to a maximum of £45/hr
# 3. Should be rounded up to the nearest 30mins (or added to Meal Delay 1 OT, and then rounded up?)
def meal_delay2_fn(time_in, time_out, unit_wrap, first_meal_start, first_meal_end, second_meal_start, second_meal_end, second_meal_delay, working_day):
if time_in and first_meal_start and first_meal_end and second_meal_start and second_meal_end and working_day:
if working_day in ["SWD10", "SWD11"] and (second_meal_start - first_meal_end) > second_meal_delay:
if min(time_out, unit_wrap) > first_meal_end + second_meal_delay:
return min(second_meal_start, min(time_out, unit_wrap)) - first_meal_end - second_meal_delay
elif time_in and first_meal_start and first_meal_end and not(second_meal_start) and not(second_meal_end) and working_day:
if working_day in ["SWD10", "SWD11"]:
if min(time_out, unit_wrap) > first_meal_end + second_meal_delay:
return min(time_out, unit_wrap) - first_meal_end - second_meal_delay
# 6th Day OT
#----------------------#
# 1. 6th Day OT is paid when a Sixth Consecutive Day needs to be worked. The fee paid is the same as the normal Daily Fee for a particular Crew Member, at 1.0T
# 7th Day OT
#----------------------#
# 1. 7th Day OT is paid when a Seventh Consecutive Day needs to be worked. The fee paid is usually double the normal Daily Fees of a particular Crew Member, at 2.0T.
# Night Premium OT
#----------------------#
# 1. Night Work should always include all shooting hours worked past midnight - whether scheduled or un-scheduled
# 2. Entitlement to paid compensatory rest accrued per continuous block of Night Work filming, whether one night only, or 4 weeks of continuous shoot
# 3. Whether time is given back, or paid back, w.r.t. Night Premiums, paid compensatory rest is capped at the length of 1 contracted working day
# 4. Only if compensatory rest is sufficient to consitute a full rest day at the end of a shoot (e.g. 10 or 11 hours), should a full paid rest day be given
# 5. If a shoot is moving from nights to days, a paid rest day should be given
# 6. Paid compensatory rest should be given by the end of the week or before the beginning of the next week
# 7. Paid compensatory rest cannot be given on scheduled rest days (e.g. weekend days). For example, if a Crew finished at 4am on Saturday, Monday would normally be given as a paid rest day
# 8. Prep & Wrap times that go past midnight does not give rise to compensatory rest
# 9. Night premiums are paid at the applicable daily rate, at 1.0T, not at 1.5T
# 10. Should be rounded up to the nearest 30mins
# 11 Day Fortnights ??
#----------------------#
## Calling Functions
#**************************************************************************************************************************#
early_call_ot = early_call_fn(time_in, unit_call, early_call, permitted_pre_call)
print("Early-Call Overtime: ", early_call_ot)
pre_call_ot = pre_call_fn(time_in, unit_call, early_call, permitted_pre_call)
print("Pre-Call Overtime: ", pre_call_ot)
camera_ot = camera_ot_fn(scheduled_finish_time, unit_wrap, time_in, time_out, shooting_crew, grace)
print("Camera Overtime: ", camera_ot)
post_wrap_ot = post_wrap_fn(scheduled_finish_time, camera_ot, unit_wrap, time_in, time_out, permitted_post_wrap, shooting_crew)
print("Post_Wrap Overtime: ", post_wrap_ot)
meal_curtailment1_ot = meal_curtailment1_fn(time_in, time_out, unit_call, unit_wrap, first_meal_start, first_meal_end, first_meal_delay, SWD_first_meal_break, working_day)
print("Meal Curtailment 1: ", meal_curtailment1_ot)
meal_curtailment2_ot = meal_curtailment2_fn(time_in, time_out, unit_call, unit_wrap, first_meal_start, first_meal_end, first_meal_delay, second_meal_start, second_meal_end, second_meal_delay, SWD_second_meal_break, working_day)
print("Meal Curtailment 2: ", meal_curtailment2_ot)
meal_delay1_ot = meal_delay1_fn(time_in, time_out, unit_call, unit_wrap, first_meal_start, first_meal_end, first_meal_delay, second_meal_start, second_meal_end, working_day)
print("Meal Delay 1: ", meal_delay1_ot)
meal_delay2_ot = meal_delay2_fn(time_in, time_out, unit_wrap, first_meal_start, first_meal_end, second_meal_start, second_meal_end, second_meal_delay, working_day)
print("Meal Delay 2: ", meal_delay2_ot)
Further Work
This is far from complete, but hopefully you can see the logic, and how the rules can be structured in a way that strives to raise the standards of how work can be or should be done.
Further work still needs to be done around:
- Writing functions to round up the different types of OTs to the nearest half or nearest one hour
- Working out out how to write the OT functions for (i) Broken Turnaround, (ii) Night Premiums, (iii) 6th Days, (iv) 7th Days
- Working out the impact of all the different Day Types: e.g. Sick Paid, Turnaround, Unpaid Leave, etc.
- Incorporating the use of SCWD, ERDD days once I can find out clearer definitions
- Scoping in the impact of there being more than 2hr of Camera OT in a 7 day period / Actual Working Week
- Working out the impact and effect on other payments such as: BOX, EQUIPMENT, COMPUTER, and MOBILE ALLOWANCE
- Considering the impact and effect of Holidays and Bank Holidays
- Building in the different Holiday Entitlement rules, for the different types of worker, e.g. (i) PAYE, (ii) Schedule-D (Sole Trader), and (iii) Loan Out (Ltd. Company)
- Plus more stuff I may not have thought of…
Feedback
Submit and view feedback