Skip to content

210 × 297 mm

HR & employment

Monthly timesheet

An A4 monthly timesheet that lists every day of the period grouped by ISO week, works out hours from start, end and break, credits leave and sickness, compares the total with the contracted hours, charts hours by activity and ends with employee, manager and payroll sign-off boxes.

A4 portraitLocale en-GBHTML + JinjaHeader & footer

Opens Dynamic Document API with this template preloaded in a free workspace.

Template language features

  • Loops
  • Conditionals
  • Macros
  • Filters
  • Date formatting
  • Number formatting
  • Grouping
  • Repeating table headers
  • Header & footer
  • Page numbers
  • signature

Filters such as format_currency and qrcode are described in the template language reference.

Variables

Derived from the sample data: 5 top-level fields, 30 paths in total. Send the same shape as data.

PathTypeExample
companyobject
company.namestring"Copperbeech Facilities Ltd"
company.sitestring"Harbour Point Business Park, Bristol"
company.emailstring"payroll@copperbeech.example.com"
employeeobject
employee.namestring"Daniel Okafor"
employee.numberstring"EMP-0472"
employee.rolestring"Maintenance Technician"
employee.departmentstring"Estates and Facilities"
employee.managerstring"Rachel Whitmore"
employee.manager_rolestring"Estates Manager"
employee.daily_hoursnumber7.5
employee.weekly_hoursnumber37.5
periodobject
period.labelstring"August 2026"
period.startdate"2026-08-01"
period.enddate"2026-08-31"
period.submitted_ondate"2026-09-01"
codesarray<object> · 4
codes[].codestring"PPM"
codes[].labelstring"Planned maintenance"
codes[].colorstring"#2f6f8f"
entriesarray<object> · 22
entries[].datedate"2026-08-03"
entries[].start?string"07:30"
entries[].end?string"16:00"
entries[].break_minutes?number30
entries[].code?string"PPM"
entries[].note?string"HVAC filter changes, Block A"
entries[].type?string"leave"

Sample data

Fictional records used for previews. Replace them with your own data when you call the API.

data.json
{
  "company": {
    "name": "Copperbeech Facilities Ltd",
    "site": "Harbour Point Business Park, Bristol",
    "email": "payroll@copperbeech.example.com"
  },
  "employee": {
    "name": "Daniel Okafor",
    "number": "EMP-0472",
    "role": "Maintenance Technician",
    "department": "Estates and Facilities",
    "manager": "Rachel Whitmore",
    "manager_role": "Estates Manager",
    "daily_hours": 7.5,
    "weekly_hours": 37.5
  },
  "period": {
    "label": "August 2026",
    "start": "2026-08-01",
    "end": "2026-08-31",
    "submitted_on": "2026-09-01"
  },
  "codes": [
    {
      "code": "PPM",
      "label": "Planned maintenance",
      "color": "#2f6f8f"
    },
    {
      "code": "REACT",
      "label": "Reactive repairs",
      "color": "#d17a22"
    },
    {
      "code": "PROJ",
      "label": "Projects",
      "color": "#5b8c3a"
    },
    {
      "code": "CALL",
      "label": "Call-outs",
      "color": "#8a4fa3"
    }
  ],
  "entries": [
    {
      "date": "2026-08-03",
      "start": "07:30",
      "end": "16:00",
      "break_minutes": 30,
      "code": "PPM",
      "note": "HVAC filter changes, Block A"
    },
    {
      "date": "2026-08-04",
      "start": "07:30",
      "end": "16:00",
      "break_minutes": 30,
      "code": "PPM",
      "note": "Fire door inspections, levels 1–4"
    },
    {
      "date": "2026-08-05",
      "start": "07:30",
      "end": "15:30",
      "break_minutes": 30,
      "code": "REACT",
      "note": "Leak under sink, level 2 kitchen"
    },
    {
      "date": "2026-08-06",
      "start": "07:30",
      "end": "15:30",
      "break_minutes": 30,
      "code": "PPM"
    },
    {
      "date": "2026-08-07",
      "start": "07:30",
      "end": "15:00",
      "break_minutes": 30,
      "code": "PPM",
      "note": "Finished early, agreed with R. Whitmore"
    },
    {
      "date": "2026-08-10",
      "start": "07:30",
      "end": "15:30",
      "break_minutes": 30,
      "code": "PPM"
    },
    {
      "date": "2026-08-11",
      "start": "07:30",
      "end": "17:00",
      "break_minutes": 30,
      "code": "PROJ",
      "note": "LED lighting upgrade, car park"
    },
    {
      "date": "2026-08-12",
      "start": "07:30",
      "end": "17:00",
      "break_minutes": 30,
      "code": "PROJ",
      "note": "LED lighting upgrade, car park"
    },
    {
      "date": "2026-08-13",
      "type": "leave",
      "note": "Annual leave"
    },
    {
      "date": "2026-08-14",
      "type": "leave",
      "note": "Annual leave"
    },
    {
      "date": "2026-08-17",
      "start": "07:30",
      "end": "15:30",
      "break_minutes": 30,
      "code": "REACT",
      "note": "Lift alarm reset, Block B"
    },
    {
      "date": "2026-08-18",
      "start": "07:30",
      "end": "15:30",
      "break_minutes": 30,
      "code": "PPM"
    },
    {
      "date": "2026-08-19",
      "start": "07:30",
      "end": "15:30",
      "break_minutes": 30,
      "code": "PPM",
      "note": "Emergency lighting test"
    },
    {
      "date": "2026-08-20",
      "type": "sick",
      "note": "Sickness, self-certified"
    },
    {
      "date": "2026-08-21",
      "start": "07:30",
      "end": "15:30",
      "break_minutes": 30,
      "code": "PPM"
    },
    {
      "date": "2026-08-22",
      "start": "09:00",
      "end": "13:00",
      "break_minutes": 0,
      "code": "CALL",
      "note": "Call-out: boiler fault, Block C"
    },
    {
      "date": "2026-08-24",
      "start": "07:30",
      "end": "15:30",
      "break_minutes": 30,
      "code": "PPM"
    },
    {
      "date": "2026-08-25",
      "start": "07:30",
      "end": "16:30",
      "break_minutes": 30,
      "code": "PROJ",
      "note": "Car park lighting, snagging"
    },
    {
      "date": "2026-08-26",
      "start": "07:30",
      "end": "16:30",
      "break_minutes": 30,
      "code": "PROJ"
    },
    {
      "date": "2026-08-27",
      "start": "07:30",
      "end": "15:30",
      "break_minutes": 30,
      "code": "PPM"
    },
    {
      "date": "2026-08-28",
      "start": "07:30",
      "end": "15:30",
      "break_minutes": 30,
      "code": "REACT",
      "note": "Door closer replaced, main entrance"
    },
    {
      "date": "2026-08-31",
      "type": "holiday",
      "note": "Summer bank holiday"
    }
  ]
}

Source

The body markup with Jinja, the stylesheet placed in <head> and the header and footer settings.

{#- Monthly timesheet. Every day of the period is listed and grouped by ISO week; worked hours come from start, end
    and break (a shift past midnight wraps), so nothing is stored twice. Expected hours are the contracted daily hours
    on each weekday that is not a public holiday; annual leave and sickness are credited at the daily hours, and the
    balance is worked + credited − expected. Entries: {date, start, end, break_minutes, code, note} for work, or
    {date, type: "leave" | "sick" | "holiday", note}. -#}
{%- set loc = "en-GB" -%}
{%- set daily = employee.daily_hours -%}
{%- set kinds = {"leave": "Annual leave", "sick": "Sick", "holiday": "Public holiday"} -%}
{%- set ns = namespace(days=[], worked=0.0, leave=0, sick=0, holidays=0, weekdays=0) -%}
{%- for i in range(0, 31) -%}
  {%- set d = period.start | date_add(days=i) | string -%}
  {%- if d <= period.end -%}
    {%- set dow = d | format_date("%u") | int -%}
    {%- set week = d | format_date("%G-%V") -%}
    {%- if dow <= 5 -%}{%- set ns.weekdays = ns.weekdays + 1 -%}{%- endif -%}
    {%- set matches = entries | selectattr("date", "equalto", d) | list -%}
    {%- if not matches -%}
      {%- set ns.days = ns.days + [{"date": d, "dow": dow, "week": week, "kind": "off" if dow > 5 else "missing", "hours": 0, "code": none, "entry": {}}] -%}
    {%- endif -%}
    {%- for e in matches -%}
      {%- set kind = e.type | default("work") -%}
      {%- if kind == "work" -%}
        {%- set s = e.start.split(":") -%}
        {%- set t = e.end.split(":") -%}
        {%- set span = ((t[0] | int) * 60 + (t[1] | int)) - ((s[0] | int) * 60 + (s[1] | int)) -%}
        {%- set span = span + 1440 if span < 0 else span -%}
        {%- set hours = ((span - (e.break_minutes | default(0))) / 60) | round(2) -%}
        {%- set ns.worked = ns.worked + hours -%}
      {%- else -%}
        {%- set hours = 0 -%}
        {%- if dow <= 5 and kind == "leave" -%}{%- set ns.leave = ns.leave + 1 -%}{%- endif -%}
        {%- if dow <= 5 and kind == "sick" -%}{%- set ns.sick = ns.sick + 1 -%}{%- endif -%}
        {%- if dow <= 5 and kind == "holiday" -%}{%- set ns.holidays = ns.holidays + 1 -%}{%- endif -%}
      {%- endif -%}
      {%- set ns.days = ns.days + [{"date": d, "dow": dow, "week": week, "kind": kind, "hours": hours, "code": e.code if kind == "work" else none, "entry": e}] -%}
    {%- endfor -%}
  {%- endif -%}
{%- endfor -%}
{%- set expected = (ns.weekdays - ns.holidays) * daily -%}
{%- set credited = (ns.leave + ns.sick) * daily -%}
{%- set worked = ns.worked | round(2) -%}
{%- set balance = (worked + credited - expected) | round(2) -%}
{%- set cns = namespace(max=0.0) -%}
{%- for c in codes -%}{%- set h = ns.days | selectattr("code", "equalto", c.code) | sum_by("hours") -%}{%- if h > cns.max -%}{%- set cns.max = h -%}{%- endif -%}{%- endfor -%}

{%- macro h(value) -%}{{ value | format_number(loc, 2) }}{%- endmacro -%}

<section class="title">
  <div>
    <p class="kicker">{{ company.name }} · Timesheet</p>
    <h1>{{ period.label }}</h1>
  </div>
  <dl class="who">
    <dt>Employee</dt><dd>{{ employee.name }} ({{ employee.number }})</dd>
    <dt>Role</dt><dd>{{ employee.role }}, {{ employee.department }}</dd>
    <dt>Manager</dt><dd>{{ employee.manager }}</dd>
    <dt>Contract</dt><dd>{{ h(employee.weekly_hours) }} hours a week, {{ h(daily) }} a day</dd>
  </dl>
</section>

<section class="summary">
  <div class="tile">
    <p class="tile-label">Expected</p>
    <p class="tile-value">{{ h(expected) }}</p>
    <p class="tile-note">{{ ns.weekdays - ns.holidays }} working days</p>
  </div>
  <div class="tile">
    <p class="tile-label">Worked</p>
    <p class="tile-value">{{ h(worked) }}</p>
    <p class="tile-note">{{ ns.days | selectattr("kind", "equalto", "work") | list | length }} shifts</p>
  </div>
  <div class="tile">
    <p class="tile-label">Absence</p>
    <p class="tile-value">{{ h(credited) }}</p>
    <p class="tile-note">{{ ns.leave }} leave, {{ ns.sick }} sick {{ "day" if ns.sick == 1 else "days" }}</p>
  </div>
  <div class="tile balance {{ 'plus' if balance > 0 else ('minus' if balance < 0 else '') }}">
    <p class="tile-label">Balance</p>
    <p class="tile-value">{{ "+" if balance > 0 else "" }}{{ h(balance) }}</p>
    <p class="tile-note">hours {{ "over" if balance >= 0 else "under" }} contract</p>
  </div>
  <div class="activity">
    <p class="tile-label">Hours by activity</p>
    {% for c in codes %}
    {%- set ch = ns.days | selectattr("code", "equalto", c.code) | sum_by("hours") %}
    <div class="activity-row">
      <span class="activity-name"><span class="swatch" style="background: {{ c.color }}"></span>{{ c.label }}</span>
      <span class="num">{{ h(ch) }}</span>
      <span class="bar"><span style="width: {{ ((ch / cns.max * 100) if cns.max else 0) | round(1) }}%; background: {{ c.color }}"></span></span>
    </div>
    {% endfor %}
  </div>
</section>

<table class="days">
  <thead>
    <tr>
      <th scope="col">Date</th>
      <th class="time" scope="col">Start</th>
      <th class="time" scope="col">End</th>
      <th class="num" scope="col">Break</th>
      <th class="num hours" scope="col">Hours</th>
      <th scope="col">Activity</th>
      <th scope="col">Notes</th>
    </tr>
  </thead>
  {% for week in ns.days | groupby("week") %}
  <tbody>
    <tr class="week-row">
      <td colspan="7">Week {{ week.grouper.split("-")[1] | int }}<span class="range">{{ (week.list | first).date | format_date("%-d %b", loc) }} – {{ (week.list | last).date | format_date("%-d %b", loc) }}</span></td>
    </tr>
    {% for day in week.list %}
    {%- set e = day.entry %}
    <tr class="{{ day.kind }}{{ ' absence' if day.kind in ['leave', 'sick'] else '' }}">
      <td class="date">{{ day.date | format_date("%a %-d %b", loc) }}</td>
      {% if day.kind == "work" %}
      {%- set code = codes | selectattr("code", "equalto", e.code) | first %}
      <td class="time">{{ e.start }}</td>
      <td class="time">{{ e.end }}</td>
      <td class="num">{{ e.break_minutes | default(0) }} min</td>
      <td class="num hours">{{ h(day.hours) }}</td>
      <td>{% if code %}<span class="code" style="background: {{ code.color }}">{{ code.code }}</span>{% else %}{{ e.code }}{% endif %}</td>
      <td class="note">{{ e.note | default("") }}</td>
      {% elif day.kind == "off" %}
      <td colspan="3">Weekend</td>
      <td class="num hours">–</td>
      <td></td>
      <td></td>
      {% elif day.kind == "missing" %}
      <td colspan="3">No entry</td>
      <td class="num hours">–</td>
      <td></td>
      <td></td>
      {% else %}
      <td colspan="3"><span class="kind {{ day.kind }}">{{ kinds[day.kind] or day.kind }}</span></td>
      <td class="num hours">{% if day.kind != "holiday" and day.dow <= 5 %}({{ h(daily) }}){% else %}–{% endif %}</td>
      <td></td>
      <td class="note">{{ e.note | default("") }}</td>
      {% endif %}
    </tr>
    {% endfor %}
    {% if week.list | rejectattr("kind", "equalto", "off") | list | length > 0 %}
    <tr class="subtotal">
      <td colspan="4" class="label">Worked in week {{ week.grouper.split("-")[1] | int }}</td>
      <td class="num hours">{{ h(week.list | sum_by("hours")) }}</td>
      <td colspan="2"></td>
    </tr>
    {% endif %}
  </tbody>
  {% endfor %}
</table>

<table class="totals">
  <tr><th scope="row">Hours worked</th><td class="num">{{ h(worked) }}</td></tr>
  <tr><th scope="row">Leave and sickness credited ({{ ns.leave + ns.sick }} × {{ h(daily) }})</th><td class="num">{{ h(credited) }}</td></tr>
  <tr><th scope="row">Expected hours ({{ ns.weekdays - ns.holidays }} × {{ h(daily) }})</th><td class="num">−{{ h(expected) }}</td></tr>
  <tr class="grand"><th scope="row">Balance</th><td class="num">{{ "+" if balance > 0 else "" }}{{ h(balance) }}</td></tr>
</table>

<section class="signoff">
  <div class="sign-box">
    <h2>Employee</h2>
    <p>I confirm that this timesheet is a true record of my hours in {{ period.label }}.</p>
    <div class="sign-line"></div>
    <div class="sign-meta"><span>{{ employee.name }}</span><span>Date</span></div>
  </div>
  <div class="sign-box">
    <h2>Approved by</h2>
    <p>Hours, call-outs and absences checked against the rota.</p>
    <div class="sign-line"></div>
    <div class="sign-meta"><span>{{ employee.manager }}, {{ employee.manager_role }}</span><span>Date</span></div>
  </div>
  <div class="sign-box payroll">
    <h2>Payroll use</h2>
    <div class="payroll-field"><p>Received</p><div class="write-line"></div></div>
    <div class="payroll-field"><p>Pay run</p><div class="write-line"></div></div>
    <div class="payroll-field"><p>Initials</p><div class="write-line"></div></div>
  </div>
</section>

Render it with the API

After adding the template to your workspace, render it by ID and pass your data. Arrays are shortened here.

curl -X POST https://api-eu.dynamicdocumentapi.com/v1/renders \
  -H "Authorization: Bearer $DYNAMIC_DOCUMENT_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
  "input": {
    "type": "template",
    "template_id": "tpl_01J9ZK3M7Q8V5W2X4Y6Z8A0B1C"
  },
  "data": {
    "company": {
      "name": "Copperbeech Facilities Ltd",
      "site": "Harbour Point Business Park, Bristol",
      "email": "payroll@copperbeech.example.com"
    },
    "employee": {
      "name": "Daniel Okafor",
      "number": "EMP-0472",
      "role": "Maintenance Technician",
      "department": "Estates and Facilities",
      "manager": "Rachel Whitmore",
      "manager_role": "Estates Manager",
      "daily_hours": 7.5,
      "weekly_hours": 37.5
    },
    "period": {
      "label": "August 2026",
      "start": "2026-08-01",
      "end": "2026-08-31",
      "submitted_on": "2026-09-01"
    },
    "codes": [
      {
        "code": "PPM",
        "label": "Planned maintenance",
        "color": "#2f6f8f"
      },
      {
        "code": "REACT",
        "label": "Reactive repairs",
        "color": "#d17a22"
      }
    ],
    "entries": [
      {
        "date": "2026-08-03",
        "start": "07:30",
        "end": "16:00",
        "break_minutes": 30,
        "code": "PPM",
        "note": "HVAC filter changes, Block A"
      },
      {
        "date": "2026-08-04",
        "start": "07:30",
        "end": "16:00",
        "break_minutes": 30,
        "code": "PPM",
        "note": "Fire door inspections, levels 1–4"
      }
    ]
  },
  "output": {
    "format": "pdf",
    "filename": "timesheet-monthly.pdf"
  }
}'