diff --git a/base/middleware.py b/base/middleware.py index a8847e976..49c7346e8 100644 --- a/base/middleware.py +++ b/base/middleware.py @@ -6,7 +6,7 @@ from django.db.models import Q from django.http import HttpResponse, HttpResponseNotAllowed from django.shortcuts import render - +from django.shortcuts import redirect from asset.models import AssetAssignment, AssetRequest from attendance.models import ( Attendance, @@ -181,3 +181,22 @@ def __call__(self, request): response = self.get_response(request) return response + + +#MIDDLEWARE TO CHECK IF EMPLOYEE IS NEW USER OR NOT +class ForcePasswordChangeMiddleware: + def __init__(self, get_response): + self.get_response = get_response + + def __call__(self, request): + # Exclude specific paths from redirection + excluded_paths = ['/change-password', '/login', '/logout'] + if request.path.rstrip('/') in excluded_paths: + return self.get_response(request) + + # Check if employee is a new employee + if hasattr(request, 'user') and request.user.is_authenticated: + if getattr(request.user, 'is_new_employee', True): + return redirect('change-password') # Adjust to match your URL name + + return self.get_response(request) \ No newline at end of file diff --git a/base/models.py b/base/models.py index 5774a1883..58c2e1644 100644 --- a/base/models.py +++ b/base/models.py @@ -8,6 +8,7 @@ from datetime import date, datetime, timedelta from typing import Iterable +from django.contrib.auth.models import AbstractUser import django from django.apps import apps from django.contrib import messages @@ -1794,3 +1795,6 @@ def create_deduction_cutleave_from_penalty(sender, instance, created, **kwargs): ) available.save() + + +User.add_to_class('is_new_employee', models.BooleanField(default=False)) \ No newline at end of file diff --git a/base/urls.py b/base/urls.py index d9088d259..684ea0239 100644 --- a/base/urls.py +++ b/base/urls.py @@ -93,6 +93,7 @@ ), path("reset-send-success", views.reset_send_success, name="reset-send-success"), path("change-password", views.change_password, name="change-password"), + path("logout", views.logout_user, name="logout"), path("settings", views.common_settings, name="settings"), path( diff --git a/base/views.py b/base/views.py index 063d75aaf..9a84c7d94 100644 --- a/base/views.py +++ b/base/views.py @@ -738,7 +738,11 @@ def change_password(request): if form.is_valid(): new_password = form.cleaned_data["new_password"] user.set_password(new_password) + + if user.is_new_employee: # Ensure this only affects new employees + user.is_new_employee = False user.save() + user = authenticate(request, username=user.username, password=new_password) login(request, user) messages.success(request, _("Password changed successfully")) @@ -748,6 +752,14 @@ def change_password(request): return render(request, "base/auth/password_change.html", {"form": form}) + + + + + + + + def logout_user(request): """ This method used to logout the user diff --git a/employee/models.py b/employee/models.py index e3a55730f..9b519c789 100644 --- a/employee/models.py +++ b/employee/models.py @@ -498,12 +498,19 @@ def save(self, *args, **kwargs): if employee.employee_user_id is None: # Create user if no corresponding user exists username = self.email + password = self.phone + + is_new_employee_flag = not employee.employee_user_id.is_new_employee if employee.employee_user_id else True + user = User.objects.create_user( + username=username, email=username, password=password, is_new_employee=is_new_employee_flag + ) user = User.objects.filter(username=username).first() if not user: user = User.objects.create_user( username=username, email=username, password=password ) + self.employee_user_id = user # default permissions change_ownprofile = Permission.objects.get(codename="change_ownprofile") diff --git a/horilla/horilla_middlewares.py b/horilla/horilla_middlewares.py index 92808ca55..cf106fff9 100644 --- a/horilla/horilla_middlewares.py +++ b/horilla/horilla_middlewares.py @@ -15,6 +15,8 @@ MIDDLEWARE.append("horilla.horilla_middlewares.MethodNotAllowedMiddleware") MIDDLEWARE.append("horilla.horilla_middlewares.ThreadLocalMiddleware") MIDDLEWARE.append("accessibility.middlewares.AccessibilityMiddleware") +MIDDLEWARE.append("accessibility.middlewares.AccessibilityMiddleware") +MIDDLEWARE.append("base.middleware.ForcePasswordChangeMiddleware") _thread_locals = threading.local() diff --git a/horilla/settings.py b/horilla/settings.py index 443e37365..1b5e75c6b 100755 --- a/horilla/settings.py +++ b/horilla/settings.py @@ -80,12 +80,15 @@ "django.contrib.sessions.middleware.SessionMiddleware", "django.middleware.common.CommonMiddleware", "corsheaders.middleware.CorsMiddleware", + + "simple_history.middleware.HistoryRequestMiddleware", "django.middleware.locale.LocaleMiddleware", "django.middleware.csrf.CsrfViewMiddleware", "django.contrib.auth.middleware.AuthenticationMiddleware", "django.contrib.messages.middleware.MessageMiddleware", "django.middleware.clickjacking.XFrameOptionsMiddleware", + "base.middleware.ForcePasswordChangeMiddleware", ] ROOT_URLCONF = "horilla.urls" diff --git a/horilla_api/api_urls/asset/urls.py b/horilla_api/api_urls/asset/urls.py index ae4d2bcc7..353894b00 100644 --- a/horilla_api/api_urls/asset/urls.py +++ b/horilla_api/api_urls/asset/urls.py @@ -1,38 +1,40 @@ -from django.urls import path, re_path +from django.urls import path, include +from rest_framework.routers import DefaultRouter -from ...api_views.asset.views import * +from ...api_views.asset.views import ( + AssetViewSet, + AssetCategoryViewSet, + AssetLotViewSet, + AssetAssignmentViewSet, + AssetRequestViewSet, +) + + +router = DefaultRouter() +router.register(r'assets', AssetViewSet, basename='api-asset') +router.register(r'asset-categories', AssetCategoryViewSet, basename='api-asset-category') +router.register(r'asset-lots', AssetLotViewSet, basename='api-asset-lot') +router.register(r'asset-allocations', AssetAssignmentViewSet, basename='api-asset-allocation') +router.register(r'asset-requests', AssetRequestViewSet, basename='api-asset-request') urlpatterns = [ - re_path( - r"^asset-categories/(?P\d+)?$", - AssetCategoryAPIView.as_view(), - name="api-asset-category-detail", - ), - re_path( - r"^asset-lots/(?P\d+)?$", - AssetLotAPIView.as_view(), - name="api-asset-lot-detail", - ), - re_path(r"^assets/(?P\d+)?$", AssetAPIView.as_view(), name="api-asset-detail"), - re_path( - r"^asset-allocations/(?P\d+)?$", - AssetAllocationAPIView.as_view(), - name="api-asset-allocation-detail", - ), - re_path( - r"^asset-requests/(?P\d+)?$", - AssetRequestAPIView.as_view(), - name="api-asset-request-detail", - ), + + path('', include(router.urls)), + + path( - "asset-return/", AssetReturnAPIView.as_view(), name="api-asset-return" + 'asset-return//', + AssetAssignmentViewSet.as_view({'put': 'return_asset'}), + name='api-asset-return' ), path( - "asset-reject/", AssetRejectAPIView.as_view(), name="api-asset-reject" + 'asset-reject//', + AssetRequestViewSet.as_view({'put': 'reject'}), + name='api-asset-reject' ), path( - "asset-approve/", - AssetApproveAPIView.as_view(), - name="api-asset-approve", + 'asset-approve//', + AssetRequestViewSet.as_view({'put': 'approve'}), + name='api-asset-approve' ), ] diff --git a/horilla_api/api_urls/payroll/urls.py b/horilla_api/api_urls/payroll/urls.py index 0cb52f645..9de544e29 100644 --- a/horilla_api/api_urls/payroll/urls.py +++ b/horilla_api/api_urls/payroll/urls.py @@ -1,33 +1,83 @@ -from django.urls import path +from django.urls import path, include +from rest_framework.routers import DefaultRouter -from ...api_views.payroll.views import * +from ...api_views.payroll.views import ( + PayslipViewSet, + ContractViewSet, + LoanAccountViewSet, + ReimbursementViewSet, + TaxBracketViewSet, + AllowanceViewSet, + DeductionViewSet, +) +# Create a router for ViewSet-based URLs +router = DefaultRouter() +router.register(r'api/contract', ContractViewSet, basename='contract') +router.register(r'api/payslip', PayslipViewSet, basename='payslip') +router.register(r'api/loan-account', LoanAccountViewSet, basename='loan-account') +router.register(r'api/reimbursement', ReimbursementViewSet, basename='reimbursement') +router.register(r'api/tax-bracket', TaxBracketViewSet, basename='tax-bracket') +router.register(r'api/allowance', AllowanceViewSet, basename='allowance') +router.register(r'api/deduction', DeductionViewSet, basename='deduction') + +# Define URL patterns, maintaining backward compatibility urlpatterns = [ + # Include router-generated URLs + path('', include(router.urls)), + + # Legacy URL patterns for backward compatibility path( "contract/", - ContractView.as_view(), + ContractViewSet.as_view({'get': 'list', 'post': 'create'}), + name="legacy-contract-list" ), path( "contract/", - ContractView.as_view(), - ), - path("payslip/", PayslipView.as_view(), name=""), - path("payslip/", PayslipView.as_view(), name=""), - path("payslip-download/", PayslipDownloadView.as_view(), name=""), - path("payslip-send-mail/", PayslipSendMailView.as_view(), name=""), - path("loan-account/", LoanAccountView.as_view(), name=""), - path("loan-account/", LoanAccountView.as_view(), name=""), - path("reimbusement/", ReimbursementView.as_view(), name=""), - path("reimbusement/", ReimbursementView.as_view(), name=""), - path( - "reimbusement-approve-reject/", - ReimbusementApproveRejectView.as_view(), - name="", - ), - path("tax-bracket/", TaxBracketView.as_view(), name=""), - path("tax-bracket/", TaxBracketView.as_view(), name=""), - path("allowance", AllowanceView.as_view(), name=""), - path("allowance/", AllowanceView.as_view(), name=""), - path("deduction", DeductionView.as_view(), name=""), - path("deduction/", DeductionView.as_view(), name=""), -] + ContractViewSet.as_view({ + 'get': 'retrieve', + 'put': 'update', + 'delete': 'destroy' + }), + name="legacy-contract-detail" + ), + path( + "payslip/", + PayslipViewSet.as_view({'get': 'list', 'post': 'create'}), + name="legacy-payslip-list" + ), + path( + "payslip/", + PayslipViewSet.as_view({'get': 'retrieve'}), + name="legacy-payslip-detail" + ), + path( + "payslip-download/", + PayslipViewSet.as_view({'get': 'download'}), + name="legacy-payslip-download" + ), + path( + "payslip-send-mail/", + PayslipViewSet.as_view({'post': 'send_mail'}), + name="legacy-payslip-send-mail" + ), + path( + "reimbursement/", + ReimbursementViewSet.as_view({'get': 'list', 'post': 'create'}), + name="legacy-reimbursement-list" + ), + path( + "reimbursement/", + ReimbursementViewSet.as_view({ + 'get': 'retrieve', + 'put': 'update', + 'delete': 'destroy' + }), + name="legacy-reimbursement-detail" + ), + path( + "reimbursement-approve-reject/", + ReimbursementViewSet.as_view({'post': 'process_request'}), + name="legacy-reimbursement-approve-reject" + ), +] \ No newline at end of file diff --git a/horilla_api/api_views/asset/views.py b/horilla_api/api_views/asset/views.py index 027abf20e..49ec10557 100644 --- a/horilla_api/api_views/asset/views.py +++ b/horilla_api/api_views/asset/views.py @@ -1,312 +1,142 @@ from datetime import date +from typing import Optional +from django.db import transaction from django.http import QueryDict from django_filters.rest_framework import DjangoFilterBackend -from rest_framework import status +from rest_framework import status, viewsets, mixins +from rest_framework.decorators import action from rest_framework.pagination import PageNumberPagination -from rest_framework.permissions import IsAuthenticated +from rest_framework.permissions import IsAuthenticated, DjangoModelPermissions from rest_framework.response import Response -from rest_framework.views import APIView +from rest_framework.exceptions import ValidationError, PermissionDenied from asset.filters import AssetFilter -from asset.models import * - +from asset.models import Asset, AssetCategory, AssetLot, AssetAssignment, AssetRequest, ReturnImages from ...api_filters.asset.filters import AssetCategoryFilter -from ...api_serializers.asset.serializers import * - +from ...api_serializers.asset.serializers import ( + AssetSerializer, AssetGetAllSerializer, AssetCategorySerializer, + AssetLotSerializer, AssetAssignmentGetSerializer, AssetAssignmentSerializer, + AssetRequestGetSerializer, AssetRequestSerializer, AssetApproveSerializer, + AssetReturnSerializer +) + +class BaseModelViewSet(viewsets.ModelViewSet): + permission_classes = [IsAuthenticated, DjangoModelPermissions] + pagination_class = PageNumberPagination + + def get_object_or_404(self, pk: int): + try: + return self.queryset.get(pk=pk) + except self.queryset.model.DoesNotExist as e: + raise ValidationError(str(e)) -class AssetAPIView(APIView): - permission_classes = [IsAuthenticated] +class AssetViewSet(BaseModelViewSet): + queryset = Asset.objects.all() + serializer_class = AssetSerializer filter_backends = [DjangoFilterBackend] filterset_class = AssetFilter - def get_asset(self, pk): - try: - return Asset.objects.get(pk=pk) - except Asset.DoesNotExist as e: - raise serializers.ValidationError(e) - - def get(self, request, pk=None): - if pk: - asset = self.get_asset(pk) - serializer = AssetSerializer(asset) - return Response(serializer.data) - paginator = PageNumberPagination() - queryset = Asset.objects.all() - filterset = self.filterset_class(request.GET, queryset=queryset) - page = paginator.paginate_queryset(filterset.qs, request) - serializer = AssetGetAllSerializer(page, many=True) - return paginator.get_paginated_response(serializer.data) - - def post(self, request): - serializer = AssetSerializer(data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def put(self, request, pk): - asset = self.get_asset(pk) - serializer = AssetSerializer(asset, data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + def get_serializer_class(self): + if self.action == 'list': + return AssetGetAllSerializer + return AssetSerializer - def delete(self, request, pk): - asset = self.get_asset(pk) - asset.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - - -class AssetCategoryAPIView(APIView): - permission_classes = [IsAuthenticated] +class AssetCategoryViewSet(BaseModelViewSet): + queryset = AssetCategory.objects.all() + serializer_class = AssetCategorySerializer filter_backends = [DjangoFilterBackend] filterset_class = AssetCategoryFilter - def get_asset_category(self, pk): - try: - return AssetCategory.objects.get(pk=pk) - except AssetCategory.DoesNotExist as e: - raise serializers.ValidationError(e) - - def get(self, request, pk=None): - if pk: - asset_category = self.get_asset_category(pk) - serializer = AssetCategorySerializer(asset_category) - return Response(serializer.data) - paginator = PageNumberPagination() - queryset = AssetCategory.objects.all() - filterset = self.filterset_class(request.GET, queryset=queryset) - page = paginator.paginate_queryset(filterset.qs, request) - serializer = AssetCategorySerializer(page, many=True) - return paginator.get_paginated_response(serializer.data) - - def post(self, request): - serializer = AssetCategorySerializer(data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def put(self, request, pk): - asset_category = self.get_asset_category(pk) - serializer = AssetCategorySerializer(asset_category, data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def delete(self, request, pk): - asset_category = self.get_asset_category(pk) - asset_category.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - - -class AssetLotAPIView(APIView): - permission_classes = [IsAuthenticated] - - def get_asset_lot(self, pk): - try: - return AssetLot.objects.get(pk=pk) - except AssetLot.DoesNotExist as e: - raise serializers.ValidationError(e) - - def get(self, request, pk=None): - if pk: - asset_lot = self.get_asset_lot(pk) - serializer = AssetLotSerializer(asset_lot) - return Response(serializer.data) - paginator = PageNumberPagination() - assets = AssetLot.objects.all() - page = paginator.paginate_queryset(assets, request) - serializer = AssetLotSerializer(page, many=True) - return paginator.get_paginated_response(serializer.data) - - def post(self, request): - serializer = AssetLotSerializer(data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def put(self, request, pk): - asset_lot = self.get_asset_lot(pk) - serializer = AssetLotSerializer(asset_lot, data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def delete(self, request, pk): - asset_lot = self.get_asset_lot(pk) - asset_lot.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - - -class AssetAllocationAPIView(APIView): - permission_classes = [IsAuthenticated] - - def get_asset_assignment(self, pk): - try: - return AssetAssignment.objects.get(pk=pk) - except AssetAssignment.DoesNotExist as e: - raise serializers.ValidationError(e) - - def get(self, request, pk=None): - if pk: - asset_assignment = self.get_asset_assignment(pk) - serializer = AssetAssignmentGetSerializer(asset_assignment) - return Response(serializer.data) - paginator = PageNumberPagination() - assets = AssetAssignment.objects.all() - page = paginator.paginate_queryset(assets, request) - serializer = AssetAssignmentGetSerializer(page, many=True) - return paginator.get_paginated_response(serializer.data) - - def post(self, request): - serializer = AssetAssignmentSerializer(data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def put(self, request, pk): - asset_assignment = self.get_asset_assignment(pk) - serializer = AssetAssignmentSerializer(asset_assignment, data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) +class AssetLotViewSet(BaseModelViewSet): + queryset = AssetLot.objects.all() + serializer_class = AssetLotSerializer + +class AssetAssignmentViewSet(BaseModelViewSet): + queryset = AssetAssignment.objects.all() + serializer_class = AssetAssignmentSerializer + + def get_serializer_class(self): + if self.action in ['list', 'retrieve']: + return AssetAssignmentGetSerializer + return AssetAssignmentSerializer + + @action(detail=True, methods=['put']) + @transaction.atomic + def return_asset(self, request, pk=None): + assignment = self.get_object_or_404(pk) + + if not request.user.has_perm('asset.change_assetassignment'): + assignment.return_request = True + assignment.save() + return Response(status=status.HTTP_200_OK) + + serializer = AssetReturnSerializer(instance=assignment, data=request.data) + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - def delete(self, request, pk): - asset_assignment = self.get_asset_assignment(pk) - asset_assignment.delete() + images = [ + ReturnImages.objects.create(image=image) + for image in request.data.getlist('image') + ] + + asset_return = serializer.save() + asset_return.return_images.set(images) + + # Update asset status based on return condition + new_status = 'Available' if asset_return.return_status == 'Healthy' else 'Not-Available' + Asset.objects.filter(id=pk).update(asset_status=new_status) + + # Update return date + assignment.return_date = date.today() + assignment.save() + + return Response(status=status.HTTP_200_OK) + +class AssetRequestViewSet(BaseModelViewSet): + queryset = AssetRequest.objects.all().order_by('-id') + serializer_class = AssetRequestSerializer + + def get_serializer_class(self): + if self.action in ['list', 'retrieve']: + return AssetRequestGetSerializer + return AssetRequestSerializer + + @action(detail=True, methods=['put']) + def reject(self, request, pk=None): + asset_request = self.get_object_or_404(pk) + if asset_request.asset_request_status != 'Requested': + raise ValidationError({'error': 'Access Denied - Invalid request status'}) + + asset_request.asset_request_status = 'Rejected' + asset_request.save() return Response(status=status.HTTP_204_NO_CONTENT) + @action(detail=True, methods=['put']) + @transaction.atomic + def approve(self, request, pk=None): + asset_request = self.get_object_or_404(pk) + if asset_request.asset_request_status != 'Requested': + raise ValidationError({'error': 'Access Denied - Invalid request status'}) + + data = request.data.dict() if isinstance(request.data, QueryDict) else request.data + data.update({ + 'assigned_to_employee_id': asset_request.requested_employee_id.id, + 'assigned_by_employee_id': request.user.employee_get.id + }) + + serializer = AssetApproveSerializer( + data=data, + context={'asset_request': asset_request} + ) + + if not serializer.is_valid(): + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) -class AssetRequestAPIView(APIView): - permission_classes = [IsAuthenticated] - - def get_asset_request(self, pk): - try: - return AssetRequest.objects.get(pk=pk) - except AssetRequest.DoesNotExist as e: - raise serializers.ValidationError(e) - - def get(self, request, pk=None): - if pk: - asset_request = self.get_asset_request(pk) - serializer = AssetRequestGetSerializer(asset_request) - return Response(serializer.data) - paginator = PageNumberPagination() - assets = AssetRequest.objects.all().order_by("-id") - page = paginator.paginate_queryset(assets, request) - serializer = AssetRequestGetSerializer(page, many=True) - return paginator.get_paginated_response(serializer.data) - - def post(self, request): - serializer = AssetRequestSerializer(data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def put(self, request, pk): - asset_request = self.get_asset_request(pk) - serializer = AssetRequestSerializer(asset_request, data=request.data) - if serializer.is_valid(): + with transaction.atomic(): serializer.save() - return Response(serializer.data) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def delete(self, request, pk): - asset_request = self.get_asset_request(pk) - asset_request.delete() - return Response(status=status.HTTP_204_NO_CONTENT) - - -class AssetRejectAPIView(APIView): - permission_classes = [IsAuthenticated] - - def get_asset_request(self, pk): - try: - return AssetRequest.objects.get(pk=pk) - except AssetRequest.DoesNotExist as e: - raise serializers.ValidationError(e) - - def put(self, request, pk): - asset_request = self.get_asset_request(pk) - if asset_request.asset_request_status == "Requested": - asset_request.asset_request_status = "Rejected" + Asset.objects.filter(id=data['asset_id']).update(asset_status='In use') + asset_request.asset_request_status = 'Approved' asset_request.save() - return Response(status=204) - raise serializers.ValidationError({"error": "Access Denied.."}) - - -class AssetApproveAPIView(APIView): - permission_classes = [IsAuthenticated] - - def get_asset_request(self, pk): - try: - return AssetRequest.objects.get(pk=pk) - except AssetRequest.DoesNotExist as e: - raise serializers.ValidationError(e) - - def put(self, request, pk): - asset_request = self.get_asset_request(pk) - if asset_request.asset_request_status == "Requested": - data = request.data - if isinstance(data, QueryDict): - data = data.dict() - data["assigned_to_employee_id"] = asset_request.requested_employee_id.id - data["assigned_by_employee_id"] = request.user.employee_get.id - serializer = AssetApproveSerializer( - data=data, context={"asset_request": asset_request} - ) - if serializer.is_valid(): - serializer.save() - asset_id = Asset.objects.get(id=data["asset_id"]) - asset_id.asset_status = "In use" - asset_id.save() - asset_request.asset_request_status = "Approved" - asset_request.save() - return Response(status=200) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - raise serializers.ValidationError({"error": "Access Denied.."}) - -class AssetReturnAPIView(APIView): - permission_classes = [IsAuthenticated] - - def get_asset_assignment(self, pk): - try: - return AssetAssignment.objects.get(pk=pk) - except AssetAssignment.DoesNotExist as e: - raise serializers.ValidationError(e) - - def put(self, request, pk): - asset_assignment = self.get_asset_assignment(pk) - if request.user.has_perm("app_name.change_mymodel"): - serializer = AssetReturnSerializer( - instance=asset_assignment, data=request.data - ) - if serializer.is_valid(): - images = [ - ReturnImages.objects.create(image=image) - for image in request.data.getlist("image") - ] - asset_return = serializer.save() - asset_return.return_images.set(images) - if asset_return.return_status == "Healthy": - Asset.objects.filter(id=pk).update(asset_status="Available") - else: - Asset.objects.filter(id=pk).update(asset_status="Not-Available") - AssetAssignment.objects.filter(id=asset_return.id).update( - return_date=date.today() - ) - return Response(status=200) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - else: - AssetAssignment.objects.filter(id=pk).update(return_request=True) - return Response(status=200) + return Response(status=status.HTTP_200_OK) diff --git a/horilla_api/api_views/payroll/views.py b/horilla_api/api_views/payroll/views.py index 430b7459f..052bbcee2 100644 --- a/horilla_api/api_views/payroll/views.py +++ b/horilla_api/api_views/payroll/views.py @@ -1,379 +1,167 @@ -import gettext +from typing import Optional, Type from collections import defaultdict - from django.contrib.auth.decorators import permission_required -from django.shortcuts import render +from django.db import transaction from django.utils.decorators import method_decorator +from rest_framework import viewsets, status, mixins +from rest_framework.decorators import action +from rest_framework.exceptions import PermissionDenied, ValidationError from rest_framework.pagination import PageNumberPagination -from rest_framework.permissions import IsAuthenticated +from rest_framework.permissions import IsAuthenticated, DjangoModelPermissions from rest_framework.response import Response -from rest_framework.views import APIView from base.backends import ConfiguredEmailBackend from base.methods import eval_validate -from payroll.filters import ( - AllowanceFilter, - ContractFilter, - DeductionFilter, - PayslipFilter, -) +from payroll.filters import AllowanceFilter, ContractFilter, DeductionFilter, PayslipFilter from payroll.models.models import ( - Allowance, - Contract, - Deduction, - LoanAccount, - Payslip, - Reimbursement, + Allowance, Contract, Deduction, LoanAccount, + Payslip, Reimbursement ) from payroll.models.tax_models import TaxBracket from payroll.threadings.mail import MailSendThread from payroll.views.views import payslip_pdf - from ...api_methods.base.methods import groupby_queryset from ...api_serializers.payroll.serializers import ( - AllowanceSerializer, - ContractSerializer, - DeductionSerializer, - LoanAccountSerializer, - PayslipSerializer, - ReimbursementSerializer, - TaxBracketSerializer, + AllowanceSerializer, ContractSerializer, DeductionSerializer, + LoanAccountSerializer, PayslipSerializer, ReimbursementSerializer, + TaxBracketSerializer ) - -class PayslipView(APIView): - permission_classes = [IsAuthenticated] - - def get(self, request, id=None): - if id: - payslip = Payslip.objects.filter(id=id).first() - if ( - request.user.has_perm("payroll.view_payslip") - or payslip.employee_id == request.user.employee_get - ): - serializer = PayslipSerializer(payslip) - return Response(serializer.data, status=200) - if request.user.has_perm("payroll.view_payslip"): - payslips = Payslip.objects.all() - else: - payslips = Payslip.objects.filter( - employee_id__employee_user_id=request.user - ) - - payslip_filter_queryset = PayslipFilter(request.GET, payslips).qs - # groupby workflow - field_name = request.GET.get("groupby_field", None) +class BasePayrollViewSet(viewsets.ModelViewSet): + """ + Base ViewSet providing common functionality for all payroll-related views. + Includes permission handling, pagination, and standard CRUD operations. + """ + permission_classes = [IsAuthenticated, DjangoModelPermissions] + pagination_class = PageNumberPagination + + def get_object_or_404(self, pk: int): + try: + return self.queryset.get(pk=pk) + except self.queryset.model.DoesNotExist as e: + raise ValidationError(str(e)) + + def handle_groupby(self, request, queryset): + field_name = request.GET.get("groupby_field") if field_name: url = request.build_absolute_uri() - return groupby_queryset(request, url, field_name, payslip_filter_queryset) - pagination = PageNumberPagination() - page = pagination.paginate_queryset(payslip_filter_queryset, request) - serializer = PayslipSerializer(page, many=True) - return pagination.get_paginated_response(serializer.data) - - -class PayslipDownloadView(APIView): - - permission_classes = [IsAuthenticated] - - def get(self, request, id): - if request.user.has_perm("payroll.view_payslip"): - return payslip_pdf(request, id) - - if Payslip.objects.filter(id=id, employee_id=request.user.employee_get): - return payslip_pdf(request, id) - else: - raise Response({"error": "You don't have permission"}) - - -class PayslipSendMailView(APIView): - permission_classes = [IsAuthenticated] - + return groupby_queryset(request, url, field_name, queryset) + return None + +class PayslipViewSet(BasePayrollViewSet): + """ + ViewSet for managing payslips with specialized handling for employee access + and email functionality. + """ + serializer_class = PayslipSerializer + filterset_class = PayslipFilter + + def get_queryset(self): + if self.request.user.has_perm("payroll.view_payslip"): + return Payslip.objects.all() + return Payslip.objects.filter(employee_id=self.request.user.employee_get) + + def list(self, request, *args, **kwargs): + queryset = self.filter_queryset(self.get_queryset()) + + # Handle groupby if requested + groupby_response = self.handle_groupby(request, queryset) + if groupby_response: + return groupby_response + + return super().list(request, *args, **kwargs) + + @action(detail=True, methods=['get']) + def download(self, request, pk=None): + payslip = self.get_object() + if not (request.user.has_perm("payroll.view_payslip") or + payslip.employee_id == request.user.employee_get): + raise PermissionDenied("You don't have permission to access this payslip") + return payslip_pdf(request, pk) + + @action(detail=False, methods=['post']) + @transaction.atomic @method_decorator(permission_required("payroll.add_payslip")) - def post(self, request): + def send_mail(self, request): email_backend = ConfiguredEmailBackend() - if not getattr( - email_backend, "dynamic_username_with_display_name", None - ) or not len(email_backend.dynamic_username_with_display_name): - return Response({"error": "Email server is not configured"}, status=400) + if not getattr(email_backend, "dynamic_username_with_display_name", None): + raise ValidationError("Email server is not configured") payslip_ids = request.data.get("id", []) - payslips = Payslip.objects.filter(id__in=payslip_ids) - result_dict = defaultdict( - lambda: {"employee_id": None, "instances": [], "count": 0} - ) - + payslips = self.get_queryset().filter(id__in=payslip_ids) + + # Group payslips by employee for efficient processing + result_dict = defaultdict(lambda: {"employee_id": None, "instances": [], "count": 0}) for payslip in payslips: - employee_id = payslip.employee_id - result_dict[employee_id]["employee_id"] = employee_id - result_dict[employee_id]["instances"].append(payslip) - result_dict[employee_id]["count"] += 1 - mail_thread = MailSendThread(request, result_dict=result_dict, ids=payslip_ids) - mail_thread.start() - return Response({"status": "success"}, status=200) - - -class ContractView(APIView): - permission_classes = [IsAuthenticated] - - def get(self, request, id=None): - if id: - contract = Contract.objects.filter(id=id).first() - serializer = ContractSerializer(contract) - return Response(serializer.data, status=200) - if request.user.has_perm("payroll.view_contract"): - contracts = Contract.objects.all() - else: - contracts = Contract.objects.filter(employee_id=request.user.employee_get) - filter_queryset = ContractFilter(request.GET, contracts).qs - # groupby workflow - field_name = request.GET.get("groupby_field", None) - if field_name: - url = request.build_absolute_uri() - return groupby_queryset(request, url, field_name, filter_queryset) - pagination = PageNumberPagination() - page = pagination.paginate_queryset(filter_queryset, request) - serializer = ContractSerializer(page, many=True) - return pagination.get_paginated_response(serializer.data) - - @method_decorator(permission_required("payroll.add_contract")) - def post(self, request): - serializer = ContractSerializer(data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=200) - return Response(serializer.errors, status=400) - - @method_decorator(permission_required("payroll.change_contract")) - def put(self, request, pk): - contract = Contract.objects.get(id=pk) - serializer = ContractSerializer(instance=contract, data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=200) - return Response(serializer.errors, status=400) - - @method_decorator(permission_required("payroll.delete_contract")) - def delete(self, request, pk): - contract = Contract.objects.get(id=pk) - contract.delete() - return Response({"status": "deleted"}, status=200) - - -class AllowanceView(APIView): - permission_classes = [IsAuthenticated] - - @method_decorator(permission_required("payroll.view_allowance")) - def get(self, request, pk=None): - if pk: - allowance = Allowance.objects.get(id=pk) - serializer = AllowanceSerializer(instance=allowance) - return Response(serializer.data, status=200) - allowance = Allowance.objects.all() - filter_queryset = AllowanceFilter(request.GET, allowance).qs - pagination = PageNumberPagination() - page = pagination.paginate_queryset(filter_queryset, request) - serializer = AllowanceSerializer(page, many=True) - return pagination.get_paginated_response(serializer.data) - - @method_decorator(permission_required("payroll.add_allowance")) - def post(self, request): - serializer = AllowanceSerializer(data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=200) - return Response(serializer.errors, status=400) - - @method_decorator(permission_required("payroll.change_allowance")) - def put(self, request, pk): - contract = Allowance.objects.get(id=pk) - serializer = AllowanceSerializer(instance=contract, data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=200) - return Response(serializer.errors, status=400) - - @method_decorator(permission_required("payroll.delete_allowance")) - def delete(self, request, pk): - contract = Allowance.objects.get(id=pk) - contract.delete() - return Response({"status": "deleted"}, status=200) - - -class DeductionView(APIView): - permission_classes = [IsAuthenticated] - - @method_decorator(permission_required("payroll.view_deduction")) - def get(self, request, pk=None): - if pk: - deduction = Deduction.objects.get(id=pk) - serializer = DeductionSerializer(instance=deduction) - return Response(serializer.data, status=200) - deduction = Deduction.objects.all() - filter_queryset = DeductionFilter(request.GET, deduction).qs - pagination = PageNumberPagination() - page = pagination.paginate_queryset(filter_queryset, request) - serializer = DeductionSerializer(page, many=True) - return pagination.get_paginated_response(serializer.data) - - @method_decorator(permission_required("payroll.add_deduction")) - def post(self, request): - serializer = DeductionSerializer(data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=200) - return Response(serializer.errors, status=400) - - @method_decorator(permission_required("payroll.change_deduction")) - def put(self, request, pk): - contract = Deduction.objects.get(id=pk) - serializer = DeductionSerializer(instance=contract, data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=200) - return Response(serializer.errors, status=400) - - @method_decorator(permission_required("payroll.delete_deduction")) - def delete(self, request, pk): - contract = Deduction.objects.get(id=pk) - contract.delete() - return Response({"status": "deleted"}, status=200) - - -class LoanAccountView(APIView): - permission_classes = [IsAuthenticated] - - @method_decorator(permission_required("payroll.add_loanaccount")) - def post(self, request): - serializer = LoanAccountSerializer(data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=200) - return Response(serializer.errors, status=400) - - @method_decorator(permission_required("payroll.view_loanaccount")) - def get(self, request, pk=None): - if pk: - loan_account = LoanAccount.objects.get(id=pk) - serializer = LoanAccountSerializer(instance=loan_account) - return Response(serializer.data, status=200) - loan_accounts = LoanAccount.objects.all() - pagination = PageNumberPagination() - page = pagination.paginate_queryset(loan_accounts, request) - serializer = LoanAccountSerializer(page, many=True) - return pagination.get_paginated_response(serializer.data) - - @method_decorator(permission_required("payroll.change_loanaccount")) - def put(self, request, pk): - loan_account = LoanAccount.objects.get(id=pk) - serializer = LoanAccountSerializer(loan_account, data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=200) - return Response(serializer.errors, status=400) - - @method_decorator(permission_required("payroll.delete_loanaccount")) - def delete(self, request, pk): - loan_account = LoanAccount.objects.get(id=pk) - loan_account.delete() - return Response(status=200) - - -class ReimbursementView(APIView): + emp_dict = result_dict[payslip.employee_id] + emp_dict["employee_id"] = payslip.employee_id + emp_dict["instances"].append(payslip) + emp_dict["count"] += 1 + + # Start email thread + MailSendThread(request, result_dict=result_dict, ids=payslip_ids).start() + return Response({"status": "Email sending initiated"}, status=status.HTTP_200_OK) + +class ReimbursementViewSet(BasePayrollViewSet): + """ + ViewSet for handling reimbursement requests with approval/rejection functionality. + """ serializer_class = ReimbursementSerializer - permission_classes = [IsAuthenticated] - - def get(self, request, pk=None): - if pk: - reimbursement = Reimbursement.objects.get(id=pk) - serializer = self.serializer_class(reimbursement) - return Response(serializer.data, status=200) - reimbursements = Reimbursement.objects.all() - - if request.user.has_perm("payroll.view_reimbursement"): - reimbursements = Reimbursement.objects.all() - else: - reimbursements = Reimbursement.objects.filter( - employee_id=request.user.employee_get - ) - pagination = PageNumberPagination() - page = pagination.paginate_queryset(reimbursements, request) - serializer = self.serializer_class(page, many=True) - return pagination.get_paginated_response(serializer.data) - - def post(self, request): - serializer = self.serializer_class( - data=request.data, context={"request": request} - ) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=200) - return Response(serializer.errors, status=400) - - @method_decorator(permission_required("payroll.change_reimbursement")) - def put(self, request, pk): - reimbursement = Reimbursement.objects.get(id=pk) - serializer = self.serializer_class(instance=reimbursement, data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=200) - return Response(serializer.errors, status=400) - - @method_decorator(permission_required("payroll.delete_reimbursement")) - def delete(self, request, pk): - reimbursement = Reimbursement.objects.get(id=pk) - reimbursement.delete() - return Response(status=200) - - -class ReimbusementApproveRejectView(APIView): - permission_classes = [IsAuthenticated] - - def post(self, request, pk): - status = request.data.get("status", None) - amount = request.data.get("amount", None) - amount = ( - eval_validate(request.data.get("amount")) - if request.data.get("amount") - else 0 - ) - amount = max(0, amount) - reimbursement = Reimbursement.objects.filter(id=pk) - if amount: - reimbursement.update(amount=amount) - reimbursement.update(status=status) - return Response({"status": reimbursement.first().status}, status=200) - - -class TaxBracketView(APIView): - - def get(self, request, pk=None): - if pk: - tax_bracket = TaxBracket.objects.get(id=pk) - serializer = TaxBracketSerializer(tax_bracket) - return Response(serializer.data, status=200) - tax_brackets = TaxBracket.objects.all() - serializer = TaxBracketSerializer(instance=tax_brackets, many=True) - return Response(serializer.data, status=200) - - def post(self, request): - serializer = TaxBracketSerializer(data=request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=200) - return Response(serializer.errors, status=400) - def put(self, request, pk): - tax_bracket = TaxBracket.objects.get(id=pk) - serializer = TaxBracketSerializer( - instance=tax_bracket, data=request.data, partial=True + def get_queryset(self): + if self.request.user.has_perm("payroll.view_reimbursement"): + return Reimbursement.objects.all() + return Reimbursement.objects.filter(employee_id=self.request.user.employee_get) + + @action(detail=True, methods=['post']) + @transaction.atomic + def process_request(self, request, pk=None): + """Handle approval/rejection of reimbursement requests""" + reimbursement = self.get_object() + status_value = request.data.get("status") + amount = eval_validate(request.data.get("amount", "0")) + + if amount is not None: + amount = max(0, float(amount)) + reimbursement.amount = amount + + reimbursement.status = status_value + reimbursement.save() + + return Response( + {"status": reimbursement.status}, + status=status.HTTP_200_OK ) - if serializer.save(): - serializer.save() - return Response(serializer.data, status=200) - return Response(serializer.errors, status=400) - def delete(self, request, pk): - tax_bracket = TaxBracket.objects.get(id=pk) - tax_bracket.delete() - return Response(status=200) +class ContractViewSet(BasePayrollViewSet): + """ViewSet for managing employment contracts.""" + serializer_class = ContractSerializer + queryset = Contract.objects.all() + filterset_class = ContractFilter + + def get_queryset(self): + if self.request.user.has_perm("payroll.view_contract"): + return Contract.objects.all() + return Contract.objects.filter(employee_id=self.request.user.employee_get) + +class AllowanceViewSet(BasePayrollViewSet): + """ViewSet for managing employee allowances.""" + serializer_class = AllowanceSerializer + queryset = Allowance.objects.all() + filterset_class = AllowanceFilter + +class DeductionViewSet(BasePayrollViewSet): + + serializer_class = DeductionSerializer + queryset = Deduction.objects.all() + filterset_class = DeductionFilter + +class LoanAccountViewSet(BasePayrollViewSet): + """ViewSet for managing employee loan accounts.""" + serializer_class = LoanAccountSerializer + queryset = LoanAccount.objects.all() + +class TaxBracketViewSet(BasePayrollViewSet): + """ViewSet for managing tax brackets.""" + serializer_class = TaxBracketSerializer + queryset = TaxBracket.objects.all() \ No newline at end of file diff --git a/leave/templates/leave/leave_type/leave_type_creation.html b/leave/templates/leave/leave_type/leave_type_creation.html index d8b81e34b..920769665 100644 --- a/leave/templates/leave/leave_type/leave_type_creation.html +++ b/leave/templates/leave/leave_type/leave_type_creation.html @@ -231,6 +231,25 @@

{% trans "Create Leave Type" %} + +
+
+ + + + +
+ + +
+
+ + {{form.employee_id}} + {{form.employee_id.errors}} +
+ {% if perms.leave.add_availableleave %}
@@ -241,6 +260,7 @@

{% trans "Create Leave Type" %} {% endif %} + @@ -434,6 +454,34 @@

{% trans "Create Leave Type" %} 0) { + + $('#id_employee_id input[type="checkbox"]').each(function() { + + $(this).prop('checked', false); + + if (isChecked) { + $(this).prop('checked', true); + } + }); + } + + $('#id_employee_id').trigger('change'); + }); +}); + {% endblock %}