Add timesheet report generation API route
This commit is contained in:
parent
9817a4ebba
commit
4469db2602
|
@ -17,14 +17,14 @@ class Client(models.Model):
|
|||
business_number = models.CharField(max_length=16)
|
||||
|
||||
def __str__(self):
|
||||
return 'Client: ' + self.user.username
|
||||
return self.user.first_name + ' ' + self.user.last_name
|
||||
|
||||
class Provider(models.Model):
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE)
|
||||
sin = models.CharField(max_length=16)
|
||||
|
||||
def __str__(self):
|
||||
return 'Provider: ' + self.user.username
|
||||
return self.user.first_name + ' ' + self.user.last_name
|
||||
|
||||
class WorkType(models.Model):
|
||||
uuid = models.UUIDField(default=uuid.uuid4, editable=False)
|
||||
|
@ -63,7 +63,6 @@ class Shift(models.Model):
|
|||
set_end = models.DateTimeField()
|
||||
actual_start = 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)
|
||||
chart = models.TextField(max_length=1000, null=True)
|
||||
deleted = models.BooleanField(default=False)
|
||||
|
|
|
@ -42,19 +42,19 @@ class PriceSerializer(serializers.ModelSerializer):
|
|||
raise serializers.ValidationError("User is not a 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 = 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 = get_object_or_404(worktype_objs, uuid=work_type_uuid)
|
||||
|
||||
if not employee.approved:
|
||||
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(deleted=False).exists():
|
||||
.filter(management=employee).exists():
|
||||
raise serializers.ValidationError("Price for this work already defined.")
|
||||
|
||||
validated_data['management'] = employee
|
||||
|
@ -96,9 +96,9 @@ class EmployeeSerializer(serializers.ModelSerializer):
|
|||
provider_email = validated_data.pop('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(deleted=False).exists():
|
||||
.filter(client=client).exists():
|
||||
raise serializers.ValidationError("Employee already added.")
|
||||
|
||||
validated_data['client'] = client
|
||||
|
@ -172,13 +172,27 @@ class UserSerializer(serializers.ModelSerializer):
|
|||
fields = ('username', 'is_active', 'first_name', 'last_name', 'email', 'userinfo', 'client', 'provider')
|
||||
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)
|
||||
|
||||
class Meta:
|
||||
model = Shift
|
||||
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):
|
||||
user = self.context['request'].user
|
||||
|
@ -187,7 +201,7 @@ class CShiftSerializer(serializers.ModelSerializer):
|
|||
raise serializers.ValidationError("User is not a 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 = get_object_or_404(price_objs, uuid=price_uuid)
|
||||
|
||||
|
@ -215,13 +229,13 @@ class CShiftSerializer(serializers.ModelSerializer):
|
|||
validated_data['price'] = price
|
||||
return serializers.ModelSerializer.create(self, validated_data)
|
||||
|
||||
class PShiftSerializer(serializers.ModelSerializer):
|
||||
class PShiftSerializer(ShiftSerializer):
|
||||
action = serializers.ChoiceField(write_only=True, choices=['checkin', 'checkout'])
|
||||
|
||||
class Meta:
|
||||
model = Shift
|
||||
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):
|
||||
action = validated_data['action']
|
||||
|
@ -247,9 +261,24 @@ class PShiftSerializer(serializers.ModelSerializer):
|
|||
raise serializers.ValidationError("No chart info provided.")
|
||||
|
||||
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.save()
|
||||
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)
|
||||
|
|
|
@ -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.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 rest_framework import viewsets, permissions, status
|
||||
from rest_framework import viewsets, permissions, status, serializers
|
||||
from rest_framework.decorators import api_view
|
||||
from rest_framework.response import Response
|
||||
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):
|
||||
lookup_field = 'username'
|
||||
|
@ -18,7 +25,7 @@ class UserViewSet(viewsets.ModelViewSet):
|
|||
return User.objects.filter(username=self.request.user)
|
||||
|
||||
class UserInfoViewSet(viewsets.ModelViewSet):
|
||||
lookup_field = "user__username"
|
||||
lookup_field = 'user__username'
|
||||
serializer_class = UserInfoSerializer
|
||||
|
||||
# Disallow DELETE
|
||||
|
@ -28,7 +35,7 @@ class UserInfoViewSet(viewsets.ModelViewSet):
|
|||
return UserInfo.objects.filter(user__username=self.request.user)
|
||||
|
||||
class ClientViewSet(viewsets.ModelViewSet):
|
||||
lookup_field = "user__username"
|
||||
lookup_field = 'user__username'
|
||||
serializer_class = ClientSerializer
|
||||
|
||||
# Disallow DELETE
|
||||
|
@ -38,7 +45,7 @@ class ClientViewSet(viewsets.ModelViewSet):
|
|||
return Client.objects.filter(user__username=self.request.user)
|
||||
|
||||
class ProviderViewSet(viewsets.ModelViewSet):
|
||||
lookup_field = "user__username"
|
||||
lookup_field = 'user__username'
|
||||
serializer_class = ProviderSerializer
|
||||
|
||||
# Disallow DELETE
|
||||
|
@ -48,7 +55,7 @@ class ProviderViewSet(viewsets.ModelViewSet):
|
|||
return Provider.objects.filter(user__username=self.request.user)
|
||||
|
||||
class WorkTypeViewSet(viewsets.ModelViewSet):
|
||||
lookup_field = "uuid"
|
||||
lookup_field = 'uuid'
|
||||
serializer_class = WorkTypeSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
|
@ -61,7 +68,7 @@ class WorkTypeViewSet(viewsets.ModelViewSet):
|
|||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
class EmployeeViewSet(viewsets.ModelViewSet):
|
||||
lookup_field = "uuid"
|
||||
lookup_field = 'uuid'
|
||||
serializer_class = EmployeeSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
|
@ -74,7 +81,7 @@ class EmployeeViewSet(viewsets.ModelViewSet):
|
|||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
class EmployerViewSet(viewsets.ModelViewSet):
|
||||
lookup_field = "uuid"
|
||||
lookup_field = 'uuid'
|
||||
serializer_class = EmployerSerializer
|
||||
|
||||
# Disallow creation and deletions of relationships
|
||||
|
@ -84,7 +91,7 @@ class EmployerViewSet(viewsets.ModelViewSet):
|
|||
return Manage.objects.filter(provider__user__username=self.request.user)
|
||||
|
||||
class PriceViewSet(viewsets.ModelViewSet):
|
||||
lookup_field = "uuid"
|
||||
lookup_field = 'uuid'
|
||||
serializer_class = PriceSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
|
@ -97,12 +104,12 @@ class PriceViewSet(viewsets.ModelViewSet):
|
|||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
class CShiftViewSet(viewsets.ModelViewSet):
|
||||
lookup_field = "uuid"
|
||||
lookup_field = 'uuid'
|
||||
serializer_class = CShiftSerializer
|
||||
|
||||
def get_queryset(self):
|
||||
return Shift.objects.filter(price__management__client__user__username=self.request.user) \
|
||||
.filter(deleted=False) \
|
||||
return Shift.objects.filter(deleted=False) \
|
||||
.filter(price__management__client__user__username=self.request.user) \
|
||||
.order_by('-set_start')
|
||||
|
||||
def destroy(self, request, *args, **kwargs):
|
||||
|
@ -116,17 +123,100 @@ class CShiftViewSet(viewsets.ModelViewSet):
|
|||
return Response(status=status.HTTP_204_NO_CONTENT)
|
||||
|
||||
class PShiftViewSet(viewsets.ModelViewSet):
|
||||
lookup_field = "uuid"
|
||||
lookup_field = 'uuid'
|
||||
serializer_class = PShiftSerializer
|
||||
|
||||
# Disallow creation and deletions of relationships
|
||||
http_method_names = ['get', 'head', 'put', 'options']
|
||||
|
||||
def get_queryset(self):
|
||||
return Shift.objects.filter(price__management__provider__user__username=self.request.user) \
|
||||
.filter(deleted=False) \
|
||||
return Shift.objects.filter(deleted=False) \
|
||||
.filter(price__management__provider__user__username=self.request.user) \
|
||||
.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()
|
||||
def null_view(request):
|
||||
return Response(status=status.HTTP_400_BAD_REQUEST)
|
||||
|
|
|
@ -34,6 +34,7 @@ router.register(r'pshift', views.PShiftViewSet, 'pshift')
|
|||
# Additionally, we include login URLs for the browsable API.
|
||||
urlpatterns = [
|
||||
url(r'^', include(router.urls)),
|
||||
url(r'^report/(?P<type>.+)/$', views.ReportView.as_view()),
|
||||
url(r'^admin/', include(admin.site.urls)),
|
||||
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'),
|
||||
|
|
Loading…
Reference in New Issue
Block a user