diff --git a/tailbone/templates/shifts/base.mako b/tailbone/templates/shifts/base.mako index f84154f4..4f57bf31 100644 --- a/tailbone/templates/shifts/base.mako +++ b/tailbone/templates/shifts/base.mako @@ -1,5 +1,6 @@ ## -*- coding: utf-8 -*- <%inherit file="/base.mako" /> +<%namespace file="/autocomplete.mako" import="autocomplete" /> <%def name="title()">${page_title}%def> @@ -132,8 +133,165 @@ <%def name="context_menu()">%def> -<%def name="render_day(day)"> - % for shift in day['shifts']: -
${render_shift(shift)}
- % endfor +<%def name="timesheet_wrapper(with_edit_form=False, change_employee=None)"> +
+
+ % if employee is not Undefined:
+
+
+
+ % endif
+
+ % if store_options is not Undefined:
+ ${form.field_div('store', h.select('store', store.uuid if store else None, store_options))}
+ % endif
+
+ % if department_options is not Undefined:
+ ${form.field_div('department', h.select('department', department.uuid if department else None, department_options))}
+ % endif
+
+
+ % if request.has_perm('{}.viewall'.format(permission_prefix)):
+ ${autocomplete('employee', url('employees.autocomplete'),
+ field_value=employee.uuid if employee else None,
+ field_display=unicode(employee or ''),
+ selected='employee_selected',
+ change_clicked=change_employee)}
+ % else:
+ ${form.hidden('employee', value=employee.uuid)}
+ ${employee}
+ % endif
+
+
+
+
+
+ ${self.edit_tools()}
+
+
+ ${week_of}
+
+ |
+
+ + | + +
+
+
+
+
+
+
+ ${form.text('date', value=sunday.strftime('%m/%d/%Y'))}
+
+ |
+
Employee | + % for day in weekdays: +${day.strftime('%A')} ${day.strftime('%b %d')} |
+ % endfor
+ Total Hours |
+
---|---|---|
+ ## TODO: add link to single employee schedule / timesheet here... + ${emp} + | + % for day in emp.weekdays: ++ % if render_day: + ${render_day(day)} + % else: + ${self.render_day(day)} + % endif + | + % endfor ++ ${self.render_employee_total(emp)} + | +
${len(employees)} employees | + % for day in weekdays: ++ % endfor + | + |
+ % for day in employee.weekdays: + | + ${self.render_employee_day_total(day)} + | + % endfor ++ ${self.render_employee_total(employee)} + | +
-
- % if employee is not UNDEFINED:
-
-
-
- % endif
-
- % if store_options is not UNDEFINED:
- ${form.field_div('store', h.select('store', store.uuid if store else None, store_options))}
- % endif
-
- % if department_options is not UNDEFINED:
- ${form.field_div('department', h.select('department', department.uuid if department else None, department_options))}
- % endif
-
-
- % if request.has_perm('{}.viewall'.format(permission_prefix)):
- ${autocomplete('employee', url('employees.autocomplete'),
- field_value=employee.uuid if employee else None,
- field_display=unicode(employee or ''),
- selected='employee_selected',
- change_clicked=change_employee)}
- % else:
- ${form.hidden('employee', value=employee.uuid)}
- ${employee}
- % endif
-
-
-
-
-
- % if edit_tools:
- ${edit_tools()}
- % endif
-
-
- ${week_of}
-
- |
-
- - | - -
-
-
-
-
-
-
- ${form.text('date', value=sunday.strftime('%m/%d/%Y'))}
-
- |
-
Employee | - % for day in weekdays: -${day.strftime('%A')} ${day.strftime('%b %d')} |
- % endfor
- Total Hours |
-
---|---|---|
- ## TODO: add link to single employee schedule / timesheet here... - ${emp} - | - % for day in emp.weekdays: -- % if render_day: - ${render_day(day)} - % endif - | - % endfor -${emp.hours_display} | -
${len(employees)} employees | - % for day in weekdays: -- % endfor - | - |
- % for day in employee.weekdays: - | ${day['hours_display']} | - % endfor -${employee.hours_display} | -
${render_shift(shift)}
+ % endfor +%def> + +<%def name="render_employee_total(employee)"> + ${employee.scheduled_hours_display} +%def> + +<%def name="render_employee_day_total(day)"> + ${day['scheduled_hours_display']} +%def> + + +${parent.body()} diff --git a/tailbone/templates/shifts/schedule_edit.mako b/tailbone/templates/shifts/schedule_edit.mako index ec771986..6bff3d11 100644 --- a/tailbone/templates/shifts/schedule_edit.mako +++ b/tailbone/templates/shifts/schedule_edit.mako @@ -1,6 +1,5 @@ ## -*- coding: utf-8 -*- <%inherit file="/shifts/base.mako" /> -<%namespace file="/shifts/lib.mako" import="timesheet_wrapper" /> <%def name="extra_javascript()"> ${parent.extra_javascript()} @@ -69,13 +68,17 @@ %def> <%def name="render_day(day)"> - % for shift in day['shifts']: + % for shift in day['scheduled_shifts']:${render_shift(shift)}
% endfor %def> +<%def name="render_employee_total(employee)"> + ${employee.scheduled_hours_display} +%def> + <%def name="edit_form()"> ${h.form(url('schedule.edit'), id='timetable-form')} ${h.csrf_token(request)} @@ -90,7 +93,8 @@ %def> -${timesheet_wrapper(edit_form=edit_form, edit_tools=edit_tools, context_menu=context_menu, render_day=render_day)} + +${self.timesheet_wrapper(with_edit_form=True)} ${edit_tools()} diff --git a/tailbone/templates/shifts/schedule_print.mako b/tailbone/templates/shifts/schedule_print.mako index 43703fda..4f1f935b 100644 --- a/tailbone/templates/shifts/schedule_print.mako +++ b/tailbone/templates/shifts/schedule_print.mako @@ -1,6 +1,6 @@ ## -*- coding: utf-8 -*- -<%namespace file="/shifts/lib.mako" import="timesheet" /> -<%namespace file="/shifts/base.mako" import="render_day" /> +<%namespace file="/shifts/base.mako" import="timesheet" /> +<%namespace file="/shifts/schedule.mako" import="render_day" /> ## TODO: this seems a little hacky..? diff --git a/tailbone/templates/shifts/timesheet.mako b/tailbone/templates/shifts/timesheet.mako index 971feb3a..6fff5027 100644 --- a/tailbone/templates/shifts/timesheet.mako +++ b/tailbone/templates/shifts/timesheet.mako @@ -1,6 +1,5 @@ ## -*- coding: utf-8 -*- <%inherit file="/shifts/base.mako" /> -<%namespace file="/shifts/lib.mako" import="timesheet_wrapper" /> <%def name="context_menu()"> % if employee is not Undefined and request.has_perm('timesheet.edit'): @@ -11,4 +10,15 @@ % endif %def> -${timesheet_wrapper(context_menu=context_menu, render_day=self.render_day)} +<%def name="render_day(day)"> + % for shift in day['worked_shifts']: +${render_shift(shift)}
+ % endfor +%def> + +<%def name="render_employee_total(employee)"> + ${employee.worked_hours_display} +%def> + + +${self.timesheet_wrapper()} diff --git a/tailbone/templates/shifts/timesheet_edit.mako b/tailbone/templates/shifts/timesheet_edit.mako index baf35634..50cb40a5 100644 --- a/tailbone/templates/shifts/timesheet_edit.mako +++ b/tailbone/templates/shifts/timesheet_edit.mako @@ -1,6 +1,5 @@ ## -*- coding: utf-8 -*- <%inherit file="/shifts/base.mako" /> -<%namespace file="/shifts/lib.mako" import="timesheet_wrapper" /> <%def name="extra_javascript()"> ${parent.extra_javascript()} @@ -22,13 +21,17 @@ %def> <%def name="render_day(day)"> - % for shift in day['shifts']: + % for shift in day['worked_shifts']:${render_shift(shift)}
% endfor %def> +<%def name="render_employee_total(employee)"> + ${employee.worked_hours_display} +%def> + <%def name="edit_form()"> ${h.form(url('timesheet.employee.edit'), id='timetable-form')} ${h.csrf_token(request)} @@ -41,7 +44,8 @@ %def> -${timesheet_wrapper(edit_form=edit_form, edit_tools=edit_tools, context_menu=context_menu, render_day=render_day, change_employee='confirm_leave')} + +${self.timesheet_wrapper(with_edit_form=True, change_employee='confirm_leave')} ${edit_tools()} diff --git a/tailbone/views/shifts/lib.py b/tailbone/views/shifts/lib.py index b1b36508..1eeb1f51 100644 --- a/tailbone/views/shifts/lib.py +++ b/tailbone/views/shifts/lib.py @@ -63,6 +63,7 @@ class TimeSheetView(View): key = None title = None model_class = None + expose_employee_views = True # Set this to False to avoid the default behavior of auto-filtering by # current store. @@ -72,6 +73,10 @@ class TimeSheetView(View): def get_title(cls): return cls.title or cls.key.capitalize() + @classmethod + def get_url_prefix(cls): + return getattr(cls, 'url_prefix', cls.key).rstrip('/') + def get_timesheet_context(self): """ Determine date/store/dept context from user's session and/or defaults. @@ -272,7 +277,7 @@ class TimeSheetView(View): department_options = self.get_department_options(departments) context = { - 'page_title': "Full {}".format(self.get_title()), + 'page_title': self.get_title_full(), 'form': forms.FormRenderer(form) if form else None, 'employees': employees, 'stores': stores, @@ -292,6 +297,9 @@ class TimeSheetView(View): context.update(kwargs) return context + def get_title_full(self): + return "Full {}".format(self.get_title()) + def render_shift(self, shift): return HTML.tag('span', c=shift.get_display(self.rattail_config)) @@ -330,26 +338,35 @@ class TimeSheetView(View): return context def modify_employees(self, employees, weekdays): + self.fetch_shift_data(self.model_class, employees, weekdays) + + def fetch_shift_data(self, cls, employees, weekdays): + """ + Fetch all shift data of the given model class (``cls``), according to + the given params. The cached shift data is attached to each employee. + """ + shift_type = 'scheduled' if cls is model.ScheduledShift else 'worked' 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 >= make_utc(min_time))\ - .filter(self.model_class.start_time < make_utc(max_time))\ + shifts = Session.query(cls)\ + .filter(cls.employee_uuid.in_([e.uuid for e in employees]))\ + .filter(cls.start_time >= make_utc(min_time))\ + .filter(cls.start_time < make_utc(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' + if not hasattr(employee, 'weekdays'): + employee.weekdays = [{} for day in weekdays] + setattr(employee, '{}_hours'.format(shift_type), datetime.timedelta(0)) + setattr(employee, '{}_hours_display'.format(shift_type), '0') - for day in weekdays: + for i, day in enumerate(weekdays): empday = { - 'shifts': [], - 'hours': datetime.timedelta(0), - 'hours_display': '', + '{}_shifts'.format(shift_type): [], + '{}_hours'.format(shift_type): datetime.timedelta(0), + '{}_hours_display'.format(shift_type): '', } while employee_shifts: @@ -357,23 +374,27 @@ class TimeSheetView(View): if shift.employee_uuid != employee.uuid: break elif shift.get_date(self.rattail_config) == day: - empday['shifts'].append(shift) + empday['{}_shifts'.format(shift_type)].append(shift) length = shift.length if length is not None: - empday['hours'] += shift.length - employee.hours += shift.length + empday['{}_hours'.format(shift_type)] += shift.length + setattr(employee, '{}_hours'.format(shift_type), + getattr(employee, '{}_hours'.format(shift_type)) + 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) + hours = empday['{}_hours'.format(shift_type)] + if hours: + minutes = (hours.days * 1440) + (hours.seconds / 60) + empday['{}_hours_display'.format(shift_type)] = '{}:{:02d}'.format(minutes // 60, minutes % 60) + employee.weekdays[i].update(empday) - if employee.hours: - minutes = (employee.hours.days * 1440) + (employee.hours.seconds / 60) - employee.hours_display = '{}:{:02d}'.format(minutes // 60, minutes % 60) + hours = getattr(employee, '{}_hours'.format(shift_type)) + if hours: + minutes = (hours.days * 1440) + (hours.seconds / 60) + setattr(employee, '{}_hours_display'.format(shift_type), + '{}:{:02d}'.format(minutes // 60, minutes % 60)) @classmethod def defaults(cls, config): @@ -388,24 +409,26 @@ class TimeSheetView(View): Provide default configuration for a time sheet view. """ title = cls.get_title() + url_prefix = cls.get_url_prefix() config.add_tailbone_permission_group(cls.key, title) - config.add_tailbone_permission(cls.key, '{}.view'.format(cls.key), "View single employee {}".format(title)) config.add_tailbone_permission(cls.key, '{}.viewall'.format(cls.key), "View full {}".format(title)) # full time sheet - config.add_route(cls.key, '/{}/'.format(cls.key)) + config.add_route(cls.key, '{}/'.format(url_prefix)) config.add_view(cls, attr='full', route_name=cls.key, renderer='/shifts/{}.mako'.format(cls.key), permission='{}.viewall'.format(cls.key)) # single employee time sheet - config.add_route('{}.employee'.format(cls.key), '/{}/employee/'.format(cls.key)) - config.add_view(cls, attr='employee', route_name='{}.employee'.format(cls.key), - renderer='/shifts/{}.mako'.format(cls.key), - permission='{}.view'.format(cls.key)) + if cls.expose_employee_views: + config.add_tailbone_permission(cls.key, '{}.view'.format(cls.key), "View single employee {}".format(title)) + config.add_route('{}.employee'.format(cls.key), '{}/employee/'.format(url_prefix)) + config.add_view(cls, attr='employee', route_name='{}.employee'.format(cls.key), + renderer='/shifts/{}.mako'.format(cls.key), + permission='{}.view'.format(cls.key)) # goto cross-view (view 'timesheet' as 'schedule' or vice-versa) other_key = 'timesheet' if cls.key == 'schedule' else 'schedule' - config.add_route('{}.goto.{}'.format(cls.key, other_key), '/{}/goto-{}'.format(cls.key, other_key)) + config.add_route('{}.goto.{}'.format(cls.key, other_key), '{}/goto-{}'.format(url_prefix, other_key)) config.add_view(cls, attr='crossview', route_name='{}.goto.{}'.format(cls.key, other_key), permission='{}.view'.format(other_key))