Last active
January 23, 2023 21:29
-
-
Save jgibbard/1c56248590008d91044d1aa65659c1d3 to your computer and use it in GitHub Desktop.
Timesheet calculator
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"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": [] | |
} | |
] | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
{ | |
"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