Add timesheet report generation API route

This commit is contained in:
Tanner Collin 2017-06-28 06:24:45 +00:00
parent 9817a4ebba
commit 4469db2602
4 changed files with 150 additions and 31 deletions

View File

@ -17,14 +17,14 @@ class Client(models.Model):
business_number = models.CharField(max_length=16) business_number = models.CharField(max_length=16)
def __str__(self): def __str__(self):
return 'Client: ' + self.user.username return self.user.first_name + ' ' + self.user.last_name
class Provider(models.Model): class Provider(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE) user = models.OneToOneField(User, on_delete=models.CASCADE)
sin = models.CharField(max_length=16) sin = models.CharField(max_length=16)
def __str__(self): def __str__(self):
return 'Provider: ' + self.user.username return self.user.first_name + ' ' + self.user.last_name
class WorkType(models.Model): class WorkType(models.Model):
uuid = models.UUIDField(default=uuid.uuid4, editable=False) uuid = models.UUIDField(default=uuid.uuid4, editable=False)
@ -63,7 +63,6 @@ class Shift(models.Model):
set_end = models.DateTimeField() set_end = models.DateTimeField()
actual_start = models.DateTimeField(null=True) actual_start = models.DateTimeField(null=True)
actual_end = models.DateTimeField(null=True) actual_end = models.DateTimeField(null=True)
amount = models.DecimalField(max_digits=8, decimal_places=2, null=True)
description = models.CharField(max_length=100, null=True) description = models.CharField(max_length=100, null=True)
chart = models.TextField(max_length=1000, null=True) chart = models.TextField(max_length=1000, null=True)
deleted = models.BooleanField(default=False) deleted = models.BooleanField(default=False)

View File

@ -42,19 +42,19 @@ class PriceSerializer(serializers.ModelSerializer):
raise serializers.ValidationError("User is not a client.") raise serializers.ValidationError("User is not a client.")
client = user.client client = user.client
manage_objs = Manage.objects.filter(client=client).filter(deleted=False) manage_objs = Manage.objects.filter(deleted=False).filter(client=client)
employee_uuid = validated_data.pop('get_employee_uuid') employee_uuid = validated_data.pop('get_employee_uuid')
employee = get_object_or_404(manage_objs, uuid=employee_uuid) employee = get_object_or_404(manage_objs, uuid=employee_uuid)
worktype_objs = WorkType.objects.filter(client=client).filter(deleted=False) worktype_objs = WorkType.objects.filter(deleted=False).filter(client=client)
work_type_uuid = validated_data.pop('get_work_type_uuid') work_type_uuid = validated_data.pop('get_work_type_uuid')
work_type = get_object_or_404(worktype_objs, uuid=work_type_uuid) work_type = get_object_or_404(worktype_objs, uuid=work_type_uuid)
if not employee.approved: if not employee.approved:
raise serializers.ValidationError("Employee hasn't approved employment yet.") raise serializers.ValidationError("Employee hasn't approved employment yet.")
if Price.objects.filter(management=employee) \ if Price.objects.filter(deleted=False) \
.filter(work_type=work_type) \ .filter(work_type=work_type) \
.filter(deleted=False).exists(): .filter(management=employee).exists():
raise serializers.ValidationError("Price for this work already defined.") raise serializers.ValidationError("Price for this work already defined.")
validated_data['management'] = employee validated_data['management'] = employee
@ -96,9 +96,9 @@ class EmployeeSerializer(serializers.ModelSerializer):
provider_email = validated_data.pop('provider_email') provider_email = validated_data.pop('provider_email')
provider = get_object_or_404(Provider, user__email=provider_email) provider = get_object_or_404(Provider, user__email=provider_email)
if Manage.objects.filter(client=client) \ if Manage.objects.filter(deleted=False) \
.filter(provider=provider) \ .filter(provider=provider) \
.filter(deleted=False).exists(): .filter(client=client).exists():
raise serializers.ValidationError("Employee already added.") raise serializers.ValidationError("Employee already added.")
validated_data['client'] = client validated_data['client'] = client
@ -172,13 +172,27 @@ class UserSerializer(serializers.ModelSerializer):
fields = ('username', 'is_active', 'first_name', 'last_name', 'email', 'userinfo', 'client', 'provider') fields = ('username', 'is_active', 'first_name', 'last_name', 'email', 'userinfo', 'client', 'provider')
depth = 1 depth = 1
class CShiftSerializer(serializers.ModelSerializer): class ShiftSerializer(serializers.ModelSerializer):
price = serializers.SerializerMethodField()
amount = serializers.SerializerMethodField()
def get_price(self, obj):
return obj.price.uuid
def get_amount(self, obj):
if obj.actual_end:
shift_length = obj.actual_end - obj.actual_start
return Decimal(shift_length.total_seconds()) / 3600 * obj.set_price
else:
return None
class CShiftSerializer(ShiftSerializer):
get_price_uuid = serializers.UUIDField(write_only=True) get_price_uuid = serializers.UUIDField(write_only=True)
class Meta: class Meta:
model = Shift model = Shift
fields = ('uuid', 'chart', 'get_price_uuid', 'price', 'set_price', 'set_date', 'set_start', 'set_end', 'actual_start', 'actual_end', 'amount', 'description', 'deleted') fields = ('uuid', 'chart', 'get_price_uuid', 'price', 'set_price', 'set_date', 'set_start', 'set_end', 'actual_start', 'actual_end', 'amount', 'description', 'deleted')
read_only_fields = ('chart', 'price', 'set_price', 'set_date', 'actual_start', 'actual_end', 'amount', 'deleted') read_only_fields = ('chart', 'price', 'set_price', 'set_date', 'actual_start', 'actual_end', 'deleted')
def create(self, validated_data): def create(self, validated_data):
user = self.context['request'].user user = self.context['request'].user
@ -187,7 +201,7 @@ class CShiftSerializer(serializers.ModelSerializer):
raise serializers.ValidationError("User is not a client.") raise serializers.ValidationError("User is not a client.")
client = user.client client = user.client
price_objs = Price.objects.filter(management__client=client).filter(deleted=False) price_objs = Price.objects.filter(deleted=False).filter(management__client=client)
price_uuid = validated_data.pop('get_price_uuid') price_uuid = validated_data.pop('get_price_uuid')
price = get_object_or_404(price_objs, uuid=price_uuid) price = get_object_or_404(price_objs, uuid=price_uuid)
@ -215,13 +229,13 @@ class CShiftSerializer(serializers.ModelSerializer):
validated_data['price'] = price validated_data['price'] = price
return serializers.ModelSerializer.create(self, validated_data) return serializers.ModelSerializer.create(self, validated_data)
class PShiftSerializer(serializers.ModelSerializer): class PShiftSerializer(ShiftSerializer):
action = serializers.ChoiceField(write_only=True, choices=['checkin', 'checkout']) action = serializers.ChoiceField(write_only=True, choices=['checkin', 'checkout'])
class Meta: class Meta:
model = Shift model = Shift
fields = ('uuid', 'action', 'chart', 'price', 'set_price', 'set_date', 'set_start', 'set_end', 'actual_start', 'actual_end', 'amount', 'description', 'deleted') fields = ('uuid', 'action', 'chart', 'price', 'set_price', 'set_date', 'set_start', 'set_end', 'actual_start', 'actual_end', 'amount', 'description', 'deleted')
read_only_fields = ('uuid', 'price', 'set_price', 'set_date', 'set_start', 'set_end', 'actual_start', 'actual_end', 'amount', 'description', 'deleted') read_only_fields = ('uuid', 'price', 'set_price', 'set_date', 'set_start', 'set_end', 'actual_start', 'actual_end', 'description', 'deleted')
def update(self, instance, validated_data): def update(self, instance, validated_data):
action = validated_data['action'] action = validated_data['action']
@ -247,9 +261,24 @@ class PShiftSerializer(serializers.ModelSerializer):
raise serializers.ValidationError("No chart info provided.") raise serializers.ValidationError("No chart info provided.")
instance.actual_end = datetime.now(timezone.utc) instance.actual_end = datetime.now(timezone.utc)
shift_length = instance.actual_end - instance.actual_start
instance.amount = Decimal(shift_length.total_seconds()) / 3600 * instance.set_price
instance.chart = chart instance.chart = chart
instance.save() instance.save()
return instance return instance
class TimeSheetSerializer(serializers.Serializer):
class ShiftSerializer(serializers.Serializer):
date = serializers.DateField()
start = serializers.DateTimeField()
end = serializers.DateTimeField()
hours = serializers.DecimalField(max_digits=8, decimal_places=2)
rate = serializers.DecimalField(max_digits=8, decimal_places=2)
amount = serializers.DecimalField(max_digits=8, decimal_places=2)
work = serializers.CharField()
client = serializers.CharField()
provider = serializers.CharField()
payday = serializers.DateField()
paystart = serializers.DateField()
payend = serializers.DateField()
shifts = ShiftSerializer(many=True)

View File

@ -1,11 +1,18 @@
import calendar, datetime
from datetime import timedelta
from decimal import Decimal
from itertools import groupby
from django.shortcuts import render from django.shortcuts import render
from django.contrib.auth.models import User, Group from django.contrib.auth.models import User, Group
from django.core import exceptions
from django.db.models import Q
from caremyway.api.models import UserInfo, Client, Provider, WorkType, Manage, Price, Shift from caremyway.api.models import UserInfo, Client, Provider, WorkType, Manage, Price, Shift
from rest_framework import viewsets, permissions, status from rest_framework import viewsets, permissions, status, serializers
from rest_framework.decorators import api_view from rest_framework.decorators import api_view
from rest_framework.response import Response from rest_framework.response import Response
from rest_framework.views import APIView from rest_framework.views import APIView
from caremyway.api.serializers import UserSerializer, UserInfoSerializer, ClientSerializer, ProviderSerializer, WorkTypeSerializer, EmployeeSerializer, EmployerSerializer, PriceSerializer, CShiftSerializer, PShiftSerializer from rest_framework.renderers import StaticHTMLRenderer
from caremyway.api.serializers import UserSerializer, UserInfoSerializer, ClientSerializer, ProviderSerializer, WorkTypeSerializer, EmployeeSerializer, EmployerSerializer, PriceSerializer, CShiftSerializer, PShiftSerializer, TimeSheetSerializer
class UserViewSet(viewsets.ModelViewSet): class UserViewSet(viewsets.ModelViewSet):
lookup_field = 'username' lookup_field = 'username'
@ -18,7 +25,7 @@ class UserViewSet(viewsets.ModelViewSet):
return User.objects.filter(username=self.request.user) return User.objects.filter(username=self.request.user)
class UserInfoViewSet(viewsets.ModelViewSet): class UserInfoViewSet(viewsets.ModelViewSet):
lookup_field = "user__username" lookup_field = 'user__username'
serializer_class = UserInfoSerializer serializer_class = UserInfoSerializer
# Disallow DELETE # Disallow DELETE
@ -28,7 +35,7 @@ class UserInfoViewSet(viewsets.ModelViewSet):
return UserInfo.objects.filter(user__username=self.request.user) return UserInfo.objects.filter(user__username=self.request.user)
class ClientViewSet(viewsets.ModelViewSet): class ClientViewSet(viewsets.ModelViewSet):
lookup_field = "user__username" lookup_field = 'user__username'
serializer_class = ClientSerializer serializer_class = ClientSerializer
# Disallow DELETE # Disallow DELETE
@ -38,7 +45,7 @@ class ClientViewSet(viewsets.ModelViewSet):
return Client.objects.filter(user__username=self.request.user) return Client.objects.filter(user__username=self.request.user)
class ProviderViewSet(viewsets.ModelViewSet): class ProviderViewSet(viewsets.ModelViewSet):
lookup_field = "user__username" lookup_field = 'user__username'
serializer_class = ProviderSerializer serializer_class = ProviderSerializer
# Disallow DELETE # Disallow DELETE
@ -48,7 +55,7 @@ class ProviderViewSet(viewsets.ModelViewSet):
return Provider.objects.filter(user__username=self.request.user) return Provider.objects.filter(user__username=self.request.user)
class WorkTypeViewSet(viewsets.ModelViewSet): class WorkTypeViewSet(viewsets.ModelViewSet):
lookup_field = "uuid" lookup_field = 'uuid'
serializer_class = WorkTypeSerializer serializer_class = WorkTypeSerializer
def get_queryset(self): def get_queryset(self):
@ -61,7 +68,7 @@ class WorkTypeViewSet(viewsets.ModelViewSet):
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
class EmployeeViewSet(viewsets.ModelViewSet): class EmployeeViewSet(viewsets.ModelViewSet):
lookup_field = "uuid" lookup_field = 'uuid'
serializer_class = EmployeeSerializer serializer_class = EmployeeSerializer
def get_queryset(self): def get_queryset(self):
@ -74,7 +81,7 @@ class EmployeeViewSet(viewsets.ModelViewSet):
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
class EmployerViewSet(viewsets.ModelViewSet): class EmployerViewSet(viewsets.ModelViewSet):
lookup_field = "uuid" lookup_field = 'uuid'
serializer_class = EmployerSerializer serializer_class = EmployerSerializer
# Disallow creation and deletions of relationships # Disallow creation and deletions of relationships
@ -84,7 +91,7 @@ class EmployerViewSet(viewsets.ModelViewSet):
return Manage.objects.filter(provider__user__username=self.request.user) return Manage.objects.filter(provider__user__username=self.request.user)
class PriceViewSet(viewsets.ModelViewSet): class PriceViewSet(viewsets.ModelViewSet):
lookup_field = "uuid" lookup_field = 'uuid'
serializer_class = PriceSerializer serializer_class = PriceSerializer
def get_queryset(self): def get_queryset(self):
@ -97,12 +104,12 @@ class PriceViewSet(viewsets.ModelViewSet):
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
class CShiftViewSet(viewsets.ModelViewSet): class CShiftViewSet(viewsets.ModelViewSet):
lookup_field = "uuid" lookup_field = 'uuid'
serializer_class = CShiftSerializer serializer_class = CShiftSerializer
def get_queryset(self): def get_queryset(self):
return Shift.objects.filter(price__management__client__user__username=self.request.user) \ return Shift.objects.filter(deleted=False) \
.filter(deleted=False) \ .filter(price__management__client__user__username=self.request.user) \
.order_by('-set_start') .order_by('-set_start')
def destroy(self, request, *args, **kwargs): def destroy(self, request, *args, **kwargs):
@ -116,17 +123,100 @@ class CShiftViewSet(viewsets.ModelViewSet):
return Response(status=status.HTTP_204_NO_CONTENT) return Response(status=status.HTTP_204_NO_CONTENT)
class PShiftViewSet(viewsets.ModelViewSet): class PShiftViewSet(viewsets.ModelViewSet):
lookup_field = "uuid" lookup_field = 'uuid'
serializer_class = PShiftSerializer serializer_class = PShiftSerializer
# Disallow creation and deletions of relationships # Disallow creation and deletions of relationships
http_method_names = ['get', 'head', 'put', 'options'] http_method_names = ['get', 'head', 'put', 'options']
def get_queryset(self): def get_queryset(self):
return Shift.objects.filter(price__management__provider__user__username=self.request.user) \ return Shift.objects.filter(deleted=False) \
.filter(deleted=False) \ .filter(price__management__provider__user__username=self.request.user) \
.order_by('-set_start') .order_by('-set_start')
def validate_param(value, field):
class ValidateParam(serializers.Serializer):
value = field
obj = ValidateParam(data={'value': value})
if obj.is_valid():
return obj.validated_data.get('value');
else:
raise serializers.ValidationError(obj.errors['value'])
def get_paystart(payday):
# Assumes payday is a valided date obj
if payday.day == 15:
paystart_day = 1
elif payday.day == calendar.monthrange(payday.year, payday.month)[1]:
paystart_day = 16
else:
raise serializers.ValidationError("Date is not a valid payday.")
return datetime.date(payday.year, payday.month, paystart_day) - timedelta(days=4)
def get_payend(payday):
return payday - timedelta(days=4)
def get_timesheet(user, payday, manage=None):
paystart = get_paystart(payday)
payend = get_payend(payday)
shifts = Shift.objects.filter(deleted=False) \
.exclude(actual_end__isnull=True) \
.filter(
Q(price__management__client__user__username=user)
| Q(price__management__provider__user__username=user)) \
.filter(set_date__gte=paystart) \
.filter(set_date__lte=payend) \
.order_by('price__management', 'actual_start')
if manage:
shifts = shifts.filter(price__management__uuid=manage)
manage_shifts = groupby(shifts, lambda x: x.price.management)
timesheets = []
for management, shifts in manage_shifts:
timesheet = {}
timesheet['client'] = management.client
timesheet['provider'] = management.provider
timesheet['payday'] = payday
timesheet['paystart'] = paystart
timesheet['payend'] = payend
timesheet['shifts'] = []
for s in shifts:
shift = {}
shift['date'] = s.set_date
shift['start'] = s.actual_start
shift['end'] = s.actual_end
shift['hours'] = (shift['end'] - shift['start']).total_seconds() / 3600
shift['rate'] = s.set_price
shift['amount'] = Decimal(shift['hours']) * shift['rate']
shift['work'] = s.price.work_type.label
timesheet['shifts'].append(shift)
timesheets.append(timesheet)
return timesheets
class ReportView(APIView):
def get(self, request, *args, **kwargs):
response = []
report_type = kwargs['type']
qp = request.query_params
if report_type == 'timesheet':
payday = validate_param(qp.get('payday'), serializers.DateField())
manage = validate_param(qp.get('manage'), serializers.UUIDField(allow_null=True))
timesheet = get_timesheet(request.user, payday, manage)
response = TimeSheetSerializer(timesheet, many=True).data
return Response(response, status=status.HTTP_200_OK)
@api_view() @api_view()
def null_view(request): def null_view(request):
return Response(status=status.HTTP_400_BAD_REQUEST) return Response(status=status.HTTP_400_BAD_REQUEST)

View File

@ -34,6 +34,7 @@ router.register(r'pshift', views.PShiftViewSet, 'pshift')
# Additionally, we include login URLs for the browsable API. # Additionally, we include login URLs for the browsable API.
urlpatterns = [ urlpatterns = [
url(r'^', include(router.urls)), url(r'^', include(router.urls)),
url(r'^report/(?P<type>.+)/$', views.ReportView.as_view()),
url(r'^admin/', include(admin.site.urls)), url(r'^admin/', include(admin.site.urls)),
url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')), url(r'^api-auth/', include('rest_framework.urls', namespace='rest_framework')),
url(r'^rest-auth/registration/account-email-verification-sent/', views.null_view, name='account_email_verification_sent'), url(r'^rest-auth/registration/account-email-verification-sent/', views.null_view, name='account_email_verification_sent'),