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} @@ -132,8 +133,165 @@ <%def name="context_menu()"> -<%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)"> +
+ + ${form.begin(id='filter-form')} + ${form.csrf_token()} + + + + + + + + + + + + + + + +
+ + % if employee is not Undefined: +
+ +
+ % 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 +
+
+ % 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 + +
+ +
+ ${week_of} +
+
+ + ${self.edit_tools()} + +
+
+
+ + + + ${form.text('date', value=sunday.strftime('%m/%d/%Y'))} +
+
+
+ + ${form.end()} + + % if with_edit_form: + ${self.edit_form()} + % endif + + ${self.timesheet()} + + % if with_edit_form: + ${h.end_form()} + % endif + +
+ +<%def name="timesheet(render_day=None)"> + + + + + + + % for day in weekdays: + + % endfor + + + + + % for emp in sorted(employees, key=unicode): + + + % for day in emp.weekdays: + + % endfor + + + % endfor + % if employee is Undefined: + + + % for day in weekdays: + + % endfor + + + % else: + + + % for day in employee.weekdays: + + % endfor + + + % endif + +
Employee${day.strftime('%A')}
${day.strftime('%b %d')}
Total
Hours
+ ## TODO: add link to single employee schedule / timesheet here... + ${emp} + + % if render_day: + ${render_day(day)} + % else: + ${self.render_day(day)} + % endif + + ${self.render_employee_total(emp)} +
${len(employees)} employees
  + ${self.render_employee_day_total(day)} + + ${self.render_employee_total(employee)} +
+ + +<%def name="edit_form()"> + +<%def name="edit_tools()"> + +<%def name="render_day(day)"> + +<%def name="render_employee_total(employee)"> + +<%def name="render_employee_day_total(day)"> + + +${self.timesheet_wrapper()} diff --git a/tailbone/templates/shifts/lib.mako b/tailbone/templates/shifts/lib.mako deleted file mode 100644 index a105ac02..00000000 --- a/tailbone/templates/shifts/lib.mako +++ /dev/null @@ -1,148 +0,0 @@ -## -*- coding: utf-8 -*- -<%namespace file="/autocomplete.mako" import="autocomplete" /> - -<%def name="timesheet_wrapper(edit_form=None, edit_tools=None, context_menu=None, render_day=None, change_employee=None)"> -
- - ${form.begin(id='filter-form')} - ${form.csrf_token()} - - - - - - - - - - - - - - - -
- - % if employee is not UNDEFINED: -
- -
- % 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 -
-
- % 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 - -
- -
- ${week_of} -
-
- - % if edit_tools: - ${edit_tools()} - % endif - -
-
-
- - - - ${form.text('date', value=sunday.strftime('%m/%d/%Y'))} -
-
-
- - ${form.end()} - - % if edit_form: - ${edit_form()} - % endif - - ${timesheet(render_day=render_day)} - - % if edit_form: - ${h.end_form()} - % endif - -
- - -<%def name="timesheet(render_day=None)"> - - - - - - - % for day in weekdays: - - % endfor - - - - - % for emp in sorted(employees, key=unicode): - - - % for day in emp.weekdays: - - % endfor - - - % endfor - % if employee is UNDEFINED: - - - % for day in weekdays: - - % endfor - - - % else: - - - % for day in employee.weekdays: - - % endfor - - - % endif - -
Employee${day.strftime('%A')}
${day.strftime('%b %d')}
Total
Hours
- ## TODO: add link to single employee schedule / timesheet here... - ${emp} - - % if render_day: - ${render_day(day)} - % endif - ${emp.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 index 52787d3a..61a69c32 100644 --- a/tailbone/templates/shifts/schedule.mako +++ b/tailbone/templates/shifts/schedule.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 request.has_perm('schedule.edit'): @@ -14,4 +13,19 @@ % endif -${timesheet_wrapper(context_menu=context_menu, render_day=self.render_day)} +<%def name="render_day(day)"> + % for shift in day['scheduled_shifts']: +

${render_shift(shift)}

+ % endfor + + +<%def name="render_employee_total(employee)"> + ${employee.scheduled_hours_display} + + +<%def name="render_employee_day_total(day)"> + ${day['scheduled_hours_display']} + + + +${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 name="render_day(day)"> - % for shift in day['shifts']: + % for shift in day['scheduled_shifts']:

${render_shift(shift)}

% endfor +<%def name="render_employee_total(employee)"> + ${employee.scheduled_hours_display} + + <%def name="edit_form()"> ${h.form(url('schedule.edit'), id='timetable-form')} ${h.csrf_token(request)} @@ -90,7 +93,8 @@ -${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 -${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 name="render_employee_total(employee)"> + ${employee.worked_hours_display} + + + +${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 name="render_day(day)"> - % for shift in day['shifts']: + % for shift in day['worked_shifts']:

${render_shift(shift)}

% endfor +<%def name="render_employee_total(employee)"> + ${employee.worked_hours_display} + + <%def name="edit_form()"> ${h.form(url('timesheet.employee.edit'), id='timetable-form')} ${h.csrf_token(request)} @@ -41,7 +44,8 @@ -${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))