From b718336ac2a47e5bcedf31ac70fbcf783478c976 Mon Sep 17 00:00:00 2001 From: Lance Edgar Date: Tue, 3 May 2016 21:19:28 -0500 Subject: [PATCH] Refactor timesheet logic, add initial/basic schedule view Clearly need to be able to filter by store/department yet. --- tailbone/static/css/timesheet.css | 48 ++++++++ tailbone/templates/shifts/base.mako | 111 +++++++++++++++++ tailbone/templates/shifts/schedule.mako | 6 + tailbone/templates/shifts/timesheet.mako | 6 + tailbone/templates/timesheet/index.mako | 120 ------------------ tailbone/views/shifts/__init__.py | 33 +++++ tailbone/views/{shifts.py => shifts/core.py} | 0 tailbone/views/shifts/lib.py | 123 +++++++++++++++++++ tailbone/views/shifts/schedule.py | 68 ++++++++++ tailbone/views/shifts/timesheet.py | 54 ++++++++ tailbone/views/timesheet.py | 123 ------------------- 11 files changed, 449 insertions(+), 243 deletions(-) create mode 100644 tailbone/static/css/timesheet.css create mode 100644 tailbone/templates/shifts/base.mako create mode 100644 tailbone/templates/shifts/schedule.mako create mode 100644 tailbone/templates/shifts/timesheet.mako delete mode 100644 tailbone/templates/timesheet/index.mako create mode 100644 tailbone/views/shifts/__init__.py rename tailbone/views/{shifts.py => shifts/core.py} (100%) create mode 100644 tailbone/views/shifts/lib.py create mode 100644 tailbone/views/shifts/schedule.py create mode 100644 tailbone/views/shifts/timesheet.py delete mode 100644 tailbone/views/timesheet.py diff --git a/tailbone/static/css/timesheet.css b/tailbone/static/css/timesheet.css new file mode 100644 index 00000000..4f92cf74 --- /dev/null +++ b/tailbone/static/css/timesheet.css @@ -0,0 +1,48 @@ + +/********************************************************************** + * styles for time sheets / schedules + **********************************************************************/ + +.timesheet-header { + position: relative; +} + +.timesheet-header .week-picker { + bottom: 0.5em; + position: absolute; + right: 0; +} + +.timesheet-header .week-picker label { + margin-left: 1em; +} + +.timesheet { + border-bottom: 1px solid black; + border-right: 1px solid black; + width: 100%; +} + +.timesheet thead th, +.timesheet tbody td { + border-left: 1px solid black; + border-top: 1px solid black; +} + +.timesheet tbody td { + padding: 5px; + text-align: center; +} + +.timesheet tbody td.employee { + text-align: left; +} + +.timesheet tbody p.shift { + display: block; + margin: 0; +} + +.timesheet tbody tr.total { + font-weight: bold; +} diff --git a/tailbone/templates/shifts/base.mako b/tailbone/templates/shifts/base.mako new file mode 100644 index 00000000..992ca02d --- /dev/null +++ b/tailbone/templates/shifts/base.mako @@ -0,0 +1,111 @@ +## -*- coding: utf-8 -*- +<%inherit file="/base.mako" /> + +<%def name="head_tags()"> + ${parent.head_tags()} + ${h.stylesheet_link(request.static_url('tailbone:static/css/timesheet.css'))} + + + +<%def name="timesheet(employees, employee_column=True)"> + +
+ +##
+## +##
+## ${employee} +##
+##
+ +
+ +
+ ${week_of} +
+
+ +
+ ${h.form(request.current_route_url())} + ${h.link_to(u"« Previous", '?date=' + prev_sunday.strftime('%Y-%m-%d'), class_='button')} + ${h.link_to(u"Next »", '?date=' + next_sunday.strftime('%Y-%m-%d'), class_='button')} + + ${h.text('date', value=sunday.strftime('%Y-%m-%d'))} + ${h.submit('go', "Go")} + ${h.end_form()} +
+ +
+ + + + + % if employee_column: + + % endif + % for day in weekdays: + + % endfor + + + + + % for employee in sorted(employees, key=unicode): + + % if employee_column: + + % endif + % for day in employee.weekdays: + + % endfor + + + % endfor + % if employee_column: + + + % for day in weekdays: + + % endfor + + + % else: + + % for day in employee.weekdays: + + % endfor + + + % endif + +
Employee${day.strftime('%A')}
${day.strftime('%b %d')}
Total
Hours
${employee} + % for shift in day['shifts']: +

${shift.get_display(request.rattail_config)}

+ % endfor +
${employee.hours_display}
${len(employees)} employees
${day['hours_display']}${employee.hours_display}
+ diff --git a/tailbone/templates/shifts/schedule.mako b/tailbone/templates/shifts/schedule.mako new file mode 100644 index 00000000..c1796a47 --- /dev/null +++ b/tailbone/templates/shifts/schedule.mako @@ -0,0 +1,6 @@ +## -*- coding: utf-8 -*- +<%inherit file="/shifts/base.mako" /> + +<%def name="title()">Schedule: ${sunday} + +${self.timesheet(employees)} diff --git a/tailbone/templates/shifts/timesheet.mako b/tailbone/templates/shifts/timesheet.mako new file mode 100644 index 00000000..7f6b1ea6 --- /dev/null +++ b/tailbone/templates/shifts/timesheet.mako @@ -0,0 +1,6 @@ +## -*- coding: utf-8 -*- +<%inherit file="/shifts/base.mako" /> + +<%def name="title()">Time Sheet: ${sunday} + +${self.timesheet(employees, employee_column=False)} diff --git a/tailbone/templates/timesheet/index.mako b/tailbone/templates/timesheet/index.mako deleted file mode 100644 index 54943bab..00000000 --- a/tailbone/templates/timesheet/index.mako +++ /dev/null @@ -1,120 +0,0 @@ -## -*- coding: utf-8 -*- -<%inherit file="/base.mako" /> - -<%def name="title()">Time Sheet - -<%def name="head_tags()"> - ${parent.head_tags()} - - - - -
- -
- -
- ${employee} -
-
- -
- -
- ${week_of} -
-
- -
- ${h.form(url('timesheet'))} - ${h.link_to(u"« Previous", '?date=' + prev_sunday.strftime('%Y-%m-%d'), class_='button')} - ${h.link_to(u"Next »", '?date=' + next_sunday.strftime('%Y-%m-%d'), class_='button')} - - ${h.text('date', value=sunday.strftime('%Y-%m-%d'))} - ${h.submit('go', "Go")} - ${h.end_form()} -
- -
- - - - - % for day in weekdays: - - % endfor - - - - - - % for day in employee.weekdays: - - % endfor - - - - % for day in employee.weekdays: - - % endfor - - - -
${day.strftime('%A')}
${day.strftime('%b %d')}
Total
Hours
- % for shift in day['shifts']: -

${shift.get_display(request.rattail_config)}

- % endfor -
${employee.hours_display}
${day['hours_display']}${employee.hours_display}
diff --git a/tailbone/views/shifts/__init__.py b/tailbone/views/shifts/__init__.py new file mode 100644 index 00000000..208014c1 --- /dev/null +++ b/tailbone/views/shifts/__init__.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2014 Lance Edgar +# +# This file is part of Rattail. +# +# Rattail is free software: you can redistribute it and/or modify it under the +# terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for +# more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Rattail. If not, see . +# +################################################################################ +""" +Views for employee shifts +""" + +from __future__ import unicode_literals, absolute_import + + +def includeme(config): + config.include('tailbone.views.shifts.core') + config.include('tailbone.views.shifts.schedule') + config.include('tailbone.views.shifts.timesheet') diff --git a/tailbone/views/shifts.py b/tailbone/views/shifts/core.py similarity index 100% rename from tailbone/views/shifts.py rename to tailbone/views/shifts/core.py diff --git a/tailbone/views/shifts/lib.py b/tailbone/views/shifts/lib.py new file mode 100644 index 00000000..1668409c --- /dev/null +++ b/tailbone/views/shifts/lib.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2016 Lance Edgar +# +# This file is part of Rattail. +# +# Rattail is free software: you can redistribute it and/or modify it under the +# terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for +# more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Rattail. If not, see . +# +################################################################################ +""" +Base views for time sheets +""" + +from __future__ import unicode_literals, absolute_import + +import datetime + +from rattail.db import model +from rattail.time import localtime, get_sunday + +from tailbone.db import Session +from tailbone.views import View + + +class TimeSheetView(View): + """ + Base view for time sheets. + """ + model_class = None + + def get_date(self): + date = None + if 'date' in self.request.GET: + try: + date = datetime.datetime.strptime(self.request.GET['date'], '%Y-%m-%d').date() + except ValueError: + self.request.session.flash("The specified date is not valid: {}".format(self.request.GET['date']), 'error') + if not date: + date = localtime(self.rattail_config).date() + return date + + def render(self, date, employees): + """ + Render a time sheet for one or more employees, for the week which + includes the specified date. + """ + sunday = get_sunday(date) + weekdays = [sunday] + for i in range(1, 7): + weekdays.append(sunday + datetime.timedelta(days=i)) + + saturday = weekdays[-1] + if saturday.year == sunday.year: + week_of = '{} - {}'.format(sunday.strftime('%a %b %d'), saturday.strftime('%a %b %d, %Y')) + else: + week_of = '{} - {}'.format(sunday.strftime('%a %b %d, Y'), saturday.strftime('%a %b %d, %Y')) + + self.modify_employees(employees, weekdays) + return { + 'employees': employees, + 'week_of': week_of, + 'sunday': sunday, + 'prev_sunday': sunday - datetime.timedelta(days=7), + 'next_sunday': sunday + datetime.timedelta(days=7), + 'weekdays': weekdays, + } + + def modify_employees(self, employees, weekdays): + min_time = localtime(self.rattail_config, datetime.datetime.combine(weekdays[0], datetime.time(0))) + max_time = localtime(self.rattail_config, datetime.datetime.combine(weekdays[-1] + datetime.timedelta(days=1), datetime.time(0))) + shifts = Session.query(self.model_class)\ + .filter(self.model_class.employee_uuid.in_([e.uuid for e in employees]))\ + .filter(self.model_class.start_time >= min_time)\ + .filter(self.model_class.start_time < max_time)\ + .all() + + for employee in employees: + employee_shifts = sorted([s for s in shifts if s.employee_uuid == employee.uuid], + key=lambda s: (s.start_time, s.end_time)) + employee.weekdays = [] + employee.hours = datetime.timedelta(0) + employee.hours_display = '0' + + for day in weekdays: + empday = { + 'shifts': [], + 'hours': datetime.timedelta(0), + 'hours_display': '', + } + + while employee_shifts: + shift = employee_shifts[0] + if shift.employee_uuid != employee.uuid: + break + elif shift.get_date(self.rattail_config) == day: + empday['shifts'].append(shift) + empday['hours'] += shift.length + employee.hours += shift.length + del employee_shifts[0] + else: + break + + if empday['hours']: + minutes = (empday['hours'].days * 1440) + (empday['hours'].seconds / 60) + empday['hours_display'] = '{}:{:02d}'.format(minutes // 60, minutes % 60) + employee.weekdays.append(empday) + + if employee.hours: + minutes = (employee.hours.days * 1440) + (employee.hours.seconds / 60) + employee.hours_display = '{}:{:02d}'.format(minutes // 60, minutes % 60) diff --git a/tailbone/views/shifts/schedule.py b/tailbone/views/shifts/schedule.py new file mode 100644 index 00000000..f5dce073 --- /dev/null +++ b/tailbone/views/shifts/schedule.py @@ -0,0 +1,68 @@ +# -*- coding: utf-8 -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2016 Lance Edgar +# +# This file is part of Rattail. +# +# Rattail is free software: you can redistribute it and/or modify it under the +# terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for +# more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Rattail. If not, see . +# +################################################################################ +""" +Views for employee schedules +""" + +from __future__ import unicode_literals, absolute_import + +from rattail import enum +from rattail.db import model + +from tailbone.db import Session +from tailbone.views.shifts.lib import TimeSheetView + + +class ScheduleView(TimeSheetView): + """ + Simple view for current user's schedule. + """ + model_class = model.ScheduledShift + + def __call__(self): + date = self.get_date() + employees = Session.query(model.Employee)\ + .filter(model.Employee.status == enum.EMPLOYEE_STATUS_CURRENT) + + # TODO: + # store = Session.query(model.Store).filter_by(id='003').one() + # department = Session.query(model.Department).filter_by(number=6).one() + + # if store: + # employees = employees.join(model.EmployeeStore)\ + # .filter(model.EmployeeStore.store == store) + # if department: + # employees = employees.join(model.EmployeeDepartment)\ + # .filter(model.EmployeeDepartment.department == department) + + return self.render(date, employees.all()) + + +def includeme(config): + + config.add_tailbone_permission('schedule', 'schedule.view', "View Schedule") + + # current user's schedule + config.add_route('schedule', '/schedule/') + config.add_view(ScheduleView, route_name='schedule', + renderer='/shifts/schedule.mako', permission='schedule.view') diff --git a/tailbone/views/shifts/timesheet.py b/tailbone/views/shifts/timesheet.py new file mode 100644 index 00000000..4349f788 --- /dev/null +++ b/tailbone/views/shifts/timesheet.py @@ -0,0 +1,54 @@ +# -*- coding: utf-8 -*- +################################################################################ +# +# Rattail -- Retail Software Framework +# Copyright © 2010-2016 Lance Edgar +# +# This file is part of Rattail. +# +# Rattail is free software: you can redistribute it and/or modify it under the +# terms of the GNU Affero General Public License as published by the Free +# Software Foundation, either version 3 of the License, or (at your option) +# any later version. +# +# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY +# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS +# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for +# more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with Rattail. If not, see . +# +################################################################################ +""" +Views for employee time sheets +""" + +from __future__ import unicode_literals, absolute_import + +from rattail.db import model + +from tailbone.views.shifts.lib import TimeSheetView + + +class TimeSheetView(TimeSheetView): + """ + Simple view for current user's time sheet. + """ + model_class = model.WorkedShift + + def __call__(self): + date = self.get_date() + employee = self.request.user.employee + assert employee + return self.render(date, [employee]) + + +def includeme(config): + + config.add_tailbone_permission('timesheet', 'timesheet.view', "View Time Sheet") + + # current user's time sheet + config.add_route('timesheet', '/timesheet/') + config.add_view(TimeSheetView, route_name='timesheet', + renderer='/shifts/timesheet.mako', permission='timesheet.view') diff --git a/tailbone/views/timesheet.py b/tailbone/views/timesheet.py deleted file mode 100644 index 2b96e358..00000000 --- a/tailbone/views/timesheet.py +++ /dev/null @@ -1,123 +0,0 @@ -# -*- coding: utf-8 -*- -################################################################################ -# -# Rattail -- Retail Software Framework -# Copyright © 2010-2016 Lance Edgar -# -# This file is part of Rattail. -# -# Rattail is free software: you can redistribute it and/or modify it under the -# terms of the GNU Affero General Public License as published by the Free -# Software Foundation, either version 3 of the License, or (at your option) -# any later version. -# -# Rattail is distributed in the hope that it will be useful, but WITHOUT ANY -# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS -# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for -# more details. -# -# You should have received a copy of the GNU Affero General Public License -# along with Rattail. If not, see . -# -################################################################################ -""" -Views for employee time sheets -""" - -from __future__ import unicode_literals, absolute_import - -import datetime - -from rattail.db import model -from rattail.time import localtime, get_sunday - -from tailbone.db import Session -from tailbone.views import View - - -class TimeSheetView(View): - """ - Simple view for current user's time sheet. - """ - - def __call__(self): - date = None - if 'date' in self.request.GET: - try: - date = datetime.datetime.strptime(self.request.GET['date'], '%Y-%m-%d').date() - except ValueError: - self.request.session.flash("The specified date is not valid: {}".format(self.request.GET['date']), 'error') - if not date: - date = localtime(self.rattail_config).date() - return self.render(date) - - def render(self, date): - employee = self.request.user.employee - assert employee - sunday = get_sunday(date) - - weekdays = [sunday] - for i in range(1, 7): - weekdays.append(sunday + datetime.timedelta(days=i)) - - saturday = weekdays[-1] - if saturday.year == sunday.year: - week_of = '{} - {}'.format(sunday.strftime('%a %b %d'), saturday.strftime('%a %b %d, %Y')) - else: - week_of = '{} - {}'.format(sunday.strftime('%a %b %d, Y'), saturday.strftime('%a %b %d, %Y')) - - min_punch = localtime(self.rattail_config, datetime.datetime.combine(sunday, datetime.time(0))) - max_punch = localtime(self.rattail_config, datetime.datetime.combine(saturday + datetime.timedelta(days=1), datetime.time(0))) - shifts = Session.query(model.WorkedShift)\ - .filter(model.WorkedShift.employee == employee)\ - .filter(model.WorkedShift.punch_in >= min_punch)\ - .filter(model.WorkedShift.punch_in < max_punch)\ - .order_by(model.WorkedShift.punch_in, model.WorkedShift.punch_out)\ - .all() - - shifts_copy = list(shifts) - employee.weekdays = [] - employee.hours = datetime.timedelta(0) - for day in weekdays: - empday = {'shifts': [], 'hours': datetime.timedelta(0)} - - while shifts_copy: - shift = shifts_copy[0] - if shift.get_date(self.rattail_config) == day: - empday['shifts'].append(shift) - empday['hours'] += shift.length - employee.hours += shift.length - del shifts_copy[0] - else: - break - - empday['hours_display'] = '0' - if empday['hours']: - minutes = (empday['hours'].days * 1440) + (empday['hours'].seconds / 60) - empday['hours_display'] = '{}:{:02d}'.format(minutes // 60, minutes % 60) - employee.weekdays.append(empday) - - employee.hours_display = '0' - if employee.hours: - minutes = (employee.hours.days * 1440) + (employee.hours.seconds / 60) - employee.hours_display = '{}:{:02d}'.format(minutes // 60, minutes % 60) - - return { - 'employee': employee, - 'week_of': week_of, - 'sunday': sunday, - 'prev_sunday': sunday - datetime.timedelta(days=7), - 'next_sunday': sunday + datetime.timedelta(days=7), - 'weekdays': weekdays, - 'shifts': shifts, - } - - -def includeme(config): - - config.add_tailbone_permission('timesheet', 'timesheet.view', "View Time Sheet") - - # current user's time sheet - config.add_route('timesheet', '/timesheet/') - config.add_view(TimeSheetView, route_name='timesheet', - renderer='/timesheet/index.mako', permission='timesheet.view')