Skip to content

Instantly share code, notes, and snippets.

@jgibbard
Last active January 23, 2023 21:29
Show Gist options
  • Save jgibbard/1c56248590008d91044d1aa65659c1d3 to your computer and use it in GitHub Desktop.
Save jgibbard/1c56248590008d91044d1aa65659c1d3 to your computer and use it in GitHub Desktop.
Timesheet calculator
Display the source blob
Display the rendered blob
Raw
{
"metadata": {
"language_info": {
"codemirror_mode": {
"name": "python",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.8"
},
"kernelspec": {
"name": "python",
"display_name": "Python (Pyodide)",
"language": "python"
}
},
"nbformat_minor": 4,
"nbformat": 4,
"cells": [
{
"cell_type": "code",
"source": "from datetime import datetime\nimport json\n\ndef time_delta(time_range):\n if time_range == \"FULL\":\n return 7.9\n start, stop = time_range.split(\"-\")\n start_time = datetime.strptime(start, \"%H:%M\")\n stop_time = datetime.strptime(stop, \"%H:%M\")\n return (((stop_time - start_time).total_seconds()) / (60.0 * 60.0))\n\ndef get_time(time_str):\n time = datetime.strptime(time_str, \"%H:%M\") - datetime.strptime(\"00:00\", \"%H:%M\")\n return time.total_seconds() / 3600.0\n\ndef _process_week(week, projects, lunch):\n \n project_list = [project[\"name\"] for project in projects]\n \n for day in week[\"data\"]:\n day[\"time\"] = time_delta(day[\"time\"]) - lunch\n new_proj = []\n reported_hours = 0.0\n rest_proj = \"\"\n over_proj = \"\"\n for project in day[\"projects\"]:\n \n if project[\"name\"] not in project_list:\n raise SystemExit(f\"Error: Project '{project['name']}' not found\")\n \n if project[\"time\"][-1] == \"*\":\n if over_proj != \"\":\n raise SystemExit(f\"Error: More than one project specifies overtime '*' on {day['day']}\")\n proj_time = project[\"time\"][:-1]\n over_proj = project[\"name\"]\n else:\n proj_time = project[\"time\"]\n \n if proj_time == \"REST\":\n if rest_proj != \"\":\n raise SystemExit(f\"Error: More than one project specifies 'REST' on {day['day']}\")\n rest_proj = project[\"name\"]\n else:\n proj_duration = get_time(proj_time)\n reported_hours += proj_duration\n new_proj.append({\"name\":project[\"name\"], \"time\": proj_duration, \"overtime_priority\":(over_proj==project[\"name\"])})\n\n if reported_hours > day[\"time\"]:\n raise SystemExit(f\"Error: more project time reported on {day['day']} than start/finish time allows\")\n if rest_proj != \"\":\n new_proj.append({\"name\":rest_proj, \"time\": day[\"time\"] - reported_hours, \"overtime_priority\":(rest_proj == over_proj)})\n\n day[\"projects\"] = new_proj\n\ndef _get_hours_worked(week):\n hours = 0.0\n for day in week[\"data\"]:\n for project in day[\"projects\"]:\n hours += project[\"time\"]\n return hours\n\n\ndef _calculate_flexi(week, contracted_hours):\n flexi_inc = week[\"flexi_inc\"]\n week_hours = _get_hours_worked(week)\n \n if flexi_inc == \"ALL\":\n if week_hours > contracted_hours:\n flexi_inc = week_hours - contracted_hours\n else:\n flexi_inc = 0.0\n else:\n flexi_inc = get_time(flexi_inc)\n\n plain_time = contracted_hours + flexi_inc\n\n if flexi_inc != 0.0 and (week_hours < plain_time):\n raise SystemExit(f\"'flexi_inc' set to {flexi_inc}, but you have only worked {week_hours} of the required {plain_time} hours\")\n\n if week_hours < plain_time:\n flex_delta = week_hours - plain_time\n else:\n flex_delta = flexi_inc\n \n return flex_delta, plain_time\n\ndef _print_timesheet(data):\n week = data[\"timesheet\"][0]\n plain_total = 0.0\n over_total = 0.0\n week_totals = {\"MON\":0.0, \"TUE\":0.0, \"WED\":0.0, \"THU\":0.0, \"FRI\":0.0}\n\n for project in data[\"projects\"]:\n print(project[\"name\"], project[\"code\"], \"Plain time\")\n for day in week[\"data\"]:\n for day_project in day[\"projects\"]:\n if day_project[\"name\"] == project[\"name\"]:\n if day_project[\"plain\"] != 0.0:\n print(f\"{day['day']}: {day_project['plain']:.2f}\")\n plain_total += day_project['plain']\n week_totals[day['day']] += day_project['plain']\n print(\"---------------------\")\n print(project[\"name\"], project[\"code\"], \"Overtime\")\n for day in week[\"data\"]:\n for day_project in day[\"projects\"]:\n if day_project[\"name\"] == project[\"name\"]:\n if day_project[\"overtime\"] != 0.0:\n print(f\"{day['day']}: {day_project['overtime']:.2f}\")\n over_total += day_project['overtime']\n week_totals[day['day']] += day_project['overtime']\n print(\"---------------------\\n\")\n\n print(\"Totals\")\n print(\"------\")\n print(f\"Plain time: {plain_total:.2f}, Overtime: {over_total:.2f}\")\n print(f\"MON: {week_totals['MON']:.2f}, TUE: {week_totals['TUE']:.2f}, WED: {week_totals['WED']:.2f}, THU: {week_totals['THU']:.2f}, FRI: {week_totals['FRI']:.2f}\")\n\n\ndef get_timesheet(filename):\n with open(filename) as f:\n data = json.load(f)\n week = data[\"timesheet\"][0]\n _process_week(week, data[\"projects\"], data[\"lunch\"])\n flex_delta, plain_time = _calculate_flexi(week, data[\"contracted_hours\"])\n week_hours = _get_hours_worked(week)\n\n overtime = 0.0\n if week_hours > plain_time:\n overtime = week_hours - plain_time\n\n print(f\"Worked: {week_hours}, Flexi change: {flex_delta}, Overtime: {overtime}\\n\")\n\n # Calculate overtime for each day\n # Work backwards so that plain time is used earlier in the week,\n # with overtime used towards the end of the week\n # Force at least data[\"contracted_hours\"] / 5.0 of non-overtime\n # per day on days where overtime is used\n daily_target = data[\"contracted_hours\"] / 5.0\n overtime_unused = overtime\n for day in reversed(week[\"data\"]):\n if day[\"time\"] > daily_target:\n over = day[\"time\"] - daily_target\n if over > overtime_unused:\n over = overtime_unused\n overtime_unused -= over\n else:\n over = 0.0\n day[\"overtime\"] = over\n\n # Sort the projects in order of the hours worked\n # But always put any project with overtime priority set first.\n # Set the overtime per project, starting with the highest priority\n for day in week[\"data\"]:\n day[\"projects\"] = sorted(day[\"projects\"], key=lambda d: 100.0 if d[\"overtime_priority\"] else d['time'], reverse=True)\n overtime_unused = day[\"overtime\"]\n for project in day[\"projects\"]:\n if project[\"time\"] >= overtime_unused:\n project[\"overtime\"] = overtime_unused\n\n overtime_unused = 0.0\n else:\n project[\"overtime\"] = project[\"time\"]\n overtime_unused -= project[\"time\"]\n project[\"plain\"] = project[\"time\"] - project[\"overtime\"]\n\n _print_timesheet(data)",
"metadata": {
"trusted": true
},
"execution_count": null,
"outputs": []
},
{
"cell_type": "code",
"source": "get_timesheet(\"data.js\")",
"metadata": {
"trusted": true
},
"execution_count": null,
"outputs": []
}
]
}
{
"timesheet":[
{"week":1, "flexi_inc":"0:00", "data":[
{"day":"MON", "time":"08:30-16:00", "projects":[{"name":"AK","time":"0:00*"},{"name":"JB","time":"REST"}]},
{"day":"TUE", "time":"08:30-17:15", "projects":[{"name":"AK","time":"REST*"},{"name":"JB","time":"0:00"}]},
{"day":"WED", "time":"08:30-17:15", "projects":[{"name":"AK","time":"1:00*"},{"name":"JB","time":"REST"}]},
{"day":"THU", "time":"08:30-17:15", "projects":[{"name":"AK","time":"1:00"},{"name":"JB","time":"REST"}]},
{"day":"FRI", "time":"08:30-18:15", "projects":[{"name":"AK","time":"1:00"},{"name":"JB","time":"REST"}]}]
},
{"week":0, "flexi_inc":"0:00", "data":[
{"day":"MON", "time":"08:30-17:15", "projects":[{"name":"AK","time":"1:00"},{"name":"JB","time":"REST"}]},
{"day":"TUE", "time":"08:30-17:15", "projects":[{"name":"AK","time":"1:00"},{"name":"JB","time":"REST"}]},
{"day":"WED", "time":"08:30-17:15", "projects":[{"name":"AK","time":"1:00"},{"name":"JB","time":"REST"}]},
{"day":"THU", "time":"08:30-17:15", "projects":[{"name":"AK","time":"1:00"},{"name":"JB","time":"REST"}]},
{"day":"FRI", "time":"08:30-17:15", "projects":[{"name":"AK","time":"1:00"},{"name":"JB","time":"REST"}]}]
}
],
"projects": [
{"name": "JB", "code": "70000:GHJ"},
{"name": "AK", "code": "48957:9358"},
{"name": "AL", "code": "LEAVE"}
],
"contracted_hours": 37.0,
"lunch": 0.5,
"start_flex": 0.0
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment