diff --git a/caremyway/api/models.py b/caremyway/api/models.py index 6b59e2c..dd79e78 100644 --- a/caremyway/api/models.py +++ b/caremyway/api/models.py @@ -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) diff --git a/caremyway/api/serializers.py b/caremyway/api/serializers.py index 9372618..a91bf30 100644 --- a/caremyway/api/serializers.py +++ b/caremyway/api/serializers.py @@ -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) diff --git a/caremyway/api/views.py b/caremyway/api/views.py index aa73173..42e1822 100644 --- a/caremyway/api/views.py +++ b/caremyway/api/views.py @@ -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) diff --git a/caremyway/urls.py b/caremyway/urls.py index 388a18a..0891b5a 100644 --- a/caremyway/urls.py +++ b/caremyway/urls.py @@ -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.+)/$', 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'),