Refactor timesheet logic, add initial/basic schedule view
Clearly need to be able to filter by store/department yet.
This commit is contained in:
parent
34482892f7
commit
b718336ac2
48
tailbone/static/css/timesheet.css
Normal file
48
tailbone/static/css/timesheet.css
Normal file
|
@ -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;
|
||||||
|
}
|
111
tailbone/templates/shifts/base.mako
Normal file
111
tailbone/templates/shifts/base.mako
Normal file
|
@ -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'))}
|
||||||
|
<script type="text/javascript">
|
||||||
|
|
||||||
|
$(function() {
|
||||||
|
|
||||||
|
$('.week-picker #date').datepicker({
|
||||||
|
dateFormat: 'yy-mm-dd',
|
||||||
|
changeYear: true,
|
||||||
|
changeMonth: true,
|
||||||
|
showButtonPanel: true,
|
||||||
|
onSelect: function(dateText, inst) {
|
||||||
|
$(this).focus().select();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('.week-picker form').submit(function() {
|
||||||
|
location.href = '?date=' + $('.week-picker #date').val();
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
</%def>
|
||||||
|
|
||||||
|
<%def name="timesheet(employees, employee_column=True)">
|
||||||
|
<style type="text/css">
|
||||||
|
.timesheet thead th {
|
||||||
|
width: ${'{:0.2f}'.format(100.0 / float(9 if employee_column else 8))}%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<div class="timesheet-header">
|
||||||
|
|
||||||
|
## <div class="field-wrapper employee">
|
||||||
|
## <label>Employee</label>
|
||||||
|
## <div class="field">
|
||||||
|
## ${employee}
|
||||||
|
## </div>
|
||||||
|
## </div>
|
||||||
|
|
||||||
|
<div class="field-wrapper week">
|
||||||
|
<label>Week of</label>
|
||||||
|
<div class="field">
|
||||||
|
${week_of}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="week-picker">
|
||||||
|
${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')}
|
||||||
|
<label>Jump to week:</label>
|
||||||
|
${h.text('date', value=sunday.strftime('%Y-%m-%d'))}
|
||||||
|
${h.submit('go', "Go")}
|
||||||
|
${h.end_form()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div><!-- timesheet-header -->
|
||||||
|
|
||||||
|
<table class="timesheet">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
% if employee_column:
|
||||||
|
<th>Employee</th>
|
||||||
|
% endif
|
||||||
|
% for day in weekdays:
|
||||||
|
<th>${day.strftime('%A')}<br />${day.strftime('%b %d')}</th>
|
||||||
|
% endfor
|
||||||
|
<th>Total<br />Hours</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
% for employee in sorted(employees, key=unicode):
|
||||||
|
<tr>
|
||||||
|
% if employee_column:
|
||||||
|
<td class="employee">${employee}</td>
|
||||||
|
% endif
|
||||||
|
% for day in employee.weekdays:
|
||||||
|
<td>
|
||||||
|
% for shift in day['shifts']:
|
||||||
|
<p class="shift">${shift.get_display(request.rattail_config)}</p>
|
||||||
|
% endfor
|
||||||
|
</td>
|
||||||
|
% endfor
|
||||||
|
<td>${employee.hours_display}</td>
|
||||||
|
</tr>
|
||||||
|
% endfor
|
||||||
|
% if employee_column:
|
||||||
|
<tr class="total">
|
||||||
|
<td class="employee">${len(employees)} employees</td>
|
||||||
|
% for day in weekdays:
|
||||||
|
<td></td>
|
||||||
|
% endfor
|
||||||
|
<td></td>
|
||||||
|
</tr>
|
||||||
|
% else:
|
||||||
|
<tr>
|
||||||
|
% for day in employee.weekdays:
|
||||||
|
<td>${day['hours_display']}</td>
|
||||||
|
% endfor
|
||||||
|
<td>${employee.hours_display}</td>
|
||||||
|
</tr>
|
||||||
|
% endif
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</%def>
|
6
tailbone/templates/shifts/schedule.mako
Normal file
6
tailbone/templates/shifts/schedule.mako
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
## -*- coding: utf-8 -*-
|
||||||
|
<%inherit file="/shifts/base.mako" />
|
||||||
|
|
||||||
|
<%def name="title()">Schedule: ${sunday}</%def>
|
||||||
|
|
||||||
|
${self.timesheet(employees)}
|
6
tailbone/templates/shifts/timesheet.mako
Normal file
6
tailbone/templates/shifts/timesheet.mako
Normal file
|
@ -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)}
|
|
@ -1,120 +0,0 @@
|
||||||
## -*- coding: utf-8 -*-
|
|
||||||
<%inherit file="/base.mako" />
|
|
||||||
|
|
||||||
<%def name="title()">Time Sheet</%def>
|
|
||||||
|
|
||||||
<%def name="head_tags()">
|
|
||||||
${parent.head_tags()}
|
|
||||||
<style type="text/css">
|
|
||||||
.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 {
|
|
||||||
width: 12.5%;
|
|
||||||
}
|
|
||||||
.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 p.shift {
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
<script type="text/javascript">
|
|
||||||
|
|
||||||
$(function() {
|
|
||||||
|
|
||||||
$('.week-picker #date').datepicker({
|
|
||||||
dateFormat: 'yy-mm-dd',
|
|
||||||
changeYear: true,
|
|
||||||
changeMonth: true,
|
|
||||||
showButtonPanel: true,
|
|
||||||
onSelect: function(dateText, inst) {
|
|
||||||
$(this).focus().select();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$('.week-picker form').submit(function() {
|
|
||||||
location.href = '${url('timesheet')}?date=' + $('.week-picker #date').val();
|
|
||||||
return false;
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
</script>
|
|
||||||
</%def>
|
|
||||||
|
|
||||||
<div class="timesheet-header">
|
|
||||||
|
|
||||||
<div class="field-wrapper employee">
|
|
||||||
<label>Employee</label>
|
|
||||||
<div class="field">
|
|
||||||
${employee}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="field-wrapper week">
|
|
||||||
<label>Week of</label>
|
|
||||||
<div class="field">
|
|
||||||
${week_of}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="week-picker">
|
|
||||||
${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')}
|
|
||||||
<label>Jump to week:</label>
|
|
||||||
${h.text('date', value=sunday.strftime('%Y-%m-%d'))}
|
|
||||||
${h.submit('go', "Go")}
|
|
||||||
${h.end_form()}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div><!-- timesheet-header -->
|
|
||||||
|
|
||||||
<table class="timesheet">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
% for day in weekdays:
|
|
||||||
<th>${day.strftime('%A')}<br />${day.strftime('%b %d')}</th>
|
|
||||||
% endfor
|
|
||||||
<th>Total<br />Hours</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
% for day in employee.weekdays:
|
|
||||||
<td>
|
|
||||||
% for shift in day['shifts']:
|
|
||||||
<p class="shift">${shift.get_display(request.rattail_config)}</p>
|
|
||||||
% endfor
|
|
||||||
</td>
|
|
||||||
% endfor
|
|
||||||
<td>${employee.hours_display}</td>
|
|
||||||
</tr>
|
|
||||||
<tr>
|
|
||||||
% for day in employee.weekdays:
|
|
||||||
<td>${day['hours_display']}</td>
|
|
||||||
% endfor
|
|
||||||
<td>${employee.hours_display}</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
33
tailbone/views/shifts/__init__.py
Normal file
33
tailbone/views/shifts/__init__.py
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
"""
|
||||||
|
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')
|
123
tailbone/views/shifts/lib.py
Normal file
123
tailbone/views/shifts/lib.py
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
"""
|
||||||
|
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)
|
68
tailbone/views/shifts/schedule.py
Normal file
68
tailbone/views/shifts/schedule.py
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
"""
|
||||||
|
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')
|
54
tailbone/views/shifts/timesheet.py
Normal file
54
tailbone/views/shifts/timesheet.py
Normal file
|
@ -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 <http://www.gnu.org/licenses/>.
|
||||||
|
#
|
||||||
|
################################################################################
|
||||||
|
"""
|
||||||
|
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')
|
|
@ -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 <http://www.gnu.org/licenses/>.
|
|
||||||
#
|
|
||||||
################################################################################
|
|
||||||
"""
|
|
||||||
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')
|
|
Loading…
Reference in a new issue