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>
+
+<%def name="timesheet(employees, employee_column=True)">
+
+
+
+
+
+
+ % if employee_column:
+ Employee |
+ % endif
+ % for day in weekdays:
+ ${day.strftime('%A')} ${day.strftime('%b %d')} |
+ % endfor
+ Total Hours |
+
+
+
+ % for employee in sorted(employees, key=unicode):
+
+ % if employee_column:
+ ${employee} |
+ % endif
+ % for day in employee.weekdays:
+
+ % for shift in day['shifts']:
+ ${shift.get_display(request.rattail_config)}
+ % endfor
+ |
+ % endfor
+ ${employee.hours_display} |
+
+ % endfor
+ % if employee_column:
+
+ ${len(employees)} employees |
+ % for day in weekdays:
+ |
+ % endfor
+ |
+
+ % else:
+
+ % for day in employee.weekdays:
+ ${day['hours_display']} |
+ % endfor
+ ${employee.hours_display} |
+
+ % endif
+
+
+%def>
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}%def>
+
+${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}%def>
+
+${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>
-
-<%def name="head_tags()">
- ${parent.head_tags()}
-
-
-%def>
-
-
-
-
-
-
- % for day in weekdays:
- ${day.strftime('%A')} ${day.strftime('%b %d')} |
- % endfor
- Total Hours |
-
-
-
-
- % for day in employee.weekdays:
-
- % for shift in day['shifts']:
- ${shift.get_display(request.rattail_config)}
- % endfor
- |
- % endfor
- ${employee.hours_display} |
-
-
- % for day in employee.weekdays:
- ${day['hours_display']} |
- % endfor
- ${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')