The hidden complexities of timesheets in Australia
A technician finishes a job at 1am Sunday and submits a timesheet on Monday morning. One row, a few timestamps, an on-call flag ticked. You’d think paying for it is almost arithmetic.
It isn’t. To pay that one row, the system needs to know which Award covers the work, which EBA sits on top of it, whether the technician was on call or rostered, whether a previous call already triggered a minimum that covers this one, and what rate applies (double time, double-and-a-half, or just regular overtime). Getting any of that wrong means underpaying someone or paying them twice for the same hour.
I’ve spent the last couple of years building the engine that handles this for a national lift services company. Some of it I knew going in. Most of it I learned by getting it wrong.
The three layers
Australian pay isn’t a single document. It’s three things stacked on top of each other.
The NES (National Employment Standards) sits at the bottom — the legislated floor everybody gets. Above that sits the relevant Modern Award, which for our case is the Building and Construction General On-site Award 2020. On top of that, the employer and employees may agree to an EBA (Enterprise Bargaining Agreement) that varies the Award.
The catch is that an EBA doesn’t replace the Award underneath it. It sits on top and interacts with it.
In our case:
- minimum hourly rates come from the EBA
- overtime and weekend penalties come from the Award
So every worked hour has to be evaluated against both documents. If your data model treats “the agreement” as one thing, you’ve already lost.
Ordinary hours aren’t ordinary
The EBA defines a normal day as 7.2 hours, with a span of ordinary hours from 6am to 6pm Monday to Friday. But technicians actually work 8 hours a day. The extra 0.8 hours accrues toward an RDO (Rostered Day Off), so over a four-week cycle they bank two paid days off.
Which means a “normal” Tuesday is doing three things at once:
- 7.2 hours paid as ordinary time
- 0.8 hours accrued, not paid this week
- An RDO balance that has to be reduced when the day off is taken
And RDOs themselves are not just holidays — the fares allowance ($35/day) is still payable on an RDO, but not on annual leave. So even a non-working day has to be classified correctly.
def classify_day(day):
if day.is_worked:
return Paid(ordinary=7.2, rdo_accrual=0.8, fares=35.00)
if day.is_rdo:
return Paid(ordinary=7.2, rdo_drawdown=7.2, fares=35.00)
if day.is_annual_leave:
return Paid(ordinary=7.2, fares=0.00)
# ...
That tiny fares=0.00 line is the kind of thing a payroll officer catches and a developer doesn’t.
The continuation-of-calls rule
The on-call rules are where it gets properly strange. When a technician is on backup and gets called out, the EBA pays a minimum number of hours regardless of how long the call actually took. The first call of a weekend opens a 2-hour window. Any subsequent fresh call opens a 4-hour window.
Here’s the part that breaks naive implementations: a second call received inside an existing window is not a fresh call-out. It is a continuation of the call that opened the window. The technician doesn’t earn a second minimum. They only earn extra pay for work that runs past the window’s close, at the anchor call’s rate.
def process_call(new_call, anchor):
if anchor is None or new_call.start >= anchor.window_end:
return open_new_anchor(new_call) # fresh call-out
return continuation_of(anchor, new_call) # no new minimum
So if Call 1 arrives at 10am Saturday and runs for 30 minutes, the technician is paid 2 hours at double time. If Call 2 arrives at 11am — inside Call 1’s 2-hour window — and also runs for 30 minutes, the technician gets nothing extra. Both calls are covered by Call 1’s minimum. But if Call 2 arrived at 12:30pm instead, it’s a fresh subsequent and opens a brand new 4-hour window.
Two identical-looking sets of calls. Two very different pay outcomes. The whole thing turns on a 90-minute difference in timing.
The 16:00 boundary on public holidays
Public holidays add another twist. Call-outs received before 4pm are paid at double time. Call-outs from 4pm onwards are paid at double-and-a-half. Fair enough.
Except the continuation rule still applies. If a call starts at 3pm and runs until 7pm, the work after 4pm is still paid at double time, not double-and-a-half, because it is a continuation of a call that opened at the lower rate.
The “obvious” rule — pay each hour at the rate that matches its time of day — turns out to be wrong. The rate is locked in by the call that opened the window, not by the clock on the wall.
What this means for software implementation
A few things I’d do differently if I were starting again.
The unit of work is the window, not the hour. Whatever data structure you reach for first, replace it with one that tracks open anchors and their close times. Almost every rule in the EBA hangs off that idea once you see it.
Test scenarios should be lifted straight from the agreement. We have a test for every worked example printed in the EBA, plus the edge cases the payroll officer has raised over the years. When the test names line up with scenario IDs from a document the payroll team can actually read, disputes get sorted in minutes rather than days.
A payroll officer has to be able to verify a payslip by hand. If the only way to check a number is to re-run the engine, you’ve built a black box. Every line should map to a clause and a calculation that someone can redo on paper.
Australian payroll looks tedious from the outside and turns out to be one of the more interesting state-machine problems I’ve worked on. You have a real document, a real workforce, real money on the line, and a finite set of rules (even though it can feel infinite sometimes). The catch is that the rules compose in ways that punish anyone who tries to flatten them into a spreadsheet.
That’s the part I keep coming back to. Timesheets aren’t data entry. They’re the input to a small, weird, pseudo-legal language, and the job of payroll software is to interpret that language correctly every fortnight, for every employee, without anyone having to argue about it afterwards.