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.:

  1. 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,
  2. 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.:

  1. on the definitions of SCWDs (is a SWCD10, 10 + 0.5 hours (BECTU), or 10.5 + 0.5 hours (PACT)?)
  2. how any Broken Turnaround should be rounded up (by 30mins, or by 60 mins after the first hour)
  3. 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.:

  1. 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!).
  2. 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:

Python
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:

  1. Writing functions to round up the different types of OTs to the nearest half or nearest one hour
  2. Working out out how to write the OT functions for (i) Broken Turnaround, (ii) Night Premiums, (iii) 6th Days, (iv) 7th Days
  3. Working out the impact of all the different Day Types: e.g. Sick Paid, Turnaround, Unpaid Leave, etc.
  4. Incorporating the use of SCWD, ERDD days once I can find out clearer definitions
  5. Scoping in the impact of there being more than 2hr of Camera OT in a 7 day period / Actual Working Week
  6. Working out the impact and effect on other payments such as: BOX, EQUIPMENT, COMPUTER, and MOBILE ALLOWANCE
  7. Considering the impact and effect of Holidays and Bank Holidays
  8. 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)
  9. Plus more stuff I may not have thought of…