From 11eb4b98cfe07ca7c20666e2f7a69778877ff6d4 Mon Sep 17 00:00:00 2001 From: tejdevarakonda Date: Sun, 19 Apr 2026 22:26:58 +0530 Subject: [PATCH 1/4] Final HR (EIS) module submission --- FusionIIIT/Fusion/__init__.py | 0 FusionIIIT/applications/__init__.py | 0 .../central_mess/migrations/0001_initial.py | 11 - .../applications/health_center/models.py | 2 +- .../applications/health_center/utils.py | 34 +- FusionIIIT/applications/hr2.zip | Bin 0 -> 245801 bytes FusionIIIT/applications/hr2/__init__.py | 1 + FusionIIIT/applications/hr2/a.py | 75 - FusionIIIT/applications/hr2/admin.py | 23 +- FusionIIIT/applications/hr2/api/__init__.py | 1 + FusionIIIT/applications/hr2/api/form_views.py | 546 ---- .../applications/hr2/api/serializers.py | 286 +- FusionIIIT/applications/hr2/api/urls.py | 103 +- FusionIIIT/applications/hr2/api/views.py | 1452 ++++++++++ FusionIIIT/applications/hr2/apps.py | 3 +- FusionIIIT/applications/hr2/form_views.py | 323 --- FusionIIIT/applications/hr2/forms.py | 78 - .../applications/hr2/management/__init__.py | 0 .../hr2/management/commands/__init__.py | 0 .../commands/convert_vl_to_earned.py | 106 + .../hr2/management/commands/seed_hr2.py | 138 + .../hr2/management/commands/seed_hr_demo.py | 530 ++++ .../management/commands/seed_leave_balance.py | 81 + .../hr2/migrations/0001_initial.py | 586 +++- .../hr2/migrations/0002_auto_20241020_1126.py | 64 - .../hr2/migrations/0002_leave_nominee.py | 26 + .../migrations/0003_leave_document_request.py | 39 + .../hr2/migrations/0004_leave_cancellation.py | 66 + .../hr2/migrations/0005_leave_extension.py | 60 + .../0006_station_leave_selection.py | 20 + .../hr2/migrations/0007_half_day_cl.py | 25 + .../migrations/0008_station_leave_length.py | 20 + .../hr2/migrations/0009_leave_resumption.py | 50 + FusionIIIT/applications/hr2/models.py | 932 +++++- FusionIIIT/applications/hr2/normal.py | 36 - FusionIIIT/applications/hr2/selectors.py | 131 + FusionIIIT/applications/hr2/serializers.py | 42 - FusionIIIT/applications/hr2/services.py | 116 + FusionIIIT/applications/hr2/test.py | 116 - FusionIIIT/applications/hr2/tests.py | 3 - FusionIIIT/applications/hr2/tests/__init__.py | 1 + FusionIIIT/applications/hr2/tests/conftest.py | 440 +++ .../hr2/tests/reports/Artifact_Evaluation.csv | 87 + .../hr2/tests/reports/BR_Test_Design.csv | 55 + .../hr2/tests/reports/Defect_Log.csv | 1 + .../hr2/tests/reports/Module_Test_Summary.csv | 20 + .../hr2/tests/reports/Test_Execution_Log.csv | 246 ++ .../hr2/tests/reports/UC_Test_Design.csv | 170 ++ .../hr2/tests/reports/WF_Test_Design.csv | 23 + FusionIIIT/applications/hr2/tests/runner.py | 379 +++ .../hr2/tests/specs/business_rules.yaml | 251 ++ .../hr2/tests/specs/use_cases.yaml | 1144 ++++++++ .../hr2/tests/specs/workflows.yaml | 103 + .../hr2/tests/test_business_rules.py | 336 +++ .../applications/hr2/tests/test_use_cases.py | 563 ++++ .../applications/hr2/tests/test_workflows.py | 593 ++++ FusionIIIT/applications/hr2/urls.py | 97 +- FusionIIIT/applications/hr2/views.py | 2514 ----------------- .../migrations/0026_add_database_indexes.py | 48 +- 59 files changed, 9005 insertions(+), 4191 deletions(-) create mode 100644 FusionIIIT/Fusion/__init__.py create mode 100644 FusionIIIT/applications/__init__.py create mode 100644 FusionIIIT/applications/hr2.zip delete mode 100644 FusionIIIT/applications/hr2/a.py create mode 100644 FusionIIIT/applications/hr2/api/__init__.py delete mode 100644 FusionIIIT/applications/hr2/api/form_views.py create mode 100644 FusionIIIT/applications/hr2/api/views.py delete mode 100644 FusionIIIT/applications/hr2/form_views.py delete mode 100644 FusionIIIT/applications/hr2/forms.py create mode 100644 FusionIIIT/applications/hr2/management/__init__.py create mode 100644 FusionIIIT/applications/hr2/management/commands/__init__.py create mode 100644 FusionIIIT/applications/hr2/management/commands/convert_vl_to_earned.py create mode 100644 FusionIIIT/applications/hr2/management/commands/seed_hr2.py create mode 100644 FusionIIIT/applications/hr2/management/commands/seed_hr_demo.py create mode 100644 FusionIIIT/applications/hr2/management/commands/seed_leave_balance.py delete mode 100644 FusionIIIT/applications/hr2/migrations/0002_auto_20241020_1126.py create mode 100644 FusionIIIT/applications/hr2/migrations/0002_leave_nominee.py create mode 100644 FusionIIIT/applications/hr2/migrations/0003_leave_document_request.py create mode 100644 FusionIIIT/applications/hr2/migrations/0004_leave_cancellation.py create mode 100644 FusionIIIT/applications/hr2/migrations/0005_leave_extension.py create mode 100644 FusionIIIT/applications/hr2/migrations/0006_station_leave_selection.py create mode 100644 FusionIIIT/applications/hr2/migrations/0007_half_day_cl.py create mode 100644 FusionIIIT/applications/hr2/migrations/0008_station_leave_length.py create mode 100644 FusionIIIT/applications/hr2/migrations/0009_leave_resumption.py delete mode 100644 FusionIIIT/applications/hr2/normal.py create mode 100644 FusionIIIT/applications/hr2/selectors.py delete mode 100644 FusionIIIT/applications/hr2/serializers.py create mode 100644 FusionIIIT/applications/hr2/services.py delete mode 100644 FusionIIIT/applications/hr2/test.py delete mode 100644 FusionIIIT/applications/hr2/tests.py create mode 100644 FusionIIIT/applications/hr2/tests/__init__.py create mode 100644 FusionIIIT/applications/hr2/tests/conftest.py create mode 100644 FusionIIIT/applications/hr2/tests/reports/Artifact_Evaluation.csv create mode 100644 FusionIIIT/applications/hr2/tests/reports/BR_Test_Design.csv create mode 100644 FusionIIIT/applications/hr2/tests/reports/Defect_Log.csv create mode 100644 FusionIIIT/applications/hr2/tests/reports/Module_Test_Summary.csv create mode 100644 FusionIIIT/applications/hr2/tests/reports/Test_Execution_Log.csv create mode 100644 FusionIIIT/applications/hr2/tests/reports/UC_Test_Design.csv create mode 100644 FusionIIIT/applications/hr2/tests/reports/WF_Test_Design.csv create mode 100644 FusionIIIT/applications/hr2/tests/runner.py create mode 100644 FusionIIIT/applications/hr2/tests/specs/business_rules.yaml create mode 100644 FusionIIIT/applications/hr2/tests/specs/use_cases.yaml create mode 100644 FusionIIIT/applications/hr2/tests/specs/workflows.yaml create mode 100644 FusionIIIT/applications/hr2/tests/test_business_rules.py create mode 100644 FusionIIIT/applications/hr2/tests/test_use_cases.py create mode 100644 FusionIIIT/applications/hr2/tests/test_workflows.py delete mode 100644 FusionIIIT/applications/hr2/views.py diff --git a/FusionIIIT/Fusion/__init__.py b/FusionIIIT/Fusion/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/FusionIIIT/applications/__init__.py b/FusionIIIT/applications/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/FusionIIIT/applications/central_mess/migrations/0001_initial.py b/FusionIIIT/applications/central_mess/migrations/0001_initial.py index 7e80bedf5..bbac3081f 100644 --- a/FusionIIIT/applications/central_mess/migrations/0001_initial.py +++ b/FusionIIIT/applications/central_mess/migrations/0001_initial.py @@ -149,17 +149,6 @@ class Migration(migrations.Migration): ('student_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='academic_information.student')), ], ), - migrations.CreateModel( - name='Payments', - fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('amount_paid', models.IntegerField(default=0)), - ('payment_month', models.CharField(default=applications.central_mess.models.current_month, max_length=20)), - ('payment_year', models.IntegerField(default=applications.central_mess.models.current_year)), - ('payment_date', models.DateField(default=datetime.date(2024, 6, 19))), - ('student_id', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='academic_information.student')), - ], - ), migrations.CreateModel( name='Mess_minutes', fields=[ diff --git a/FusionIIIT/applications/health_center/models.py b/FusionIIIT/applications/health_center/models.py index 7f46307f0..a5b5ca3d5 100644 --- a/FusionIIIT/applications/health_center/models.py +++ b/FusionIIIT/applications/health_center/models.py @@ -4,9 +4,9 @@ from django.contrib.auth.models import User from applications.globals.models import ExtraInfo -from applications.hr2.models import EmpDependents # Create your models here. +# Note: EmpDependents model (previously imported from hr2) does not exist - removed from imports class Constants: DAYS_OF_WEEK = ( diff --git a/FusionIIIT/applications/health_center/utils.py b/FusionIIIT/applications/health_center/utils.py index 220efea76..88eac0d15 100644 --- a/FusionIIIT/applications/health_center/utils.py +++ b/FusionIIIT/applications/health_center/utils.py @@ -18,7 +18,7 @@ from django.shortcuts import get_object_or_404 from django.db.models import Q from applications.globals.models import ExtraInfo -from applications.hr2.models import EmpDependents +# from applications.hr2.models import EmpDependents # Model does not exist def datetime_handler(date): ''' @@ -463,21 +463,23 @@ def compounder_view_handler(request): data = {'status': status, 'stock': stock} return JsonResponse(data) elif 'user_for_dependents' in request.POST: - user = request.POST.get('user_for_dependents') - if not User.objects.filter(username__iexact = user).exists(): - return JsonResponse({"status":-1}) - user_id = User.objects.get(username__iexact = user) - info = ExtraInfo.objects.get(user = user_id) - dep_info = EmpDependents.objects.filter(extra_info = info) - dep=[] - for d in dep_info: - obj={} - obj['name'] = d.name - obj['relation'] = d.relationship - dep.append(obj) - if(len(dep) == 0) : - return JsonResponse({'status':-2}) - return JsonResponse({'status':1,'dep':dep}) + # EmpDependents model does not exist - functionality disabled + return JsonResponse({"status":-2}) + # user = request.POST.get('user_for_dependents') + # if not User.objects.filter(username__iexact = user).exists(): + # return JsonResponse({"status":-1}) + # user_id = User.objects.get(username__iexact = user) + # info = ExtraInfo.objects.get(user = user_id) + # dep_info = EmpDependents.objects.filter(extra_info = info) + # dep=[] + # for d in dep_info: + # obj={} + # obj['name'] = d.name + # obj['relation'] = d.relationship + # dep.append(obj) + # if(len(dep) == 0) : + # return JsonResponse({'status':-2}) + # return JsonResponse({'status':1,'dep':dep}) elif 'prescribe_b' in request.POST: user_id = request.POST.get('user') doctor_id = request.POST.get('doctor') diff --git a/FusionIIIT/applications/hr2.zip b/FusionIIIT/applications/hr2.zip new file mode 100644 index 0000000000000000000000000000000000000000..5e3265d66f81edcea3bd4ed1eb934c84281e0032 GIT binary patch literal 245801 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¥e9ry6=&pZ2OZ@uyf0it~Yot1xAO__B3u3-Wypbn(YM-Mzho zwmpo+P9})&7R}SR{UPmH)tRjuw*#~Ncl#mVobMnUH&Xy_)je&Su;hE}_c;R7o*Oya z>03uRoF|(Lua|+zFMjYZd7dxupcZen9Pi?ban-Tyuk54;#`&+l>n(dOeA>SgiDqAd zrr~uC_bQ7{6Rj2QbMYkORJ^H@%H_fBUYP-(8UUZ10H1F8+J7P(zZ(I3C;@zY1Nexr zZ>nj>aw@D~)JN88lD0ri9jXcB@c}Nn&LoK5`c{0sHuH6(eCGz_7-_m}PucYZe182o z(~9o^z1u*@AEe1IeBmq5MRW;?;gMx1w-y67o>6g{FB*0 z?z-}Oq+cfW8!5wX4Pbh!>Hq`MhxRr|OE_;!EA@wN)HcfooX&=fPM2`y!1brYQd{C; z8}&AB$TB}88T!Z0MarfxtEY5nJG-y^kCcty^AGAT1i7TGVD&(hpi6mKkk$a_KT0<) znn(eLi(mvh`YVJ1zJqPk)w}`VpkpO(V}YjGsb9KEb^+TEp{yZC$ReRR%v_*Wlnn4S zT=03*ruoz~0`hEkTlc_xgJ}#INOLxr;?hC&3Ek0@$D;zPLbOE9>k_;jKJuwSt%YLn zDYTYlAN2$~D?!7(MB|QaZ}--P4lz;Yc5$@1W=Y*u9*7=c?6ydEL8=O=%Vd>@fx0RK zbfW>tMe}O`*{%Y7x&!bM1K?u@;3ojcMQts*vW~f?ofLNO+5XlAa5h-mN`l-xNB-y`$;PRK!oM4nJE! zndYgJWg|bb-j&l-D*QqpZ=E{CtL0{?01ene|HJrm?S8mP;j@s0MMdZ#S47B+=O)!U zK3>y}4!PMCWYy_vqCe4BRmF@60Jn9E?;(%l7oi&-==8xAPRP;h6%M6K-y>>43bInE zpI#-lO^|-@#__d7?7f?z>a9u+fZo*|D$qi$U+ALNn^7I;{3*EfI#oE^gI9!!%98 z^xcKzA;gvJUu&;X*itP78s;=Q4|*m}c`A}e8@=Z_Kx&|4GI9@f`bcMvnhk@YF2K8f7) zWL$Q1b?5+m!VhCJ^|59gDjzEYo9LYnS~UCl77D286E|y79u?ed!`Sz}N3$040`(sl zrh?BSICtaZsQ27Tx#@u_Efnb;#!Yh8kWGNWAGXmQlF1K_ArFH8PWSVT_KdNc=B$RF zN9fr?Sa9s=LUZ`N^XNjtToZZ(OJ0dH#Pize8121@(z?XnvOL{l@d0yMMN7PSINHC7 z7bmLd>!0&INZlasC|e0E|GgS*#ru1huKV=+gY_kB)~PpOx_9?yQUTdkP=D73cw;Tj zWnlg?e6Ue}!vH+^0kjYQ?sX#meIx(kH-3EkUaws~w^3hy?KhK|oa_a7whDV}-j>(0 z+Kjl;u|40*y_&CP^!Pz}&(+*(hvSBPo(~<0hui#R`XHn3

vQ8YaAVc53R{(^NSX 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�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 literal 0 HcmV?d00001 diff --git a/FusionIIIT/applications/hr2/__init__.py b/FusionIIIT/applications/hr2/__init__.py index e69de29bb..5fb708b8f 100644 --- a/FusionIIIT/applications/hr2/__init__.py +++ b/FusionIIIT/applications/hr2/__init__.py @@ -0,0 +1 @@ +# HR2 Module - Leave, Appraisal, LTC, and CPDA Management diff --git a/FusionIIIT/applications/hr2/a.py b/FusionIIIT/applications/hr2/a.py deleted file mode 100644 index a308ef3ad..000000000 --- a/FusionIIIT/applications/hr2/a.py +++ /dev/null @@ -1,75 +0,0 @@ -def reverse_ltc_pre_processing(data): - reversed_data = {} - - # Copying over simple key-value pairs - simple_keys = [ - 'block_year', 'pf_no', 'basic_pay_salary', 'name', 'designation', 'department_info', - 'leave_availability', 'leave_start_date', 'leave_end_date', 'date_of_leave_for_family', - 'nature_of_leave', 'purpose_of_leave', 'hometown_or_not', 'place_of_visit', - 'address_during_leave', 'amount_of_advance_required', 'certified_family_dependents', - 'certified_advance', 'adjusted_month', 'date', 'phone_number_for_contact' - ] - for key in simple_keys: - value = data[key] - reversed_data[key] = value if value != 'None' else '' - - # Reversing array-like values - reversed_data['details_of_family_members_already_done'] = data['details_of_family_members_already_done'].split(',') - - family_members_about_to_avail = data['family_members_about_to_avail'].split(',') - for index, value in enumerate(family_members_about_to_avail): - family_members_about_to_avail[index] = value if value != 'None' else '' - - reversed_data['info_1_1'] = family_members_about_to_avail[0] - reversed_data['info_1_2'] = family_members_about_to_avail[1] - reversed_data['info_1_3'] = family_members_about_to_avail[2] - reversed_data['info_2_1'] = family_members_about_to_avail[3] - reversed_data['info_2_2'] = family_members_about_to_avail[4] - reversed_data['info_2_3'] = family_members_about_to_avail[5] - reversed_data['info_3_1'] = family_members_about_to_avail[6] - reversed_data['info_3_2'] = family_members_about_to_avail[7] - reversed_data['info_3_3'] = family_members_about_to_avail[8] - - # Reversing details_of_dependents - details_of_dependents = data['details_of_dependents'].split(',') - for i in range(1, 7): - for j in range(1, 4): - key = f'd_info_{i}_{j}' - value = details_of_dependents.pop(0) - reversed_data[key] = value if value != 'None' else '' - - return reversed_data - -# Sample data -data = { - 'block_year': '232', 'pf_no': '4324', 'basic_pay_salary': '324', 'name': 'sdf', 'designation': 'fds', - 'department_info': 'dfs', 'leave_availability': 'True', 'leave_start_date': '2024-03-13', - 'leave_end_date': '2024-03-17', 'date_of_leave_for_family': '2024-03-16', 'nature_of_leave': 'erds', - 'purpose_of_leave': 'fds', 'hometown_or_not': 'True', 'place_of_visit': 'fds', 'address_during_leave': 'dfsfsdf', - 'details_of_family_members_already_done': 'fds,dfs,dfs', 'family_members_about_to_avail': '1,dfsf,21,2,dsf,23,3,dfs,12', - 'details_of_dependents': '1,ds,12,2,sds,2,3,ds,13,None,None,None,None,None,None,None,None,None', 'amount_of_advance_required': '1221', - 'certified_family_dependents': '213', 'certified_advance': '213', 'adjusted_month': '213', 'date': '2024-03-15', - 'phone_number_for_contact': '21313123132' -} - -# Reverse processing -reversed_data = reverse_ltc_pre_processing(data) -print(reversed_data) - - - -{'block_year': 232, 'pf_no': None, 'basic_pay_salary': 324, 'name': 'sdf', 'designation': 'fds', - 'department_info': 'dfs', 'leave_availability': True, 'leave_start_date': datetime.date(2024, 3, 13), - 'leave_end_date': datetime.date(2024, 3, 17), 'date_of_leave_for_family': datetime.date(2024, 3, 16), - 'nature_of_leave': 'erds', 'purpose_of_leave': 'fds', 'hometown_or_not': True, 'place_of_visit': 'fds', - 'address_during_leave': 'dfsfsdf', 'amount_of_advance_required': 1221, 'certified_family_dependents': '213', - 'certified_advance': 213, 'adjusted_month': '213', 'date': datetime.date(2024, 3, 15), - 'phone_number_for_contact': 21313123132, - 'details_of_family_members_already_done': ['fds', 'dfs', 'dfs'], - 'info_1_1': '1', 'info_1_2': 'dfsf', 'info_1_3': '21', 'info_2_1': - '2', 'info_2_2': 'dsf', 'info_2_3': '23', 'info_3_1': '3', 'info_3_2': - 'dfs', 'info_3_3': '12', 'd_info_1_1': '1', 'd_info_1_2': 'ds', 'd_info_1_3': - '12', 'd_info_2_1': '2', 'd_info_2_2': 'sds', 'd_info_2_3': '2', 'd_info_3_1': '3', - 'd_info_3_2': 'ds', 'd_info_3_3': '13', 'd_info_4_1': '', 'd_info_4_2': '', 'd_info_4_3': '', - - 'd_info_5_1': '', 'd_info_5_2': '', 'd_info_5_3': '', 'd_info_6_1': '', 'd_info_6_2': '', 'd_info_6_3': ''} diff --git a/FusionIIIT/applications/hr2/admin.py b/FusionIIIT/applications/hr2/admin.py index 3c9b8379a..3b6af051f 100644 --- a/FusionIIIT/applications/hr2/admin.py +++ b/FusionIIIT/applications/hr2/admin.py @@ -1,18 +1,23 @@ from django.contrib import admin +from .models import ( + Employee, EmpConfidentialDetails, EmpDependents, ForeignService, + EmpAppraisalForm, WorkAssignemnt, LTCform, CPDAAdvanceform, + CPDAReimbursementform, LeaveForm, LeaveClaim, LeaveBalance, + LeavePerYear, Appraisalform +) -from .models import * -# from .models import CPDAReimbursementform -# Register your models here. - +# Register existing models admin.site.register(Employee) admin.site.register(EmpConfidentialDetails) admin.site.register(EmpDependents) admin.site.register(ForeignService) admin.site.register(EmpAppraisalForm) admin.site.register(WorkAssignemnt) -admin.site.register(LeaveBalance) -admin.site.register(LeaveForm) admin.site.register(LTCform) -admin.site.register(Appraisalform) -admin.site.register(CPDAAdvanceform) -admin.site.register(CPDAReimbursementform) \ No newline at end of file +admin.site.register(CPDAAdvanceform) +admin.site.register(CPDAReimbursementform) +admin.site.register(LeaveForm) +admin.site.register(LeaveClaim) +admin.site.register(LeaveBalance) +admin.site.register(LeavePerYear) +admin.site.register(Appraisalform) \ No newline at end of file diff --git a/FusionIIIT/applications/hr2/api/__init__.py b/FusionIIIT/applications/hr2/api/__init__.py new file mode 100644 index 000000000..7f76b8ef9 --- /dev/null +++ b/FusionIIIT/applications/hr2/api/__init__.py @@ -0,0 +1 @@ +# HR2 API Module diff --git a/FusionIIIT/applications/hr2/api/form_views.py b/FusionIIIT/applications/hr2/api/form_views.py deleted file mode 100644 index 984e1fbaf..000000000 --- a/FusionIIIT/applications/hr2/api/form_views.py +++ /dev/null @@ -1,546 +0,0 @@ -from .serializers import LTC_serializer, CPDAAdvance_serializer, Appraisal_serializer, CPDAReimbursement_serializer, Leave_serializer, LeaveBalanace_serializer -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status -# from rest_framework.decorators import permission_classes, api_view -from rest_framework.permissions import IsAuthenticated -from applications.hr2.models import LTCform, CPDAAdvanceform, CPDAReimbursementform, LeaveForm, Appraisalform, LeaveBalance -from django.contrib.auth import get_user_model -from django.core.exceptions import MultipleObjectsReturned -from applications.filetracking.sdk.methods import * -from applications.globals.models import Designation, HoldsDesignation, ExtraInfo -from applications.filetracking.models import * -# from django.contrib.auth.models import User - - -class LTC(APIView): - serializer_class = LTC_serializer - permission_classes = (IsAuthenticated, ) - - def post(self, request): - print("hello") - user_info = request.data[1] - print(request.data[1]) - serializer = self.serializer_class(data=request.data[0]) - if serializer.is_valid(): - serializer.save() - fileId = create_file(uploader=user_info['uploader_name'], uploader_designation=user_info['uploader_designation'], receiver=user_info['receiver_name'], - receiver_designation=user_info['receiver_designation'], src_module="HR", src_object_id=str(serializer.data['id']), file_extra_JSON={"type": "LTC"}, attached_file=None) - # forwarded = forward_file(file_id=fileId, receiver=user_info['receiver_name'], receiver_designation=user_info['receiver_designation'], - # remarks="Forwarded to Receiver Inbox", file_extra_JSON={"type": "LTC"}) - return Response(serializer.data, status=status.HTTP_200_OK) - else: - print(serializer.errors) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def get(self, request, *args, **kwargs): - pk = request.query_params.get("name") - try: - forms = LTCform.objects.get(created_by=pk) - serializer = self.serializer_class(forms, many=False) - except MultipleObjectsReturned: - forms = LTCform.objects.filter(created_by=pk) - serializer = self.serializer_class(forms, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - - def put(self, request, *args, **kwargs): - pk = request.query_params.get("id") - receiver = request.data[0] - # send_to = receiver['receiver'] - # receiver_value = User.objects.get(username=send_to) - # receiver_value_designation = HoldsDesignation.objects.filter( - # user=receiver_value) - # lis = list(receiver_value_designation) - # obj = lis[0].designation - form = LTCform.objects.get(id=pk) - serializer = self.serializer_class(form, data=request.data[1]) - if serializer.is_valid(): - serializer.save() - forward_file(file_id=receiver['file_id'], receiver=receiver['receiver'], receiver_designation=receiver['receiver_designation'], - remarks=receiver['remarks'], file_extra_JSON=receiver['file_extra_JSON']) - return Response(serializer.data, status=status.HTTP_200_OK) - else: - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def delete(self, request, *args, **kwargs): - id = request.query_params.get("id") - is_archived = archive_file(file_id=id) - if (is_archived): - return Response(status=status.HTTP_200_OK) - else: - return Response(status=status.HTTP_400_BAD_REQUEST) - - -class FormManagement(APIView): - permission_classes = (IsAuthenticated, ) - - def get(self, request, *args, **kwargs): - username = request.query_params.get("username") - designation = request.query_params.get("designation") - inbox = view_inbox(username=username, - designation=designation, src_module="HR") - print(inbox) - return Response(inbox, status=status.HTTP_200_OK) - - def post(self, request, *args, **kwargs): - username = request.data['receiver'] - receiver_value = User.objects.get(username=username) - receiver_value_designation = HoldsDesignation.objects.filter( - user=receiver_value) - lis = list(receiver_value_designation) - obj = lis[0].designation - forward_file(file_id=request.data['file_id'], receiver=request.data['receiver'], receiver_designation=request.data['receiver_designation'], - remarks=request.data['remarks'], file_extra_JSON=request.data['file_extra_JSON']) - return Response(status=status.HTTP_200_OK) - - -class CPDAAdvance(APIView): - serializer_class = CPDAAdvance_serializer - permission_classes = (IsAuthenticated, ) - - def post(self, request): - print(request.data[0]) - user_info = request.data[1] - # receiver_value = User.objects.get(username=user_info['receiver_name']) - # receiver_value_designation = HoldsDesignation.objects.filter( - # user=receiver_value) - # lis = list(receiver_value_designation) - # obj = lis[0].designation - serializer = self.serializer_class(data=request.data[0]) - if serializer.is_valid(): - serializer.save() - print('1') - fileId = create_file(uploader=user_info['uploader_name'], uploader_designation=user_info['uploader_designation'], receiver=user_info['receiver_name'], - receiver_designation=user_info['receiver_designation'], src_module="HR", src_object_id=str(serializer.data['id']), file_extra_JSON={"type": "CPDAAdvance"}, attached_file=None) - # forwarded = forward_file(file_id=fileId, receiver=user_info['receiver_name'], receiver_designation=user_info['receiver_designation'], - # remarks="Forwarded to Receiver Inbox", file_extra_JSON={"type": "CPDAAdvance"}) - return Response(serializer.data, status=status.HTTP_200_OK) - else: - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def get(self, request, *args, **kwargs): - pk = request.query_params.get("name") - try: - forms = CPDAAdvanceform.objects.get(created_by=pk) - serializer = self.serializer_class(forms, many=False) - except MultipleObjectsReturned: - forms = CPDAAdvanceform.objects.filter(created_by=pk) - serializer = self.serializer_class(forms, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - - def put(self, request, *args, **kwargs): - pk = request.query_params.get("id") - receiver = request.data[0] - print(request.data) - send_to = receiver['receiver'] - print(send_to) - receiver_value = User.objects.get(username=send_to) - receiver_value_designation = HoldsDesignation.objects.filter( - user=receiver_value) - lis = list(receiver_value_designation) - obj = lis[0].designation - form = CPDAAdvanceform.objects.get(id=pk) - serializer = self.serializer_class(form, data=request.data[1]) - if serializer.is_valid(): - serializer.save() - forward_file(file_id=receiver['file_id'], receiver=receiver['receiver'], receiver_designation=receiver['receiver_designation'], - remarks=receiver['remarks'], file_extra_JSON=receiver['file_extra_JSON']) - return Response(serializer.data, status=status.HTTP_200_OK) - else: - print(serializer.errors) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def delete(self, request, *args, **kwargs): - id = request.query_params.get("id") - is_archived = archive_file(file_id=id) - if (is_archived): - return Response(status=status.HTTP_200_OK) - else: - return Response(status=status.HTTP_400_BAD_REQUEST) - - -class CPDAReimbursement(APIView): - serializer_class = CPDAReimbursement_serializer - permission_classes = (IsAuthenticated, ) - - def post(self, request): - user_info = request.data[1] - serializer = self.serializer_class(data=request.data[0]) - if serializer.is_valid(): - serializer.save() - fileId = create_file(uploader=user_info['uploader_name'], uploader_designation=user_info['uploader_designation'], receiver=user_info['receiver_name'], - receiver_designation=user_info['receiver_designation'], src_module="HR", src_object_id=str(serializer.data['id']), file_extra_JSON={"type": "CPDAReimbursement"}, attached_file=None) - # forwarded = forward_file(file_id=fileId, receiver=user_info['receiver_name'], receiver_designation=user_info['receiver_designation'], - # remarks="Forwarded to Receiver Inbox", file_extra_JSON={"type": "CPDAReimbursement"}) - return Response(serializer.data, status=status.HTTP_200_OK) - else: - print(serializer.errors) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def get(self, request, *args, **kwargs): - pk = request.query_params.get("name") - try: - forms = CPDAReimbursementform.objects.get(created_by=pk) - serializer = self.serializer_class(forms, many=False) - except MultipleObjectsReturned: - forms = CPDAReimbursementform.objects.filter(created_by=pk) - serializer = self.serializer_class(forms, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - - def put(self, request, *args, **kwargs): - pk = request.query_params.get("id") - print(request.data) - receiver = request.data[0] - # send_to = receiver['receiver'] - # receiver_value = User.objects.get(username=send_to) - # receiver_value_designation = HoldsDesignation.objects.filter( - # user=receiver_value) - # lis = list(receiver_value_designation) - # obj = lis[0].designation - form = CPDAReimbursementform.objects.get(id=pk) - serializer = self.serializer_class(form, data=request.data[1]) - if serializer.is_valid(): - serializer.save() - forward_file(file_id=receiver['file_id'], receiver=receiver['receiver'], receiver_designation=receiver['receiver_designation'], - remarks=receiver['remarks'], file_extra_JSON=receiver['file_extra_JSON']) - return Response(serializer.data, status=status.HTTP_200_OK) - else: - print(serializer.errors) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def delete(self, request, *args, **kwargs): - id = request.query_params.get("id") - is_archived = archive_file(file_id=id) - if (is_archived): - return Response(status=status.HTTP_200_OK) - else: - return Response(status=status.HTTP_400_BAD_REQUEST) - - -class Leave(APIView): - serializer_class = Leave_serializer - permission_classes = (IsAuthenticated, ) - - def post(self, request): - user_info = request.data[1] - serializer = self.serializer_class(data=request.data[0]) - if serializer.is_valid(): - serializer.save() - fileId = create_file(uploader=user_info['uploader_name'], uploader_designation=user_info['uploader_designation'], receiver=user_info['receiver_name'], - receiver_designation=user_info['receiver_designation'], src_module="HR", src_object_id=str(serializer.data['id']), file_extra_JSON={"type": "Leave"}, attached_file=None) - # forwarded = forward_file(file_id=fileId, receiver=user_info['receiver_name'], receiver_designation=user_info['receiver_designation'], - # remarks="Forwarded to Receiver Inbox", file_extra_JSON={"type": "Leave"}) - return Response(serializer.data, status=status.HTTP_200_OK) - else: - print(serializer.errors) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def get(self, request, *args, **kwargs): - pk = request.query_params.get("name") - try: - forms = LeaveForm.objects.get(created_by=pk) - serializer = self.serializer_class(forms, many=False) - except MultipleObjectsReturned: - forms = LeaveForm.objects.filter(created_by=pk) - serializer = self.serializer_class(forms, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - - def put(self, request, *args, **kwargs): - pk = request.query_params.get("id") - receiver = request.data[0] - # send_to = receiver['receiver'] - # receiver_value = User.objects.get(username=send_to) - # receiver_value_designation = HoldsDesignation.objects.filter( - # user=receiver_value) - # lis = list(receiver_value_designation) - # obj = lis[0].designation - form = LeaveForm.objects.get(id=pk) - serializer = self.serializer_class(form, data=request.data[1]) - if serializer.is_valid(): - serializer.save() - forward_file(file_id=receiver['file_id'], receiver=receiver['receiver'], receiver_designation=receiver['receiver_designation'], - remarks=receiver['remarks'], file_extra_JSON=receiver['file_extra_JSON']) - return Response(serializer.data, status=status.HTTP_200_OK) - else: - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def delete(self, request, *args, **kwargs): - id = request.query_params.get("id") - is_archived = archive_file(file_id=id) - if (is_archived): - return Response(status=status.HTTP_200_OK) - else: - return Response(status=status.HTTP_400_BAD_REQUEST) - - -class Appraisal(APIView): - serializer_class = Appraisal_serializer - permission_classes = (IsAuthenticated, ) - - def post(self, request): - user_info = request.data[1] - print(request.data) - serializer = self.serializer_class(data=request.data[0]) - if serializer.is_valid(): - serializer.save() - fileId = create_file(uploader=user_info['uploader_name'], uploader_designation=user_info['uploader_designation'], receiver=user_info['receiver_name'], - receiver_designation=user_info['receiver_designation'], src_module="HR", src_object_id=str(serializer.data['id']), file_extra_JSON={"type": "Appraisal"}, attached_file=None) - # forwarded = forward_file(file_id=fileId, receiver=user_info['receiver_name'], receiver_designation=user_info['receiver_designation'], - # remarks="Forwarded to Receiver Inbox", file_extra_JSON={"type": "Appraisal"}) - return Response(serializer.data, status=status.HTTP_200_OK) - else: - print(serializer.errors) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def get(self, request, *args, **kwargs): - pk = request.query_params.get("name") - try: - forms = Appraisalform.objects.get(created_by=pk) - serializer = self.serializer_class(forms, many=False) - except MultipleObjectsReturned: - forms = Appraisalform.objects.filter(created_by=pk) - serializer = self.serializer_class(forms, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - - def put(self, request, *args, **kwargs): - pk = request.query_params.get("id") - print(request.data) - form = Appraisalform.objects.get(id=pk) - receiver = request.data[0] - send_to = receiver['receiver_name'] - receiver_value = User.objects.get(username=send_to) - receiver_value_designation = HoldsDesignation.objects.filter( - user=receiver_value) - lis = list(receiver_value_designation) - obj = lis[0].designation - serializer = self.serializer_class(form, data=request.data[1]) - if serializer.is_valid(): - forward_file(file_id=receiver['file_id'], receiver=send_to, receiver_designation=receiver['receiver_designation'], - remarks=receiver['remarks'], file_extra_JSON=receiver['file_extra_JSON']) - serializer.save() - return Response(serializer.data, status=status.HTTP_200_OK) - else: - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - def delete(self, request, *args, **kwargs): - id = request.query_params.get("id") - is_archived = archive_file(file_id=id) - if (is_archived): - return Response(status=status.HTTP_200_OK) - else: - return Response(status=status.HTTP_400_BAD_REQUEST) - -# class Forward(APIView): -# def post(self, request, *args, **kwargs): -# forward_file(file_id = request.data['file_id'], receiver = request.data['receiver'], receiver_designation = 'hradmin', remarks = request.data['remarks'], file_extra_JSON = request.data['file_extra_JSON']) -# return Response(status = status.HTTP_200_OK) - - -class GetFormHistory(APIView): - permission_classes = (IsAuthenticated, ) - - def get(self, request, *args, **kwargs): - print(request.query_params) - form_type = request.query_params.get("type") - id = request.query_params.get("id") - person = User.objects.get(username=id) - print(type(person)) - id = person - if form_type == "LTC": - try: - forms = LTCform.objects.get(created_by=id) - serializer = LTC_serializer(forms, many=False) - return Response([serializer.data], status=status.HTTP_200_OK) - except MultipleObjectsReturned: - forms = LTCform.objects.filter(created_by=id) - serializer = LTC_serializer(forms, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - except LTCform.DoesNotExist: - return Response([], status=status.HTTP_200_OK) - elif form_type == "CPDAReimbursement": - try: - forms = CPDAReimbursementform.objects.get(created_by=id) - serializer = CPDAReimbursement_serializer(forms, many=False) - return Response([serializer.data], status=status.HTTP_200_OK) - except MultipleObjectsReturned: - forms = CPDAReimbursementform.objects.filter(created_by=id) - serializer = CPDAReimbursement_serializer(forms, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - except CPDAReimbursementform.DoesNotExist: - return Response([], status=status.HTTP_200_OK) - elif form_type == "CPDAAdvance": - try: - forms = CPDAAdvanceform.objects.get(created_by=id) - serializer = CPDAAdvance_serializer(forms, many=False) - return Response([serializer.data], status=status.HTTP_200_OK) - except MultipleObjectsReturned: - forms = CPDAAdvanceform.objects.filter(created_by=id) - serializer = CPDAAdvance_serializer(forms, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - except CPDAAdvanceform.DoesNotExist: - return Response([], status=status.HTTP_200_OK) - elif form_type == "Appraisal": - try: - forms = Appraisalform.objects.get(created_by=id) - serializer = Appraisal_serializer(forms, many=False) - return Response([serializer.data], status=status.HTTP_200_OK) - except MultipleObjectsReturned: - forms = Appraisalform.objects.filter(created_by=id) - serializer = Appraisal_serializer(forms, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - except Appraisalform.DoesNotExist: - return Response([], status=status.HTTP_200_OK) - elif form_type == "Leave": - try: - forms = LeaveForm.objects.get(created_by=id) - serializer = Leave_serializer(forms, many=False) - return Response([serializer.data], status=status.HTTP_200_OK) - except MultipleObjectsReturned: - forms = LeaveForm.objects.filter(created_by=id) - serializer = Leave_serializer(forms, many=True) - return Response(serializer.data, status=status.HTTP_200_OK) - except LeaveForm.DoesNotExist: - return Response([], status=status.HTTP_200_OK) - - -class TrackProgress(APIView): - permission_classes = (IsAuthenticated, ) - - def get(self, request, *args, **kwargs): - file_id = request.query_params.get("id") - progress = view_history(file_id) - return Response({"status": progress}, status=status.HTTP_200_OK) - - -class FormFetch(APIView): - permission_classes = (IsAuthenticated, ) - - def get(self, request, *args, **kwargs): - fileId = request.query_params.get("file_id") - print(fileId) - form_id = request.query_params.get("id") - form_type = request.query_params.get("type") - if form_type == "LTC": - forms = LTCform.objects.get(id=form_id) - serializer = LTC_serializer(forms, many=False) - form = serializer.data - user = User.objects.get(id=int(form["created_by"])) - owner = Tracking.objects.filter(file_id=fileId) - current_owner = owner.last() - current_owner = current_owner.receiver_id - current_owner = current_owner.username - elif form_type == "CPDAReimbursement": - forms = CPDAReimbursementform.objects.get(id=form_id) - serializer = CPDAReimbursement_serializer(forms, many=False) - form = serializer.data - user = User.objects.get(id=int(form["created_by"])) - owner = Tracking.objects.filter(file_id=fileId) - current_owner = owner.last() - current_owner = current_owner.receiver_id - current_owner = current_owner.username - elif form_type == "CPDAAdvance": - forms = CPDAAdvanceform.objects.get(id=form_id) - serializer = CPDAAdvance_serializer(forms, many=False) - form = serializer.data - user = User.objects.get(id=int(form["created_by"])) - owner = Tracking.objects.filter(file_id=fileId) - current_owner = owner.last() - current_owner = current_owner.receiver_id - current_owner = current_owner.username - elif form_type == "Appraisal": - forms = Appraisalform.objects.get(id=form_id) - serializer = Appraisal_serializer(forms, many=False) - form = serializer.data - user = User.objects.get(id=int(form["created_by"])) - owner = Tracking.objects.filter(file_id=fileId) - current_owner = owner.last() - current_owner = current_owner.receiver_id - current_owner = current_owner.username - elif form_type == "Leave": - forms = LeaveForm.objects.get(id=form_id) - serializer = Leave_serializer(forms, many=False) - form = serializer.data - user = User.objects.get(id=int(form["created_by"])) - owner = Tracking.objects.filter(file_id=fileId) - current_owner = owner.last() - current_owner = current_owner.receiver_id - current_owner = current_owner.username - return Response({"form": serializer.data, "creator": user.username, "current_owner": current_owner}, status=status.HTTP_200_OK) - - -class CheckLeaveBalance(APIView): - permission_classes = (IsAuthenticated, ) - serializer_class = LeaveBalanace_serializer - - def get(self, request, *args, **kwargs): - name = request.query_params.get("name") - person = User.objects.get(username=name) - extrainfo = ExtraInfo.objects.get(user=person) - leave_balance = LeaveBalance.objects.get(employeeId=extrainfo) - serializer = self.serializer_class(leave_balance, many=False) - return Response(serializer.data, status=status.HTTP_200_OK) - # return Response([], status=status.HTTP_200_OK) - - def put(self, request, *args, **kwargs): - name = request.query_params.get("name") - # print(request.data) - person = User.objects.get(username=name) - extrainfo = ExtraInfo.objects.get(user=person) - leave_balance = LeaveBalance.objects.get(employeeId=extrainfo) - data1 = request.data - data1['employeeId'] = extrainfo.id - serializer = self.serializer_class(leave_balance, data=data1) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status=status.HTTP_200_OK) - else: - print(serializer.error_messages) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) - - -class DropDown(APIView): - permission_classes = (IsAuthenticated, ) - - def get(self, request, *args, **kwargs): - user_id = request.query_params.get("username") - user = User.objects.get(username=user_id) - designations = HoldsDesignation.objects.filter(user=user.id) - designation_list = [] - - for design in designations: - design = design.designation - design = design.name - designation_list.append(design) - # print(designation_list) - return Response(designation_list, status=status.HTTP_200_OK) - - -class UserById(APIView): - permission_classes = (IsAuthenticated, ) - - def get(self, request, *args, **kwargs): - user_id = request.query_params.get("id") - user = User.objects.get(id=user_id) - return Response({"username": user.username}, status=status.HTTP_200_OK) - - -class ViewArchived(APIView): - permission_classes = (IsAuthenticated, ) - - def get(self, request, *args, **kwargs): - user_name = request.query_params.get("username") - user_designation = request.query_params.get("designation") - archived_inbox = view_archived( - username=user_name, designation=user_designation, src_module="HR") - return Response(archived_inbox, status=status.HTTP_200_OK) - - -class GetOutbox(APIView): - permission_classes = (IsAuthenticated, ) - - def get(self, request, *args, **kwargs): - name = request.query_params.get("username") - user_designation = request.query_params.get("designation") - outbox = view_outbox( - username=name, designation=user_designation, src_module="HR") - return Response(outbox, status=status.HTTP_200_OK) diff --git a/FusionIIIT/applications/hr2/api/serializers.py b/FusionIIIT/applications/hr2/api/serializers.py index 63efc27cc..7828a69a4 100644 --- a/FusionIIIT/applications/hr2/api/serializers.py +++ b/FusionIIIT/applications/hr2/api/serializers.py @@ -1,65 +1,289 @@ from rest_framework import serializers -from applications.hr2.models import LTCform, CPDAAdvanceform, CPDAReimbursementform, LeaveForm, Appraisalform, LeaveBalance +from django.utils import timezone +from decimal import Decimal +from applications.globals.models import ExtraInfo +from ..models import ( + Employee, ServiceHistory, LeaveType, EmployeeLeaveBalance, LeaveApplicationNew, + AppraisalPeriod, PerformanceAppraisalNew, TrainingProgram, TrainingNomination, + PromotionApplication, EmployeeAttendance, FacultyWorkload, + EducationalQualification, ProfessionalQualification, PreviousExperience +) +class EmployeeDetailsSerializer(serializers.ModelSerializer): + class Meta: + model = Employee + fields = '__all__' + read_only_fields = ['id'] + +class LeaveApplicationSerializer(serializers.ModelSerializer): + employee_id = serializers.CharField(write_only=True, required=False) + nominee_employee_id = serializers.CharField(write_only=True, required=False, allow_blank=True) + nominee_employee_name = serializers.SerializerMethodField(read_only=True) + is_owner = serializers.SerializerMethodField(read_only=True) -class LTC_serializer(serializers.ModelSerializer): class Meta: - model = LTCform + model = LeaveApplicationNew fields = '__all__' + read_only_fields = [ + 'id', + 'applied_date', + 'approval_status', + 'cancel_status', + 'cancel_requested_at', + 'cancel_decided_at', + 'cancel_requested_by_role', + 'cancel_current_approver_role', + 'cancel_reason', + 'cancel_decision_remarks', + 'extension_status', + 'extension_requested_at', + 'extension_decided_at', + 'extension_requested_by_role', + 'extension_current_approver_role', + 'extension_reason', + 'extension_new_end_date', + 'extension_new_total_days', + 'extension_decision_remarks', + 'resumption_status', + 'resumption_date', + 'resumption_reason', + 'resumption_submitted_at', + 'resumption_decided_at', + 'resumption_current_approver_role', + 'resumption_decision_remarks', + ] + extra_kwargs = { + 'employee': {'required': False}, + } def create(self, validated_data): - return LTCform.objects.create(**validated_data) + # Remove serializer-only fields before model create. + validated_data.pop('nominee_employee_id', None) + validated_data.pop('employee_id', None) + return super().create(validated_data) + + def validate(self, data): + leave_type_name = data.get('leave_type') + if not leave_type_name and self.instance is not None: + leave_type_name = self.instance.leave_type + station_leave_value = None + if 'station_leave' in data: + station_leave_value = data.get('station_leave') + elif self.instance is not None: + station_leave_value = self.instance.station_leave + start_date = data.get('start_date') + end_date = data.get('end_date') + is_half_day = data.get('is_half_day', False) + half_day_slot = data.get('half_day_slot') + if start_date: + today = timezone.now().date() + if start_date < today: + raise serializers.ValidationError({'start_date': 'Start date cannot be in the past.'}) + if start_date and end_date and start_date > end_date: + raise serializers.ValidationError("Start date must be before or equal to end date.") + if start_date and end_date and data.get('total_days') is not None: + if is_half_day: + expected_days = Decimal('0.5') + else: + expected_days = Decimal((end_date - start_date).days + 1) + try: + provided_days = Decimal(str(data.get('total_days'))) + except (TypeError, ValueError): + raise serializers.ValidationError({'total_days': 'Total days must be a valid number.'}) + if provided_days != expected_days: + raise serializers.ValidationError({'total_days': f'Total days should be {expected_days} based on selected dates.'}) + if is_half_day: + if leave_type_name != 'Casual': + raise serializers.ValidationError({'is_half_day': 'Half-day is only allowed for Casual leave.'}) + if not half_day_slot: + raise serializers.ValidationError({'half_day_slot': 'Select AM or PM for half-day leave.'}) + if start_date and end_date and start_date != end_date: + raise serializers.ValidationError({'end_date': 'Half-day leave must be for a single day.'}) + else: + if half_day_slot: + raise serializers.ValidationError({'half_day_slot': 'Half-day slot is only for half-day leave.'}) + if start_date and end_date: + employee = None + request = self.context.get('request') if hasattr(self, 'context') else None + if request and hasattr(request, 'user'): + try: + employee = request.user.extrainfo + except ExtraInfo.DoesNotExist: + employee = None + if employee is None: + employee_id = data.get('employee_id') + if employee_id: + employee = ExtraInfo.objects.filter(id=employee_id).first() + if employee is not None: + overlapping = LeaveApplicationNew.objects.filter( + employee=employee, + approval_status__in=['PENDING', 'FORWARDED', 'APPROVED'], + start_date__lte=end_date, + end_date__gte=start_date, + ) + if self.instance is not None: + overlapping = overlapping.exclude(id=self.instance.id) + if overlapping.exists(): + overlap_found = False + for existing in overlapping: + if not is_half_day or not existing.is_half_day: + overlap_found = True + break + if start_date != end_date: + overlap_found = True + break + if existing.start_date != existing.end_date: + overlap_found = True + break + if existing.start_date != start_date: + continue + if existing.half_day_slot == half_day_slot: + overlap_found = True + break + if overlap_found: + raise serializers.ValidationError({'start_date': 'Leave dates overlap with an existing leave request.'}) + leave_type_name = data.get('leave_type') + if leave_type_name and data.get('total_days') is not None: + leave_type = LeaveType.objects.filter(name__iexact=leave_type_name).first() + if leave_type: + year = start_date.year + balance = EmployeeLeaveBalance.objects.filter( + employee=employee, + leave_type=leave_type, + year=year, + ).first() + if balance is None: + balance = EmployeeLeaveBalance.objects.filter( + employee=employee, + leave_type=leave_type, + ).order_by('-year').first() + if balance is None: + raise serializers.ValidationError({'leave_type': 'Leave balance not found for this leave type.'}) + if Decimal(str(data.get('total_days'))) > (balance.current_balance or 0): + raise serializers.ValidationError({'total_days': 'Requested days exceed remaining leave balance.'}) + nominee_id = (data.get('nominee_employee_id') or '').strip() + if nominee_id: + nominee = ExtraInfo.objects.filter(id=nominee_id).first() + if not nominee: + raise serializers.ValidationError({'nominee_employee_id': 'Employee not found.'}) + if employee is not None and str(employee.id) == nominee_id: + raise serializers.ValidationError({'nominee_employee_id': 'Nominee must be different from the applicant.'}) + if start_date and end_date: + nominee_overlapping = LeaveApplicationNew.objects.filter( + employee=nominee, + approval_status__in=['PENDING', 'FORWARDED', 'APPROVED'], + start_date__lte=end_date, + end_date__gte=start_date, + ).exists() + if nominee_overlapping: + raise serializers.ValidationError({'nominee_employee_id': 'Nominee has overlapping pending or approved leave.'}) + is_cl_rh_leave = leave_type_name in ['Casual', 'Restricted'] + is_station_only = is_cl_rh_leave and station_leave_value == 'NOT_REQUIRED' + is_vacation_leave = leave_type_name == 'Vacation' + if is_cl_rh_leave and not station_leave_value: + raise serializers.ValidationError({'station_leave': 'Select a station leave option for this leave type.'}) + if self.instance is None and not is_station_only and not is_vacation_leave and not nominee_id: + raise serializers.ValidationError({'nominee_employee_id': 'Nominee Employee ID is required for this leave type.'}) + return data + def get_nominee_employee_name(self, obj): + if not obj.handover_to: + return '' + nominee = ExtraInfo.objects.filter(id=obj.handover_to).first() + if not nominee: + return '' + return nominee.user.get_full_name() or nominee.user.username + + def get_is_owner(self, obj): + request = self.context.get('request') if hasattr(self, 'context') else None + if not request or not hasattr(request, 'user'): + return False + try: + return obj.employee == request.user.extrainfo + except ExtraInfo.DoesNotExist: + return False + +class LeaveBalanceSerializer(serializers.ModelSerializer): + leave_type_name = serializers.CharField(source='leave_type.name', read_only=True) -class CPDAAdvance_serializer(serializers.ModelSerializer): class Meta: - model = CPDAAdvanceform + model = EmployeeLeaveBalance fields = '__all__' - def create(self, validated_data): - return CPDAAdvanceform.objects.create(**validated_data) +class PerformanceAppraisalSerializer(serializers.ModelSerializer): + class Meta: + model = PerformanceAppraisalNew + fields = '__all__' +class TrainingProgramSerializer(serializers.ModelSerializer): + class Meta: + model = TrainingProgram + fields = '__all__' -class Appraisal_serializer(serializers.ModelSerializer): +class TrainingNominationSerializer(serializers.ModelSerializer): class Meta: - model = Appraisalform + model = TrainingNomination fields = '__all__' - def create(self, validated_data): - return Appraisalform.objects.create(**validated_data) +class PromotionApplicationSerializer(serializers.ModelSerializer): + class Meta: + model = PromotionApplication + fields = '__all__' +class EmployeeAttendanceSerializer(serializers.ModelSerializer): + class Meta: + model = EmployeeAttendance + fields = '__all__' -class CPDAReimbursement_serializer(serializers.ModelSerializer): +class FacultyWorkloadSerializer(serializers.ModelSerializer): class Meta: - model = CPDAReimbursementform + model = FacultyWorkload fields = '__all__' - def create(self, validated_data): - return CPDAReimbursementform.objects.create(**validated_data) +class AppraisalPeriodSerializer(serializers.ModelSerializer): + class Meta: + model = AppraisalPeriod + fields = '__all__' + +from ..models import LTCApplicationNew, CPDAAdvanceNew, CPDAReimbursementNew, AppraisalFormNew +class LTCApplicationSerializer(serializers.ModelSerializer): + employee_id = serializers.CharField(write_only=True, required=False) -class Leave_serializer(serializers.ModelSerializer): class Meta: - model = LeaveForm + model = LTCApplicationNew fields = '__all__' + read_only_fields = ['id', 'applied_date', 'approval_status'] + extra_kwargs = { + 'employee': {'required': False}, + } - def create(self, validated_data): - return LeaveForm.objects.create(**validated_data) +class CPDAAdvanceSerializer(serializers.ModelSerializer): + employee_id = serializers.CharField(write_only=True, required=False) - -class LeaveBalanace_serializer(serializers.ModelSerializer): class Meta: - model = LeaveBalance + model = CPDAAdvanceNew fields = '__all__' + read_only_fields = ['id', 'applied_date', 'approval_status'] + extra_kwargs = { + 'employee': {'required': False}, + } - def create(self, validated_data): - return LeaveBalance.objects.create(**validated_data) +class CPDAReimbursementSerializer(serializers.ModelSerializer): + employee_id = serializers.CharField(write_only=True, required=False) + class Meta: + model = CPDAReimbursementNew + fields = '__all__' + read_only_fields = ['id', 'applied_date', 'approval_status'] -# class Deignations(serializers.ModelSerializer): -# class Meta: -# model = Deignations -# fields = '__all__' +class AppraisalFormSerializer(serializers.ModelSerializer): + employee_id = serializers.CharField(write_only=True, required=False) -# def create(self,validated_data): -# return + class Meta: + model = AppraisalFormNew + fields = '__all__' + read_only_fields = ['id', 'status', 'submitted_at'] + extra_kwargs = { + 'employee': {'required': False}, + } \ No newline at end of file diff --git a/FusionIIIT/applications/hr2/api/urls.py b/FusionIIIT/applications/hr2/api/urls.py index 835434b76..36ce6040f 100644 --- a/FusionIIIT/applications/hr2/api/urls.py +++ b/FusionIIIT/applications/hr2/api/urls.py @@ -1,42 +1,69 @@ -from django.conf.urls import url from django.urls import path -# from . import views -from . import form_views +from . import views - -app_name = 'hr2' +app_name = 'hr_api' urlpatterns = [ - # LTC form - url('ltc/', form_views.LTC.as_view(), name='LTC_form'), - # cpda advance form - url('cpdaadv/', form_views.CPDAAdvance.as_view(), name='CPDAAdvance_form'), - # appraisal form - url('appraisal/', form_views.Appraisal.as_view(), name='Appraisal_form'), - # cpda reimbursement form - url('cpdareim/', form_views.CPDAReimbursement.as_view(), - name='CPDAReimbursement_form'), - # leave form - url('leave/', form_views.Leave.as_view(), name='Leave_form'), - url('formManagement/', form_views.FormManagement.as_view(), name='formManagement'), - url('tracking/', form_views.TrackProgress.as_view(), name='tracking'), - url('formFetch/', form_views.FormFetch.as_view(), name='fetch_form'), - # create for GetForms - url('getForms/', form_views.GetFormHistory.as_view(), name='getForms'), - url('leaveBalance/', form_views.CheckLeaveBalance.as_view(), name='leaveBalance'), - url('getDesignations/', form_views.DropDown.as_view(), name="designations"), - url('getOutbox/', form_views.GetOutbox.as_view(), name='outbox'), - url('getArchive/', form_views.ViewArchived.as_view(), name='archive'), - url('getuserbyid/', form_views.UserById.as_view(), name='userById'), - # url(r'^$', views.service_book, name='hr2'), - # url(r'^hradmin/$', views.hr_admin, name='hradmin'), - # url(r'^edit/(?P\d+)/$', views.edit_employee_details, - # name='editEmployeeDetails'), - # url(r'^viewdetails/(?P\d+)/$', - # views.view_employee_details, name='viewEmployeeDetails'), - # url(r'^editServiceBook/(?P\d+)/$', - # views.edit_employee_servicebook, name='editServiceBook'), - # url(r'^administrativeProfile/$', views.administrative_profile, - # name='administrativeProfile'), - # url(r'^addnew/$', views.add_new_user, name='addnew'), -] + # Employee + path('employees/', views.EmployeeListView.as_view(), name='employee-list'), + path('employees//', views.EmployeeDetailView.as_view(), name='employee-detail'), + + # Leave + path('leave-applications/', views.LeaveApplicationListCreateView.as_view(), name='leave-list-create'), + path('leave-applications//', views.LeaveApplicationDetailView.as_view(), name='leave-detail'), + path('leave-balance/', views.LeaveBalanceView.as_view(), name='leave-balance'), + path('leave-balance//', views.LeaveBalanceView.as_view(), name='leave-balance-other'), + path('leave-applications//responsibility//', views.LeaveResponsibilityView.as_view(), name='leave-responsibility'), + path('leave-applications//request-document/', views.LeaveDocumentRequestView.as_view(), name='leave-request-document'), + path('leave-applications//submit-document/', views.LeaveDocumentSubmitView.as_view(), name='leave-submit-document'), + path('leave-applications//download/', views.LeaveApplicationDownloadView.as_view(), name='leave-download'), + path('leave-applications//withdraw/', views.LeaveApplicationWithdrawView.as_view(), name='leave-withdraw'), + path('leave-applications//cancel-request/', views.LeaveApplicationCancelRequestView.as_view(), name='leave-cancel-request'), + path('leave-applications//cancel-decision//', views.LeaveApplicationCancelDecisionView.as_view(), name='leave-cancel-decision'), + path('leave-applications//extension-request/', views.LeaveApplicationExtensionRequestView.as_view(), name='leave-extension-request'), + path('leave-applications//extension-decision//', views.LeaveApplicationExtensionDecisionView.as_view(), name='leave-extension-decision'), + path('leave-applications//resumption/', views.LeaveResumptionSubmitView.as_view(), name='leave-resumption'), + path('leave-applications//resumption-decision//', views.LeaveResumptionDecisionView.as_view(), name='leave-resumption-decision'), + path('leave-applications///', views.LeaveApproveRejectView.as_view(), name='leave-decision'), + path('leave-nominee/', views.LeaveNomineeDashboardView.as_view(), name='leave-nominee-dashboard'), + path('leave-nominee//', views.LeaveNomineeDecisionView.as_view(), name='leave-nominee-decision'), + + # Attendance + path('attendance/', views.AttendanceView.as_view(), name='attendance'), + + # Appraisal + path('appraisal-periods/', views.AppraisalPeriodListView.as_view(), name='appraisal-periods'), + path('appraisals/', views.AppraisalListView.as_view(), name='appraisals'), + + # Training + path('training-programs/', views.TrainingProgramListView.as_view(), name='training-programs'), + path('training-nominations/', views.TrainingNominationView.as_view(), name='training-nominations'), + + # Promotion + path('promotions/', views.PromotionApplicationView.as_view(), name='promotions'), + + # Faculty Workload + path('workload/', views.FacultyWorkloadView.as_view(), name='workload'), + # Add these to the urlpatterns list + + path('ltc/', views.LTCApplicationListCreateView.as_view(), name='ltc-list-create'), + path('ltc//', views.LTCApplicationDetailView.as_view(), name='ltc-detail'), + path('ltc//download/', views.LTCApplicationDownloadView.as_view(), name='ltc-download'), + path('ltc//withdraw/', views.LTCApplicationWithdrawView.as_view(), name='ltc-withdraw'), + path('ltc///', views.LTCApproveRejectView.as_view(), name='ltc-decision'), + + path('cpda-advances/', views.CPDAAdvanceListCreateView.as_view(), name='cpda-advance-list'), + path('cpda-advances//', views.CPDAAdvanceDetailView.as_view(), name='cpda-advance-detail'), + path('cpda-advances//download/', views.CPDAAdvanceDownloadView.as_view(), name='cpda-advance-download'), + path('cpda-advances//withdraw/', views.CPDAAdvanceWithdrawView.as_view(), name='cpda-advance-withdraw'), + path('cpda-advances///', views.CPDAAdvanceApproveRejectView.as_view(), name='cpda-advance-decision'), + + path('cpda-reimbursements/', views.CPDAReimbursementListCreateView.as_view(), name='cpda-reimbursement-list'), + path('cpda-reimbursements//', views.CPDAReimbursementDetailView.as_view(), name='cpda-reimbursement-detail'), + path('cpda-reimbursements///', views.CPDAReimbursementApproveRejectView.as_view(), name='cpda-reimbursement-decision'), + + path('appraisal-forms/', views.AppraisalFormListCreateView.as_view(), name='appraisal-form-list'), + path('appraisal-forms//', views.AppraisalFormDetailView.as_view(), name='appraisal-form-detail'), + path('appraisal-forms//download/', views.AppraisalFormDownloadView.as_view(), name='appraisal-form-download'), + path('appraisal-forms//review/', views.AppraisalReviewView.as_view(), name='appraisal-form-review'), +] \ No newline at end of file diff --git a/FusionIIIT/applications/hr2/api/views.py b/FusionIIIT/applications/hr2/api/views.py new file mode 100644 index 000000000..d768a592c --- /dev/null +++ b/FusionIIIT/applications/hr2/api/views.py @@ -0,0 +1,1452 @@ +import datetime + +from rest_framework.views import APIView +from rest_framework.response import Response +from rest_framework import status +from rest_framework.permissions import IsAuthenticated +from rest_framework.parsers import MultiPartParser, FormParser, JSONParser +from django.shortcuts import get_object_or_404 +from django.db.models import Q +from django.http import HttpResponse +from django.utils import timezone +from applications.globals.models import ExtraInfo, HoldsDesignation +from decimal import Decimal +from ..models import LeaveApplicationNew, EmployeeLeaveBalance, AppraisalFormNew, LeaveType +from ..services import ( + approve_leave_application, reject_leave_application, + handle_academic_responsibility, handle_administrative_responsibility, + mark_attendance, calculate_faculty_workload, + InsufficientLeaveBalanceError, DuplicateLeaveApplicationError, InvalidWorkflowTransitionError +) +from ..selectors import ( + get_employee_by_id, get_all_employees, get_leave_balance_for_employee, + get_leave_applications, get_pending_responsibility_leaves, + get_attendance_for_employee, get_appraisal_periods, get_appraisals_for_employee, + get_available_training_programs, get_nominations_for_employee, + get_promotion_applications, get_faculty_workload +) +from .serializers import ( + EmployeeDetailsSerializer, LeaveApplicationSerializer, LeaveBalanceSerializer, + PerformanceAppraisalSerializer, TrainingProgramSerializer, TrainingNominationSerializer, + PromotionApplicationSerializer, EmployeeAttendanceSerializer, FacultyWorkloadSerializer, + AppraisalPeriodSerializer +) + +# ==================== EMPLOYEE VIEWS ==================== + +class EmployeeListView(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request): + employee_type = request.query_params.get('type') + department_id = request.query_params.get('department') + employees = get_all_employees(employee_type, department_id) + serializer = EmployeeDetailsSerializer(employees, many=True) + return Response(serializer.data) + +class EmployeeDetailView(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request, employee_id): + employee = get_employee_by_id(employee_id) + serializer = EmployeeDetailsSerializer(employee) + return Response(serializer.data) + + def put(self, request, employee_id): + employee = get_employee_by_id(employee_id) + serializer = EmployeeDetailsSerializer(employee, data=request.data, partial=True) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + +# ==================== LEAVE VIEWS ==================== + +class LeaveApplicationListCreateView(APIView): + permission_classes = [IsAuthenticated] + parser_classes = [JSONParser, MultiPartParser, FormParser] + + def get(self, request): + is_hr_staff = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='hr', + ).exists() or ( + request.user.extrainfo.user_type == 'staff' + and request.user.extrainfo.department + and request.user.extrainfo.department.name == 'HR' + ) + is_hod = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='hod', + ).exists() + is_director = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='director', + ).exists() + is_registrar = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='registrar', + ).exists() + + if is_hr_staff: + leaves = LeaveApplicationNew.objects.all() + elif is_director: + leaves = LeaveApplicationNew.objects.filter( + Q( + approval_status='FORWARDED', + current_approver_role__iexact='Director', + ) | Q(employee=request.user.extrainfo) | Q( + cancel_status='REQUESTED', + cancel_current_approver_role__iexact='Director', + ) | Q( + extension_status='REQUESTED', + extension_current_approver_role__iexact='Director', + ) + ) + elif is_registrar: + leaves = LeaveApplicationNew.objects.filter( + Q( + approval_status='FORWARDED', + current_approver_role__iexact='Registrar', + ) | Q(employee=request.user.extrainfo) | Q( + cancel_status='REQUESTED', + cancel_current_approver_role__iexact='Registrar', + ) | Q( + extension_status='REQUESTED', + extension_current_approver_role__iexact='Registrar', + ) + ) + elif is_hod: + leaves = LeaveApplicationNew.objects.filter( + department=request.user.extrainfo.department.name + ) + else: + leaves = get_leave_applications(request.user.extrainfo) + serializer = LeaveApplicationSerializer(leaves, many=True, context={'request': request}) + return Response(serializer.data) + + def post(self, request): + serializer = LeaveApplicationSerializer(data=request.data, context={'request': request}) + if serializer.is_valid(): + employee = getattr(request.user, 'extrainfo', None) + if employee is None: + employee_id = request.data.get('employee_id') + if employee_id: + employee = get_employee_by_id(employee_id) + if employee is None: + return Response( + {'error': 'Employee profile not found for this user.'}, + status=status.HTTP_400_BAD_REQUEST, + ) + nominee_id = (request.data.get('nominee_employee_id') or '').strip() + nominee_status = 'PENDING' if nominee_id else 'NOT_REQUIRED' + is_director = HoldsDesignation.objects.filter( + working=employee.user, + designation__name__icontains='director', + ).exists() + is_hod = HoldsDesignation.objects.filter( + working=employee.user, + designation__name__icontains='hod', + ).exists() + is_registrar = HoldsDesignation.objects.filter( + working=employee.user, + designation__name__icontains='registrar', + ).exists() + is_hr_admin = HoldsDesignation.objects.filter( + working=employee.user, + designation__name__iregex=r'hr admin|hr administrator', + ).exists() + is_accountant = HoldsDesignation.objects.filter( + working=employee.user, + designation__name__icontains='accountant', + ).exists() + leave_type_name = (request.data.get('leave_type') or '').strip() + is_cl_rh_leave = leave_type_name in ['Casual', 'Restricted'] + employee_name = employee.user.get_full_name() or employee.user.username + department_name = employee.department.name if employee.department else (request.data.get('department') or '') + designation_name = '' + designation_record = HoldsDesignation.objects.filter(working=employee.user).select_related('designation').first() + if designation_record: + designation_name = designation_record.designation.full_name or designation_record.designation.name + else: + designation_name = request.data.get('designation') or '' + approval_status = 'PENDING' + approver_role = '' + if is_director: + approval_status = 'APPROVED' + approver_role = 'Director' + elif is_registrar: + approval_status = 'FORWARDED' + approver_role = 'Director' + elif is_hod: + if is_cl_rh_leave: + approval_status = 'APPROVED' + approver_role = 'HOD' + else: + approval_status = 'FORWARDED' + approver_role = 'Director' + elif is_hr_admin or is_accountant: + approval_status = 'FORWARDED' + approver_role = 'Registrar' + + leave_app = serializer.save( + employee=employee, + employee_name=employee_name, + department=department_name, + designation=designation_name, + handover_to=nominee_id, + nominee_status=nominee_status, + approval_status=approval_status, + current_approver_role=approver_role, + ) + if is_director or (is_hod and is_cl_rh_leave): + _apply_leave_balance_for_approval(leave_app) + leave_app.save(update_fields=['leave_balance_before', 'leave_balance_after']) + refreshed_serializer = LeaveApplicationSerializer(leave_app, context={'request': request}) + return Response(refreshed_serializer.data, status=status.HTTP_201_CREATED) + # Log validation errors to server console for easier debugging without DevTools. + print("LeaveApplication validation errors:", serializer.errors) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + +class LeaveApplicationDetailView(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request, pk): + leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) + serializer = LeaveApplicationSerializer(leave_app) + return Response(serializer.data) + + def put(self, request, pk): + leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) + if leave_app.employee != request.user.extrainfo and not request.user.is_staff: + return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) + serializer = LeaveApplicationSerializer(leave_app, data=request.data, partial=True) + 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): + leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) + if leave_app.status != 'PENDING': + return Response({'error': 'Cannot delete non-pending application'}, status=status.HTTP_400_BAD_REQUEST) + leave_app.delete() + return Response(status=status.HTTP_204_NO_CONTENT) + +class LeaveApplicationDownloadView(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request, pk): + leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) + if leave_app.employee != request.user.extrainfo and not request.user.is_staff: + return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) + + lines = [ + f"Leave Application #{leave_app.id}", + "", + f"Employee: {leave_app.employee_name}", + f"Employee ID: {leave_app.employee.id}", + f"Department: {leave_app.department}", + f"Designation: {leave_app.designation}", + "", + f"Leave Type: {leave_app.leave_type}", + f"Station Leave: {leave_app.station_leave or 'N/A'}", + f"Half-day: {'Yes' if leave_app.is_half_day else 'No'}", + f"Half-day Slot: {leave_app.half_day_slot or 'N/A'}", + f"Start Date: {leave_app.start_date}", + f"End Date: {leave_app.end_date}", + f"Total Days: {leave_app.total_days}", + "", + f"Reason: {leave_app.reason}", + f"Contact During Leave: {leave_app.contact_during_leave}", + f"Address During Leave: {leave_app.address_during_leave}", + "", + f"Nominee Employee ID: {leave_app.handover_to or 'N/A'}", + f"Nominee Status: {leave_app.nominee_status}", + "", + f"Approval Status: {leave_app.approval_status}", + f"Applied Date: {leave_app.applied_date}", + ] + + content = "\n".join(lines) + response = HttpResponse(content, content_type='text/plain; charset=utf-8') + response['Content-Disposition'] = f'attachment; filename="leave-application-{leave_app.id}.txt"' + return response + +class LeaveApplicationWithdrawView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request, pk): + leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) + if leave_app.employee != request.user.extrainfo: + return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) + if leave_app.approval_status not in ['PENDING', 'FORWARDED']: + return Response({'error': 'Only pending or forwarded requests can be withdrawn.'}, status=status.HTTP_400_BAD_REQUEST) + + is_registrar = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='registrar', + ).exists() + is_accountant = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='accountant', + ).exists() + is_hr_admin = HoldsDesignation.objects.filter( + working=request.user, + designation__name__iregex=r'hr admin|hr administrator', + ).exists() + + if is_registrar or is_accountant or is_hr_admin: + leave_app.approval_status = 'REJECTED' + if is_registrar: + leave_app.current_approver_role = 'Registrar' + elif is_accountant: + leave_app.current_approver_role = 'Accountant' + else: + leave_app.current_approver_role = 'HR Admin' + else: + leave_app.approval_status = 'WITHDRAWN' + leave_app.current_approver_role = 'Employee' + leave_app.remarks = (request.data.get('remarks') or '').strip() + leave_app.save(update_fields=['approval_status', 'current_approver_role', 'remarks']) + serializer = LeaveApplicationSerializer(leave_app) + return Response(serializer.data) + +class LeaveApplicationCancelRequestView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request, pk): + leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) + if leave_app.employee != request.user.extrainfo: + return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) + if leave_app.approval_status != 'APPROVED': + return Response({'error': 'Only approved requests can be cancelled.'}, status=status.HTTP_400_BAD_REQUEST) + if leave_app.cancel_status != 'NOT_REQUESTED': + return Response({'error': 'Cancellation already processed or pending.'}, status=status.HTTP_400_BAD_REQUEST) + + today = timezone.now().date() + if today >= leave_app.start_date: + return Response( + {'error': 'Cancellation allowed only up to 1 day prior to start date.'}, + status=status.HTTP_400_BAD_REQUEST, + ) + + is_director = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='director', + ).exists() + is_hod = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='hod', + ).exists() + is_registrar = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='registrar', + ).exists() + is_accountant = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='accountant', + ).exists() + is_hr_admin = HoldsDesignation.objects.filter( + working=request.user, + designation__name__iregex=r'hr admin|hr administrator', + ).exists() + + requester_role = 'Employee' + if is_director: + requester_role = 'Director' + elif is_hod: + requester_role = 'HOD' + elif is_registrar: + requester_role = 'Registrar' + elif is_accountant: + requester_role = 'Accountant' + elif is_hr_admin: + requester_role = 'HR Admin' + + cancel_approver_role = 'HOD' + if requester_role in ['HOD', 'Director', 'Registrar']: + cancel_approver_role = 'Director' + elif requester_role in ['Accountant', 'HR Admin']: + cancel_approver_role = 'Registrar' + + leave_app.cancel_status = 'REQUESTED' + leave_app.cancel_requested_at = timezone.now() + leave_app.cancel_requested_by_role = requester_role + leave_app.cancel_current_approver_role = cancel_approver_role + leave_app.cancel_reason = (request.data.get('reason') or '').strip() + leave_app.save(update_fields=[ + 'cancel_status', + 'cancel_requested_at', + 'cancel_requested_by_role', + 'cancel_current_approver_role', + 'cancel_reason', + ]) + serializer = LeaveApplicationSerializer(leave_app) + return Response(serializer.data) + +class LeaveApplicationCancelDecisionView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request, pk, decision): + leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) + decision = (decision or '').lower() + if decision not in ['approve', 'reject']: + return Response({'error': 'Invalid decision'}, status=status.HTTP_400_BAD_REQUEST) + if leave_app.cancel_status != 'REQUESTED': + return Response({'error': 'No cancellation request pending.'}, status=status.HTTP_400_BAD_REQUEST) + + approver_role = (leave_app.cancel_current_approver_role or '').lower() + if approver_role == 'hod': + allowed = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='hod', + ).exists() + elif approver_role == 'director': + allowed = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='director', + ).exists() + elif approver_role == 'registrar': + allowed = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='registrar', + ).exists() + else: + allowed = False + + if not allowed: + return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) + + remarks = (request.data.get('remarks') or '').strip() + leave_app.cancel_decided_at = timezone.now() + leave_app.cancel_decision_remarks = remarks + + if decision == 'approve': + leave_app.cancel_status = 'APPROVED' + leave_app.approval_status = 'CANCELLED' + leave_app.current_approver_role = leave_app.cancel_current_approver_role + _restore_leave_balance_for_cancellation(leave_app) + else: + leave_app.cancel_status = 'REJECTED' + + leave_app.save(update_fields=[ + 'cancel_status', + 'cancel_decided_at', + 'cancel_decision_remarks', + 'approval_status', + 'current_approver_role', + 'leave_balance_before', + 'leave_balance_after', + ]) + serializer = LeaveApplicationSerializer(leave_app) + return Response(serializer.data) + +class LeaveApplicationExtensionRequestView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request, pk): + leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) + if leave_app.employee != request.user.extrainfo: + return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) + if leave_app.approval_status != 'APPROVED': + return Response({'error': 'Only approved requests can be extended.'}, status=status.HTTP_400_BAD_REQUEST) + if leave_app.extension_status != 'NOT_REQUESTED': + return Response({'error': 'Extension already processed or pending.'}, status=status.HTTP_400_BAD_REQUEST) + + today = timezone.now().date() + if today >= leave_app.end_date: + return Response({'error': 'Extension allowed only before the original end date.'}, status=status.HTTP_400_BAD_REQUEST) + + new_end_date_raw = request.data.get('new_end_date') + if not new_end_date_raw: + return Response({'error': 'New end date is required.'}, status=status.HTTP_400_BAD_REQUEST) + try: + new_end_date = datetime.datetime.strptime(new_end_date_raw, '%Y-%m-%d').date() + except ValueError: + return Response({'error': 'New end date must be in YYYY-MM-DD format.'}, status=status.HTTP_400_BAD_REQUEST) + if new_end_date <= leave_app.end_date: + return Response({'error': 'New end date must be after the current end date.'}, status=status.HTTP_400_BAD_REQUEST) + + new_total_days = Decimal((new_end_date - leave_app.start_date).days + 1) + + is_director = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='director', + ).exists() + is_hod = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='hod', + ).exists() + is_registrar = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='registrar', + ).exists() + is_accountant = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='accountant', + ).exists() + is_hr_admin = HoldsDesignation.objects.filter( + working=request.user, + designation__name__iregex=r'hr admin|hr administrator', + ).exists() + + requester_role = 'Employee' + if is_director: + requester_role = 'Director' + elif is_hod: + requester_role = 'HOD' + elif is_registrar: + requester_role = 'Registrar' + elif is_accountant: + requester_role = 'Accountant' + elif is_hr_admin: + requester_role = 'HR Admin' + + approver_role = 'HOD' + if requester_role in ['HOD', 'Director', 'Registrar']: + approver_role = 'Director' + elif requester_role in ['Accountant', 'HR Admin']: + approver_role = 'Registrar' + + leave_app.extension_status = 'REQUESTED' + leave_app.extension_requested_at = timezone.now() + leave_app.extension_requested_by_role = requester_role + leave_app.extension_current_approver_role = approver_role + leave_app.extension_reason = (request.data.get('reason') or '').strip() + leave_app.extension_new_end_date = new_end_date + leave_app.extension_new_total_days = new_total_days + leave_app.save(update_fields=[ + 'extension_status', + 'extension_requested_at', + 'extension_requested_by_role', + 'extension_current_approver_role', + 'extension_reason', + 'extension_new_end_date', + 'extension_new_total_days', + ]) + serializer = LeaveApplicationSerializer(leave_app) + return Response(serializer.data) + +class LeaveApplicationExtensionDecisionView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request, pk, decision): + leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) + decision = (decision or '').lower() + if decision not in ['approve', 'reject']: + return Response({'error': 'Invalid decision'}, status=status.HTTP_400_BAD_REQUEST) + if leave_app.extension_status != 'REQUESTED': + return Response({'error': 'No extension request pending.'}, status=status.HTTP_400_BAD_REQUEST) + + approver_role = (leave_app.extension_current_approver_role or '').lower() + if approver_role == 'hod': + allowed = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='hod', + ).exists() + elif approver_role == 'director': + allowed = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='director', + ).exists() + elif approver_role == 'registrar': + allowed = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='registrar', + ).exists() + else: + allowed = False + + if not allowed: + return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) + + remarks = (request.data.get('remarks') or '').strip() + leave_app.extension_decided_at = timezone.now() + leave_app.extension_decision_remarks = remarks + + if decision == 'approve': + if not _apply_leave_balance_for_extension(leave_app): + return Response({'error': 'Insufficient leave balance for extension.'}, status=status.HTTP_400_BAD_REQUEST) + leave_app.extension_status = 'APPROVED' + leave_app.current_approver_role = leave_app.extension_current_approver_role + leave_app.end_date = leave_app.extension_new_end_date + leave_app.total_days = leave_app.extension_new_total_days + else: + leave_app.extension_status = 'REJECTED' + + leave_app.save(update_fields=[ + 'extension_status', + 'extension_decided_at', + 'extension_decision_remarks', + 'current_approver_role', + 'leave_balance_before', + 'leave_balance_after', + 'end_date', + 'total_days', + ]) + serializer = LeaveApplicationSerializer(leave_app) + return Response(serializer.data) + +class LeaveResumptionSubmitView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request, pk): + leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) + if leave_app.employee != request.user.extrainfo: + return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) + if leave_app.approval_status != 'APPROVED': + return Response({'error': 'Resumption allowed only for approved leaves.'}, status=status.HTTP_400_BAD_REQUEST) + if leave_app.resumption_status != 'NOT_REQUESTED': + return Response({'error': 'Resumption already submitted or processed.'}, status=status.HTTP_400_BAD_REQUEST) + + today = timezone.now().date() + resumption_date_raw = (request.data.get('resumption_date') or '').strip() + if resumption_date_raw: + try: + resumption_date = datetime.datetime.strptime(resumption_date_raw, '%Y-%m-%d').date() + except ValueError: + return Response({'error': 'Resumption date must be in YYYY-MM-DD format.'}, status=status.HTTP_400_BAD_REQUEST) + else: + resumption_date = today + + if resumption_date <= leave_app.end_date: + return Response({'error': 'Resumption date must be after the leave end date.'}, status=status.HTTP_400_BAD_REQUEST) + + leave_app.resumption_status = 'SUBMITTED' + leave_app.resumption_date = resumption_date + leave_app.resumption_reason = (request.data.get('reason') or '').strip() + leave_app.resumption_submitted_at = timezone.now() + leave_app.resumption_current_approver_role = 'HOD' + leave_app.save(update_fields=[ + 'resumption_status', + 'resumption_date', + 'resumption_reason', + 'resumption_submitted_at', + 'resumption_current_approver_role', + ]) + serializer = LeaveApplicationSerializer(leave_app) + return Response(serializer.data) + +class LeaveResumptionDecisionView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request, pk, decision): + leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) + decision = (decision or '').lower() + if decision not in ['approve', 'reject']: + return Response({'error': 'Invalid decision'}, status=status.HTTP_400_BAD_REQUEST) + if leave_app.resumption_status != 'SUBMITTED': + return Response({'error': 'No resumption request pending.'}, status=status.HTTP_400_BAD_REQUEST) + + allowed = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='hod', + ).exists() + if not allowed: + return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) + + leave_app.resumption_decided_at = timezone.now() + leave_app.resumption_decision_remarks = (request.data.get('remarks') or '').strip() + if decision == 'approve': + leave_app.resumption_status = 'APPROVED' + leave_app.current_approver_role = 'HOD' + else: + leave_app.resumption_status = 'REJECTED' + + leave_app.save(update_fields=[ + 'resumption_status', + 'resumption_decided_at', + 'resumption_decision_remarks', + 'current_approver_role', + ]) + serializer = LeaveApplicationSerializer(leave_app) + return Response(serializer.data) + +class LeaveBalanceView(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request, employee_id=None): + if employee_id: + employee = get_object_or_404(ExtraInfo, id=employee_id) + else: + employee = request.user.extrainfo + balances_qs = ( + EmployeeLeaveBalance.objects.filter(employee=employee) + .select_related('leave_type') + .order_by('leave_type_id', '-year', '-id') + ) + # Collect the latest balance per leave type without relying on DISTINCT ON. + balances = [] + seen_leave_types = set() + for balance in balances_qs: + if balance.leave_type_id in seen_leave_types: + continue + seen_leave_types.add(balance.leave_type_id) + balances.append(balance) + serializer = LeaveBalanceSerializer(balances, many=True) + return Response(serializer.data) + +class LeaveNomineeDashboardView(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request): + employee = request.user.extrainfo + leaves = LeaveApplicationNew.objects.filter( + handover_to=employee.id, + nominee_status='PENDING', + ).order_by('-applied_date') + serializer = LeaveApplicationSerializer(leaves, many=True) + return Response(serializer.data) + +class LeaveNomineeDecisionView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request, pk): + action = (request.data.get('action') or '').lower() + if action not in ['accept', 'decline']: + return Response({'error': 'Invalid action'}, status=status.HTTP_400_BAD_REQUEST) + + leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) + employee = request.user.extrainfo + if leave_app.handover_to != employee.id: + return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) + + leave_app.nominee_status = 'ACCEPTED' if action == 'accept' else 'DECLINED' + leave_app.nominee_responded_at = datetime.datetime.utcnow() + leave_app.save(update_fields=['nominee_status', 'nominee_responded_at']) + serializer = LeaveApplicationSerializer(leave_app) + return Response(serializer.data) + +class LeaveDocumentRequestView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request, pk): + message = (request.data.get('message') or '').strip() + if not message: + return Response({'error': 'Document request message is required.'}, status=status.HTTP_400_BAD_REQUEST) + + is_hod = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='hod', + ).exists() + if not is_hod: + return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) + + leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) + if leave_app.document_request_status == 'REQUESTED': + return Response({'error': 'Document already requested.'}, status=status.HTTP_400_BAD_REQUEST) + leave_app.document_request_message = message + leave_app.document_request_status = 'REQUESTED' + leave_app.document_requested_at = datetime.datetime.utcnow() + leave_app.save(update_fields=['document_request_message', 'document_request_status', 'document_requested_at']) + serializer = LeaveApplicationSerializer(leave_app) + return Response(serializer.data) + +class LeaveDocumentSubmitView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request, pk): + submission = (request.data.get('submission') or '').strip() + if not submission: + return Response({'error': 'Document submission is required.'}, status=status.HTTP_400_BAD_REQUEST) + + leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) + if leave_app.employee != request.user.extrainfo: + return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) + if leave_app.document_request_status != 'REQUESTED': + return Response({'error': 'No document requested for this leave.'}, status=status.HTTP_400_BAD_REQUEST) + + leave_app.document_submission = submission + leave_app.document_request_status = 'SUBMITTED' + leave_app.document_submitted_at = datetime.datetime.utcnow() + leave_app.save(update_fields=['document_submission', 'document_request_status', 'document_submitted_at']) + serializer = LeaveApplicationSerializer(leave_app) + return Response(serializer.data) + +class LeaveResponsibilityView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request, pk, responsibility_type): + leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) + action = request.data.get('action') + remarks = request.data.get('remarks', '') + try: + if responsibility_type == 'academic': + leave_app = handle_academic_responsibility(leave_app, request.user.extrainfo, action, remarks) + else: + leave_app = handle_administrative_responsibility(leave_app, request.user.extrainfo, action, remarks) + serializer = LeaveApplicationSerializer(leave_app) + return Response(serializer.data) + except (PermissionError, InvalidWorkflowTransitionError) as e: + return Response({'error': str(e)}, status=status.HTTP_403_FORBIDDEN) + +class LeaveApproveRejectView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request, pk, decision): + leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) + remarks = request.data.get('remarks', '') + decision = (decision or '').lower() + if decision not in ['approve', 'reject', 'forward']: + return Response({'error': 'Invalid decision'}, status=status.HTTP_400_BAD_REQUEST) + + is_registrar = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='registrar', + ).exists() + is_director = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='director', + ).exists() + approver_role = 'HOD' + if is_registrar: + approver_role = 'Registrar' + elif is_director: + approver_role = 'Director' + + leave_type_name = (leave_app.leave_type or '').strip() + is_cl_rh_leave = leave_type_name in ['Casual', 'Restricted'] + if decision == 'approve' and not is_cl_rh_leave and approver_role == 'HOD': + return Response( + {'error': 'Only CL/RH leaves can be approved by HOD. Please forward to Director.'}, + status=status.HTTP_400_BAD_REQUEST, + ) + if decision == 'forward' and is_cl_rh_leave: + decision = 'approve' + + if decision == 'approve': + leave_app.approval_status = 'APPROVED' + leave_app.current_approver_role = approver_role + _apply_leave_balance_for_approval(leave_app) + elif decision == 'forward': + leave_app.approval_status = 'FORWARDED' + leave_app.current_approver_role = 'Director' + else: + leave_app.approval_status = 'REJECTED' + leave_app.current_approver_role = approver_role + + leave_app.remarks = remarks + leave_app.save(update_fields=[ + 'approval_status', + 'remarks', + 'current_approver_role', + 'leave_balance_before', + 'leave_balance_after', + ]) + + serializer = LeaveApplicationSerializer(leave_app) + return Response(serializer.data) + + +def _apply_leave_balance_for_approval(leave_app): + leave_type = LeaveType.objects.filter(name__iexact=leave_app.leave_type).first() + if not leave_type: + return + year = leave_app.start_date.year + balance = EmployeeLeaveBalance.objects.filter( + employee=leave_app.employee, + leave_type=leave_type, + year=year, + ).first() + if balance is None: + balance = EmployeeLeaveBalance.objects.filter( + employee=leave_app.employee, + leave_type=leave_type, + ).order_by('-year').first() + if balance is None or balance.year != year: + balance = EmployeeLeaveBalance.objects.create( + employee=leave_app.employee, + leave_type=leave_type, + year=year, + opening_balance=Decimal('0'), + accrued=Decimal('0'), + availed=Decimal('0'), + current_balance=Decimal('0'), + ) + total_days = Decimal(str(leave_app.total_days or 0)) + before_balance = balance.current_balance + balance.availed = (balance.availed or 0) + total_days + balance.current_balance = (balance.current_balance or 0) - total_days + balance.save(update_fields=['availed', 'current_balance']) + + if leave_app.leave_balance_before is None: + leave_app.leave_balance_before = before_balance + leave_app.leave_balance_after = balance.current_balance + +def _restore_leave_balance_for_cancellation(leave_app): + leave_type = LeaveType.objects.filter(name__iexact=leave_app.leave_type).first() + if not leave_type: + return + year = leave_app.start_date.year + balance = EmployeeLeaveBalance.objects.filter( + employee=leave_app.employee, + leave_type=leave_type, + year=year, + ).first() + if balance is None: + balance = EmployeeLeaveBalance.objects.filter( + employee=leave_app.employee, + leave_type=leave_type, + ).order_by('-year').first() + if balance is None: + return + + total_days = Decimal(str(leave_app.total_days or 0)) + before_balance = balance.current_balance + balance.availed = (balance.availed or 0) - total_days + balance.current_balance = (balance.current_balance or 0) + total_days + balance.save(update_fields=['availed', 'current_balance']) + + if leave_app.leave_balance_before is None: + leave_app.leave_balance_before = before_balance + leave_app.leave_balance_after = balance.current_balance + +def _apply_leave_balance_for_extension(leave_app): + if not leave_app.extension_new_total_days: + return False + delta_days = Decimal(str(leave_app.extension_new_total_days)) - Decimal(str(leave_app.total_days or 0)) + if delta_days <= 0: + return False + + leave_type = LeaveType.objects.filter(name__iexact=leave_app.leave_type).first() + if not leave_type: + return False + year = leave_app.start_date.year + balance = EmployeeLeaveBalance.objects.filter( + employee=leave_app.employee, + leave_type=leave_type, + year=year, + ).first() + if balance is None: + balance = EmployeeLeaveBalance.objects.filter( + employee=leave_app.employee, + leave_type=leave_type, + ).order_by('-year').first() + if balance is None: + return False + + if (balance.current_balance or 0) < delta_days: + return False + + before_balance = balance.current_balance + balance.availed = (balance.availed or 0) + delta_days + balance.current_balance = (balance.current_balance or 0) - delta_days + balance.save(update_fields=['availed', 'current_balance']) + + if leave_app.leave_balance_before is None: + leave_app.leave_balance_before = before_balance + leave_app.leave_balance_after = balance.current_balance + return True + +# ==================== ATTENDANCE VIEWS ==================== + +class AttendanceView(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request): + from_date = request.query_params.get('from_date') + to_date = request.query_params.get('to_date') + attendance = get_attendance_for_employee(request.user.extrainfo, from_date, to_date) + serializer = EmployeeAttendanceSerializer(attendance, many=True) + return Response(serializer.data) + + def post(self, request): + attendance = mark_attendance( + employee_extra_info=request.user.extrainfo, + date=request.data.get('date'), + status=request.data.get('status'), + in_time=request.data.get('in_time'), + out_time=request.data.get('out_time'), + remarks=request.data.get('remarks', '') + ) + serializer = EmployeeAttendanceSerializer(attendance) + return Response(serializer.data, status=status.HTTP_201_CREATED) + +# ==================== APPRAISAL VIEWS ==================== + +class AppraisalPeriodListView(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request): + is_active = request.query_params.get('is_active') + periods = get_appraisal_periods(is_active) + serializer = AppraisalPeriodSerializer(periods, many=True) + return Response(serializer.data) + +class AppraisalListView(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request): + period_id = request.query_params.get('period') + appraisals = get_appraisals_for_employee(request.user.extrainfo, period_id) + serializer = PerformanceAppraisalSerializer(appraisals, many=True) + return Response(serializer.data) + + def post(self, request): + serializer = PerformanceAppraisalSerializer(data=request.data) + if serializer.is_valid(): + serializer.save(employee=request.user.extrainfo) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + +# ==================== TRAINING VIEWS ==================== + +class TrainingProgramListView(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request): + programs = get_available_training_programs() + serializer = TrainingProgramSerializer(programs, many=True) + return Response(serializer.data) + +class TrainingNominationView(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request): + nominations = get_nominations_for_employee(request.user.extrainfo) + serializer = TrainingNominationSerializer(nominations, many=True) + return Response(serializer.data) + + def post(self, request): + serializer = TrainingNominationSerializer(data=request.data) + if serializer.is_valid(): + serializer.save(employee=request.user.extrainfo, nominated_by=request.user.extrainfo) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + +# ==================== PROMOTION VIEWS ==================== + +class PromotionApplicationView(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request): + applications = get_promotion_applications(request.user.extrainfo) + serializer = PromotionApplicationSerializer(applications, many=True) + return Response(serializer.data) + + def post(self, request): + serializer = PromotionApplicationSerializer(data=request.data) + if serializer.is_valid(): + serializer.save(employee=request.user.extrainfo) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + +# ==================== FACULTY WORKLOAD VIEWS ==================== + +class FacultyWorkloadView(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request): + semester = request.query_params.get('semester') + year = request.query_params.get('year') + workloads = get_faculty_workload(request.user.extrainfo, semester, year) + serializer = FacultyWorkloadSerializer(workloads, many=True) + return Response(serializer.data) + + def post(self, request): + workload = calculate_faculty_workload( + request.user.extra_info, + request.data.get('semester'), + request.data.get('year') + ) + serializer = FacultyWorkloadSerializer(workload) + return Response(serializer.data) + +from ..models import LTCApplicationNew, CPDAAdvanceNew, CPDAReimbursementNew, AppraisalFormNew +from ..services import apply_ltc, approve_ltc, reject_ltc, apply_cpda_advance, approve_cpda_advance, reject_cpda_advance, apply_cpda_reimbursement, approve_cpda_reimbursement, reject_cpda_reimbursement, submit_appraisal, review_appraisal +from ..selectors import get_ltc_applications, get_cpda_advances, get_cpda_reimbursements, get_appraisal_forms +from .serializers import LTCApplicationSerializer, CPDAAdvanceSerializer, CPDAReimbursementSerializer, AppraisalFormSerializer + +# ==================== LTC VIEWS ==================== + +class LTCApplicationListCreateView(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request): + is_hr_staff = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='hr', + ).exists() or ( + request.user.extrainfo.user_type == 'staff' + and request.user.extrainfo.department + and request.user.extrainfo.department.name == 'HR' + ) + is_accountant = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='accountant', + ).exists() + + if is_hr_staff: + ltcs = LTCApplicationNew.objects.filter(approval_status__in=['PENDING', 'FORWARDED']) + elif is_accountant: + ltcs = LTCApplicationNew.objects.filter( + approval_status='FORWARDED', + accountant_status__iexact='PENDING', + ) + else: + ltcs = get_ltc_applications(request.user.extrainfo) + serializer = LTCApplicationSerializer(ltcs, many=True) + return Response(serializer.data) + + def post(self, request): + serializer = LTCApplicationSerializer(data=request.data) + if serializer.is_valid(): + serializer.save(employee=request.user.extrainfo) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + +class LTCApplicationDetailView(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request, pk): + ltc = get_object_or_404(LTCApplicationNew, pk=pk) + serializer = LTCApplicationSerializer(ltc) + return Response(serializer.data) + + def put(self, request, pk): + ltc = get_object_or_404(LTCApplicationNew, pk=pk) + if ltc.employee != request.user.extrainfo: + return Response({'error': 'Not authorized'}, status=403) + serializer = LTCApplicationSerializer(ltc, data=request.data, partial=True) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data) + return Response(serializer.errors, status=400) + +class LTCApplicationDownloadView(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request, pk): + ltc = get_object_or_404(LTCApplicationNew, pk=pk) + if ltc.employee != request.user.extrainfo and not request.user.is_staff: + return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) + + lines = [ + f"LTC Application #{ltc.id}", + "", + f"Employee: {ltc.employee_name}", + f"Employee ID: {ltc.employee.id}", + f"Department: {ltc.department}", + f"Designation: {ltc.designation}", + "", + f"Block Year: {ltc.ltc_block_year}", + f"Travel Start: {ltc.travel_start_date}", + f"Travel End: {ltc.travel_end_date}", + f"Destination: {ltc.destination}", + f"Purpose: {ltc.purpose_of_travel}", + "", + f"Approval Status: {ltc.approval_status}", + f"Applied Date: {ltc.applied_date}", + ] + + content = "\n".join(lines) + response = HttpResponse(content, content_type='text/plain; charset=utf-8') + response['Content-Disposition'] = f'attachment; filename="ltc-application-{ltc.id}.txt"' + return response + +class LTCApplicationWithdrawView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request, pk): + ltc = get_object_or_404(LTCApplicationNew, pk=pk) + if ltc.employee != request.user.extrainfo: + return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) + if ltc.approval_status != 'PENDING': + return Response({'error': 'Only pending requests can be withdrawn.'}, status=status.HTTP_400_BAD_REQUEST) + + ltc.approval_status = 'WITHDRAWN' + ltc.remarks = (request.data.get('remarks') or '').strip() + ltc.save(update_fields=['approval_status', 'remarks']) + serializer = LTCApplicationSerializer(ltc) + return Response(serializer.data) + +class LTCApproveRejectView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request, pk, decision): + ltc = get_object_or_404(LTCApplicationNew, pk=pk) + remarks = request.data.get('remarks', '') + decision = (decision or '').lower() + if decision not in ['approve', 'reject', 'forward']: + return Response({'error': 'Invalid decision'}, status=status.HTTP_400_BAD_REQUEST) + + if decision == 'approve': + ltc.approval_status = 'APPROVED' + ltc.accountant_status = 'APPROVED' + elif decision == 'forward': + ltc.approval_status = 'FORWARDED' + ltc.verified_by_hr = True + ltc.accountant_status = 'PENDING' + else: + ltc.approval_status = 'REJECTED' + ltc.accountant_status = 'REJECTED' + + ltc.remarks = remarks + ltc.save(update_fields=['approval_status', 'remarks', 'verified_by_hr', 'accountant_status']) + + serializer = LTCApplicationSerializer(ltc) + return Response(serializer.data) + +# ==================== CPDA ADVANCE VIEWS ==================== + +class CPDAAdvanceListCreateView(APIView): + permission_classes = [IsAuthenticated] + def get(self, request): + is_hr_staff = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='hr', + ).exists() or ( + request.user.extrainfo.user_type == 'staff' + and request.user.extrainfo.department + and request.user.extrainfo.department.name == 'HR' + ) + is_accountant = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='accountant', + ).exists() + is_director = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='director', + ).exists() + + if is_director: + advances = CPDAAdvanceNew.objects.filter( + approval_status='FORWARDED', + accountant_processing_status__iexact='DIRECTOR_REVIEW', + ) + elif is_hr_staff: + advances = CPDAAdvanceNew.objects.filter(approval_status='PENDING') + elif is_accountant: + advances = CPDAAdvanceNew.objects.filter( + approval_status='FORWARDED', + accountant_processing_status__in=['PENDING', 'DIRECTOR_APPROVED'], + ) + else: + advances = get_cpda_advances(request.user.extrainfo) + serializer = CPDAAdvanceSerializer(advances, many=True) + return Response(serializer.data) + def post(self, request): + serializer = CPDAAdvanceSerializer(data=request.data) + if serializer.is_valid(): + serializer.save(employee=request.user.extrainfo) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + +class CPDAAdvanceDetailView(APIView): + permission_classes = [IsAuthenticated] + def get(self, request, pk): + cpda = get_object_or_404(CPDAAdvanceNew, pk=pk) + serializer = CPDAAdvanceSerializer(cpda) + return Response(serializer.data) + +class CPDAAdvanceDownloadView(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request, pk): + cpda = get_object_or_404(CPDAAdvanceNew, pk=pk) + if cpda.employee != request.user.extrainfo and not request.user.is_staff: + return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) + + lines = [ + f"CPDA Advance #{cpda.id}", + "", + f"Employee: {cpda.employee_name}", + f"Employee ID: {cpda.employee.id}", + f"Department: {cpda.department}", + f"Designation: {cpda.designation}", + "", + f"Event Name: {cpda.event_name}", + f"Event Type: {cpda.event_type}", + f"Start Date: {cpda.start_date}", + f"End Date: {cpda.end_date}", + f"Total Amount: {cpda.total_amount}", + "", + f"Approval Status: {cpda.approval_status}", + f"Applied Date: {cpda.applied_date}", + ] + + content = "\n".join(lines) + response = HttpResponse(content, content_type='text/plain; charset=utf-8') + response['Content-Disposition'] = f'attachment; filename="cpda-advance-{cpda.id}.txt"' + return response + +class CPDAAdvanceWithdrawView(APIView): + permission_classes = [IsAuthenticated] + + def post(self, request, pk): + cpda = get_object_or_404(CPDAAdvanceNew, pk=pk) + if cpda.employee != request.user.extrainfo: + return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) + if cpda.approval_status != 'PENDING': + return Response({'error': 'Only pending requests can be withdrawn.'}, status=status.HTTP_400_BAD_REQUEST) + + cpda.approval_status = 'WITHDRAWN' + cpda.remarks = (request.data.get('remarks') or '').strip() + cpda.save(update_fields=['approval_status', 'remarks']) + serializer = CPDAAdvanceSerializer(cpda) + return Response(serializer.data) + +class CPDAAdvanceApproveRejectView(APIView): + permission_classes = [IsAuthenticated] + def post(self, request, pk, decision): + cpda = get_object_or_404(CPDAAdvanceNew, pk=pk) + remarks = request.data.get('remarks', '') + decision = (decision or '').lower() + if decision not in ['approve', 'reject', 'forward-accountant', 'forward-director']: + return Response({'error': 'Invalid decision'}, status=status.HTTP_400_BAD_REQUEST) + + if decision == 'forward-accountant': + cpda.approval_status = 'FORWARDED' + cpda.verified_by_hr = True + cpda.accountant_processing_status = 'PENDING' + elif decision == 'forward-director': + cpda.approval_status = 'FORWARDED' + cpda.accountant_processing_status = 'DIRECTOR_REVIEW' + elif decision == 'approve': + if cpda.accountant_processing_status == 'DIRECTOR_REVIEW': + cpda.accountant_processing_status = 'DIRECTOR_APPROVED' + cpda.approval_status = 'FORWARDED' + else: + cpda.approval_status = 'APPROVED' + cpda.accountant_processing_status = 'APPROVED' + else: + cpda.approval_status = 'REJECTED' + cpda.accountant_processing_status = 'REJECTED' + cpda.remarks = remarks + cpda.save(update_fields=['approval_status', 'remarks', 'verified_by_hr', 'accountant_processing_status']) + serializer = CPDAAdvanceSerializer(cpda) + return Response(serializer.data) + +# ==================== CPDA REIMBURSEMENT VIEWS ==================== + +class CPDAReimbursementListCreateView(APIView): + permission_classes = [IsAuthenticated] + def get(self, request): + reims = get_cpda_reimbursements(request.user.extrainfo) + serializer = CPDAReimbursementSerializer(reims, many=True) + return Response(serializer.data) + def post(self, request): + serializer = CPDAReimbursementSerializer(data=request.data) + if serializer.is_valid(): + serializer.save(employee=request.user.extrainfo) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + +class CPDAReimbursementDetailView(APIView): + permission_classes = [IsAuthenticated] + def get(self, request, pk): + reim = get_object_or_404(CPDAReimbursementNew, pk=pk) + serializer = CPDAReimbursementSerializer(reim) + return Response(serializer.data) + +class CPDAReimbursementApproveRejectView(APIView): + permission_classes = [IsAuthenticated] + def post(self, request, pk, decision): + reim = get_object_or_404(CPDAReimbursementNew, pk=pk) + remarks = request.data.get('remarks', '') + if decision == 'approve': + reim = approve_cpda_reimbursement(reim, request.user.extrainfo, remarks) + else: + reim = reject_cpda_reimbursement(reim, request.user.extrainfo, remarks) + serializer = CPDAReimbursementSerializer(reim) + return Response(serializer.data) + +# ==================== APPRAISAL FORM VIEWS ==================== + +class AppraisalFormListCreateView(APIView): + permission_classes = [IsAuthenticated] + def get(self, request): + is_hr_staff = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='hr', + ).exists() or ( + request.user.extrainfo.user_type == 'staff' + and request.user.extrainfo.department + and request.user.extrainfo.department.name == 'HR' + ) + is_hod = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='hod', + ).exists() + is_director = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='director', + ).exists() + + if is_hr_staff: + appraisals = AppraisalFormNew.objects.all() + elif is_director: + appraisals = AppraisalFormNew.objects.filter(status='REVIEWED') + elif is_hod: + appraisals = AppraisalFormNew.objects.filter( + department=request.user.extrainfo.department.name + ) + else: + appraisals = get_appraisal_forms(request.user.extrainfo) + serializer = AppraisalFormSerializer(appraisals, many=True) + return Response(serializer.data) + def post(self, request): + serializer = AppraisalFormSerializer(data=request.data) + if serializer.is_valid(): + serializer.save(employee=request.user.extrainfo) + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + +class AppraisalFormDetailView(APIView): + permission_classes = [IsAuthenticated] + def get(self, request, pk): + appraisal = get_object_or_404(AppraisalFormNew, pk=pk) + serializer = AppraisalFormSerializer(appraisal) + return Response(serializer.data) + +class AppraisalFormDownloadView(APIView): + permission_classes = [IsAuthenticated] + + def get(self, request, pk): + appraisal = get_object_or_404(AppraisalFormNew, pk=pk) + if appraisal.employee != request.user.extrainfo and not request.user.is_staff: + return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) + + lines = [ + f"Appraisal Form #{appraisal.id}", + "", + f"Employee: {appraisal.employee_name}", + f"Employee ID: {appraisal.employee.id}", + f"Department: {appraisal.department}", + f"Designation: {appraisal.designation}", + "", + f"Appraisal Year: {appraisal.appraisal_year}", + f"Status: {appraisal.status}", + f"Submitted At: {appraisal.submitted_at}", + ] + + content = "\n".join(lines) + response = HttpResponse(content, content_type='text/plain; charset=utf-8') + response['Content-Disposition'] = f'attachment; filename="appraisal-{appraisal.id}.txt"' + return response + +class AppraisalReviewView(APIView): + permission_classes = [IsAuthenticated] + def post(self, request, pk): + appraisal = get_object_or_404(AppraisalFormNew, pk=pk) + action = (request.data.get('action') or 'review').lower() + remarks = request.data.get('remarks', '') + rating = request.data.get('rating', '') + + appraisal.reviewer_id = str(request.user.extrainfo.id) + appraisal.reviewer_comments = remarks + if rating: + appraisal.rating = str(rating) + + if action == 'approve': + appraisal.status = 'APPROVED' + elif action == 'forward': + appraisal.status = 'REVIEWED' + else: + appraisal.status = 'REVIEWED' + + appraisal.save(update_fields=['reviewer_id', 'reviewer_comments', 'rating', 'status']) + serializer = AppraisalFormSerializer(appraisal) + return Response(serializer.data) + diff --git a/FusionIIIT/applications/hr2/apps.py b/FusionIIIT/applications/hr2/apps.py index 002c0de9d..a3848b069 100644 --- a/FusionIIIT/applications/hr2/apps.py +++ b/FusionIIIT/applications/hr2/apps.py @@ -1,5 +1,6 @@ from django.apps import AppConfig - class Hr2Config(AppConfig): name = 'applications.hr2' + label = 'hr2' + # For Django 3.1.5, no default_auto_field needed; it uses AutoField \ No newline at end of file diff --git a/FusionIIIT/applications/hr2/form_views.py b/FusionIIIT/applications/hr2/form_views.py deleted file mode 100644 index 3cc08fb93..000000000 --- a/FusionIIIT/applications/hr2/form_views.py +++ /dev/null @@ -1,323 +0,0 @@ -from .serializers import LTC_serializer, CPDAAdvance_serializer, Appraisal_serializer, CPDAReimbursement_serializer, Leave_serializer -from rest_framework.views import APIView -from rest_framework.response import Response -from rest_framework import status -from rest_framework.decorators import permission_classes, api_view -from rest_framework.permissions import IsAuthenticated, AllowAny -from .models import LTCform, CPDAAdvanceform, CPDAReimbursementform, Leaveform, Appraisalform -from django.contrib.auth import get_user_model -from django.core.exceptions import MultipleObjectsReturned -from applications.filetracking.sdk.methods import * -from applications.globals.models import Designation, HoldsDesignation - -class LTC(APIView): - serializer_class = LTC_serializer - permission_classes = (AllowAny, ) - def post(self, request): - if 'Mobile-OS' in request.META: - user_info = request.data[0] - serializer = self.serializer_class(data = request.data[1]) - if serializer.is_valid(): - serializer.save() - file_id = create_file(uploader = user_info['uploader_name'], uploader_designation = user_info['uploader_designation'], receiver = "21BCS140", receiver_designation="hradmin", src_module="HR", src_object_id= str(serializer.data['id']), file_extra_JSON= {"type": "LTC"}, attached_file= None) - return Response(serializer.data, status= status.HTTP_200_OK) - else: - return Response(serializer.errors, status= status.HTTP_400_BAD_REQUEST) - id = request.query_params.get("id") - try: - employee = ExtraInfo.objects.get(user__id=id) - except: - raise Http404("Post does not exist! id doesnt exist") - - print(employee.user_type) - - - if(employee.user_type == 'faculty'): - template = 'hr2Module/ltc_form.html' - - if request.method == "POST": - family_mem_a = request.POST.get('id_family_mem_a', '') - family_mem_b = request.POST.get('id_family_mem_b', '') - family_mem_c = request.POST.get('id_family_mem_c', '') - - - detailsOfFamilyMembers = ', '.join(filter(None, [family_mem_a, family_mem_b, family_mem_c])) - - - request.POST = request.POST.copy() - request.POST['detailsOfFamilyMembersAlreadyDone'] = detailsOfFamilyMembers - - - family_members = [] - for i in range(1, 7): # Loop through input fields for each family member - name = request.POST.get(f'info_{i}_2', '') # Get the name - age = request.POST.get(f'info_{i}_3', '') # Get the age - if name and age: # Check if both name and age are provided - family_members.append(f"{name} ({age} years)") # Concatenate name and age - - family_members_str = ', '.join(family_members) - - # Populate the form with concatenated family member details - request.POST['familyMembersAboutToAvail'] = family_members_str - - dependents = [] - for i in range(1, 7): # Loop through input fields for each dependent - name = request.POST.get(f'd_info_{i}_2', '') # Get the name - age = request.POST.get(f'd_info_{i}_3', '') # Get the age - why_dependent = request.POST.get(f'd_info_{i}_4', '') # Get the reason for dependency - if name and age: # Check if both name and age are provided - dependents.append(f"{name} ({age} years), {why_dependent}") # Concatenate name, age, and reason - - - # Concatenate all dependent strings into a single string - dependents_str = ', '.join(dependents) - - # Populate the form with concatenated dependent details - request.POST['detailsOfDependents'] = dependents_str - - # print("first",request.POST['familyMembersAboutToAvail']) - pf_no = int(request.POST.get('pf_no')) if request.POST.get('pf_no') else None - basicPay = int(request.POST.get('basicPay')) if request.POST.get('basicPay') else None - amountOfAdvanceRequired = int(request.POST.get('amountOfAdvanceRequired')) if request.POST.get('amountOfAdvanceRequired') else None - phoneNumberForContact = int(request.POST.get('phoneNumberForContact')) if request.POST.get('phoneNumberForContact') else None - - - try: - ltc_request = LTCform.objects.create( - employee_id = id, - detailsOfFamilyMembersAlreadyDone=request.POST.get('detailsOfFamilyMembersAlreadyDone', ''), - familyMembersAboutToAvail=request.POST.get('familyMembersAboutToAvail', ''), - detailsOfDependents=request.POST.get('detailsOfDependents', ''), - name=request.POST.get('name', ''), - blockYear=request.POST.get('blockYear', ''), - pf_no=request.POST.get('pf_no', ''), - basicPay=request.POST.get('basicPay', ''), - designation=request.POST.get('designation', ''), - departmentInfo=request.POST.get('departmentInfo', ''), - leaveAvailability=request.POST.get('leaveAvailability', ''), - leaveStartDate=request.POST.get('leaveStartDate', ''), - leaveEndDate=request.POST.get('leaveEndDate', ''), - dateOfLeaveForFamily=request.POST.get('dateOfLeaveForFamily', ''), - natureOfLeave=request.POST.get('natureOfLeave', ''), - purposeOfLeave=request.POST.get('purposeOfLeave', ''), - hometownOrNot=request.POST.get('hometownOrNot', ''), - placeOfVisit=request.POST.get('placeOfVisit', ''), - addressDuringLeave=request.POST.get('addressDuringLeave', ''), - modeForVacation=request.POST.get('modeForVacation', ''), - detailsOfFamilyMembers=request.POST.get('detailsOfFamilyMembers', ''), - amountOfAdvanceRequired=request.POST.get('amountOfAdvanceRequired', ''), - certifiedFamilyDependents=request.POST.get('certifiedFamilyDependents', ''), - certifiedAdvance =request.POST.get('certifiedAdvance ', ''), - adjustedMonth=request.POST.get('adjustedMonth', ''), - date=request.POST.get('date', ''), - phoneNumberForContact=request.POST.get('phoneNumberForContact', '') - ) - print("done") - messages.success(request, "Ltc form filled successfully") - except Exception as e: - print("error" , e) - messages.warning(request, "Fill not correctly") - context = {'employee': employee} - return render(request, template, context) - - - # Query all LTC requests - ltc_requests = LTCform.objects.filter(employee_id=id) - - context = {'employee': employee, 'ltc_requests': ltc_requests} - - return render(request, template, context) - else: - return render(request, 'hr2Module/edit.html') - - - - def get(self, request, *args, **kwargs): - pk = request.query_params.get("name") - print(pk) - try: - forms = LTCform.objects.get(name = pk) - serializer = self.serializer_class(forms, many = False) - except MultipleObjectsReturned: - forms = LTCform.objects.filter(name = pk) - serializer = self.serializer_class(forms, many = True) - return Response(serializer.data, status = status.HTTP_200_OK) - - def put(self, request, *args, **kwargs): - pk = request.query_params.get("id") - print(pk) - form = LTCform.objects.get(id = pk) - print(form) - serializer = self.serializer_class(form, data = request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status = status.HTTP_200_OK) - else: - return Response(serializer.errors, status = status.HTTP_400_BAD_REQUEST) - -class FormManagement(APIView): - permission_classes = (AllowAny, ) - def get(self, request, *args, **kwargs): - username = request.query_params.get("username") - designation = request.query_params.get("designation") - inbox = view_inbox(username = username, designation = designation, src_module = "HR") - return Response(inbox, status = status.HTTP_200_OK) - - def post(self, request, *args, **kwargs): - username = request.data['receiver'] - receiver_value = User.objects.get(username=username) - receiver_value_designation= HoldsDesignation.objects.filter(user=receiver_value) - lis = list(receiver_value_designation) - obj=lis[0].designation - forward_file(file_id = request.data['file_id'], receiver = request.data['receiver'], receiver_designation = obj.name, remarks = request.data['remarks'], file_extra_JSON = request.data['file_extra_JSON']) - return Response(status = status.HTTP_200_OK) - - -class CPDAAdvance(APIView): - serializer_class = CPDAAdvance_serializer - permission_classes = (AllowAny, ) - def post(self, request): - user_info = request.data[0] - serializer = self.serializer_class(data = request.data[1]) - if serializer.is_valid(): - serializer.save() - file_id = create_file(uploader = user_info['uploader_name'], uploader_designation = user_info['uploader_designation'], receiver = "vkjain", receiver_designation="CSE HOD", src_module="HR", src_object_id= str(serializer.data['id']), file_extra_JSON= {"type": "CPDAAdvance"}, attached_file= None) - return Response(serializer.data, status= status.HTTP_200_OK) - else: - return Response(serializer.errors, status= status.HTTP_400_BAD_REQUEST) - - def get(self, request, *args, **kwargs): - pk = request.query_params.get("name") - print(pk) - try: - forms = CPDAAdvanceform.objects.get(name = pk) - serializer = self.serializer_class(forms, many = False) - except MultipleObjectsReturned: - forms = CPDAAdvanceform.objects.filter(name = pk) - serializer = self.serializer_class(forms, many = True) - return Response(serializer.data, status = status.HTTP_200_OK) - - def put(self, request, *args, **kwargs): - pk = request.query_params.get("id") - print(pk) - form = CPDAAdvanceform.objects.get(id = pk) - print(form) - serializer = self.serializer_class(form, data = request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status = status.HTTP_200_OK) - else: - return Response(serializer.errors, status = status.HTTP_400_BAD_REQUEST) - -class CPDAReimbursement(APIView): - serializer_class = CPDAReimbursement_serializer - permission_classes = (AllowAny, ) - def post(self, request): - user_info = request.data[0] - serializer = self.serializer_class(data = request.data[1]) - if serializer.is_valid(): - serializer.save() - file_id = create_file(uploader = user_info['uploader_name'], uploader_designation = user_info['uploader_designation'], receiver = "vkjain", receiver_designation="CSE HOD", src_module="HR", src_object_id= str(serializer.data['id']), file_extra_JSON= {"type": "CPDAReimbursement"}, attached_file= None) - return Response(serializer.data, status= status.HTTP_200_OK) - else: - return Response(serializer.errors, status= status.HTTP_400_BAD_REQUEST) - - def get(self, request, *args, **kwargs): - pk = request.query_params.get("name") - print(pk) - try: - forms = CPDAReimbursementform.objects.get(name = pk) - serializer = self.serializer_class(forms, many = False) - except MultipleObjectsReturned: - forms = CPDAReimbursementform.objects.filter(name = pk) - serializer = self.serializer_class(forms, many = True) - return Response(serializer.data, status = status.HTTP_200_OK) - - def put(self, request, *args, **kwargs): - pk = request.query_params.get("id") - print(pk) - form = CPDAReimbursementform.objects.get(id = pk) - print(form) - serializer = self.serializer_class(form, data = request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status = status.HTTP_200_OK) - else: - return Response(serializer.errors, status = status.HTTP_400_BAD_REQUEST) - -class Leave(APIView): - serializer_class = Leave_serializer - permission_classes = (AllowAny, ) - def post(self, request): - user_info = request.data[0] - serializer = self.serializer_class(data = request.data[1]) - if serializer.is_valid(): - serializer.save() - file_id = create_file(uploader = user_info['uploader_name'], uploader_designation = user_info['uploader_designation'], receiver = "vkjain", receiver_designation="CSE HOD", src_module="HR", src_object_id= str(serializer.data['id']), file_extra_JSON= {"type": "Leave"}, attached_file= None) - return Response(serializer.data, status= status.HTTP_200_OK) - else: - return Response(serializer.errors, status= status.HTTP_400_BAD_REQUEST) - - def get(self, request, *args, **kwargs): - pk = request.query_params.get("name") - print(pk) - try: - forms = Leaveform.objects.get(name = pk) - serializer = self.serializer_class(forms, many = False) - except MultipleObjectsReturned: - forms = Leaveform.objects.filter(name = pk) - serializer = self.serializer_class(forms, many = True) - return Response(serializer.data, status = status.HTTP_200_OK) - - def put(self, request, *args, **kwargs): - pk = request.query_params.get("id") - print(pk) - form = Leaveform.objects.get(id = pk) - print(form) - serializer = self.serializer_class(form, data = request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status = status.HTTP_200_OK) - else: - return Response(serializer.errors, status = status.HTTP_400_BAD_REQUEST) - -class Appraisal(APIView): - serializer_class = Appraisal_serializer - permission_classes = (AllowAny, ) - def post(self, request): - user_info = request.data[0] - serializer = self.serializer_class(data = request.data[1]) - if serializer.is_valid(): - serializer.save() - file_id = create_file(uploader = user_info['uploader_name'], uploader_designation = user_info['uploader_designation'], receiver = "vkjain", receiver_designation="CSE HOD", src_module="HR", src_object_id= str(serializer.data['id']), file_extra_JSON= {"type": "Appraisal"}, attached_file= None) - return Response(serializer.data, status= status.HTTP_200_OK) - else: - return Response(serializer.errors, status= status.HTTP_400_BAD_REQUEST) - - def get(self, request, *args, **kwargs): - pk = request.query_params.get("name") - print(pk) - try: - forms = Appraisalform.objects.get(name = pk) - serializer = self.serializer_class(forms, many = False) - except MultipleObjectsReturned: - forms = Appraisalform.objects.filter(name = pk) - serializer = self.serializer_class(forms, many = True) - return Response(serializer.data, status = status.HTTP_200_OK) - - def put(self, request, *args, **kwargs): - pk = request.query_params.get("id") - print(pk) - form = Appraisalform.objects.get(id = pk) - print(form) - serializer = self.serializer_class(form, data = request.data) - if serializer.is_valid(): - serializer.save() - return Response(serializer.data, status = status.HTTP_200_OK) - else: - return Response(serializer.errors, status = status.HTTP_400_BAD_REQUEST) - -class AssignerReviewer(APIView): - def post(self, request, *args, **kwargs): - forward_file(file_id = request.data['file_id'], receiver = "21BCS140", receiver_designation = 'hradmin', remarks = request.data['remarks'], file_extra_JSON = request.data['file_extra_JSON']) - return Response(status = status.HTTP_200_OK) \ No newline at end of file diff --git a/FusionIIIT/applications/hr2/forms.py b/FusionIIIT/applications/hr2/forms.py deleted file mode 100644 index d0b80b92a..000000000 --- a/FusionIIIT/applications/hr2/forms.py +++ /dev/null @@ -1,78 +0,0 @@ -from django import forms -from .models import Employee, EmpConfidentialDetails, ForeignService -from applications.globals.models import ExtraInfo -from django.contrib.auth.forms import UserCreationForm -from django.contrib.auth.models import User - - -class DateInput(forms.DateInput): - input_type = 'date' - - -class EditDetailsForm(forms.ModelForm): - - class Meta: - model = Employee - fields = ['extra_info', 'father_name', 'mother_name', 'religion', 'category', - 'cast', 'home_state', 'home_district', 'date_of_joining', 'designation', 'blood_group'] - - widgets = { - 'date_of_joining': DateInput() - } - - def __init__(self, *args, **kwargs): - super(EditDetailsForm, self).__init__(*args, **kwargs) - - -class EditConfidentialDetailsForm(forms.ModelForm): - - class Meta: - model = EmpConfidentialDetails - fields = ['extra_info', 'aadhar_no', - 'maritial_status', 'bank_account_no', 'salary'] - - def __init__(self, *args, **kwargs): - super(EditConfidentialDetailsForm, self).__init__(*args, **kwargs) - - -class EditServiceBookForm(forms.ModelForm): - - class Meta: - model = ForeignService - fields = ['extra_info', 'start_date', 'end_date', 'job_title', 'organisation', - 'description', 'salary_source', 'designation', 'service_type'] - widgets = {'start_date': DateInput(), 'end_date': DateInput()} - - def __init__(self, *args, **kwargs): - super(EditServiceBookForm, self).__init__(*args, **kwargs) - - -class NewUserForm(UserCreationForm): - first_name = forms.CharField(max_length=50, required=True) - last_name = forms.CharField(max_length=50, required=True) - email = forms.EmailField(required=True) - - class Meta: - model = User - fields = ("username", "email", "password1", - "password2", 'first_name', 'last_name') - - def save(self, commit=True): - user = super(NewUserForm, self).save(commit=False) - user.email = self.cleaned_data['email'] - user.first_name = self.cleaned_data['first_name'] - user.last_name = self.cleaned_data['last_name'] - if commit: - user.save() - return user - - -class AddExtraInfo(forms.ModelForm): - class Meta: - model = ExtraInfo - fields = ['id', 'user', 'title', 'sex', 'date_of_birth', 'title', 'phone_no', - 'address', 'user_type', 'about_me', 'user_status'] - widgets = {'date_of_birth': DateInput()} - - def __init__(self, *args, **kwargs): - super(AddExtraInfo, self).__init__(*args, **kwargs) diff --git a/FusionIIIT/applications/hr2/management/__init__.py b/FusionIIIT/applications/hr2/management/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/FusionIIIT/applications/hr2/management/commands/__init__.py b/FusionIIIT/applications/hr2/management/commands/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/FusionIIIT/applications/hr2/management/commands/convert_vl_to_earned.py b/FusionIIIT/applications/hr2/management/commands/convert_vl_to_earned.py new file mode 100644 index 000000000..bd1ea05f4 --- /dev/null +++ b/FusionIIIT/applications/hr2/management/commands/convert_vl_to_earned.py @@ -0,0 +1,106 @@ +import datetime +from decimal import Decimal, ROUND_HALF_UP + +from django.core.management.base import BaseCommand + +from applications.globals.models import ExtraInfo +from applications.hr2.models import EmployeeLeaveBalance, LeaveType + + +class Command(BaseCommand): + help = "Convert unavailed Vacation Leave (VL) to Earned Leave (EL) for faculty at 2:1 for the next year." + + def add_arguments(self, parser): + parser.add_argument( + "--year", + type=int, + default=datetime.date.today().year, + help="Source year to convert from (default: current year).", + ) + parser.add_argument( + "--dry-run", + action="store_true", + dest="dry_run", + help="Show changes without updating balances.", + ) + + def handle(self, *args, **options): + source_year = options["year"] + target_year = source_year + 1 + dry_run = options.get("dry_run", False) + + vl_type = LeaveType.objects.filter(code__iexact="VL").first() or LeaveType.objects.filter(name__iexact="Vacation").first() + el_type = LeaveType.objects.filter(code__iexact="EL").first() or LeaveType.objects.filter(name__iexact="Earned").first() + + if not vl_type or not el_type: + self.stderr.write(self.style.ERROR("Leave types VL/Earned not found. Ensure LeaveType records exist.")) + return + + all_employees = ExtraInfo.objects.all() + converted_count = 0 + total_converted = Decimal("0.0") + + next_year_defaults = { + "CL": Decimal("8.0"), + "RL": Decimal("2.0"), + "VL": Decimal("60.0"), + } + leave_types = {lt.code.upper(): lt for lt in LeaveType.objects.all() if lt.code} + + for employee in all_employees: + is_faculty = employee.user_type == "faculty" + converted = Decimal("0.0") + + vl_balance = EmployeeLeaveBalance.objects.filter( + employee=employee, + leave_type=vl_type, + year=source_year, + ).first() + if is_faculty and vl_balance: + vl_current = Decimal(str(vl_balance.current_balance or 0)) + if vl_current > 0: + converted = (vl_current / Decimal("2")).quantize(Decimal("0.1"), rounding=ROUND_HALF_UP) + if not dry_run: + vl_balance.current_balance = Decimal("0.0") + vl_balance.save(update_fields=["current_balance"]) + if converted > 0: + converted_count += 1 + total_converted += converted + + if dry_run: + continue + + for code, leave_type in leave_types.items(): + if code == "EL": + opening = Decimal("0.0") + accrued = converted + current = converted + elif code in next_year_defaults: + opening = next_year_defaults[code] + accrued = Decimal("0.0") + current = opening + else: + opening = Decimal("0.0") + accrued = Decimal("0.0") + current = Decimal("0.0") + + EmployeeLeaveBalance.objects.update_or_create( + employee=employee, + leave_type=leave_type, + year=target_year, + defaults={ + "opening_balance": opening, + "accrued": accrued, + "availed": Decimal("0.0"), + "current_balance": current, + }, + ) + + if dry_run: + self.stdout.write(self.style.WARNING( + f"Dry run: would convert VL to EL for {converted_count} faculty (total EL added: {total_converted})" + )) + else: + self.stdout.write(self.style.SUCCESS( + f"Converted VL to EL for {converted_count} faculty (total EL added: {total_converted})" + )) diff --git a/FusionIIIT/applications/hr2/management/commands/seed_hr2.py b/FusionIIIT/applications/hr2/management/commands/seed_hr2.py new file mode 100644 index 000000000..8d53650d6 --- /dev/null +++ b/FusionIIIT/applications/hr2/management/commands/seed_hr2.py @@ -0,0 +1,138 @@ +from decimal import Decimal + +from django.contrib.auth import get_user_model +from django.core.management.base import BaseCommand +from django.utils import timezone + +from applications.globals.models import DepartmentInfo, Designation, ExtraInfo, HoldsDesignation, ModuleAccess +from applications.hr2.models import Employee, EmployeeLeaveBalance, LeaveType + +try: + from notifications.signals import notify +except ImportError: # pragma: no cover - optional dependency + notify = None + + +class Command(BaseCommand): + help = "Seed HR2 demo data (employee, access, leave balance)." + + def handle(self, *args, **options): + User = get_user_model() + now = timezone.now() + + department, _ = DepartmentInfo.objects.get_or_create(name="Computer Science") + + designation, _ = Designation.objects.get_or_create( + name="Faculty", + defaults={"full_name": "Faculty", "type": "academic"}, + ) + + module_access, _ = ModuleAccess.objects.get_or_create(designation="Faculty") + if not module_access.hr: + module_access.hr = True + module_access.save() + + user, created = User.objects.get_or_create( + username="rahul123", + defaults={ + "first_name": "Rahul", + "last_name": "Sharma", + "email": "rahul.sharma@iiitdmj.ac.in", + }, + ) + if created: + user.set_password("user@123") + user.save() + else: + user.email = "rahul.sharma@iiitdmj.ac.in" + user.first_name = user.first_name or "Rahul" + user.last_name = user.last_name or "Sharma" + user.set_password("user@123") + user.save() + + extra_info, _ = ExtraInfo.objects.get_or_create( + id="EMP001", + defaults={ + "user": user, + "title": "Dr.", + "sex": "M", + "date_of_birth": "1990-05-12", + "user_status": "PRESENT", + "address": "IIITDMJ Campus", + "phone_no": 9876543210, + "user_type": "faculty", + "department": department, + "about_me": "Faculty member", + "last_selected_role": "Faculty", + }, + ) + if extra_info.user_id != user.id: + extra_info.user = user + extra_info.department = department + extra_info.phone_no = 9876543210 + extra_info.last_selected_role = "Faculty" + extra_info.save() + + HoldsDesignation.objects.get_or_create( + user=user, + working=user, + designation=designation, + ) + + Employee.objects.get_or_create( + id=user, + defaults={ + "father_name": "Rajesh Sharma", + "mother_name": "Sunita Sharma", + "category": "General", + "caste": "N/A", + "home_state": "Madhya Pradesh", + "home_district": "Jabalpur", + "full_address": "IIITDMJ Campus, Dumna Airport Road", + "date_of_joining": "2021-08-01", + "date_of_birth": "1990-05-12", + "blood_group": "O+", + "phone_number": "9876543210", + "personal_email": "rahul.sharma@iiitdmj.ac.in", + "emergency_contact_number": "9876543211", + "emergency_contact_name": "Rajesh Sharma", + "employee_type": "Faculty", + }, + ) + + leave_type_map = { + "Casual": ("CL", Decimal("10")), + "Earned": ("EL", Decimal("18")), + "Medical": ("ML", Decimal("12")), + "Restricted": ("RL", Decimal("5")), + "Vacation": ("VL", Decimal("25")), + "Sabbatical": ("SL", Decimal("0")), + } + + current_year = now.year + for name, (code, balance) in leave_type_map.items(): + leave_type, _ = LeaveType.objects.get_or_create( + name=name, + defaults={"code": code, "is_active": True}, + ) + EmployeeLeaveBalance.objects.get_or_create( + employee=extra_info, + leave_type=leave_type, + year=current_year, + defaults={ + "opening_balance": balance, + "accrued": Decimal("0"), + "availed": Decimal("0"), + "current_balance": balance, + }, + ) + + if notify: + notify.send( + sender=user, + recipient=user, + verb="Welcome to HR Portal", + description="Welcome to HR Portal", + ) + + self.stdout.write(self.style.SUCCESS("HR2 seed data created/updated.")) diff --git a/FusionIIIT/applications/hr2/management/commands/seed_hr_demo.py b/FusionIIIT/applications/hr2/management/commands/seed_hr_demo.py new file mode 100644 index 000000000..53fc84c8a --- /dev/null +++ b/FusionIIIT/applications/hr2/management/commands/seed_hr_demo.py @@ -0,0 +1,530 @@ +import json +import datetime + +from django.contrib.auth.models import User +from django.core.management.base import BaseCommand +from django.db import transaction + +from applications.globals.models import DepartmentInfo, Designation, ExtraInfo, HoldsDesignation +from applications.hr2.models import ( + AppraisalFormNew, + CPDAAdvanceNew, + EmployeeCategory, + EmployeeDetailsExtended, + EmployeeLeaveBalance, + LeaveApplicationNew, + LeaveType, + LTCApplicationNew, +) + + +class Command(BaseCommand): + help = "Seed HR demo data for form testing." + + @staticmethod + def _parse_date(value): + if not value: + return None + return datetime.date.fromisoformat(value) + + @staticmethod + def _parse_gender(value): + if not value: + return "M" + value = value.strip().lower() + if value.startswith("f"): + return "F" + if value.startswith("m"): + return "M" + return "O" + + @staticmethod + def _split_name(full_name): + if not full_name: + return "", "" + parts = full_name.strip().split() + if len(parts) == 1: + return parts[0], "" + return parts[0], " ".join(parts[1:]) + + def handle(self, *args, **options): + departments = [ + "Computer Science and Engineering", + "Administration", + "Finance", + "Director Office", + ] + + employees = [ + { + "employee_id": "EMP1001", + "name": "Rahul Sharma", + "email": "rahul.sharma@iiitdmj.ac.in", + "phone": "9876543210", + "gender": "Male", + "dob": "1990-05-12", + "department": "Computer Science and Engineering", + "designation": "Assistant Professor", + "role": "Employee", + "employment_type": "Permanent", + "date_of_joining": "2021-08-01", + "reporting_to": "EMP1002", + "status": "Active", + }, + { + "employee_id": "EMP1007", + "name": "Dr. Anjali Mehta", + "email": "anjali.mehta@iiitdmj.ac.in", + "phone": "9876543216", + "gender": "Female", + "dob": "1985-11-08", + "department": "Computer Science and Engineering", + "designation": "Professor", + "role": "Employee", + "employment_type": "Permanent", + "date_of_joining": "2016-07-20", + "reporting_to": "EMP1002", + "status": "Active", + }, + { + "employee_id": "EMP1002", + "name": "Dr. Anil Kumar", + "email": "anil.kumar@iiitdmj.ac.in", + "phone": "9876543211", + "gender": "Male", + "dob": "1980-07-20", + "department": "Computer Science and Engineering", + "designation": "Professor and HOD", + "role": "HOD", + "employment_type": "Permanent", + "date_of_joining": "2015-06-15", + "reporting_to": "EMP1003", + "status": "Active", + }, + { + "employee_id": "EMP1003", + "name": "Dr. Meena Verma", + "email": "director@iiitdmj.ac.in", + "phone": "9876543212", + "gender": "Female", + "dob": "1975-02-11", + "department": "Director Office", + "designation": "Director", + "role": "Director", + "employment_type": "Permanent", + "date_of_joining": "2019-01-10", + "reporting_to": None, + "status": "Active", + }, + { + "employee_id": "EMP1004", + "name": "Suresh Verma", + "email": "registrar@iiitdmj.ac.in", + "phone": "9876543213", + "gender": "Male", + "dob": "1982-03-10", + "department": "Administration", + "designation": "Registrar", + "role": "Registrar", + "employment_type": "Permanent", + "date_of_joining": "2018-01-15", + "reporting_to": "EMP1003", + "status": "Active", + }, + { + "employee_id": "EMP1005", + "name": "Priya Nair", + "email": "hr.admin@iiitdmj.ac.in", + "phone": "9876543214", + "gender": "Female", + "dob": "1987-09-25", + "department": "Administration", + "designation": "HR Administrator", + "role": "HR Admin", + "employment_type": "Permanent", + "date_of_joining": "2020-11-05", + "reporting_to": "EMP1004", + "status": "Active", + }, + { + "employee_id": "EMP1006", + "name": "Arun Joshi", + "email": "accountant@iiitdmj.ac.in", + "phone": "9876543215", + "gender": "Male", + "dob": "1985-12-18", + "department": "Finance", + "designation": "Accountant", + "role": "Accountant", + "employment_type": "Permanent", + "date_of_joining": "2019-08-12", + "reporting_to": "EMP1004", + "status": "Active", + }, + ] + + users = [ + { + "linked_employee_id": "EMP1001", + "username": "rahul1001", + "password": "rahul123", + }, + { + "linked_employee_id": "EMP1007", + "username": "anjali1007", + "password": "anjali123", + }, + { + "linked_employee_id": "EMP1002", + "username": "hod1002", + "password": "hod123", + }, + { + "linked_employee_id": "EMP1003", + "username": "director1003", + "password": "director123", + }, + { + "linked_employee_id": "EMP1004", + "username": "registrar1004", + "password": "registrar123", + }, + { + "linked_employee_id": "EMP1005", + "username": "hradmin1005", + "password": "hradmin123", + }, + { + "linked_employee_id": "EMP1006", + "username": "accountant1006", + "password": "accountant123", + }, + ] + + leave_balance = { + "employee_id": "EMP1001", + "casual_leave": 10, + "restricted_leave": 5, + "medical_leave": 12, + "earned_leave": 18, + "vacation_leave": 20, + "sabbatical_leave": 0, + } + + leave_request = { + "employee_id": "EMP1001", + "employee_name": "Rahul Sharma", + "department": "Computer Science and Engineering", + "designation": "Assistant Professor", + "leave_type": "Casual", + "start_date": "2026-04-10", + "end_date": "2026-04-12", + "total_days": 3, + "reason": "Personal work", + "contact_during_leave": "9876543210", + "address_during_leave": "Jabalpur, MP", + "handover_notes": "Classes handed over to Dr. X", + "attachment_file": "", + "leave_balance_before": 10, + "leave_balance_after": 7, + "approval_status": "PENDING", + "current_approver_role": "HOD", + "remarks": "", + } + + appraisal_request = { + "employee_id": "EMP1001", + "employee_name": "Rahul Sharma", + "department": "Computer Science and Engineering", + "designation": "Assistant Professor", + "appraisal_year": "2025-2026", + "self_summary": "Completed teaching and research responsibilities effectively.", + "teaching_performance": "Good", + "research_work": "Worked on 2 projects", + "publications": "1 journal paper", + "trainings_attended": "AI workshop", + "administrative_contributions": "Exam coordination", + "goals_achieved": "Completed syllabus and guided students", + "future_goals": "Publish more papers", + "reviewer_id": "EMP1002", + "status": "PENDING", + "remarks": "", + } + + ltc_request = { + "employee_id": "EMP1001", + "employee_name": "Rahul Sharma", + "department": "Computer Science and Engineering", + "designation": "Assistant Professor", + "ltc_block_year": "2024-2027", + "travel_start_date": "2026-05-05", + "travel_end_date": "2026-05-12", + "destination": "Delhi", + "purpose_of_travel": "Family travel", + "family_members": [ + {"name": "Priya Sharma", "relationship": "Spouse"} + ], + "travel_mode": "Train", + "ticket_number": "IRCTC12345", + "ticket_cost": 12000, + "accommodation_cost": 8000, + "other_expenses": 2000, + "total_amount_claimed": 22000, + "tickets_upload": "", + "bills_upload": "", + "previous_ltc_used": True, + "last_ltc_date": "2023-06-15", + "verified_by_hr": False, + "approval_status": "PENDING", + "accountant_status": "Not Started", + "remarks": "", + } + + cpda_request = { + "employee_id": "EMP1001", + "employee_name": "Rahul Sharma", + "department": "Computer Science and Engineering", + "designation": "Assistant Professor", + "event_name": "National Conference on AI", + "event_type": "Conference", + "organized_by": "IIT Delhi", + "venue": "New Delhi", + "start_date": "2026-06-20", + "end_date": "2026-06-22", + "registration_fee": 5000, + "travel_expense": 8000, + "accommodation_expense": 6000, + "other_expenses": 1000, + "total_amount": 20000, + "purpose_of_attending": "Present paper and improve research skills", + "benefits_to_institution": "Research development and academic exposure", + "invitation_letter": "", + "receipts": "", + "certificates": "", + "verified_by_hr": False, + "approval_status": "PENDING", + "accountant_processing_status": "Not Started", + "remarks": "", + } + + with transaction.atomic(): + for name in departments: + DepartmentInfo.objects.get_or_create(name=name) + + teaching_category, _ = EmployeeCategory.objects.get_or_create( + name="Teaching", defaults={"category_type": "TEACHING"} + ) + non_teaching_category, _ = EmployeeCategory.objects.get_or_create( + name="Non-Teaching", defaults={"category_type": "NON_TEACHING"} + ) + + user_lookup = {item["linked_employee_id"]: item for item in users} + + for employee in employees: + user_info = user_lookup.get(employee["employee_id"], {}) + username = user_info.get("username") or employee["employee_id"].lower() + first_name, last_name = self._split_name(employee["name"]) + + user, created = User.objects.get_or_create( + username=username, + defaults={ + "email": employee["email"], + "first_name": first_name, + "last_name": last_name, + }, + ) + if created and user_info.get("password"): + user.set_password(user_info["password"]) + user.save() + + department_obj = DepartmentInfo.objects.get(name=employee["department"]) + + extra_info, _ = ExtraInfo.objects.get_or_create( + id=employee["employee_id"], + defaults={ + "user": user, + "sex": self._parse_gender(employee["gender"]), + "date_of_birth": self._parse_date(employee["dob"]), + "user_type": "faculty" + if employee["department"] == "Computer Science and Engineering" + else "staff", + "department": department_obj, + "phone_no": int(employee["phone"]), + "address": "", + }, + ) + + category = teaching_category if extra_info.user_type == "faculty" else non_teaching_category + EmployeeDetailsExtended.objects.get_or_create( + extra_info=extra_info, + defaults={ + "category": category, + "date_of_joining": self._parse_date(employee["date_of_joining"]), + "appointment_type": employee["employment_type"], + }, + ) + + designation_type = "academic" if extra_info.user_type == "faculty" else "administrative" + designation, _ = Designation.objects.get_or_create( + name=employee["designation"], + defaults={ + "full_name": employee["designation"], + "type": designation_type, + }, + ) + + HoldsDesignation.objects.get_or_create( + user=user, + working=user, + designation=designation, + ) + + leave_types = [ + ("Casual", "CL", leave_balance["casual_leave"]), + ("Restricted", "RL", leave_balance["restricted_leave"]), + ("Medical", "ML", leave_balance["medical_leave"]), + ("Earned", "EL", leave_balance["earned_leave"]), + ("Vacation", "VL", leave_balance["vacation_leave"]), + ("Sabbatical", "SL", leave_balance["sabbatical_leave"]), + ] + + for name, code, _value in leave_types: + LeaveType.objects.get_or_create( + name=name, + code=code, + defaults={"is_active": True}, + ) + + employee_user = ExtraInfo.objects.get(id=leave_balance["employee_id"]) + year = datetime.date.today().year + + for name, code, value in leave_types: + leave_type = LeaveType.objects.get(code=code) + EmployeeLeaveBalance.objects.update_or_create( + employee=employee_user, + leave_type=leave_type, + year=year, + defaults={ + "opening_balance": value, + "accrued": 0, + "availed": 0, + "current_balance": value, + }, + ) + + LeaveApplicationNew.objects.get_or_create( + employee=employee_user, + start_date=self._parse_date(leave_request["start_date"]), + end_date=self._parse_date(leave_request["end_date"]), + defaults={ + "employee_name": leave_request["employee_name"], + "department": leave_request["department"], + "designation": leave_request["designation"], + "leave_type": leave_request["leave_type"], + "total_days": leave_request["total_days"], + "reason": leave_request["reason"], + "contact_during_leave": leave_request["contact_during_leave"], + "address_during_leave": leave_request["address_during_leave"], + "handover_to": "Dr. X", + "handover_notes": leave_request["handover_notes"], + "medical_certificate": "", + "attachment_file": leave_request["attachment_file"], + "leave_balance_before": leave_request["leave_balance_before"], + "leave_balance_after": leave_request["leave_balance_after"], + "approval_status": leave_request["approval_status"], + "current_approver_role": leave_request["current_approver_role"], + "remarks": leave_request["remarks"], + }, + ) + + AppraisalFormNew.objects.get_or_create( + employee=employee_user, + appraisal_year=appraisal_request["appraisal_year"], + defaults={ + "employee_name": appraisal_request["employee_name"], + "department": appraisal_request["department"], + "designation": appraisal_request["designation"], + "self_summary": appraisal_request["self_summary"], + "key_responsibilities": "Teaching, research, and academic mentoring.", + "achievements": appraisal_request["goals_achieved"], + "challenges_faced": "", + "teaching_performance": appraisal_request["teaching_performance"], + "research_work": appraisal_request["research_work"], + "publications": appraisal_request["publications"], + "projects_handled": "", + "administrative_contributions": appraisal_request["administrative_contributions"], + "trainings_attended": appraisal_request["trainings_attended"], + "certifications": "", + "workshops": "", + "goals_achieved": appraisal_request["goals_achieved"], + "future_goals": appraisal_request["future_goals"], + "supporting_documents": "", + "reviewer_id": appraisal_request["reviewer_id"], + "reviewer_comments": "", + "rating": "", + "status": appraisal_request["status"], + "remarks": appraisal_request["remarks"], + }, + ) + + block_year = int(ltc_request["ltc_block_year"].split("-")[0]) + LTCApplicationNew.objects.get_or_create( + employee=employee_user, + travel_start_date=self._parse_date(ltc_request["travel_start_date"]), + travel_end_date=self._parse_date(ltc_request["travel_end_date"]), + defaults={ + "employee_name": ltc_request["employee_name"], + "department": ltc_request["department"], + "designation": ltc_request["designation"], + "ltc_block_year": block_year, + "destination": ltc_request["destination"], + "purpose_of_travel": ltc_request["purpose_of_travel"], + "family_members": json.dumps(ltc_request["family_members"]), + "relationship_details": "Spouse", + "travel_mode": ltc_request["travel_mode"], + "ticket_number": ltc_request["ticket_number"], + "ticket_cost": ltc_request["ticket_cost"], + "accommodation_cost": ltc_request["accommodation_cost"], + "other_expenses": ltc_request["other_expenses"], + "total_amount_claimed": ltc_request["total_amount_claimed"], + "tickets_upload": ltc_request["tickets_upload"], + "bills_upload": ltc_request["bills_upload"], + "previous_ltc_used": ltc_request["previous_ltc_used"], + "last_ltc_date": self._parse_date(ltc_request["last_ltc_date"]), + "verified_by_hr": ltc_request["verified_by_hr"], + "approval_status": ltc_request["approval_status"], + "accountant_status": ltc_request["accountant_status"], + "remarks": ltc_request["remarks"], + }, + ) + + CPDAAdvanceNew.objects.get_or_create( + employee=employee_user, + start_date=self._parse_date(cpda_request["start_date"]), + end_date=self._parse_date(cpda_request["end_date"]), + defaults={ + "employee_name": cpda_request["employee_name"], + "department": cpda_request["department"], + "designation": cpda_request["designation"], + "event_name": cpda_request["event_name"], + "event_type": cpda_request["event_type"], + "organized_by": cpda_request["organized_by"], + "venue": cpda_request["venue"], + "registration_fee": cpda_request["registration_fee"], + "travel_expense": cpda_request["travel_expense"], + "accommodation_expense": cpda_request["accommodation_expense"], + "other_expenses": cpda_request["other_expenses"], + "total_amount": cpda_request["total_amount"], + "purpose_of_attending": cpda_request["purpose_of_attending"], + "benefits_to_institution": cpda_request["benefits_to_institution"], + "invitation_letter": cpda_request["invitation_letter"], + "receipts": cpda_request["receipts"], + "certificates": cpda_request["certificates"], + "verified_by_hr": cpda_request["verified_by_hr"], + "approval_status": cpda_request["approval_status"], + "accountant_processing_status": cpda_request["accountant_processing_status"], + "remarks": cpda_request["remarks"], + }, + ) + + self.stdout.write(self.style.SUCCESS("HR demo data seeded.")) diff --git a/FusionIIIT/applications/hr2/management/commands/seed_leave_balance.py b/FusionIIIT/applications/hr2/management/commands/seed_leave_balance.py new file mode 100644 index 000000000..591c33ee7 --- /dev/null +++ b/FusionIIIT/applications/hr2/management/commands/seed_leave_balance.py @@ -0,0 +1,81 @@ +import datetime + +from django.core.management.base import BaseCommand, CommandError + +from applications.globals.models import ExtraInfo +from applications.hr2.models import EmployeeLeaveBalance, LeaveType + + +DEFAULT_BALANCES = [ + ("Casual", "CL", 10), + ("Restricted", "RL", 5), + ("Medical", "ML", 12), + ("Earned", "EL", 18), + ("Vacation", "VL", 20), + ("Sabbatical", "SL", 0), +] + +ROLE_BALANCES = { + "EMP1002": {"CL": 12, "RL": 6, "ML": 15, "EL": 25, "VL": 30, "SL": 10}, + "EMP1003": {"CL": 15, "RL": 8, "ML": 20, "EL": 30, "VL": 35, "SL": 15}, + "EMP1004": {"CL": 12, "RL": 6, "ML": 15, "EL": 22, "VL": 28, "SL": 5}, + "EMP1005": {"CL": 10, "RL": 5, "ML": 12, "EL": 20, "VL": 25, "SL": 0}, + "EMP1006": {"CL": 10, "RL": 5, "ML": 12, "EL": 18, "VL": 22, "SL": 0}, + "EMP1007": {"CL": 12, "RL": 6, "ML": 15, "EL": 25, "VL": 30, "SL": 12}, +} + + +class Command(BaseCommand): + help = "Seed leave balances for testing." + + def add_arguments(self, parser): + parser.add_argument( + "--employee-id", + dest="employee_id", + default="EMP1001", + help="ExtraInfo ID to seed (default: EMP1001)", + ) + parser.add_argument( + "--all", + action="store_true", + dest="seed_all", + help="Seed balances for all ExtraInfo records", + ) + + def handle(self, *args, **options): + year = datetime.date.today().year + + for name, code, _value in DEFAULT_BALANCES: + LeaveType.objects.get_or_create( + name=name, + code=code, + defaults={"is_active": True}, + ) + + if options.get("seed_all"): + employees = ExtraInfo.objects.all() + else: + employee_id = options.get("employee_id") + try: + employees = [ExtraInfo.objects.get(id=employee_id)] + except ExtraInfo.DoesNotExist as exc: + raise CommandError(f"Employee not found: {employee_id}") from exc + + for employee in employees: + balance_map = ROLE_BALANCES.get(employee.id, {}) + for name, code, default_value in DEFAULT_BALANCES: + value = balance_map.get(code, default_value) + leave_type = LeaveType.objects.get(code=code) + EmployeeLeaveBalance.objects.update_or_create( + employee=employee, + leave_type=leave_type, + year=year, + defaults={ + "opening_balance": value, + "accrued": 0, + "availed": 0, + "current_balance": value, + }, + ) + + self.stdout.write(self.style.SUCCESS("Leave balances seeded.")) diff --git a/FusionIIIT/applications/hr2/migrations/0001_initial.py b/FusionIIIT/applications/hr2/migrations/0001_initial.py index 78ce3a457..e20d6a0a9 100644 --- a/FusionIIIT/applications/hr2/migrations/0001_initial.py +++ b/FusionIIIT/applications/hr2/migrations/0001_initial.py @@ -1,7 +1,7 @@ -# Generated by Django 3.1.5 on 2024-07-16 15:44 +# Generated by Django 3.1.5 on 2026-04-05 23:33 +import datetime from django.conf import settings -import django.core.validators from django.db import migrations, models import django.db.models.deletion @@ -11,11 +11,119 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('globals', '0001_initial'), + ('auth', '0012_alter_user_first_name_max_length'), migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('globals', '0005_moduleaccess_database'), ] operations = [ + migrations.CreateModel( + name='AppraisalPeriod', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('start_date', models.DateField()), + ('end_date', models.DateField()), + ('submission_deadline', models.DateField()), + ('is_active', models.BooleanField(default=False)), + ], + ), + migrations.CreateModel( + name='Employee', + fields=[ + ('id', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='employee_details', serialize=False, to='auth.user')), + ('father_name', models.CharField(max_length=100)), + ('mother_name', models.CharField(max_length=100)), + ('religion', models.CharField(blank=True, max_length=20, null=True)), + ('category', models.CharField(choices=[('General', 'General'), ('OBC', 'OBC'), ('SC', 'SC'), ('ST', 'ST')], max_length=20)), + ('caste', models.CharField(max_length=50)), + ('home_state', models.CharField(max_length=50)), + ('home_district', models.CharField(max_length=50)), + ('full_address', models.TextField()), + ('date_of_joining', models.DateField()), + ('date_of_birth', models.DateField()), + ('blood_group', models.CharField(choices=[('A+', 'A+'), ('A-', 'A-'), ('B+', 'B+'), ('B-', 'B-'), ('O+', 'O+'), ('O-', 'O-'), ('AB+', 'AB+'), ('AB-', 'AB-')], max_length=3)), + ('phone_number', models.CharField(max_length=15)), + ('personal_email', models.EmailField(max_length=254)), + ('emergency_contact_number', models.CharField(max_length=15)), + ('emergency_contact_name', models.CharField(max_length=100)), + ('employee_type', models.CharField(choices=[('Faculty', 'Faculty'), ('Staff', 'Staff'), ('Other', 'Other')], default='Faculty', max_length=10)), + ], + ), + migrations.CreateModel( + name='EmployeeCategory', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('category_type', models.CharField(choices=[('TEACHING', 'Teaching'), ('NON_TEACHING', 'Non-Teaching')], max_length=20)), + ('pay_level', models.CharField(blank=True, max_length=20)), + ('is_active', models.BooleanField(default=True)), + ], + ), + migrations.CreateModel( + name='LeaveType', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=50)), + ('code', models.CharField(max_length=10, unique=True)), + ('max_days_per_year', models.IntegerField(blank=True, null=True)), + ('carry_forward', models.BooleanField(default=False)), + ('max_carry_forward', models.IntegerField(blank=True, null=True)), + ('is_active', models.BooleanField(default=True)), + ], + ), + migrations.CreateModel( + name='QualificationType', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(max_length=100)), + ('level', models.IntegerField()), + ], + ), + migrations.CreateModel( + name='TrainingProgram', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=200)), + ('description', models.TextField()), + ('organizer', models.CharField(max_length=200)), + ('venue', models.CharField(max_length=200)), + ('start_date', models.DateField()), + ('end_date', models.DateField()), + ('max_participants', models.IntegerField(blank=True, null=True)), + ('is_mandatory', models.BooleanField(default=False)), + ], + ), + migrations.CreateModel( + name='LeaveBalance', + fields=[ + ('empid', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='leave_balance', serialize=False, to='hr2.employee')), + ('casual_leave_taken', models.IntegerField(default=0)), + ('special_casual_leave_taken', models.IntegerField(default=0)), + ('earned_leave_taken', models.IntegerField(default=0)), + ('half_pay_leave_taken', models.IntegerField(default=0)), + ('maternity_leave_taken', models.IntegerField(default=0)), + ('child_care_leave_taken', models.IntegerField(default=0)), + ('paternity_leave_taken', models.IntegerField(default=0)), + ('leave_encashment_taken', models.IntegerField(default=0)), + ('restricted_holiday_taken', models.IntegerField(default=0)), + ], + ), + migrations.CreateModel( + name='LeavePerYear', + fields=[ + ('empid', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, primary_key=True, related_name='yearly_leave', serialize=False, to='hr2.employee')), + ('casual_leave', models.IntegerField(default=8)), + ('special_casual_leave', models.IntegerField(default=15)), + ('earned_leave', models.IntegerField(default=15)), + ('half_pay_leave', models.IntegerField(default=15)), + ('maternity_leave', models.IntegerField(default=180)), + ('child_care_leave', models.IntegerField(default=730)), + ('paternity_leave', models.IntegerField(default=15)), + ('leave_encashment', models.IntegerField(default=60)), + ('restricted_holiday', models.IntegerField(default=2)), + ], + ), migrations.CreateModel( name='WorkAssignemnt', fields=[ @@ -27,6 +135,102 @@ class Migration(migrations.Migration): ('extra_info', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='globals.extrainfo')), ], ), + migrations.CreateModel( + name='TrainingNomination', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('status', models.CharField(choices=[('NOMINATED', 'Nominated'), ('APPROVED', 'Approved'), ('REJECTED', 'Rejected'), ('ATTENDED', 'Attended'), ('COMPLETED', 'Completed')], default='NOMINATED', max_length=20)), + ('feedback', models.TextField(blank=True)), + ('certificate', models.FileField(blank=True, upload_to='hr/training/')), + ('employee', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='training_nominations', to='globals.extrainfo')), + ('nominated_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='training_nominations_made', to='globals.extrainfo')), + ('program', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='hr2.trainingprogram')), + ], + ), + migrations.CreateModel( + name='ServiceHistory', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('from_date', models.DateField()), + ('to_date', models.DateField(blank=True, null=True)), + ('pay_scale', models.CharField(blank=True, max_length=50)), + ('basic_pay', models.DecimalField(blank=True, decimal_places=2, max_digits=12, null=True)), + ('remarks', models.TextField(blank=True)), + ('department', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='globals.departmentinfo')), + ('designation', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='globals.designation')), + ('employee', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='service_history', to='globals.extrainfo')), + ], + options={ + 'ordering': ['-from_date'], + }, + ), + migrations.CreateModel( + name='PromotionApplication', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('application_date', models.DateField()), + ('eligibility_date', models.DateField()), + ('api_score', models.IntegerField(blank=True, null=True)), + ('documents', models.FileField(blank=True, upload_to='hr/promotions/')), + ('status', models.CharField(choices=[('SUBMITTED', 'Submitted'), ('UNDER_REVIEW', 'Under Review'), ('COMMITTEE_STAGE', 'At Committee'), ('APPROVED', 'Approved'), ('REJECTED', 'Rejected')], default='SUBMITTED', max_length=20)), + ('remarks', models.TextField(blank=True)), + ('approved_date', models.DateField(blank=True, null=True)), + ('effective_date', models.DateField(blank=True, null=True)), + ('applied_designation', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='applied_promotions', to='globals.designation')), + ('current_designation', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='current_for_promotions', to='globals.designation')), + ('employee', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='promotion_applications', to='globals.extrainfo')), + ], + ), + migrations.CreateModel( + name='ProfessionalQualification', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('title', models.CharField(max_length=200)), + ('certifying_body', models.CharField(max_length=200)), + ('date_obtained', models.DateField()), + ('valid_until', models.DateField(blank=True, null=True)), + ('document', models.FileField(blank=True, upload_to='hr/professional/')), + ('employee', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='professional_qualifications', to='globals.extrainfo')), + ], + ), + migrations.CreateModel( + name='PreviousExperience', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('organization', models.CharField(max_length=200)), + ('designation', models.CharField(max_length=100)), + ('from_date', models.DateField()), + ('to_date', models.DateField()), + ('experience_type', models.CharField(max_length=50)), + ('description', models.TextField(blank=True)), + ('document', models.FileField(blank=True, upload_to='hr/experience/')), + ('employee', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='previous_experiences', to='globals.extrainfo')), + ], + ), + migrations.CreateModel( + name='PerformanceAppraisalNew', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('teaching_score', models.IntegerField(blank=True, null=True)), + ('research_score', models.IntegerField(blank=True, null=True)), + ('admin_score', models.IntegerField(blank=True, null=True)), + ('extension_score', models.IntegerField(blank=True, null=True)), + ('self_remarks', models.TextField(blank=True)), + ('reviewer_teaching_score', models.IntegerField(blank=True, null=True)), + ('reviewer_research_score', models.IntegerField(blank=True, null=True)), + ('reviewer_admin_score', models.IntegerField(blank=True, null=True)), + ('reviewer_extension_score', models.IntegerField(blank=True, null=True)), + ('reviewer_remarks', models.TextField(blank=True)), + ('final_score', models.DecimalField(blank=True, decimal_places=2, max_digits=5, null=True)), + ('final_grade', models.CharField(blank=True, max_length=10)), + ('status', models.CharField(choices=[('DRAFT', 'Draft'), ('SUBMITTED', 'Submitted'), ('REVIEWED', 'Reviewed'), ('APPROVED', 'Approved'), ('FINALIZED', 'Finalized')], default='DRAFT', max_length=20)), + ('submitted_at', models.DateTimeField(blank=True, null=True)), + ('finalized_at', models.DateTimeField(blank=True, null=True)), + ('employee', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='appraisals_new', to='globals.extrainfo')), + ('period', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='hr2.appraisalperiod')), + ('reviewer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='reviewed_performance_appraisals_new', to='globals.extrainfo')), + ], + ), migrations.CreateModel( name='LTCform', fields=[ @@ -34,7 +238,7 @@ class Migration(migrations.Migration): ('employeeId', models.IntegerField()), ('name', models.CharField(max_length=100, null=True)), ('blockYear', models.TextField()), - ('pfNo', models.IntegerField(max_length=50)), + ('pfNo', models.IntegerField()), ('basicPaySalary', models.IntegerField(null=True)), ('designation', models.CharField(max_length=50)), ('departmentInfo', models.CharField(max_length=50)), @@ -63,41 +267,142 @@ class Migration(migrations.Migration): ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='LTC_created_by', to=settings.AUTH_USER_MODEL)), ], ), + migrations.CreateModel( + name='LTCApplicationNew', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('employee_name', models.CharField(max_length=100)), + ('department', models.CharField(max_length=100)), + ('designation', models.CharField(max_length=100)), + ('ltc_block_year', models.IntegerField()), + ('travel_start_date', models.DateField()), + ('travel_end_date', models.DateField()), + ('destination', models.CharField(max_length=200)), + ('purpose_of_travel', models.TextField()), + ('family_members', models.TextField(blank=True)), + ('relationship_details', models.TextField(blank=True)), + ('travel_mode', models.CharField(max_length=50)), + ('ticket_number', models.CharField(blank=True, max_length=100)), + ('ticket_cost', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)), + ('accommodation_cost', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)), + ('other_expenses', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)), + ('total_amount_claimed', models.DecimalField(decimal_places=2, max_digits=10)), + ('tickets_upload', models.CharField(blank=True, max_length=200)), + ('bills_upload', models.CharField(blank=True, max_length=200)), + ('previous_ltc_used', models.BooleanField(default=False)), + ('last_ltc_date', models.DateField(blank=True, null=True)), + ('applied_date', models.DateField(auto_now_add=True)), + ('verified_by_hr', models.BooleanField(default=False)), + ('approval_status', models.CharField(choices=[('PENDING', 'Pending'), ('FORWARDED', 'Forwarded'), ('APPROVED', 'Approved'), ('REJECTED', 'Rejected')], default='PENDING', max_length=20)), + ('accountant_status', models.CharField(blank=True, max_length=20)), + ('remarks', models.TextField(blank=True)), + ('employee', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ltc_applications_new', to='globals.extrainfo')), + ], + ), migrations.CreateModel( name='LeaveForm', fields=[ ('id', models.AutoField(primary_key=True, serialize=False)), - ('employeeId', models.IntegerField(max_length=22, null=True)), ('name', models.CharField(max_length=40, null=True)), ('designation', models.CharField(max_length=40, null=True)), - ('submissionDate', models.DateField(blank=True, null=True)), - ('pfNo', models.IntegerField(max_length=30, null=True)), + ('submissionDate', models.DateField(default=datetime.date.today)), + ('personalfileNo', models.CharField(max_length=50, null=True)), ('departmentInfo', models.CharField(max_length=40, null=True)), - ('natureOfLeave', models.TextField(max_length=40, null=True)), ('leaveStartDate', models.DateField(blank=True, null=True)), ('leaveEndDate', models.DateField(blank=True, null=True)), - ('purposeOfLeave', models.TextField(max_length=40, null=True)), - ('addressDuringLeave', models.TextField(blank=True, max_length=40, null=True)), - ('academicResponsibility', models.TextField(blank=True, max_length=40, null=True)), - ('addministrativeResponsibiltyAssigned', models.TextField(max_length=40, null=True)), - ('approved', models.BooleanField(null=True)), + ('Noof_CasualLeave', models.IntegerField(default=0)), + ('Noof_specialCasualLeave', models.IntegerField(default=0)), + ('Noof_earnedLeave', models.IntegerField(default=0)), + ('Noof_commutedLeave', models.IntegerField(default=0)), + ('Noof_restrictedHoliday', models.IntegerField(default=0)), + ('Noof_vacationLeave', models.IntegerField(default=0)), + ('Noof_maternityLeave', models.IntegerField(default=0)), + ('Noof_childCareLeave', models.IntegerField(default=0)), + ('Noof_paternityLeave', models.IntegerField(default=0)), + ('Noof_halfPayLeave', models.IntegerField(default=0)), + ('LeavingStation', models.BooleanField(default=False)), + ('StationLeave_startdate', models.DateField(blank=True, null=True)), + ('StationLeave_enddate', models.DateField(blank=True, null=True)), + ('Address_During_StationLeave', models.TextField(blank=True, null=True)), + ('Purpose_of_leave', models.TextField(blank=True, null=True)), + ('AcademicResponsibility_status', models.CharField(choices=[('Accepted', 'Accepted'), ('Pending', 'Pending'), ('Rejected', 'Rejected')], default='Pending', max_length=10)), + ('AdministrativeResponsibility_status', models.CharField(choices=[('Accepted', 'Accepted'), ('Pending', 'Pending'), ('Rejected', 'Rejected')], default='Pending', max_length=10)), + ('Remarks', models.TextField(blank=True, null=True)), ('approvedDate', models.DateField(auto_now_add=True, null=True)), - ('approved_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='Leave_approved_by', to=settings.AUTH_USER_MODEL)), - ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='Leave_created_by', to=settings.AUTH_USER_MODEL)), + ('status', models.CharField(choices=[('Accepted', 'Accepted'), ('Pending', 'Pending'), ('Rejected', 'Rejected')], default='Pending', max_length=10)), + ('attached_pdf', models.BinaryField(blank=True, null=True)), + ('attached_pdf_name', models.CharField(blank=True, max_length=100, null=True)), + ('file_id', models.IntegerField(blank=True, null=True)), + ('application_type', models.CharField(choices=[('Online', 'Online'), ('Offline', 'Offline')], default='Online', max_length=10)), + ('AcademicResponsibility_designation', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='leave_academic_responsibility_designation', to='globals.designation')), + ('AcademicResponsibility_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='academic_responsibility_user', to='hr2.employee')), + ('AdministrativeResponsibility_designation', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='leave_administrative_responsibility_designation', to='globals.designation')), + ('AdministrativeResponsibility_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='administrative_responsibility_user', to='hr2.employee')), + ('approved_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='leave_approved_by', to='hr2.employee')), + ('approved_by_designation', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='leave_approved_by_designation', to='globals.designation')), + ('employee', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='leave_applications', to='hr2.employee')), + ('first_recieved_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='leave_first_recieved_by', to='hr2.employee')), + ('first_recieved_designation', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='leave_first_recieved_designation', to='globals.designation')), ], ), migrations.CreateModel( - name='LeaveBalance', + name='LeaveClaim', fields=[ ('id', models.AutoField(primary_key=True, serialize=False)), - ('casualLeave', models.IntegerField(default=0)), - ('specialCasualLeave', models.IntegerField(default=0)), - ('earnedLeave', models.IntegerField(default=0)), - ('commutedLeave', models.IntegerField(default=0)), - ('restrictedHoliday', models.IntegerField(default=0)), - ('stationLeave', models.IntegerField(default=0)), - ('vacationLeave', models.IntegerField(default=0)), - ('employeeId', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='globals.extrainfo')), + ('claim_date', models.DateField(default=datetime.date.today)), + ('leaveStartDate', models.DateField(blank=True, null=True)), + ('leaveEndDate', models.DateField(blank=True, null=True)), + ('Noof_CasualLeave', models.IntegerField(default=0)), + ('Noof_specialCasualLeave', models.IntegerField(default=0)), + ('Noof_earnedLeave', models.IntegerField(default=0)), + ('Noof_commutedLeave', models.IntegerField(default=0)), + ('Noof_restrictedHoliday', models.IntegerField(default=0)), + ('Noof_vacationLeave', models.IntegerField(default=0)), + ('Noof_maternityLeave', models.IntegerField(default=0)), + ('Noof_childCareLeave', models.IntegerField(default=0)), + ('Noof_paternityLeave', models.IntegerField(default=0)), + ('Noof_halfPayLeave', models.IntegerField(default=0)), + ('remarks', models.TextField(blank=True, null=True)), + ('approvedDate', models.DateField(auto_now_add=True, null=True)), + ('status', models.CharField(choices=[('Accepted', 'Accepted'), ('Pending', 'Pending'), ('Rejected', 'Rejected')], default='Pending', max_length=10)), + ('attached_pdf', models.BinaryField(blank=True, null=True)), + ('attached_pdf_name', models.CharField(blank=True, max_length=100, null=True)), + ('file_id', models.IntegerField(blank=True, null=True)), + ('application_type', models.CharField(choices=[('Online', 'Online'), ('Offline', 'Offline')], default='Online', max_length=10)), + ('approved_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='leave_claim_approved_by', to='hr2.employee')), + ('approved_by_designation', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='leave_claim_approved_by_designation', to='globals.designation')), + ('leave_form', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='leave_claims', to='hr2.leaveform')), + ], + options={ + 'verbose_name': 'Leave Claim', + 'verbose_name_plural': 'Leave Claims', + }, + ), + migrations.CreateModel( + name='LeaveApplicationNew', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('employee_name', models.CharField(max_length=100)), + ('department', models.CharField(max_length=100)), + ('designation', models.CharField(max_length=100)), + ('leave_type', models.CharField(choices=[('Casual', 'Casual'), ('Restricted', 'Restricted'), ('Medical', 'Medical'), ('Earned', 'Earned'), ('Vacation', 'Vacation'), ('Sabbatical', 'Sabbatical')], max_length=30)), + ('start_date', models.DateField()), + ('end_date', models.DateField()), + ('total_days', models.DecimalField(decimal_places=1, max_digits=5)), + ('reason', models.TextField()), + ('contact_during_leave', models.CharField(max_length=15)), + ('address_during_leave', models.TextField()), + ('handover_to', models.CharField(max_length=100)), + ('handover_notes', models.TextField(blank=True)), + ('medical_certificate', models.CharField(blank=True, max_length=200)), + ('attachment_file', models.CharField(blank=True, max_length=200)), + ('applied_date', models.DateField(auto_now_add=True)), + ('leave_balance_before', models.DecimalField(blank=True, decimal_places=1, max_digits=5, null=True)), + ('leave_balance_after', models.DecimalField(blank=True, decimal_places=1, max_digits=5, null=True)), + ('approval_status', models.CharField(choices=[('PENDING', 'Pending'), ('FORWARDED', 'Forwarded'), ('APPROVED', 'Approved'), ('REJECTED', 'Rejected')], default='PENDING', max_length=20)), + ('current_approver_role', models.CharField(blank=True, max_length=50)), + ('remarks', models.TextField(blank=True)), + ('employee', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='leave_applications_new', to='globals.extrainfo')), ], ), migrations.CreateModel( @@ -116,42 +421,55 @@ class Migration(migrations.Migration): ], ), migrations.CreateModel( - name='Employee', + name='EmployeeDetailsExtended', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('father_name', models.CharField(default='', max_length=40)), - ('mother_name', models.CharField(default='', max_length=40)), - ('religion', models.CharField(default='', max_length=40)), - ('category', models.CharField(choices=[('SC', 'SC'), ('ST', 'ST'), ('OBC', 'OBC'), ('GENERAL', 'GENERAL'), ('PWD', 'PWD')], max_length=50)), - ('cast', models.CharField(default='', max_length=40)), - ('home_state', models.CharField(default='', max_length=40)), - ('home_district', models.CharField(default='', max_length=40)), + ('father_name', models.CharField(blank=True, max_length=100)), + ('mother_name', models.CharField(blank=True, max_length=100)), + ('spouse_name', models.CharField(blank=True, max_length=100)), + ('marital_status', models.CharField(blank=True, choices=[('SINGLE', 'Single'), ('MARRIED', 'Married'), ('WIDOWED', 'Widowed'), ('DIVORCED', 'Divorced')], max_length=20)), + ('pan_number', models.CharField(blank=True, max_length=15)), + ('aadhar_number', models.CharField(blank=True, max_length=15)), + ('passport_number', models.CharField(blank=True, max_length=20)), ('date_of_joining', models.DateField(blank=True, null=True)), - ('designation', models.CharField(default='', max_length=40)), - ('blood_group', models.CharField(choices=[('AB+', 'AB+'), ('O+', 'O+'), ('AB-', 'AB-'), ('B+', 'B+'), ('B-', 'B-'), ('O-', 'O-'), ('A+', 'A+'), ('A-', 'A-')], max_length=50)), - ('extra_info', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='globals.extrainfo')), + ('date_of_superannuation', models.DateField(blank=True, null=True)), + ('appointment_type', models.CharField(blank=True, max_length=50)), + ('employee_status', models.CharField(choices=[('ACTIVE', 'Active'), ('ON_LEAVE', 'On Leave'), ('DEPUTATION', 'On Deputation'), ('SUSPENDED', 'Suspended'), ('RETIRED', 'Retired'), ('RESIGNED', 'Resigned'), ('TERMINATED', 'Terminated')], default='ACTIVE', max_length=20)), + ('bank_name', models.CharField(blank=True, max_length=100)), + ('bank_account', models.CharField(blank=True, max_length=30)), + ('ifsc_code', models.CharField(blank=True, max_length=15)), + ('emergency_contact_name', models.CharField(blank=True, max_length=100)), + ('emergency_contact_phone', models.CharField(blank=True, max_length=15)), + ('emergency_contact_relation', models.CharField(blank=True, max_length=50)), + ('category', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='hr2.employeecategory')), + ('extra_info', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='employee_details_extended', to='globals.extrainfo')), ], ), migrations.CreateModel( name='EmpDependents', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('name', models.CharField(default='', max_length=100)), - ('gender', models.CharField(choices=[('M', 'Male'), ('F', 'Female'), ('O', 'Other')], max_length=50)), - ('dob', models.DateField(max_length=6, null=True)), - ('relationship', models.CharField(default='', max_length=40)), - ('extra_info', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='globals.extrainfo')), + ('id', models.AutoField(primary_key=True, serialize=False)), + ('name', models.CharField(max_length=100)), + ('gender', models.CharField(choices=[('Male', 'Male'), ('Female', 'Female'), ('Other', 'Other')], max_length=10)), + ('relation', models.CharField(max_length=50)), + ('contact_number', models.CharField(max_length=15)), + ('contact_email', models.EmailField(blank=True, max_length=254, null=True)), + ('date_of_birth', models.DateField()), + ('empid', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='dependents', to='hr2.employee')), ], ), migrations.CreateModel( name='EmpConfidentialDetails', fields=[ - ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('aadhar_no', models.BigIntegerField(default=0, max_length=12, validators=[django.core.validators.MaxValueValidator(999999999999), django.core.validators.MinValueValidator(99999999999)])), - ('maritial_status', models.CharField(choices=[('MARRIED', 'MARRIED'), ('UN-MARRIED', 'UN-MARRIED'), ('WIDOW', 'WIDOW')], max_length=50)), - ('bank_account_no', models.IntegerField(default=0)), - ('salary', models.IntegerField(default=0)), - ('extra_info', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='globals.extrainfo')), + ('id', models.AutoField(primary_key=True, serialize=False)), + ('aadhar_number', models.CharField(max_length=12, unique=True)), + ('pan_number', models.CharField(max_length=10, unique=True)), + ('marital_status', models.CharField(choices=[('Single', 'Single'), ('Married', 'Married'), ('Divorced', 'Divorced'), ('Widowed', 'Widowed')], max_length=10)), + ('personal_file_number', models.CharField(max_length=50, unique=True)), + ('bank_account_number', models.CharField(max_length=20, unique=True)), + ('ifsc_code', models.CharField(max_length=20, null=True)), + ('basic_pay', models.DecimalField(decimal_places=2, max_digits=10)), + ('empid', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='confidential_details', to='hr2.employee')), ], ), migrations.CreateModel( @@ -163,14 +481,60 @@ class Migration(migrations.Migration): ('extra_info', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='globals.extrainfo')), ], ), + migrations.CreateModel( + name='EducationalQualification', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('degree', models.CharField(max_length=100)), + ('specialization', models.CharField(blank=True, max_length=200)), + ('institution', models.CharField(max_length=200)), + ('university', models.CharField(blank=True, max_length=200)), + ('year_of_passing', models.IntegerField()), + ('division_grade', models.CharField(blank=True, max_length=50)), + ('document', models.FileField(blank=True, upload_to='hr/qualifications/')), + ('employee', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='qualifications', to='globals.extrainfo')), + ('qualification_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='hr2.qualificationtype')), + ], + ), + migrations.CreateModel( + name='CPDAReimbursementNew', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('employee_name', models.CharField(max_length=100)), + ('department', models.CharField(max_length=100)), + ('designation', models.CharField(max_length=100)), + ('event_name', models.CharField(max_length=200)), + ('event_type', models.CharField(max_length=50)), + ('organized_by', models.CharField(blank=True, max_length=200)), + ('venue', models.CharField(blank=True, max_length=200)), + ('start_date', models.DateField()), + ('end_date', models.DateField()), + ('registration_fee', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)), + ('travel_expense', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)), + ('accommodation_expense', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)), + ('other_expenses', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)), + ('total_amount', models.DecimalField(decimal_places=2, max_digits=10)), + ('purpose_of_attending', models.TextField()), + ('benefits_to_institution', models.TextField()), + ('invitation_letter', models.CharField(blank=True, max_length=200)), + ('receipts', models.CharField(blank=True, max_length=200)), + ('certificates', models.CharField(blank=True, max_length=200)), + ('applied_date', models.DateField(auto_now_add=True)), + ('verified_by_hr', models.BooleanField(default=False)), + ('approval_status', models.CharField(choices=[('PENDING', 'Pending'), ('FORWARDED', 'Forwarded'), ('APPROVED', 'Approved'), ('REJECTED', 'Rejected')], default='PENDING', max_length=20)), + ('accountant_processing_status', models.CharField(blank=True, max_length=30)), + ('remarks', models.TextField(blank=True)), + ('employee', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cpda_reimbursements_new', to='globals.extrainfo')), + ], + ), migrations.CreateModel( name='CPDAReimbursementform', fields=[ ('id', models.AutoField(primary_key=True, serialize=False)), - ('employeeId', models.IntegerField(max_length=22, null=True)), + ('employeeId', models.IntegerField(null=True)), ('name', models.CharField(max_length=50)), ('designation', models.CharField(max_length=50)), - ('pfNo', models.IntegerField(max_length=20)), + ('pfNo', models.IntegerField()), ('advanceTaken', models.IntegerField()), ('purpose', models.TextField()), ('adjustmentSubmitted', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)), @@ -178,23 +542,54 @@ class Migration(migrations.Migration): ('advanceDueAdjustment', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)), ('advanceAmountPDA', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)), ('amountCheckedInPDA', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)), - ('submissionDate', models.DateField(auto_now_add=True)), + ('submissionDate', models.DateField(blank=True, null=True)), ('approved', models.BooleanField(null=True)), ('approvedDate', models.DateField(auto_now_add=True, null=True)), ('approved_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='CPDAR_approved_by', to=settings.AUTH_USER_MODEL)), ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='CPDAR_created_by', to=settings.AUTH_USER_MODEL)), ], ), + migrations.CreateModel( + name='CPDAAdvanceNew', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('employee_name', models.CharField(max_length=100)), + ('department', models.CharField(max_length=100)), + ('designation', models.CharField(max_length=100)), + ('event_name', models.CharField(max_length=200)), + ('event_type', models.CharField(max_length=50)), + ('organized_by', models.CharField(blank=True, max_length=200)), + ('venue', models.CharField(blank=True, max_length=200)), + ('start_date', models.DateField()), + ('end_date', models.DateField()), + ('registration_fee', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)), + ('travel_expense', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)), + ('accommodation_expense', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)), + ('other_expenses', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)), + ('total_amount', models.DecimalField(decimal_places=2, max_digits=10)), + ('purpose_of_attending', models.TextField()), + ('benefits_to_institution', models.TextField()), + ('invitation_letter', models.CharField(blank=True, max_length=200)), + ('receipts', models.CharField(blank=True, max_length=200)), + ('certificates', models.CharField(blank=True, max_length=200)), + ('applied_date', models.DateField(auto_now_add=True)), + ('verified_by_hr', models.BooleanField(default=False)), + ('approval_status', models.CharField(choices=[('PENDING', 'Pending'), ('FORWARDED', 'Forwarded'), ('APPROVED', 'Approved'), ('REJECTED', 'Rejected')], default='PENDING', max_length=20)), + ('accountant_processing_status', models.CharField(blank=True, max_length=30)), + ('remarks', models.TextField(blank=True)), + ('employee', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='cpda_advances_new', to='globals.extrainfo')), + ], + ), migrations.CreateModel( name='CPDAAdvanceform', fields=[ ('id', models.AutoField(primary_key=True, serialize=False)), - ('employeeId', models.IntegerField(max_length=22, null=True)), + ('employeeId', models.IntegerField(null=True)), ('name', models.CharField(max_length=40, null=True)), ('designation', models.CharField(max_length=40, null=True)), - ('pfNo', models.IntegerField(max_length=30, null=True)), + ('pfNo', models.IntegerField(null=True)), ('purpose', models.TextField(max_length=40, null=True)), - ('amountRequired', models.IntegerField(max_length=30, null=True)), + ('amountRequired', models.IntegerField(null=True)), ('advanceDueAdjustment', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)), ('submissionDate', models.DateField(blank=True, null=True)), ('balanceAvailable', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)), @@ -202,15 +597,47 @@ class Migration(migrations.Migration): ('amountCheckedInPDA', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True)), ('approved', models.BooleanField(null=True)), ('approvedDate', models.DateField(auto_now_add=True, null=True)), - ('approved_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='CPDA_approved_by', to=settings.AUTH_USER_MODEL)), + ('approved_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='CPDA_approved_by', to=settings.AUTH_USER_MODEL)), ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='CPDA_created_by', to=settings.AUTH_USER_MODEL)), ], ), + migrations.CreateModel( + name='AppraisalFormNew', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('employee_name', models.CharField(max_length=100)), + ('department', models.CharField(max_length=100)), + ('designation', models.CharField(max_length=100)), + ('appraisal_year', models.CharField(max_length=20)), + ('self_summary', models.TextField()), + ('key_responsibilities', models.TextField()), + ('achievements', models.TextField()), + ('challenges_faced', models.TextField(blank=True)), + ('teaching_performance', models.TextField(blank=True)), + ('research_work', models.TextField(blank=True)), + ('publications', models.TextField(blank=True)), + ('projects_handled', models.TextField(blank=True)), + ('administrative_contributions', models.TextField(blank=True)), + ('trainings_attended', models.TextField(blank=True)), + ('certifications', models.TextField(blank=True)), + ('workshops', models.TextField(blank=True)), + ('goals_achieved', models.TextField()), + ('future_goals', models.TextField()), + ('supporting_documents', models.CharField(blank=True, max_length=200)), + ('reviewer_id', models.CharField(blank=True, max_length=50)), + ('reviewer_comments', models.TextField(blank=True)), + ('rating', models.CharField(blank=True, max_length=20)), + ('status', models.CharField(choices=[('PENDING', 'Pending'), ('REVIEWED', 'Reviewed'), ('APPROVED', 'Approved')], default='PENDING', max_length=20)), + ('remarks', models.TextField(blank=True)), + ('submitted_at', models.DateTimeField(auto_now_add=True)), + ('employee', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='appraisal_forms_new', to='globals.extrainfo')), + ], + ), migrations.CreateModel( name='Appraisalform', fields=[ ('id', models.AutoField(primary_key=True, serialize=False)), - ('employeeId', models.IntegerField(max_length=22, null=True)), + ('employeeId', models.IntegerField(null=True)), ('name', models.CharField(max_length=22)), ('designation', models.CharField(max_length=50)), ('disciplineInfo', models.CharField(max_length=22, null=True)), @@ -243,4 +670,53 @@ class Migration(migrations.Migration): ('created_by', models.ForeignKey(null=True, on_delete=django.db.models.deletion.CASCADE, related_name='Appraisal_created_by', to=settings.AUTH_USER_MODEL)), ], ), + migrations.CreateModel( + name='FacultyWorkload', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('semester', models.CharField(max_length=20)), + ('year', models.IntegerField()), + ('lecture_hours', models.IntegerField(default=0)), + ('tutorial_hours', models.IntegerField(default=0)), + ('lab_hours', models.IntegerField(default=0)), + ('total_hours', models.IntegerField(default=0)), + ('total_students', models.IntegerField(default=0)), + ('phd_scholars', models.IntegerField(default=0)), + ('faculty', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='workloads', to='globals.faculty')), + ], + options={ + 'unique_together': {('faculty', 'semester', 'year')}, + }, + ), + migrations.CreateModel( + name='EmployeeLeaveBalance', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('year', models.IntegerField()), + ('opening_balance', models.DecimalField(decimal_places=1, default=0, max_digits=5)), + ('accrued', models.DecimalField(decimal_places=1, default=0, max_digits=5)), + ('availed', models.DecimalField(decimal_places=1, default=0, max_digits=5)), + ('current_balance', models.DecimalField(decimal_places=1, default=0, max_digits=5)), + ('employee', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='leave_balances_new', to='globals.extrainfo')), + ('leave_type', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='hr2.leavetype')), + ], + options={ + 'unique_together': {('employee', 'leave_type', 'year')}, + }, + ), + migrations.CreateModel( + name='EmployeeAttendance', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date', models.DateField()), + ('status', models.CharField(choices=[('PRESENT', 'Present'), ('ABSENT', 'Absent'), ('HALF_DAY', 'Half Day'), ('ON_LEAVE', 'On Leave'), ('ON_TOUR', 'On Tour'), ('WORK_FROM_HOME', 'WFH')], max_length=20)), + ('in_time', models.TimeField(blank=True, null=True)), + ('out_time', models.TimeField(blank=True, null=True)), + ('remarks', models.TextField(blank=True)), + ('employee', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attendance_records', to='globals.extrainfo')), + ], + options={ + 'unique_together': {('employee', 'date')}, + }, + ), ] diff --git a/FusionIIIT/applications/hr2/migrations/0002_auto_20241020_1126.py b/FusionIIIT/applications/hr2/migrations/0002_auto_20241020_1126.py deleted file mode 100644 index 5b99015f7..000000000 --- a/FusionIIIT/applications/hr2/migrations/0002_auto_20241020_1126.py +++ /dev/null @@ -1,64 +0,0 @@ -# Generated by Django 3.1.5 on 2024-10-20 11:26 - -import django.core.validators -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('hr2', '0001_initial'), - ] - - operations = [ - migrations.AlterField( - model_name='appraisalform', - name='employeeId', - field=models.IntegerField(null=True), - ), - migrations.AlterField( - model_name='cpdaadvanceform', - name='amountRequired', - field=models.IntegerField(null=True), - ), - migrations.AlterField( - model_name='cpdaadvanceform', - name='employeeId', - field=models.IntegerField(null=True), - ), - migrations.AlterField( - model_name='cpdaadvanceform', - name='pfNo', - field=models.IntegerField(null=True), - ), - migrations.AlterField( - model_name='cpdareimbursementform', - name='employeeId', - field=models.IntegerField(null=True), - ), - migrations.AlterField( - model_name='cpdareimbursementform', - name='pfNo', - field=models.IntegerField(), - ), - migrations.AlterField( - model_name='empconfidentialdetails', - name='aadhar_no', - field=models.BigIntegerField(default=0, validators=[django.core.validators.MaxValueValidator(999999999999), django.core.validators.MinValueValidator(99999999999)]), - ), - migrations.AlterField( - model_name='leaveform', - name='employeeId', - field=models.IntegerField(null=True), - ), - migrations.AlterField( - model_name='leaveform', - name='pfNo', - field=models.IntegerField(null=True), - ), - migrations.AlterField( - model_name='ltcform', - name='pfNo', - field=models.IntegerField(), - ), - ] diff --git a/FusionIIIT/applications/hr2/migrations/0002_leave_nominee.py b/FusionIIIT/applications/hr2/migrations/0002_leave_nominee.py new file mode 100644 index 000000000..6c4f771a5 --- /dev/null +++ b/FusionIIIT/applications/hr2/migrations/0002_leave_nominee.py @@ -0,0 +1,26 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hr2', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + model_name='leaveapplicationnew', + name='handover_to', + field=models.CharField(blank=True, max_length=100), + ), + migrations.AddField( + model_name='leaveapplicationnew', + name='nominee_status', + field=models.CharField(choices=[('NOT_REQUIRED', 'Not Required'), ('PENDING', 'Pending'), ('ACCEPTED', 'Accepted'), ('DECLINED', 'Declined')], default='NOT_REQUIRED', max_length=20), + ), + migrations.AddField( + model_name='leaveapplicationnew', + name='nominee_responded_at', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/FusionIIIT/applications/hr2/migrations/0003_leave_document_request.py b/FusionIIIT/applications/hr2/migrations/0003_leave_document_request.py new file mode 100644 index 000000000..827007000 --- /dev/null +++ b/FusionIIIT/applications/hr2/migrations/0003_leave_document_request.py @@ -0,0 +1,39 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ('hr2', '0002_leave_nominee'), + ] + + operations = [ + migrations.AddField( + model_name='leaveapplicationnew', + name='document_request_message', + field=models.TextField(blank=True), + ), + migrations.AddField( + model_name='leaveapplicationnew', + name='document_request_status', + field=models.CharField( + choices=[('NOT_REQUESTED', 'Not Requested'), ('REQUESTED', 'Requested'), ('SUBMITTED', 'Submitted')], + default='NOT_REQUESTED', + max_length=20, + ), + ), + migrations.AddField( + model_name='leaveapplicationnew', + name='document_requested_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='leaveapplicationnew', + name='document_submission', + field=models.TextField(blank=True), + ), + migrations.AddField( + model_name='leaveapplicationnew', + name='document_submitted_at', + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/FusionIIIT/applications/hr2/migrations/0004_leave_cancellation.py b/FusionIIIT/applications/hr2/migrations/0004_leave_cancellation.py new file mode 100644 index 000000000..4ca5c3bf3 --- /dev/null +++ b/FusionIIIT/applications/hr2/migrations/0004_leave_cancellation.py @@ -0,0 +1,66 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hr2', '0003_leave_document_request'), + ] + + operations = [ + migrations.AddField( + model_name='leaveapplicationnew', + name='cancel_status', + field=models.CharField( + choices=[('NOT_REQUESTED', 'Not Requested'), ('REQUESTED', 'Requested'), ('APPROVED', 'Approved'), ('REJECTED', 'Rejected')], + default='NOT_REQUESTED', + max_length=20, + ), + ), + migrations.AddField( + model_name='leaveapplicationnew', + name='cancel_requested_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='leaveapplicationnew', + name='cancel_decided_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='leaveapplicationnew', + name='cancel_requested_by_role', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='leaveapplicationnew', + name='cancel_current_approver_role', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='leaveapplicationnew', + name='cancel_reason', + field=models.TextField(blank=True), + ), + migrations.AddField( + model_name='leaveapplicationnew', + name='cancel_decision_remarks', + field=models.TextField(blank=True), + ), + migrations.AlterField( + model_name='leaveapplicationnew', + name='approval_status', + field=models.CharField( + choices=[ + ('PENDING', 'Pending'), + ('FORWARDED', 'Forwarded'), + ('APPROVED', 'Approved'), + ('REJECTED', 'Rejected'), + ('WITHDRAWN', 'Withdrawn'), + ('CANCELLED', 'Cancelled'), + ], + default='PENDING', + max_length=20, + ), + ), + ] diff --git a/FusionIIIT/applications/hr2/migrations/0005_leave_extension.py b/FusionIIIT/applications/hr2/migrations/0005_leave_extension.py new file mode 100644 index 000000000..00bfa44af --- /dev/null +++ b/FusionIIIT/applications/hr2/migrations/0005_leave_extension.py @@ -0,0 +1,60 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hr2', '0004_leave_cancellation'), + ] + + operations = [ + migrations.AddField( + model_name='leaveapplicationnew', + name='extension_status', + field=models.CharField( + choices=[('NOT_REQUESTED', 'Not Requested'), ('REQUESTED', 'Requested'), ('APPROVED', 'Approved'), ('REJECTED', 'Rejected')], + default='NOT_REQUESTED', + max_length=20, + ), + ), + migrations.AddField( + model_name='leaveapplicationnew', + name='extension_requested_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='leaveapplicationnew', + name='extension_decided_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='leaveapplicationnew', + name='extension_requested_by_role', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='leaveapplicationnew', + name='extension_current_approver_role', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='leaveapplicationnew', + name='extension_reason', + field=models.TextField(blank=True), + ), + migrations.AddField( + model_name='leaveapplicationnew', + name='extension_new_end_date', + field=models.DateField(blank=True, null=True), + ), + migrations.AddField( + model_name='leaveapplicationnew', + name='extension_new_total_days', + field=models.DecimalField(blank=True, decimal_places=1, max_digits=5, null=True), + ), + migrations.AddField( + model_name='leaveapplicationnew', + name='extension_decision_remarks', + field=models.TextField(blank=True), + ), + ] diff --git a/FusionIIIT/applications/hr2/migrations/0006_station_leave_selection.py b/FusionIIIT/applications/hr2/migrations/0006_station_leave_selection.py new file mode 100644 index 000000000..53149b7f6 --- /dev/null +++ b/FusionIIIT/applications/hr2/migrations/0006_station_leave_selection.py @@ -0,0 +1,20 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hr2', '0005_leave_extension'), + ] + + operations = [ + migrations.AddField( + model_name='leaveapplicationnew', + name='station_leave', + field=models.CharField( + blank=True, + choices=[('WITH', 'With Station Leave'), ('WITHOUT', 'Without Station Leave')], + max_length=10, + ), + ), + ] diff --git a/FusionIIIT/applications/hr2/migrations/0007_half_day_cl.py b/FusionIIIT/applications/hr2/migrations/0007_half_day_cl.py new file mode 100644 index 000000000..3757b3f1c --- /dev/null +++ b/FusionIIIT/applications/hr2/migrations/0007_half_day_cl.py @@ -0,0 +1,25 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hr2', '0006_station_leave_selection'), + ] + + operations = [ + migrations.AddField( + model_name='leaveapplicationnew', + name='is_half_day', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='leaveapplicationnew', + name='half_day_slot', + field=models.CharField( + blank=True, + choices=[('AM', 'AM'), ('PM', 'PM')], + max_length=2, + ), + ), + ] diff --git a/FusionIIIT/applications/hr2/migrations/0008_station_leave_length.py b/FusionIIIT/applications/hr2/migrations/0008_station_leave_length.py new file mode 100644 index 000000000..2b38ec128 --- /dev/null +++ b/FusionIIIT/applications/hr2/migrations/0008_station_leave_length.py @@ -0,0 +1,20 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hr2', '0007_half_day_cl'), + ] + + operations = [ + migrations.AlterField( + model_name='leaveapplicationnew', + name='station_leave', + field=models.CharField( + blank=True, + choices=[('WITH', 'With Station Leave'), ('WITHOUT', 'Without Station Leave'), ('NOT_REQUIRED', 'Not Required')], + max_length=12, + ), + ), + ] diff --git a/FusionIIIT/applications/hr2/migrations/0009_leave_resumption.py b/FusionIIIT/applications/hr2/migrations/0009_leave_resumption.py new file mode 100644 index 000000000..11ea10ab8 --- /dev/null +++ b/FusionIIIT/applications/hr2/migrations/0009_leave_resumption.py @@ -0,0 +1,50 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hr2', '0008_station_leave_length'), + ] + + operations = [ + migrations.AddField( + model_name='leaveapplicationnew', + name='resumption_status', + field=models.CharField( + choices=[('NOT_REQUESTED', 'Not Requested'), ('SUBMITTED', 'Submitted'), ('APPROVED', 'Approved'), ('REJECTED', 'Rejected')], + default='NOT_REQUESTED', + max_length=20, + ), + ), + migrations.AddField( + model_name='leaveapplicationnew', + name='resumption_date', + field=models.DateField(blank=True, null=True), + ), + migrations.AddField( + model_name='leaveapplicationnew', + name='resumption_reason', + field=models.TextField(blank=True), + ), + migrations.AddField( + model_name='leaveapplicationnew', + name='resumption_submitted_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='leaveapplicationnew', + name='resumption_decided_at', + field=models.DateTimeField(blank=True, null=True), + ), + migrations.AddField( + model_name='leaveapplicationnew', + name='resumption_current_approver_role', + field=models.CharField(blank=True, max_length=50), + ), + migrations.AddField( + model_name='leaveapplicationnew', + name='resumption_decision_remarks', + field=models.TextField(blank=True), + ), + ] diff --git a/FusionIIIT/applications/hr2/models.py b/FusionIIIT/applications/hr2/models.py index e225ca076..d14a60269 100644 --- a/FusionIIIT/applications/hr2/models.py +++ b/FusionIIIT/applications/hr2/models.py @@ -1,10 +1,13 @@ from django.db import models -from applications.globals.models import ExtraInfo -from django.core.validators import MaxValueValidator, MinValueValidator +from applications.globals.models import ExtraInfo, Designation, DepartmentInfo, Faculty, Staff +# from django.core.validators import MaxValueValidator, MinValueValidator from django.contrib.auth.models import User +from datetime import date +from applications.filetracking.models import File + +# ==================== EXISTING MODELS (KEPT FOR BACKWARD COMPATIBILITY) ==================== class Constants: - # Class for various choices on the enumerations GENDER_CHOICES = ( ('M', 'Male'), ('F', 'Female'), @@ -22,15 +25,12 @@ class Constants: ('OBC', 'OBC'), ('GENERAL', 'GENERAL'), ('PWD', 'PWD'), - ) MARITIAL_STATUS = ( ('MARRIED', 'MARRIED'), ('UN-MARRIED', 'UN-MARRIED'), ('WIDOW', 'WIDOW'), - ) - BLOOD_GROUP = ( ('AB+', 'AB+'), ('O+', 'O+'), @@ -40,7 +40,6 @@ class Constants: ('O-', 'O-'), ('A+', 'A+'), ('A-', 'A-'), - ) FOREIGN_SERVICE = ( ('LIEN', 'LIEN'), @@ -48,67 +47,91 @@ class Constants: ('OTHER', 'OTHER'), ) - - -# Employee model +# Employee Table class Employee(models.Model): - """ - table for employee details - """ - extra_info = models.OneToOneField(ExtraInfo, on_delete=models.CASCADE) - father_name = models.CharField(max_length=40, default='') - mother_name = models.CharField(max_length=40, default='') - religion = models.CharField(max_length=40, default='') - category = models.CharField(max_length=50, null=False, choices=Constants.CATEGORY) - cast = models.CharField(max_length=40, default='') - home_state = models.CharField(max_length=40, default='') - home_district = models.CharField(max_length=40, default='') - date_of_joining = models.DateField(null=True, blank=True) - designation = models.CharField(max_length=40, default='') - blood_group = models.CharField( - max_length=50, choices=Constants.BLOOD_GROUP) + id = models.OneToOneField(User, on_delete=models.CASCADE, related_name='employee_details', primary_key=True) + father_name = models.CharField(max_length=100) + mother_name = models.CharField(max_length=100) + religion = models.CharField(max_length=20, null=True, blank=True) + CATEGORY_CHOICES = [ + ('General', 'General'), + ('OBC', 'OBC'), + ('SC', 'SC'), + ('ST', 'ST'), + ] + category = models.CharField(max_length=20, choices=CATEGORY_CHOICES) + caste = models.CharField(max_length=50) + home_state = models.CharField(max_length=50) + home_district = models.CharField(max_length=50) + full_address = models.TextField() + date_of_joining = models.DateField() + date_of_birth = models.DateField() + BLOOD_GROUP_CHOICES = [ + ('A+', 'A+'), + ('A-', 'A-'), + ('B+', 'B+'), + ('B-', 'B-'), + ('O+', 'O+'), + ('O-', 'O-'), + ('AB+', 'AB+'), + ('AB-', 'AB-'), + ] + blood_group = models.CharField(max_length=3, choices=BLOOD_GROUP_CHOICES) + phone_number = models.CharField(max_length=15) + personal_email = models.EmailField() + emergency_contact_number = models.CharField(max_length=15) + emergency_contact_name = models.CharField(max_length=100) + Employee_Type = [ + ('Faculty', 'Faculty'), + ('Staff', 'Staff'), + ('Other', 'Other'), + ] + employee_type = models.CharField(max_length=10, choices=Employee_Type, default='Faculty') def __str__(self): - return self.extra_info.user.first_name - + return f"{self.id.username} - Employee Details" -# table for employee confidential details +# Employee Confidential Table class EmpConfidentialDetails(models.Model): - """ - table for employee confidential details - """ - extra_info = models.OneToOneField(ExtraInfo, on_delete=models.CASCADE) - aadhar_no = models.BigIntegerField(default=0, - validators=[MaxValueValidator(999999999999),MinValueValidator(99999999999)]) - - maritial_status = models.CharField( - max_length=50, null=False, choices=Constants.MARITIAL_STATUS) - bank_account_no = models.IntegerField(default=0) - salary = models.IntegerField(default=0) + id = models.AutoField(primary_key=True) + empid = models.OneToOneField(Employee, on_delete=models.CASCADE, related_name='confidential_details') + aadhar_number = models.CharField(max_length=12, unique=True) + pan_number = models.CharField(max_length=10, unique=True) + MARITAL_STATUS_CHOICES = [ + ('Single', 'Single'), + ('Married', 'Married'), + ('Divorced', 'Divorced'), + ('Widowed', 'Widowed'), + ] + marital_status = models.CharField(max_length=10, choices=MARITAL_STATUS_CHOICES) + personal_file_number = models.CharField(max_length=50, unique=True) + bank_account_number = models.CharField(max_length=20, unique=True) + ifsc_code = models.CharField(max_length=20, null=True) + basic_pay = models.DecimalField(max_digits=10, decimal_places=2) def __str__(self): - return self.extra_info.user.first_name - -# table for employee's dependent details - + return f"Confidential Details of {self.empid.id.username}" +# Employee Dependents Table class EmpDependents(models.Model): - """Table for employee's dependent details """ - extra_info = models.OneToOneField(ExtraInfo, on_delete=models.CASCADE) - name = models.CharField(max_length=100, default='') - gender = models.CharField(max_length=50, choices=Constants.GENDER_CHOICES) - dob = models.DateField(max_length=6, null=True) - relationship = models.CharField(max_length=40, default='') + id = models.AutoField(primary_key=True) + empid = models.ForeignKey(Employee, on_delete=models.CASCADE, related_name='dependents') + name = models.CharField(max_length=100) + GENDER_CHOICES = [ + ('Male', 'Male'), + ('Female', 'Female'), + ('Other', 'Other'), + ] + gender = models.CharField(max_length=10, choices=GENDER_CHOICES) + relation = models.CharField(max_length=50) + contact_number = models.CharField(max_length=15) + contact_email = models.EmailField(null=True, blank=True) + date_of_birth = models.DateField() def __str__(self): - return self.extra_info.user.first_name - + return f"Dependent {self.name} of {self.empid.id.username}" class ForeignService(models.Model): - """ - This table contains details about deputation, lien - and other foreign services of employee - """ extra_info = models.ForeignKey(ExtraInfo, on_delete=models.CASCADE) start_date = models.DateField(max_length=6, null=True, blank=True) end_date = models.DateField(max_length=6, null=True, blank=True) @@ -117,26 +140,19 @@ class ForeignService(models.Model): description = models.CharField(max_length=300, default='') salary_source = models.CharField(max_length=100, default='') designation = models.CharField(max_length=100, default='') - # award_name = models.CharField(max_length=100, default='') - # award_type = models.CharField(max_length=100, default='') - # achievement_date = models.CharField(max_length=100, default='') - service_type = models.CharField( - max_length=100, choices=Constants.FOREIGN_SERVICE) + service_type = models.CharField(max_length=100, choices=Constants.FOREIGN_SERVICE) def __str__(self): return self.extra_info.user.first_name - class EmpAppraisalForm(models.Model): extra_info = models.ForeignKey(ExtraInfo, on_delete=models.CASCADE) year = models.DateField(max_length=6, null=True, blank=True) - appraisal_form = models.FileField( - upload_to='Hr2/appraisal_form', null=True, default=" ") + appraisal_form = models.FileField(upload_to='Hr2/appraisal_form', null=True, default=" ") def __str__(self): return self.extra_info.user.first_name - class WorkAssignemnt(models.Model): extra_info = models.ForeignKey(ExtraInfo, on_delete=models.CASCADE) start_date = models.DateField(max_length=6, null=True, blank=True) @@ -148,28 +164,28 @@ class LTCform(models.Model): id = models.AutoField(primary_key=True) employeeId = models.IntegerField() name = models.CharField(max_length=100, null=True) - blockYear = models.TextField() # + blockYear = models.TextField() pfNo = models.IntegerField() basicPaySalary = models.IntegerField(null=True) designation = models.CharField(max_length=50) departmentInfo = models.CharField(max_length=50) - leaveRequired = models.BooleanField(default=False,null=True) # + leaveRequired = models.BooleanField(default=False, null=True) leaveStartDate = models.DateField(null=True, blank=True) leaveEndDate = models.DateField(null=True, blank=True) - dateOfDepartureForFamily = models.DateField(null=True, blank=True) # - natureOfLeave = models.TextField(null=True,blank=True) - purposeOfLeave = models.TextField(null=True,blank=True) + dateOfDepartureForFamily = models.DateField(null=True, blank=True) + natureOfLeave = models.TextField(null=True, blank=True) + purposeOfLeave = models.TextField(null=True, blank=True) hometownOrNot = models.BooleanField(default=False) - placeOfVisit = models.TextField(max_length=100, null=True, blank=True) + placeOfVisit = models.TextField(max_length=100, null=True, blank=True) addressDuringLeave = models.TextField(null=True) - modeofTravel = models.TextField(max_length=10, null=True,blank=True) # - detailsOfFamilyMembersAlreadyDone = models.JSONField(null=True,blank=True) - detailsOfFamilyMembersAboutToAvail = models.JSONField(max_length=100, null=True,blank=True) - detailsOfDependents = models.JSONField(blank=True,null=True) + modeofTravel = models.TextField(max_length=10, null=True, blank=True) + detailsOfFamilyMembersAlreadyDone = models.JSONField(null=True, blank=True) + detailsOfFamilyMembersAboutToAvail = models.JSONField(max_length=100, null=True, blank=True) + detailsOfDependents = models.JSONField(blank=True, null=True) amountOfAdvanceRequired = models.IntegerField(null=True, blank=True) - certifiedThatFamilyDependents = models.BooleanField(blank=True,null=True) - certifiedThatAdvanceTakenOn = models.DateField(null=True, blank=True) - adjustedMonth = models.TextField(max_length=50, null=True,blank=True) + certifiedThatFamilyDependents = models.BooleanField(blank=True, null=True) + certifiedThatAdvanceTakenOn = models.DateField(null=True, blank=True) + adjustedMonth = models.TextField(max_length=50, null=True, blank=True) submissionDate = models.DateField(null=True) phoneNumberForContact = models.BigIntegerField() approved = models.BooleanField(null=True) @@ -177,63 +193,206 @@ class LTCform(models.Model): created_by = models.ForeignKey(User, on_delete=models.CASCADE, null=True, related_name='LTC_created_by') approved_by = models.ForeignKey(User, on_delete=models.CASCADE, null=True, related_name='LTC_approved_by') - - class CPDAAdvanceform(models.Model): id = models.AutoField(primary_key=True) employeeId = models.IntegerField(null=True) - name = models.CharField(max_length=40,null=True) - designation = models.CharField(max_length=40,null=True) + name = models.CharField(max_length=40, null=True) + designation = models.CharField(max_length=40, null=True) pfNo = models.IntegerField(null=True) purpose = models.TextField(max_length=40, null=True) amountRequired = models.IntegerField(null=True) - advanceDueAdjustment = models.DecimalField(max_digits=10, decimal_places=2, null=True,blank=True) - + advanceDueAdjustment = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) submissionDate = models.DateField(blank=True, null=True) - balanceAvailable = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True) advanceAmountPDA = models.DecimalField(max_digits=10, decimal_places=2, blank=True, null=True) amountCheckedInPDA = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) - approved = models.BooleanField(null=True) approvedDate = models.DateField(auto_now_add=True, null=True) created_by = models.ForeignKey(User, on_delete=models.CASCADE, null=True, related_name='CPDA_created_by') - approved_by = models.ForeignKey(User, on_delete=models.CASCADE, null=True, related_name='CPDA_approved_by') + approved_by = models.ForeignKey(User, on_delete=models.CASCADE, null=True, blank=True, related_name='CPDA_approved_by') -class LeaveForm(models.Model): +class CPDAReimbursementform(models.Model): id = models.AutoField(primary_key=True) employeeId = models.IntegerField(null=True) - name = models.CharField(max_length=40,null=True) - designation = models.CharField(max_length=40,null=True) + name = models.CharField(max_length=50) + designation = models.CharField(max_length=50) + pfNo = models.IntegerField() + advanceTaken = models.IntegerField() + purpose = models.TextField() + adjustmentSubmitted = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) + balanceAvailable = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) + advanceDueAdjustment = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) + advanceAmountPDA = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) + amountCheckedInPDA = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) submissionDate = models.DateField(blank=True, null=True) - pfNo = models.IntegerField(null=True) - departmentInfo = models.CharField(max_length=40,null=True) - natureOfLeave = models.TextField(max_length=40,null=True) + approved = models.BooleanField(null=True) + approvedDate = models.DateField(auto_now_add=True, null=True) + created_by = models.ForeignKey(User, on_delete=models.CASCADE, null=True, related_name='CPDAR_created_by') + approved_by = models.ForeignKey(User, on_delete=models.CASCADE, null=True, related_name='CPDAR_approved_by') + +# Leave Application Table (old) +class LeaveForm(models.Model): + STATUS_CHOICES = [ + ('Accepted', 'Accepted'), + ('Pending', 'Pending'), + ('Rejected', 'Rejected'), + ] + Application_type_choices = [ + ('Online', 'Online'), + ('Offline', 'Offline'), + ] + + id = models.AutoField(primary_key=True) + employee = models.ForeignKey(Employee, on_delete=models.CASCADE, related_name='leave_applications') + name = models.CharField(max_length=40, null=True) + designation = models.CharField(max_length=40, null=True) + submissionDate = models.DateField(default=date.today) + personalfileNo = models.CharField(max_length=50, null=True) + departmentInfo = models.CharField(max_length=40, null=True) leaveStartDate = models.DateField(blank=True, null=True) leaveEndDate = models.DateField(blank=True, null=True) - - purposeOfLeave = models.TextField(max_length=40,null=True) - addressDuringLeave = models.TextField(max_length=40, blank=True, null=True) - academicResponsibility = models.TextField(max_length=40, blank=True, null=True) - addministrativeResponsibiltyAssigned = models.TextField(max_length=40,null=True) + Noof_CasualLeave = models.IntegerField(default=0) + Noof_specialCasualLeave = models.IntegerField(default=0) + Noof_earnedLeave = models.IntegerField(default=0) + Noof_commutedLeave = models.IntegerField(default=0) + Noof_restrictedHoliday = models.IntegerField(default=0) + Noof_vacationLeave = models.IntegerField(default=0) + + Noof_maternityLeave = models.IntegerField(default=0) + Noof_childCareLeave = models.IntegerField(default=0) + Noof_paternityLeave = models.IntegerField(default=0) + Noof_halfPayLeave = models.IntegerField(default=0) + + LeavingStation = models.BooleanField(default=False) + StationLeave_startdate = models.DateField(blank=True, null=True) + StationLeave_enddate = models.DateField(blank=True, null=True) + Address_During_StationLeave = models.TextField(null=True, blank=True) + Purpose_of_leave = models.TextField(null=True, blank=True) + + AcademicResponsibility_user = models.ForeignKey( + Employee, + on_delete=models.SET_NULL, + null=True, + related_name='academic_responsibility_user' + ) + AcademicResponsibility_designation = models.ForeignKey(Designation, on_delete=models.CASCADE, null=True, related_name='leave_academic_responsibility_designation') + AcademicResponsibility_status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='Pending') + + AdministrativeResponsibility_user = models.ForeignKey( + Employee, + on_delete=models.SET_NULL, + null=True, + related_name='administrative_responsibility_user' + ) + AdministrativeResponsibility_designation = models.ForeignKey(Designation, on_delete=models.CASCADE, null=True, related_name='leave_administrative_responsibility_designation') + AdministrativeResponsibility_status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='Pending') + + Remarks = models.TextField(null=True, blank=True) - approved = models.BooleanField(null=True) approvedDate = models.DateField(auto_now_add=True, null=True) - created_by = models.ForeignKey(User, on_delete=models.CASCADE, null=True, related_name='Leave_created_by') - approved_by = models.ForeignKey(User, on_delete=models.CASCADE, null=True, related_name='Leave_approved_by') + approved_by = models.ForeignKey(Employee, on_delete=models.CASCADE, null=True, related_name='leave_approved_by') + approved_by_designation = models.ForeignKey(Designation, on_delete=models.CASCADE, null=True, related_name='leave_approved_by_designation') + + first_recieved_by = models.ForeignKey(Employee, on_delete=models.CASCADE, null=True, related_name='leave_first_recieved_by') + first_recieved_designation = models.ForeignKey(Designation, on_delete=models.CASCADE, null=True, related_name='leave_first_recieved_designation') + + status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='Pending') + attached_pdf = models.BinaryField(null=True, blank=True) + attached_pdf_name = models.CharField(max_length=100, null=True, blank=True) + file_id = models.IntegerField(null=True, blank=True) + application_type = models.CharField(max_length=10, choices=Application_type_choices, default='Online') + + def __str__(self): + return f"Leave Application {self.id} - {self.employee.id.username}" + +class LeaveClaim(models.Model): + STATUS_CHOICES = [ + ('Accepted', 'Accepted'), + ('Pending', 'Pending'), + ('Rejected', 'Rejected'), + ] + APPLICATION_TYPE_CHOICES = [ + ('Online', 'Online'), + ('Offline', 'Offline'), + ] -class LeaveBalance(models.Model): id = models.AutoField(primary_key=True) - employeeId = models.OneToOneField(ExtraInfo, on_delete=models.CASCADE) - casualLeave = models.IntegerField(default=0) - specialCasualLeave = models.IntegerField(default=0) - earnedLeave = models.IntegerField(default=0) - commutedLeave = models.IntegerField(default=0) - restrictedHoliday = models.IntegerField(default=0) - stationLeave = models.IntegerField(default=0) - vacationLeave = models.IntegerField(default=0) + leave_form = models.ForeignKey(LeaveForm, on_delete=models.CASCADE, related_name='leave_claims') + claim_date = models.DateField(default=date.today) + + leaveStartDate = models.DateField(blank=True, null=True) + leaveEndDate = models.DateField(blank=True, null=True) + + Noof_CasualLeave = models.IntegerField(default=0) + Noof_specialCasualLeave = models.IntegerField(default=0) + Noof_earnedLeave = models.IntegerField(default=0) + Noof_commutedLeave = models.IntegerField(default=0) + Noof_restrictedHoliday = models.IntegerField(default=0) + Noof_vacationLeave = models.IntegerField(default=0) + Noof_maternityLeave = models.IntegerField(default=0) + Noof_childCareLeave = models.IntegerField(default=0) + Noof_paternityLeave = models.IntegerField(default=0) + Noof_halfPayLeave = models.IntegerField(default=0) + + remarks = models.TextField(null=True, blank=True) + approvedDate = models.DateField(auto_now_add=True, null=True) + approved_by = models.ForeignKey( + Employee, + on_delete=models.CASCADE, + null=True, + related_name='leave_claim_approved_by' + ) + approved_by_designation = models.ForeignKey( + Designation, + on_delete=models.CASCADE, + null=True, + related_name='leave_claim_approved_by_designation' + ) + + status = models.CharField(max_length=10, choices=STATUS_CHOICES, default='Pending') + attached_pdf = models.BinaryField(null=True, blank=True) + attached_pdf_name = models.CharField(max_length=100, null=True, blank=True) + file_id = models.IntegerField(null=True, blank=True) + application_type = models.CharField(max_length=10, choices=APPLICATION_TYPE_CHOICES, default='Online') + + def __str__(self): + return f"Leave Claim {self.id} for Form {self.leave_form.id}" + + class Meta: + verbose_name = "Leave Claim" + verbose_name_plural = "Leave Claims" + +class LeaveBalance(models.Model): + empid = models.OneToOneField(Employee, on_delete=models.CASCADE, related_name='leave_balance', primary_key=True) + casual_leave_taken = models.IntegerField(default=0) + special_casual_leave_taken = models.IntegerField(default=0) + earned_leave_taken = models.IntegerField(default=0) + half_pay_leave_taken = models.IntegerField(default=0) + maternity_leave_taken = models.IntegerField(default=0) + child_care_leave_taken = models.IntegerField(default=0) + paternity_leave_taken = models.IntegerField(default=0) + leave_encashment_taken = models.IntegerField(default=0) + restricted_holiday_taken = models.IntegerField(default=0) + + def __str__(self): + return f"Leave Balance for {self.empid.id.username}" + +class LeavePerYear(models.Model): + empid = models.OneToOneField(Employee, on_delete=models.CASCADE, related_name='yearly_leave', primary_key=True) + casual_leave = models.IntegerField(default=8) + special_casual_leave = models.IntegerField(default=15) + earned_leave = models.IntegerField(default=15) + half_pay_leave = models.IntegerField(default=15) + maternity_leave = models.IntegerField(default=180) + child_care_leave = models.IntegerField(default=730) + paternity_leave = models.IntegerField(default=15) + leave_encashment = models.IntegerField(default=60) + restricted_holiday = models.IntegerField(default=2) + + def __str__(self): + return f"Yearly Leave Allotment for {self.empid.id.username}" class Appraisalform(models.Model): id = models.AutoField(primary_key=True) @@ -264,33 +423,548 @@ class Appraisalform(models.Model): otherContribution = models.TextField(max_length=40, null=True) performanceComments = models.TextField(max_length=100, null=True) submissionDate = models.DateField(max_length=6, null=True) - approved = models.BooleanField(null=True) approvedDate = models.DateField(auto_now_add=True, null=True) created_by = models.ForeignKey(User, on_delete=models.CASCADE, null=True, related_name='Appraisal_created_by') approved_by = models.ForeignKey(User, on_delete=models.CASCADE, null=True, related_name='Appraisal_approved_by') -class CPDAReimbursementform(models.Model): - id = models.AutoField(primary_key=True) - employeeId = models.IntegerField(null=True) - name = models.CharField(max_length=50) - designation = models.CharField(max_length=50) - pfNo = models.IntegerField() - advanceTaken = models.IntegerField() - purpose = models.TextField() - adjustmentSubmitted = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) - balanceAvailable = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) - advanceDueAdjustment = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) - advanceAmountPDA = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) - amountCheckedInPDA = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) - submissionDate = models.DateField(auto_now_add=True) - approved = models.BooleanField(null=True) - approvedDate = models.DateField(auto_now_add=True, null=True) - created_by = models.ForeignKey(User, on_delete=models.CASCADE, null=True, related_name='CPDAR_created_by') - approved_by = models.ForeignKey(User, on_delete=models.CASCADE, null=True, related_name='CPDAR_approved_by') - +# ==================== NEW MODELS FOR REST API (DO NOT OVERLAP) ==================== + +class EmployeeCategory(models.Model): + CATEGORY_TYPE = [ + ('TEACHING', 'Teaching'), + ('NON_TEACHING', 'Non-Teaching'), + ] + name = models.CharField(max_length=100) + category_type = models.CharField(max_length=20, choices=CATEGORY_TYPE) + pay_level = models.CharField(max_length=20, blank=True) + is_active = models.BooleanField(default=True) + + def __str__(self): + return self.name + +class EmployeeDetailsExtended(models.Model): + MARITAL_STATUS = [ + ('SINGLE', 'Single'), + ('MARRIED', 'Married'), + ('WIDOWED', 'Widowed'), + ('DIVORCED', 'Divorced'), + ] + EMPLOYEE_STATUS = [ + ('ACTIVE', 'Active'), + ('ON_LEAVE', 'On Leave'), + ('DEPUTATION', 'On Deputation'), + ('SUSPENDED', 'Suspended'), + ('RETIRED', 'Retired'), + ('RESIGNED', 'Resigned'), + ('TERMINATED', 'Terminated'), + ] + + extra_info = models.OneToOneField(ExtraInfo, on_delete=models.CASCADE, related_name='employee_details_extended') + category = models.ForeignKey(EmployeeCategory, on_delete=models.PROTECT) + + father_name = models.CharField(max_length=100, blank=True) + mother_name = models.CharField(max_length=100, blank=True) + spouse_name = models.CharField(max_length=100, blank=True) + marital_status = models.CharField(max_length=20, choices=MARITAL_STATUS, blank=True) + + pan_number = models.CharField(max_length=15, blank=True) + aadhar_number = models.CharField(max_length=15, blank=True) + passport_number = models.CharField(max_length=20, blank=True) + + date_of_joining = models.DateField(null=True, blank=True) + date_of_superannuation = models.DateField(null=True, blank=True) + appointment_type = models.CharField(max_length=50, blank=True) + + employee_status = models.CharField(max_length=20, choices=EMPLOYEE_STATUS, default='ACTIVE') + + bank_name = models.CharField(max_length=100, blank=True) + bank_account = models.CharField(max_length=30, blank=True) + ifsc_code = models.CharField(max_length=15, blank=True) + + emergency_contact_name = models.CharField(max_length=100, blank=True) + emergency_contact_phone = models.CharField(max_length=15, blank=True) + emergency_contact_relation = models.CharField(max_length=50, blank=True) + + def __str__(self): + return f"{self.extra_info.user.username} - EmployeeDetailsExtended" + +class ServiceHistory(models.Model): + employee = models.ForeignKey(ExtraInfo, on_delete=models.CASCADE, related_name='service_history') + designation = models.ForeignKey(Designation, on_delete=models.PROTECT) + department = models.ForeignKey(DepartmentInfo, on_delete=models.PROTECT) + from_date = models.DateField() + to_date = models.DateField(null=True, blank=True) + pay_scale = models.CharField(max_length=50, blank=True) + basic_pay = models.DecimalField(max_digits=12, decimal_places=2, null=True, blank=True) + remarks = models.TextField(blank=True) + + class Meta: + ordering = ['-from_date'] + +class QualificationType(models.Model): + name = models.CharField(max_length=100) + level = models.IntegerField() # 1=School, 2=UG, 3=PG, 4=Doctoral + + def __str__(self): + return self.name + +class EducationalQualification(models.Model): + employee = models.ForeignKey(ExtraInfo, on_delete=models.CASCADE, related_name='qualifications') + qualification_type = models.ForeignKey(QualificationType, on_delete=models.PROTECT) + degree = models.CharField(max_length=100) + specialization = models.CharField(max_length=200, blank=True) + institution = models.CharField(max_length=200) + university = models.CharField(max_length=200, blank=True) + year_of_passing = models.IntegerField() + division_grade = models.CharField(max_length=50, blank=True) + document = models.FileField(upload_to='hr/qualifications/', blank=True) + +class ProfessionalQualification(models.Model): + employee = models.ForeignKey(ExtraInfo, on_delete=models.CASCADE, related_name='professional_qualifications') + title = models.CharField(max_length=200) + certifying_body = models.CharField(max_length=200) + date_obtained = models.DateField() + valid_until = models.DateField(null=True, blank=True) + document = models.FileField(upload_to='hr/professional/', blank=True) + +class PreviousExperience(models.Model): + employee = models.ForeignKey(ExtraInfo, on_delete=models.CASCADE, related_name='previous_experiences') + organization = models.CharField(max_length=200) + designation = models.CharField(max_length=100) + from_date = models.DateField() + to_date = models.DateField() + experience_type = models.CharField(max_length=50) # Teaching, Industry, Research + description = models.TextField(blank=True) + document = models.FileField(upload_to='hr/experience/', blank=True) + +class LeaveType(models.Model): + name = models.CharField(max_length=50) # CL, EL, HPL, etc. + code = models.CharField(max_length=10, unique=True) + max_days_per_year = models.IntegerField(null=True, blank=True) + carry_forward = models.BooleanField(default=False) + max_carry_forward = models.IntegerField(null=True, blank=True) + is_active = models.BooleanField(default=True) + def __str__(self): + return f"{self.name} ({self.code})" + +class EmployeeLeaveBalance(models.Model): + employee = models.ForeignKey(ExtraInfo, on_delete=models.CASCADE, related_name='leave_balances_new') + leave_type = models.ForeignKey(LeaveType, on_delete=models.PROTECT) + year = models.IntegerField() + opening_balance = models.DecimalField(max_digits=5, decimal_places=1, default=0) + accrued = models.DecimalField(max_digits=5, decimal_places=1, default=0) + availed = models.DecimalField(max_digits=5, decimal_places=1, default=0) + current_balance = models.DecimalField(max_digits=5, decimal_places=1, default=0) + + class Meta: + unique_together = ['employee', 'leave_type', 'year'] + +class LeaveApplicationNew(models.Model): + STATUS_CHOICES = [ + ('PENDING', 'Pending'), + ('FORWARDED', 'Forwarded'), + ('APPROVED', 'Approved'), + ('REJECTED', 'Rejected'), + ('WITHDRAWN', 'Withdrawn'), + ('CANCELLED', 'Cancelled'), + ] + + LEAVE_TYPE_CHOICES = [ + ('Casual', 'Casual'), + ('Restricted', 'Restricted'), + ('Medical', 'Medical'), + ('Earned', 'Earned'), + ('Vacation', 'Vacation'), + ('Sabbatical', 'Sabbatical'), + ] + + employee = models.ForeignKey(ExtraInfo, on_delete=models.CASCADE, related_name='leave_applications_new') + employee_name = models.CharField(max_length=100) + department = models.CharField(max_length=100) + designation = models.CharField(max_length=100) + + leave_type = models.CharField(max_length=30, choices=LEAVE_TYPE_CHOICES) + start_date = models.DateField() + end_date = models.DateField() + total_days = models.DecimalField(max_digits=5, decimal_places=1) + reason = models.TextField() + station_leave = models.CharField( + max_length=12, + choices=[('WITH', 'With Station Leave'), ('WITHOUT', 'Without Station Leave'), ('NOT_REQUIRED', 'Not Required')], + blank=True, + ) + is_half_day = models.BooleanField(default=False) + half_day_slot = models.CharField( + max_length=2, + choices=[('AM', 'AM'), ('PM', 'PM')], + blank=True, + ) + contact_during_leave = models.CharField(max_length=15) + address_during_leave = models.TextField() + handover_to = models.CharField(max_length=100, blank=True) + handover_notes = models.TextField(blank=True) + nominee_status = models.CharField( + max_length=20, + choices=[('NOT_REQUIRED', 'Not Required'), ('PENDING', 'Pending'), ('ACCEPTED', 'Accepted'), ('DECLINED', 'Declined')], + default='NOT_REQUIRED', + ) + nominee_responded_at = models.DateTimeField(null=True, blank=True) + + medical_certificate = models.CharField(max_length=200, blank=True) + attachment_file = models.CharField(max_length=200, blank=True) + + applied_date = models.DateField(auto_now_add=True) + leave_balance_before = models.DecimalField(max_digits=5, decimal_places=1, null=True, blank=True) + leave_balance_after = models.DecimalField(max_digits=5, decimal_places=1, null=True, blank=True) + approval_status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='PENDING') + current_approver_role = models.CharField(max_length=50, blank=True) + remarks = models.TextField(blank=True) + + document_request_message = models.TextField(blank=True) + document_request_status = models.CharField( + max_length=20, + choices=[('NOT_REQUESTED', 'Not Requested'), ('REQUESTED', 'Requested'), ('SUBMITTED', 'Submitted')], + default='NOT_REQUESTED', + ) + document_requested_at = models.DateTimeField(null=True, blank=True) + document_submission = models.TextField(blank=True) + document_submitted_at = models.DateTimeField(null=True, blank=True) + + cancel_status = models.CharField( + max_length=20, + choices=[('NOT_REQUESTED', 'Not Requested'), ('REQUESTED', 'Requested'), ('APPROVED', 'Approved'), ('REJECTED', 'Rejected')], + default='NOT_REQUESTED', + ) + cancel_requested_at = models.DateTimeField(null=True, blank=True) + cancel_decided_at = models.DateTimeField(null=True, blank=True) + cancel_requested_by_role = models.CharField(max_length=50, blank=True) + cancel_current_approver_role = models.CharField(max_length=50, blank=True) + cancel_reason = models.TextField(blank=True) + cancel_decision_remarks = models.TextField(blank=True) + + extension_status = models.CharField( + max_length=20, + choices=[('NOT_REQUESTED', 'Not Requested'), ('REQUESTED', 'Requested'), ('APPROVED', 'Approved'), ('REJECTED', 'Rejected')], + default='NOT_REQUESTED', + ) + extension_requested_at = models.DateTimeField(null=True, blank=True) + extension_decided_at = models.DateTimeField(null=True, blank=True) + extension_requested_by_role = models.CharField(max_length=50, blank=True) + extension_current_approver_role = models.CharField(max_length=50, blank=True) + extension_reason = models.TextField(blank=True) + extension_new_end_date = models.DateField(null=True, blank=True) + extension_new_total_days = models.DecimalField(max_digits=5, decimal_places=1, null=True, blank=True) + extension_decision_remarks = models.TextField(blank=True) + + resumption_status = models.CharField( + max_length=20, + choices=[('NOT_REQUESTED', 'Not Requested'), ('SUBMITTED', 'Submitted'), ('APPROVED', 'Approved'), ('REJECTED', 'Rejected')], + default='NOT_REQUESTED', + ) + resumption_date = models.DateField(null=True, blank=True) + resumption_reason = models.TextField(blank=True) + resumption_submitted_at = models.DateTimeField(null=True, blank=True) + resumption_decided_at = models.DateTimeField(null=True, blank=True) + resumption_current_approver_role = models.CharField(max_length=50, blank=True) + resumption_decision_remarks = models.TextField(blank=True) + + def __str__(self): + return f"LeaveNew #{self.id} - {self.employee.user.username}" + +class AppraisalPeriod(models.Model): + name = models.CharField(max_length=100) + start_date = models.DateField() + end_date = models.DateField() + submission_deadline = models.DateField() + is_active = models.BooleanField(default=False) + def __str__(self): + return self.name + +class PerformanceAppraisalNew(models.Model): + STATUS_CHOICES = [ + ('DRAFT', 'Draft'), + ('SUBMITTED', 'Submitted'), + ('REVIEWED', 'Reviewed'), + ('APPROVED', 'Approved'), + ('FINALIZED', 'Finalized'), + ] + + employee = models.ForeignKey(ExtraInfo, on_delete=models.CASCADE, related_name='appraisals_new') + period = models.ForeignKey(AppraisalPeriod, on_delete=models.PROTECT) + + teaching_score = models.IntegerField(null=True, blank=True) + research_score = models.IntegerField(null=True, blank=True) + admin_score = models.IntegerField(null=True, blank=True) + extension_score = models.IntegerField(null=True, blank=True) + self_remarks = models.TextField(blank=True) + + reviewer_teaching_score = models.IntegerField(null=True, blank=True) + reviewer_research_score = models.IntegerField(null=True, blank=True) + reviewer_admin_score = models.IntegerField(null=True, blank=True) + reviewer_extension_score = models.IntegerField(null=True, blank=True) + reviewer = models.ForeignKey(ExtraInfo, on_delete=models.SET_NULL, null=True, blank=True, related_name='reviewed_performance_appraisals_new') + reviewer_remarks = models.TextField(blank=True) + + final_score = models.DecimalField(max_digits=5, decimal_places=2, null=True, blank=True) + final_grade = models.CharField(max_length=10, blank=True) + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='DRAFT') + + submitted_at = models.DateTimeField(null=True, blank=True) + finalized_at = models.DateTimeField(null=True, blank=True) + +class TrainingProgram(models.Model): + title = models.CharField(max_length=200) + description = models.TextField() + organizer = models.CharField(max_length=200) + venue = models.CharField(max_length=200) + start_date = models.DateField() + end_date = models.DateField() + max_participants = models.IntegerField(null=True, blank=True) + is_mandatory = models.BooleanField(default=False) + +class TrainingNomination(models.Model): + STATUS_CHOICES = [ + ('NOMINATED', 'Nominated'), + ('APPROVED', 'Approved'), + ('REJECTED', 'Rejected'), + ('ATTENDED', 'Attended'), + ('COMPLETED', 'Completed'), + ] + employee = models.ForeignKey(ExtraInfo, on_delete=models.CASCADE, related_name='training_nominations') + program = models.ForeignKey(TrainingProgram, on_delete=models.CASCADE) + nominated_by = models.ForeignKey(ExtraInfo, on_delete=models.SET_NULL, null=True, related_name='training_nominations_made') + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='NOMINATED') + feedback = models.TextField(blank=True) + certificate = models.FileField(upload_to='hr/training/', blank=True) + +class PromotionApplication(models.Model): + STATUS_CHOICES = [ + ('SUBMITTED', 'Submitted'), + ('UNDER_REVIEW', 'Under Review'), + ('COMMITTEE_STAGE', 'At Committee'), + ('APPROVED', 'Approved'), + ('REJECTED', 'Rejected'), + ] + employee = models.ForeignKey(ExtraInfo, on_delete=models.CASCADE, related_name='promotion_applications') + current_designation = models.ForeignKey(Designation, on_delete=models.PROTECT, related_name='current_for_promotions') + applied_designation = models.ForeignKey(Designation, on_delete=models.PROTECT, related_name='applied_promotions') + application_date = models.DateField() + eligibility_date = models.DateField() + api_score = models.IntegerField(null=True, blank=True) + documents = models.FileField(upload_to='hr/promotions/', blank=True) + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='SUBMITTED') + remarks = models.TextField(blank=True) + approved_date = models.DateField(null=True, blank=True) + effective_date = models.DateField(null=True, blank=True) + +class EmployeeAttendance(models.Model): + ATTENDANCE_STATUS = [ + ('PRESENT', 'Present'), + ('ABSENT', 'Absent'), + ('HALF_DAY', 'Half Day'), + ('ON_LEAVE', 'On Leave'), + ('ON_TOUR', 'On Tour'), + ('WORK_FROM_HOME', 'WFH'), + ] + employee = models.ForeignKey(ExtraInfo, on_delete=models.CASCADE, related_name='attendance_records') + date = models.DateField() + status = models.CharField(max_length=20, choices=ATTENDANCE_STATUS) + in_time = models.TimeField(null=True, blank=True) + out_time = models.TimeField(null=True, blank=True) + remarks = models.TextField(blank=True) + + class Meta: + unique_together = ['employee', 'date'] + +class FacultyWorkload(models.Model): + faculty = models.ForeignKey(Faculty, on_delete=models.CASCADE, related_name='workloads') + semester = models.CharField(max_length=20) + year = models.IntegerField() + lecture_hours = models.IntegerField(default=0) + tutorial_hours = models.IntegerField(default=0) + lab_hours = models.IntegerField(default=0) + total_hours = models.IntegerField(default=0) + total_students = models.IntegerField(default=0) + phd_scholars = models.IntegerField(default=0) + + class Meta: + unique_together = ['faculty', 'semester', 'year'] + +class LTCApplicationNew(models.Model): + STATUS_CHOICES = [ + ('PENDING', 'Pending'), + ('FORWARDED', 'Forwarded'), + ('APPROVED', 'Approved'), + ('REJECTED', 'Rejected'), + ('WITHDRAWN', 'Withdrawn'), + ] + + employee = models.ForeignKey(ExtraInfo, on_delete=models.CASCADE, related_name='ltc_applications_new') + employee_name = models.CharField(max_length=100) + department = models.CharField(max_length=100) + designation = models.CharField(max_length=100) + + ltc_block_year = models.IntegerField() + travel_start_date = models.DateField() + travel_end_date = models.DateField() + destination = models.CharField(max_length=200) + purpose_of_travel = models.TextField() + + family_members = models.TextField(blank=True) + relationship_details = models.TextField(blank=True) + + travel_mode = models.CharField(max_length=50) + ticket_number = models.CharField(max_length=100, blank=True) + ticket_cost = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) + accommodation_cost = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) + other_expenses = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) + total_amount_claimed = models.DecimalField(max_digits=10, decimal_places=2) + + tickets_upload = models.CharField(max_length=200, blank=True) + bills_upload = models.CharField(max_length=200, blank=True) + + previous_ltc_used = models.BooleanField(default=False) + last_ltc_date = models.DateField(null=True, blank=True) + + applied_date = models.DateField(auto_now_add=True) + verified_by_hr = models.BooleanField(default=False) + approval_status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='PENDING') + accountant_status = models.CharField(max_length=20, blank=True) + remarks = models.TextField(blank=True) + + def __str__(self): + return f"LTCNew #{self.id} - {self.employee.user.username}" + +class CPDAAdvanceNew(models.Model): + STATUS_CHOICES = [ + ('PENDING', 'Pending'), + ('FORWARDED', 'Forwarded'), + ('APPROVED', 'Approved'), + ('REJECTED', 'Rejected'), + ('WITHDRAWN', 'Withdrawn'), + ] + + employee = models.ForeignKey(ExtraInfo, on_delete=models.CASCADE, related_name='cpda_advances_new') + employee_name = models.CharField(max_length=100) + department = models.CharField(max_length=100) + designation = models.CharField(max_length=100) + + event_name = models.CharField(max_length=200) + event_type = models.CharField(max_length=50) + organized_by = models.CharField(max_length=200, blank=True) + venue = models.CharField(max_length=200, blank=True) + start_date = models.DateField() + end_date = models.DateField() + + registration_fee = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) + travel_expense = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) + accommodation_expense = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) + other_expenses = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) + total_amount = models.DecimalField(max_digits=10, decimal_places=2) + + purpose_of_attending = models.TextField() + benefits_to_institution = models.TextField() + + invitation_letter = models.CharField(max_length=200, blank=True) + receipts = models.CharField(max_length=200, blank=True) + certificates = models.CharField(max_length=200, blank=True) + + applied_date = models.DateField(auto_now_add=True) + verified_by_hr = models.BooleanField(default=False) + approval_status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='PENDING') + accountant_processing_status = models.CharField(max_length=30, blank=True) + remarks = models.TextField(blank=True) + def __str__(self): + return f"CPDAAdvanceNew #{self.id} - {self.employee.user.username}" + +class CPDAReimbursementNew(models.Model): + STATUS_CHOICES = [ + ('PENDING', 'Pending'), + ('FORWARDED', 'Forwarded'), + ('APPROVED', 'Approved'), + ('REJECTED', 'Rejected'), + ] + + employee = models.ForeignKey(ExtraInfo, on_delete=models.CASCADE, related_name='cpda_reimbursements_new') + employee_name = models.CharField(max_length=100) + department = models.CharField(max_length=100) + designation = models.CharField(max_length=100) + + event_name = models.CharField(max_length=200) + event_type = models.CharField(max_length=50) + organized_by = models.CharField(max_length=200, blank=True) + venue = models.CharField(max_length=200, blank=True) + start_date = models.DateField() + end_date = models.DateField() + + registration_fee = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) + travel_expense = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) + accommodation_expense = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) + other_expenses = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True) + total_amount = models.DecimalField(max_digits=10, decimal_places=2) + + purpose_of_attending = models.TextField() + benefits_to_institution = models.TextField() + + invitation_letter = models.CharField(max_length=200, blank=True) + receipts = models.CharField(max_length=200, blank=True) + certificates = models.CharField(max_length=200, blank=True) + + applied_date = models.DateField(auto_now_add=True) + verified_by_hr = models.BooleanField(default=False) + approval_status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='PENDING') + accountant_processing_status = models.CharField(max_length=30, blank=True) + remarks = models.TextField(blank=True) + + def __str__(self): + return f"CPDAReimbursementNew #{self.id} - {self.employee.user.username}" + +class AppraisalFormNew(models.Model): + STATUS_CHOICES = [ + ('PENDING', 'Pending'), + ('REVIEWED', 'Reviewed'), + ('APPROVED', 'Approved'), + ] + + employee = models.ForeignKey(ExtraInfo, on_delete=models.CASCADE, related_name='appraisal_forms_new') + employee_name = models.CharField(max_length=100) + department = models.CharField(max_length=100) + designation = models.CharField(max_length=100) + appraisal_year = models.CharField(max_length=20) + + self_summary = models.TextField() + key_responsibilities = models.TextField() + achievements = models.TextField() + challenges_faced = models.TextField(blank=True) + + teaching_performance = models.TextField(blank=True) + research_work = models.TextField(blank=True) + publications = models.TextField(blank=True) + projects_handled = models.TextField(blank=True) + administrative_contributions = models.TextField(blank=True) + + trainings_attended = models.TextField(blank=True) + certifications = models.TextField(blank=True) + workshops = models.TextField(blank=True) + + goals_achieved = models.TextField() + future_goals = models.TextField() + + supporting_documents = models.CharField(max_length=200, blank=True) + + reviewer_id = models.CharField(max_length=50, blank=True) + reviewer_comments = models.TextField(blank=True) + rating = models.CharField(max_length=20, blank=True) + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='PENDING') + remarks = models.TextField(blank=True) + + submitted_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"AppraisalNew #{self.id} - {self.employee.user.username}" \ No newline at end of file diff --git a/FusionIIIT/applications/hr2/normal.py b/FusionIIIT/applications/hr2/normal.py deleted file mode 100644 index 0e344ff51..000000000 --- a/FusionIIIT/applications/hr2/normal.py +++ /dev/null @@ -1,36 +0,0 @@ -'block_year': ['232'], - 'pf_no': ['222'], - 'basic_pay_salary': ['2322'], - 'name': ['dsds'], - 'designation':['dsdsd'], - 'department_info': ['ds'], - 'leave_availability': ['True', 'True'], - 'leave_start_date': ['2024-02-22'], - 'leave_end_date': ['2024-02-22'], - 'date_of_leave_for_family': ['2024-02-22'], - 'nature_of_leave': ['dsds'], - 'purpose_of_leave': ['dsdsd'], - 'hometown_or_not': ['True'], - 'place_of_visit': [''], - 'address_during_leave': ['full street address'], - 'details_of_family_members_already_done': ['sds', 'dsd', 'dsd'], - 'info_1_1': ['1'], 'info_1_2': ['dsds'], 'info_1_3': ['12'], - 'info_2_1': ['2'], 'info_2_2': ['sds'], 'info_2_3': ['121'], - 'info_3_1': ['3'], 'info_3_2': ['dsds'], 'info_3_3': ['21'], - 'info_4_1': [''], 'info_4_2': [''], 'info_4_3': [''], - 'info_5_1': [''], 'info_5_2': [''], 'info_5_3': [''], - 'info_6_1': [''], 'info_6_2': [''], 'info_6_3': [''], - - 'd_info_1_1': ['1'], 'd_info_1_2': ['sds'], 'd_info_1_3': ['21'], 'd_info_1_4': ['sdd'], - - 'd_info_2_1': ['2'], 'd_info_2_2': ['dsd'], 'd_info_2_3': ['23'], 'd_info_2_4': ['sds'], - 'd_info_3_1': ['3'], 'd_info_3_2': ['sd'], 'd_info_3_3': ['21'], 'd_info_3_4': ['dds'], - 'd_info_4_1': [''], 'd_info_4_2': [''], 'd_info_4_3': [''], 'd_info_4_4': [''], - 'd_info_5_1': [''], 'd_info_5_2': [''], 'd_info_5_3': [''], 'd_info_5_4': [''], - 'd_info_6_1': [''], 'd_info_6_2': [''], 'd_info_6_3': [''], 'd_info_6_4': [''], - 'amount_of_advance_required': ['211'], - 'certified_family_dependents': ['dqwd'], - 'certified_advance': ['dqwd'], - 'adjusted_month': ['qwdwd'], - 'date': ['2024-02-22'], - 'phone_number_for_contact': ['2312123'] \ No newline at end of file diff --git a/FusionIIIT/applications/hr2/selectors.py b/FusionIIIT/applications/hr2/selectors.py new file mode 100644 index 000000000..0f8113ccb --- /dev/null +++ b/FusionIIIT/applications/hr2/selectors.py @@ -0,0 +1,131 @@ +from datetime import date +from django.db.models import Q +from applications.globals.models import ExtraInfo, HoldsDesignation +from .models import ( + EmployeeLeaveBalance, LeaveApplicationNew, EmployeeAttendance, FacultyWorkload, + PerformanceAppraisalNew, AppraisalPeriod, TrainingProgram, TrainingNomination, + PromotionApplication, LTCApplicationNew, CPDAAdvanceNew, CPDAReimbursementNew, + AppraisalFormNew +) + +# ==================== EMPLOYEE SELECTORS ==================== + +def get_employee_by_id(employee_id): + return ExtraInfo.objects.select_related('user', 'department').get(id=employee_id) + +def get_all_employees(employee_type=None, department_id=None): + qs = ExtraInfo.objects.select_related('user', 'department') + if employee_type: + qs = qs.filter(user_type=employee_type) + if department_id: + qs = qs.filter(department_id=department_id) + return qs + +def get_employee_current_designation(employee_extra_info): + held = HoldsDesignation.objects.filter(working=employee_extra_info).order_by('-held_at').first() + return held.designation if held else None + +# ==================== LEAVE SELECTORS ==================== + +def get_leave_balance_for_employee(employee_extra_info, leave_type, year=None): + if year is None: + year = date.today().year + return EmployeeLeaveBalance.objects.select_related('leave_type').get( + employee=employee_extra_info, + leave_type=leave_type, + year=year + ) + +def get_leave_applications(employee_extra_info, status=None, from_date=None, to_date=None): + qs = LeaveApplicationNew.objects.filter(employee=employee_extra_info) + if status: + qs = qs.filter(approval_status=status) + if from_date: + qs = qs.filter(start_date__gte=from_date) + if to_date: + qs = qs.filter(end_date__lte=to_date) + return qs.order_by('-applied_date') + +def get_pending_responsibility_leaves(employee_extra_info, responsibility_type='academic'): + if responsibility_type == 'academic': + return LeaveApplicationNew.objects.filter(employee=employee_extra_info, approval_status='PENDING') + return LeaveApplicationNew.objects.filter(employee=employee_extra_info, approval_status='PENDING') + +# ==================== ATTENDANCE SELECTORS ==================== + +def get_attendance_for_employee(employee_extra_info, from_date=None, to_date=None): + qs = EmployeeAttendance.objects.filter(employee=employee_extra_info) + if from_date: + qs = qs.filter(date__gte=from_date) + if to_date: + qs = qs.filter(date__lte=to_date) + return qs.order_by('date') + +# ==================== APPRAISAL SELECTORS ==================== + +def get_appraisal_periods(is_active=None): + qs = AppraisalPeriod.objects.all() + if is_active is not None: + qs = qs.filter(is_active=is_active) + return qs + +def get_appraisals_for_employee(employee_extra_info, period_id=None): + qs = PerformanceAppraisalNew.objects.filter(employee=employee_extra_info).select_related('period') + if period_id: + qs = qs.filter(period_id=period_id) + return qs + +# ==================== TRAINING SELECTORS ==================== + +def get_available_training_programs(): + today = date.today() + return TrainingProgram.objects.filter(start_date__gte=today) + +def get_nominations_for_employee(employee_extra_info): + return TrainingNomination.objects.filter(employee=employee_extra_info).select_related('program') + +# ==================== PROMOTION SELECTORS ==================== + +def get_promotion_applications(employee_extra_info=None): + qs = PromotionApplication.objects.select_related('employee', 'current_designation', 'applied_designation') + if employee_extra_info: + qs = qs.filter(employee=employee_extra_info) + return qs + +# ==================== FACULTY WORKLOAD SELECTORS ==================== + +def get_faculty_workload(faculty_extra_info, semester=None, year=None): + qs = FacultyWorkload.objects.filter(faculty=faculty_extra_info.faculty_profile) + if semester: + qs = qs.filter(semester=semester) + if year: + qs = qs.filter(year=year) + return qs + +# LTC +def get_ltc_applications(employee_extra_info, status=None): + qs = LTCApplicationNew.objects.filter(employee=employee_extra_info) + if status: + qs = qs.filter(approval_status=status) + return qs.order_by('-applied_date') + +# CPDA Advance +def get_cpda_advances(employee_extra_info, status=None): + qs = CPDAAdvanceNew.objects.filter(employee=employee_extra_info) + if status: + qs = qs.filter(approval_status=status) + return qs.order_by('-applied_date') + +# CPDA Reimbursement +def get_cpda_reimbursements(employee_extra_info, status=None): + qs = CPDAReimbursementNew.objects.filter(employee=employee_extra_info) + if status: + qs = qs.filter(approval_status=status) + return qs.order_by('-applied_date') + +# Appraisal Form +def get_appraisal_forms(employee_extra_info, status=None): + qs = AppraisalFormNew.objects.filter(employee=employee_extra_info) + if status: + qs = qs.filter(status=status) + return qs.order_by('-submitted_at') \ No newline at end of file diff --git a/FusionIIIT/applications/hr2/serializers.py b/FusionIIIT/applications/hr2/serializers.py deleted file mode 100644 index b28cdcc45..000000000 --- a/FusionIIIT/applications/hr2/serializers.py +++ /dev/null @@ -1,42 +0,0 @@ -from rest_framework import serializers -from .models import LTCform, CPDAAdvanceform, CPDAReimbursementform, Leaveform, Appraisalform - -class LTC_serializer(serializers.ModelSerializer): - class Meta: - model = LTCform - fields = '__all__' - - def create(self, validated_data): - return LTCform.objects.create(**validated_data) - -class CPDAAdvance_serializer(serializers.ModelSerializer): - class Meta: - model = CPDAAdvanceform - fields = '__all__' - - def create(self, validated_data): - return CPDAAdvanceform.objects.create(**validated_data) - -class Appraisal_serializer(serializers.ModelSerializer): - class Meta: - model = Appraisalform - fields = '__all__' - - def create(self, validated_data): - return Appraisalform.objects.create(**validated_data) - -class CPDAReimbursement_serializer(serializers.ModelSerializer): - class Meta: - model = CPDAReimbursementform - fields = '__all__' - - def create(self, validated_data): - return CPDAReimbursementform.objects.create(**validated_data) - -class Leave_serializer(serializers.ModelSerializer): - class Meta: - model = Leaveform - fields = '__all__' - - def create(self, validated_data): - return Leaveform.objects.create(**validated_data) \ No newline at end of file diff --git a/FusionIIIT/applications/hr2/services.py b/FusionIIIT/applications/hr2/services.py new file mode 100644 index 000000000..38bd2c07f --- /dev/null +++ b/FusionIIIT/applications/hr2/services.py @@ -0,0 +1,116 @@ +""" +HR2 Module Services - Stub Implementation + +This is a stub implementation that allows the module to import without errors. +Full implementations will use actual Django models from models.py. +""" + +from datetime import date +from django.core.exceptions import ValidationError + +# ==================== CUSTOM EXCEPTIONS ==================== + +class InsufficientLeaveBalanceError(Exception): + """Raised when employee doesn't have enough leave balance.""" + pass + +class DuplicateLeaveApplicationError(Exception): + """Raised when overlapping leave already exists.""" + pass + +class InvalidWorkflowTransitionError(Exception): + """Raised when workflow transition is invalid.""" + pass + +class ResponsibilityNotAssignedError(Exception): + """Raised when responsibility is not assigned.""" + pass + +# ==================== LEAVE MANAGEMENT SERVICES ==================== + +def apply_for_leave(employee_extra_info, leave_type_id, from_date, to_date, reason, + address_during_leave="", contact_during_leave="", document=None, + academic_responsibility_user=None, academic_responsibility_designation=None, + administrative_responsibility_user=None, administrative_responsibility_designation=None): + """Apply for leave - STUB IMPLEMENTATION.""" + raise NotImplementedError("Leave application service not yet fully implemented. Using LeaveForm model.") + +def approve_leave_application(leave_app, approver_extra_info, remarks=""): + """Approve leave application - STUB IMPLEMENTATION.""" + raise NotImplementedError("Leave approval service not yet fully implemented.") + +def reject_leave_application(leave_app, approver_extra_info, remarks=""): + """Reject leave application - STUB IMPLEMENTATION.""" + raise NotImplementedError("Leave rejection service not yet fully implemented.") + +def handle_academic_responsibility(leave_app, approver_extra_info, action, remarks=""): + """Handle academic responsibility for leave - STUB IMPLEMENTATION.""" + raise NotImplementedError("Academic responsibility handler not yet fully implemented.") + +def handle_administrative_responsibility(leave_app, approver_extra_info, action, remarks=""): + """Handle administrative responsibility for leave - STUB IMPLEMENTATION.""" + raise NotImplementedError("Administrative responsibility handler not yet fully implemented.") + +# ==================== ATTENDANCE SERVICES ==================== + +def mark_attendance(employee_extra_info, date_val, status, in_time=None, out_time=None, remarks=""): + """Mark attendance - STUB IMPLEMENTATION.""" + raise NotImplementedError("Attendance marking service not yet fully implemented.") + +# ==================== FACULTY WORKLOAD SERVICES ==================== + +def calculate_faculty_workload(faculty_extra_info, semester, year): + """Calculate faculty workload - STUB IMPLEMENTATION.""" + raise NotImplementedError("Faculty workload calculation service not yet fully implemented.") + +# ==================== LTC SERVICES ==================== + +def apply_ltc(employee_extra_info, data): + """Apply for LTC - STUB IMPLEMENTATION.""" + raise NotImplementedError("LTC application service not yet fully implemented.") + +def approve_ltc(ltc_app, approver_extra_info, remarks=""): + """Approve LTC - STUB IMPLEMENTATION.""" + raise NotImplementedError("LTC approval service not yet fully implemented.") + +def reject_ltc(ltc_app, approver_extra_info, remarks=""): + """Reject LTC - STUB IMPLEMENTATION.""" + raise NotImplementedError("LTC rejection service not yet fully implemented.") + +# ==================== CPDA ADVANCE SERVICES ==================== + +def apply_cpda_advance(employee_extra_info, data): + """Apply for CPDA Advance - STUB IMPLEMENTATION.""" + raise NotImplementedError("CPDA Advance application service not yet fully implemented.") + +def approve_cpda_advance(cpda_adv, approver_extra_info, remarks=""): + """Approve CPDA Advance - STUB IMPLEMENTATION.""" + raise NotImplementedError("CPDA Advance approval service not yet fully implemented.") + +def reject_cpda_advance(cpda_adv, approver_extra_info, remarks=""): + """Reject CPDA Advance - STUB IMPLEMENTATION.""" + raise NotImplementedError("CPDA Advance rejection service not yet fully implemented.") + +# ==================== CPDA REIMBURSEMENT SERVICES ==================== + +def apply_cpda_reimbursement(employee_extra_info, data): + """Apply for CPDA Reimbursement - STUB IMPLEMENTATION.""" + raise NotImplementedError("CPDA Reimbursement application service not yet fully implemented.") + +def approve_cpda_reimbursement(cpda_reim, approver_extra_info, remarks=""): + """Approve CPDA Reimbursement - STUB IMPLEMENTATION.""" + raise NotImplementedError("CPDA Reimbursement approval service not yet fully implemented.") + +def reject_cpda_reimbursement(cpda_reim, approver_extra_info, remarks=""): + """Reject CPDA Reimbursement - STUB IMPLEMENTATION.""" + raise NotImplementedError("CPDA Reimbursement rejection service not yet fully implemented.") + +# ==================== APPRAISAL SERVICES ==================== + +def submit_appraisal(employee_extra_info, data): + """Submit appraisal - STUB IMPLEMENTATION.""" + raise NotImplementedError("Appraisal submission service not yet fully implemented.") + +def review_appraisal(appraisal_id, reviewer_extra_info, reviewer_scores, reviewer_remarks): + """Review appraisal - STUB IMPLEMENTATION.""" + raise NotImplementedError("Appraisal review service not yet fully implemented.") \ No newline at end of file diff --git a/FusionIIIT/applications/hr2/test.py b/FusionIIIT/applications/hr2/test.py deleted file mode 100644 index 89ec7917f..000000000 --- a/FusionIIIT/applications/hr2/test.py +++ /dev/null @@ -1,116 +0,0 @@ -def ltc_pre_processing(request): - ltc_form_data = {} - - # Extract general information - ltc_form_data['name'] = request.POST['name'] - ltc_form_data['block_year'] = int(request.POST['block_year']) - ltc_form_data['pf_no'] = int(request.POST['pf_no']) - ltc_form_data['basic_pay_salary'] = int(request.POST['basic_pay_salary']) - ltc_form_data['designation'] = request.POST['designation'] - ltc_form_data['department_info'] = request.POST['department_info'] - ltc_form_data['leave_availability'] = request.POST.getlist('leave_availability') == ['True', 'True'] - ltc_form_data['leave_start_date'] = request.POST['leave_start_date'] - ltc_form_data['leave_end_date'] = request.POST['leave_end_date'] - ltc_form_data['date_of_leave_for_family'] = request.POST['date_of_leave_for_family'] - ltc_form_data['nature_of_leave'] = request.POST['nature_of_leave'] - ltc_form_data['purpose_of_leave'] = request.POST['purpose_of_leave'] - ltc_form_data['hometown_or_not'] = request.POST.get('hometown_or_not') == 'True' - ltc_form_data['place_of_visit'] = request.POST['place_of_visit'] - ltc_form_data['address_during_leave'] = request.POST['address_during_leave'] - - # Extract details of family members - family_members = [] - for i in range(1, 7): - if request.POST.get(f'info_{i}_1'): - family_member = ','.join(request.POST.getlist(f'info_{i}_{j}')[0] for j in range(1, 4)) - family_members.append(family_member) - ltc_form_data['details_of_family_members_already_done'] = ','.join(family_members) - - # Extract details of dependents - dependents = [] - for i in range(1, 7): - if request.POST.get(f'd_info_{i}_1'): - dependent = ','.join(request.POST.getlist(f'd_info_{i}_{j}')[0] for j in range(1, 5)) - dependents.append(dependent) - ltc_form_data['details_of_dependents'] = ','.join(dependents) - - # Extract remaining fields - ltc_form_data['amount_of_advance_required'] = int(request.POST['amount_of_advance_required']) - ltc_form_data['certified_family_dependents'] = request.POST['certified_family_dependents'] - ltc_form_data['certified_advance'] = int(request.POST['certified_advance']) - ltc_form_data['adjusted_month'] = request.POST['adjusted_month'] - ltc_form_data['date'] = request.POST['date'] - ltc_form_data['phone_number_for_contact'] = int(request.POST['phone_number_for_contact']) - - return ltc_form_data - -# Example usage -request_data = { - 'csrfmiddlewaretoken': ['yLyPMZMWRBnDU3hSh5kPGq6AgOFNY5WTK1HaZxAuiozCzXBf8qfOML5irZJd8MkM'], - 'block_year': ['232'], - 'pf_no': ['222'], - 'basic_pay_salary': ['2322'], - 'name': ['dsds'], - 'designation': ['dsdsd'], - 'department_info': ['ds'], - 'leave_availability': ['True', 'True'], - 'leave_start_date': ['2024-02-22'], - 'leave_end_date': ['2024-02-22'], - 'date_of_leave_for_family': ['2024-02-22'], - 'nature_of_leave': ['dsds'], - 'purpose_of_leave': ['dsdsd'], - 'hometown_or_not': ['True'], - 'place_of_visit': [''], - 'address_during_leave': ['full street address'], - 'details_of_family_members_already_done': ['sds', 'dsd', 'dsd'], - 'info_1_1': ['1'], - 'info_1_2': ['dsds'], - 'info_1_3': ['12'], - 'info_2_1': ['2'], - 'info_2_2': ['sds'], - 'info_2_3': ['121'], - 'info_3_1': ['3'], - 'info_3_2': ['dsds'], - 'info_3_3': ['21'], - 'info_4_1': [''], - 'info_4_2': [''], - 'info_4_3': [''], - 'info_5_1': [''], - 'info_5_2': [''], - 'info_5_3': [''], - 'info_6_1': [''], - 'info_6_2': [''], - 'info_6_3': [''], - 'd_info_1_1': ['1'], - 'd_info_1_2': ['sds'], - 'd_info_1_3': ['21'], - 'd_info_1_4': ['sdd'], - 'd_info_2_1': ['2'], - 'd_info_2_2': ['dsd'], - 'd_info_2_3': ['23'], - 'd_info_2_4': ['sds'], - 'd_info_3_1': ['3'], - 'd_info_3_2': ['sd'], - 'd_info_3_3': ['21'], - 'd_info_3_4': ['dds'], - 'd_info_4_1': [''], - 'd_info_4_2': [''], - 'd_info_4_3': [''], - 'd_info_4_4': [''], - 'd_info_5_1': [''], - 'd_info_5_2': [''], - 'd_info_5_3': [''], - 'd_info_5_4': [''], - 'd_info_6_1': [''], - 'd_info_6_2': [''], - 'd_info_6_3': [''], - 'd_info_6_4': [''], - 'amount_of_advance_required': ['211'], - 'certified_family_dependents': ['dqwd'], - 'certified_advance': ['dqwd'], - 'adjusted_month': ['qwdwd'], - 'date': ['2024-02-22'], - 'phone_number_for_contact': ['2312123'] -} - -print(ltc_pre_processing(request_data)) diff --git a/FusionIIIT/applications/hr2/tests.py b/FusionIIIT/applications/hr2/tests.py deleted file mode 100644 index 7ce503c2d..000000000 --- a/FusionIIIT/applications/hr2/tests.py +++ /dev/null @@ -1,3 +0,0 @@ -from django.test import TestCase - -# Create your tests here. diff --git a/FusionIIIT/applications/hr2/tests/__init__.py b/FusionIIIT/applications/hr2/tests/__init__.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/FusionIIIT/applications/hr2/tests/__init__.py @@ -0,0 +1 @@ + diff --git a/FusionIIIT/applications/hr2/tests/conftest.py b/FusionIIIT/applications/hr2/tests/conftest.py new file mode 100644 index 000000000..27d79cabe --- /dev/null +++ b/FusionIIIT/applications/hr2/tests/conftest.py @@ -0,0 +1,440 @@ +import datetime +from decimal import Decimal +from typing import Any, Dict, List, Optional + +from django.contrib.auth import get_user_model +from django.test import TestCase +from rest_framework.test import APIClient + +from applications.globals.models import DepartmentInfo, Designation, ExtraInfo, HoldsDesignation +from applications.hr2.models import ( + AppraisalPeriod, + Employee, + EmployeeLeaveBalance, + LeaveBalance, + LeavePerYear, + LeaveType, + TrainingProgram, +) + +from .runner import REPORT_STORE + + +User = get_user_model() + + +class BaseModuleTestCase(TestCase): + """Shared helpers and base data for HR2 tests.""" + + @classmethod + def setUpTestData(cls): + cls.department_cse = DepartmentInfo.objects.create( + name="Computer Science and Engineering" + ) + cls.department_admin = DepartmentInfo.objects.create(name="Administration") + cls.department_finance = DepartmentInfo.objects.create(name="Finance") + cls.department_director = DepartmentInfo.objects.create(name="Director Office") + + cls.employee_user = User.objects.create_user( + username="rahul1001", + password="rahul123", + first_name="Rahul", + last_name="Sharma", + email="rahul.sharma@iiitdmj.ac.in", + ) + cls.hod_user = User.objects.create_user( + username="hod1002", + password="hod123", + first_name="Anil", + last_name="Kumar", + email="anil.kumar@iiitdmj.ac.in", + ) + cls.director_user = User.objects.create_user( + username="director1003", + password="director123", + first_name="Meena", + last_name="Verma", + email="director@iiitdmj.ac.in", + ) + cls.registrar_user = User.objects.create_user( + username="registrar1004", + password="registrar123", + first_name="Suresh", + last_name="Verma", + email="registrar@iiitdmj.ac.in", + ) + cls.staff_user = User.objects.create_user( + username="hradmin1005", + password="hradmin123", + first_name="Priya", + last_name="Nair", + email="hr.admin@iiitdmj.ac.in", + ) + cls.accountant_user = User.objects.create_user( + username="accountant1006", + password="accountant123", + first_name="Arun", + last_name="Joshi", + email="accountant@iiitdmj.ac.in", + ) + cls.nominee_user = User.objects.create_user( + username="nominee", + password="test123", + first_name="Nominee", + last_name="User", + email="nominee@example.com", + ) + + cls.employee_extra = ExtraInfo.objects.create( + user=cls.employee_user, + id="1001", + user_type="faculty", + department=cls.department_cse, + ) + cls.hod_extra = ExtraInfo.objects.create( + user=cls.hod_user, + id="1002", + user_type="staff", + department=cls.department_cse, + ) + cls.director_extra = ExtraInfo.objects.create( + user=cls.director_user, + id="1003", + user_type="staff", + department=cls.department_director, + ) + cls.registrar_extra = ExtraInfo.objects.create( + user=cls.registrar_user, + id="1004", + user_type="staff", + department=cls.department_admin, + ) + cls.staff_extra = ExtraInfo.objects.create( + user=cls.staff_user, + id="1005", + user_type="staff", + department=cls.department_admin, + ) + cls.accountant_extra = ExtraInfo.objects.create( + user=cls.accountant_user, + id="1006", + user_type="staff", + department=cls.department_finance, + ) + cls.nominee_extra = ExtraInfo.objects.create( + user=cls.nominee_user, + id="2001", + user_type="staff", + department=cls.department_cse, + ) + + cls.employee = cls._create_employee( + cls.employee_user, + department_name="Computer Science and Engineering", + employee_type="Faculty", + phone="9876543210", + personal_email="rahul.sharma@iiitdmj.ac.in", + date_of_joining=datetime.date(2021, 8, 1), + date_of_birth=datetime.date(1990, 5, 12), + ) + cls.hod_employee = cls._create_employee( + cls.hod_user, + department_name="Computer Science and Engineering", + employee_type="Faculty", + phone="9876543211", + personal_email="anil.kumar@iiitdmj.ac.in", + date_of_joining=datetime.date(2015, 6, 15), + date_of_birth=datetime.date(1980, 7, 20), + ) + cls.director_employee = cls._create_employee( + cls.director_user, + department_name="Director Office", + employee_type="Faculty", + phone="9876543212", + personal_email="director@iiitdmj.ac.in", + date_of_joining=datetime.date(2019, 1, 10), + date_of_birth=datetime.date(1975, 2, 11), + ) + cls.registrar_employee = cls._create_employee( + cls.registrar_user, + department_name="Administration", + employee_type="Staff", + phone="9876543213", + personal_email="registrar@iiitdmj.ac.in", + date_of_joining=datetime.date(2018, 1, 15), + date_of_birth=datetime.date(1982, 3, 10), + ) + cls.staff_employee = cls._create_employee( + cls.staff_user, + department_name="Administration", + employee_type="Staff", + phone="9876543214", + personal_email="hr.admin@iiitdmj.ac.in", + date_of_joining=datetime.date(2020, 11, 5), + date_of_birth=datetime.date(1987, 9, 25), + ) + cls.accountant_employee = cls._create_employee( + cls.accountant_user, + department_name="Finance", + employee_type="Staff", + phone="9876543215", + personal_email="accountant@iiitdmj.ac.in", + date_of_joining=datetime.date(2019, 8, 12), + date_of_birth=datetime.date(1985, 12, 18), + ) + cls.nominee_employee = cls._create_employee(cls.nominee_user) + + cls._ensure_leave_balances(cls.employee, casual_leave=10) + cls._ensure_leave_balances(cls.hod_employee) + cls._ensure_leave_balances(cls.director_employee) + cls._ensure_leave_balances(cls.registrar_employee) + cls._ensure_leave_balances(cls.staff_employee) + cls._ensure_leave_balances(cls.accountant_employee) + cls._ensure_leave_balances(cls.nominee_employee) + + cls._create_designation("hod", cls.hod_user) + cls._create_designation("registrar", cls.registrar_user) + cls._create_designation("director", cls.director_user) + cls._create_designation("accountant", cls.accountant_user) + cls._create_designation("hr_admin", cls.staff_user) + + cls.leave_types = cls._create_leave_types() + cls._create_leave_balances_for_employee(cls.employee_extra) + + cls.appraisal_period = AppraisalPeriod.objects.create( + name="2025-2026", + start_date=datetime.date(2025, 7, 1), + end_date=datetime.date(2026, 6, 30), + submission_deadline=datetime.date(2026, 5, 31), + is_active=True, + ) + cls.training_program = TrainingProgram.objects.create( + title="AI Workshop", + description="AI fundamentals", + organizer="IIITDMJ", + venue="Jabalpur", + start_date=datetime.date.today() + datetime.timedelta(days=10), + end_date=datetime.date.today() + datetime.timedelta(days=12), + max_participants=30, + is_mandatory=False, + ) + cls.promotion_current_designation, _ = Designation.objects.get_or_create( + name="assistant_professor", + defaults={"full_name": "Assistant Professor", "type": "academic"}, + ) + cls.promotion_applied_designation, _ = Designation.objects.get_or_create( + name="associate_professor", + defaults={"full_name": "Associate Professor", "type": "academic"}, + ) + + @classmethod + def _create_employee( + cls, + user: User, + department_name: str = "Computer Science and Engineering", + employee_type: str = "Faculty", + phone: str = "9999999999", + personal_email: Optional[str] = None, + date_of_joining: Optional[datetime.date] = None, + date_of_birth: Optional[datetime.date] = None, + ) -> Employee: + return Employee.objects.create( + id=user, + father_name="Father", + mother_name="Mother", + category="General", + caste="NA", + home_state="Madhya Pradesh", + home_district="Jabalpur", + full_address=f"{department_name} quarters", + date_of_joining=date_of_joining or datetime.date(2024, 1, 1), + date_of_birth=date_of_birth or datetime.date(1990, 1, 1), + blood_group="A+", + phone_number=phone, + personal_email=personal_email or f"{user.username}@example.com", + emergency_contact_number="8888888888", + emergency_contact_name="Emergency", + employee_type=employee_type, + ) + + @classmethod + def _ensure_leave_balances(cls, employee: Employee, casual_leave: int = 8) -> None: + LeaveBalance.objects.create( + empid=employee, + casual_leave_taken=0, + ) + LeavePerYear.objects.create(empid=employee) + + @classmethod + def _create_leave_types(cls) -> Dict[str, LeaveType]: + leave_types = {} + for name, code in ( + ("Casual", "CL"), + ("Vacation", "VL"), + ("Earned", "EL"), + ("Medical", "ML"), + ("Restricted", "RL"), + ("Sabbatical", "SL"), + ): + leave_type, _ = LeaveType.objects.get_or_create( + name=name, + code=code, + defaults={"max_days_per_year": 30, "carry_forward": False}, + ) + leave_types[name] = leave_type + return leave_types + + @classmethod + def _create_leave_balances_for_employee(cls, employee_extra: ExtraInfo) -> None: + current_year = datetime.date.today().year + for leave_type in cls.leave_types.values(): + EmployeeLeaveBalance.objects.get_or_create( + employee=employee_extra, + leave_type=leave_type, + year=current_year, + defaults={ + "opening_balance": Decimal("10"), + "accrued": Decimal("0"), + "availed": Decimal("0"), + "current_balance": Decimal("10"), + }, + ) + + @classmethod + def _create_designation(cls, name: str, user: User) -> None: + designation, _ = Designation.objects.get_or_create(name=name) + HoldsDesignation.objects.get_or_create( + user=user, + working=user, + designation=designation, + ) + + def setUp(self): + super().setUp() + self.client = APIClient() + self._result_recorded = False + self._steps: List[Dict[str, Any]] = [] + + def tearDown(self): + if not self._result_recorded: + error_message = self._get_test_error_message() + if error_message: + self._record_result("Unhandled error", "Fail", error_message) + super().tearDown() + + def _get_test_error_message(self) -> Optional[str]: + outcome = getattr(self, "_outcome", None) + if not outcome: + return None + for _, error in outcome.errors: + if error: + return str(error) + return None + + def login_as_user(self, user: User) -> None: + self.client.force_authenticate(user=user) + + def logout(self) -> None: + self.client.force_authenticate(user=None) + + def login_as_employee(self) -> None: + self.login_as_user(self.employee_user) + + def login_as_staff(self) -> None: + self.login_as_user(self.staff_user) + + def login_as_hod(self) -> None: + self.login_as_user(self.hod_user) + + def login_as_registrar(self) -> None: + self.login_as_user(self.registrar_user) + + def login_as_director(self) -> None: + self.login_as_user(self.director_user) + + def login_as_accountant(self) -> None: + self.login_as_user(self.accountant_user) + + def login_as_nominee(self) -> None: + self.login_as_user(self.nominee_user) + + def api_get(self, path: str, expected_status: Optional[int] = 200, **kwargs): + response = self.client.get(path, **kwargs) + if expected_status is not None: + self.assertEqual(response.status_code, expected_status) + return response + + def api_post(self, path: str, data: Optional[Dict[str, Any]] = None, expected_status: Optional[int] = 200, **kwargs): + response = self.client.post(path, data=data or {}, format="json", **kwargs) + if expected_status is not None: + self.assertEqual(response.status_code, expected_status) + return response + + def api_put(self, path: str, data: Optional[Dict[str, Any]] = None, expected_status: Optional[int] = 200, **kwargs): + response = self.client.put(path, data=data or {}, format="json", **kwargs) + if expected_status is not None: + self.assertEqual(response.status_code, expected_status) + return response + + def api_delete(self, path: str, expected_status: Optional[int] = 200, **kwargs): + response = self.client.delete(path, **kwargs) + if expected_status is not None: + self.assertEqual(response.status_code, expected_status) + return response + + def today(self) -> str: + return datetime.date.today().isoformat() + + def future_date(self, days: int) -> str: + return (datetime.date.today() + datetime.timedelta(days=days)).isoformat() + + def past_date(self, days: int) -> str: + return (datetime.date.today() - datetime.timedelta(days=days)).isoformat() + + def _record_result(self, message: str, status: str, evidence: str = "") -> None: + REPORT_STORE.add_execution( + test_id=getattr(self, "_test_id", self._testMethodName), + artifact_id=( + getattr(self, "_uc_id", None) + or getattr(self, "_br_id", None) + or getattr(self, "_wf_id", None) + ), + artifact_type=self._artifact_type, + category=getattr(self, "_test_category", ""), + scenario=getattr(self, "_scenario", ""), + preconditions=getattr(self, "_preconditions", ""), + input_action=getattr(self, "_input_action", ""), + expected_result=getattr(self, "_expected_result", "") + or getattr(self, "_expected_final_state", ""), + status=status, + message=message, + evidence=evidence, + steps=self._steps, + ) + self._result_recorded = True + + def _add_step(self, step_number: int, action: str, expected: str, actual: str, passed: bool) -> None: + self._steps.append( + { + "step": step_number, + "action": action, + "expected": expected, + "actual": actual, + "passed": passed, + } + ) + + def _all_steps_passed(self) -> bool: + return all(step["passed"] for step in self._steps) + + +class UCTestBase(BaseModuleTestCase): + _artifact_type = "UC" + + +class BRTestBase(BaseModuleTestCase): + _artifact_type = "BR" + + +class WFTestBase(BaseModuleTestCase): + _artifact_type = "WF" diff --git a/FusionIIIT/applications/hr2/tests/reports/Artifact_Evaluation.csv b/FusionIIIT/applications/hr2/tests/reports/Artifact_Evaluation.csv new file mode 100644 index 000000000..f81b62b72 --- /dev/null +++ b/FusionIIIT/applications/hr2/tests/reports/Artifact_Evaluation.csv @@ -0,0 +1,87 @@ +artifact_type,artifact_id,artifact_title,status +UC,UC-HR2-001,List employees,Partially Implemented +UC,UC-HR2-002,View employee details,Incorrectly Implemented +UC,UC-HR2-003,Update employee details,Incorrectly Implemented +UC,UC-HR2-004,Apply for leave,Partially Implemented +UC,UC-HR2-005,View leave applications,Implemented Correctly +UC,UC-HR2-006,Withdraw leave application,Partially Implemented +UC,UC-HR2-007,Request leave cancellation,Partially Implemented +UC,UC-HR2-008,Request leave extension,Partially Implemented +UC,UC-HR2-009,View leave balance,Implemented Correctly +UC,UC-HR2-010,Download leave application,Partially Implemented +UC,UC-HR2-011,Submit LTC application,Partially Implemented +UC,UC-HR2-012,Submit CPDA advance,Partially Implemented +UC,UC-HR2-013,Submit appraisal form,Partially Implemented +UC,UC-HR2-014,View leave application details,Partially Implemented +UC,UC-HR2-015,Update leave application,Partially Implemented +UC,UC-HR2-016,Delete leave application,Incorrectly Implemented +UC,UC-HR2-017,Approve or reject leave,Partially Implemented +UC,UC-HR2-018,Nominee dashboard,Incorrectly Implemented +UC,UC-HR2-019,Nominee decision,Partially Implemented +UC,UC-HR2-020,Request leave documents,Partially Implemented +UC,UC-HR2-021,Submit leave documents,Partially Implemented +UC,UC-HR2-022,Cancellation decision,Partially Implemented +UC,UC-HR2-023,Extension decision,Partially Implemented +UC,UC-HR2-024,Record attendance,Incorrectly Implemented +UC,UC-HR2-025,View attendance,Implemented Correctly +UC,UC-HR2-026,List appraisal periods,Partially Implemented +UC,UC-HR2-027,Submit appraisal (performance),Partially Implemented +UC,UC-HR2-028,List appraisals,Implemented Correctly +UC,UC-HR2-029,List training programs,Implemented Correctly +UC,UC-HR2-030,Nominate for training,Partially Implemented +UC,UC-HR2-031,View training nominations,Implemented Correctly +UC,UC-HR2-032,Submit promotion application,Partially Implemented +UC,UC-HR2-033,View promotion applications,Implemented Correctly +UC,UC-HR2-034,View faculty workload,Partially Implemented +UC,UC-HR2-035,Submit LTC update,Partially Implemented +UC,UC-HR2-036,View LTC applications,Implemented Correctly +UC,UC-HR2-037,Download LTC application,Partially Implemented +UC,UC-HR2-038,Withdraw LTC application,Partially Implemented +UC,UC-HR2-039,Approve or reject LTC,Partially Implemented +UC,UC-HR2-040,View CPDA advances,Implemented Correctly +UC,UC-HR2-041,View CPDA advance details,Partially Implemented +UC,UC-HR2-042,Download CPDA advance,Partially Implemented +UC,UC-HR2-043,Withdraw CPDA advance,Partially Implemented +UC,UC-HR2-044,Decide CPDA advance,Partially Implemented +UC,UC-HR2-045,Submit CPDA reimbursement,Partially Implemented +UC,UC-HR2-046,View CPDA reimbursements,Implemented Correctly +UC,UC-HR2-047,View CPDA reimbursement details,Partially Implemented +UC,UC-HR2-048,Decide CPDA reimbursement,Partially Implemented +UC,UC-HR2-049,List appraisal forms,Implemented Correctly +UC,UC-HR2-050,View appraisal form details,Partially Implemented +UC,UC-HR2-051,Download appraisal form,Partially Implemented +UC,UC-HR2-052,Review appraisal form,Partially Implemented +BR,BR-HR2-001,Leave start date cannot be in the past,Partially Implemented +BR,BR-HR2-002,Leave end date must be on/after start date,Partially Implemented +BR,BR-HR2-003,Total days must match date range,Partially Implemented +BR,BR-HR2-004,Leave requests cannot overlap,Partially Implemented +BR,BR-HR2-005,Leave balance must be sufficient,Partially Implemented +BR,BR-HR2-006,Nominee employee must exist,Partially Implemented +BR,BR-HR2-007,Only owner can withdraw leave,Partially Implemented +BR,BR-HR2-008,Cancellation only before start date,Partially Implemented +BR,BR-HR2-009,Leave delete only when pending,Partially Implemented +BR,BR-HR2-010,Cancellation requires approved leave and no prior request,Partially Implemented +BR,BR-HR2-011,Extension requires approved leave before end date,Partially Implemented +BR,BR-HR2-012,Extension approval needs balance,Partially Implemented +BR,BR-HR2-013,Document request requires HOD role,Partially Implemented +BR,BR-HR2-014,Document submit requires owner and request,Partially Implemented +BR,BR-HR2-015,Nominee decision only by nominee,Partially Implemented +BR,BR-HR2-016,Leave decision action must be valid,Partially Implemented +BR,BR-HR2-017,Leave balance record must exist,Partially Implemented +BR,BR-HR2-018,Leave application requires employee profile,Partially Implemented +BR,BR-HR2-019,LTC withdrawal only when pending,Incorrectly Implemented +BR,BR-HR2-020,LTC decision action must be valid,Partially Implemented +BR,BR-HR2-021,CPDA advance withdrawal only when pending,Incorrectly Implemented +BR,BR-HR2-022,CPDA advance decision action must be valid,Partially Implemented +BR,BR-HR2-023,Download access restricted to owner or staff,Partially Implemented +BR,BR-HR2-024,Appraisal review sets status,Partially Implemented +WF,WF-HR2-001,Leave application approval flow,Incorrectly Implemented +WF,WF-HR2-002,Leave withdrawal flow,Incorrectly Implemented +WF,WF-HR2-003,Leave cancellation flow,Incorrectly Implemented +WF,WF-HR2-004,Leave extension flow,Incorrectly Implemented +WF,WF-HR2-005,Nominee response flow,Incorrectly Implemented +WF,WF-HR2-006,Document request flow,Incorrectly Implemented +WF,WF-HR2-007,LTC approval flow,Partially Implemented +WF,WF-HR2-008,CPDA advance approval flow,Partially Implemented +WF,WF-HR2-009,CPDA reimbursement decision flow,Incorrectly Implemented +WF,WF-HR2-010,Appraisal form review flow,Partially Implemented diff --git a/FusionIIIT/applications/hr2/tests/reports/BR_Test_Design.csv b/FusionIIIT/applications/hr2/tests/reports/BR_Test_Design.csv new file mode 100644 index 000000000..600331cc9 --- /dev/null +++ b/FusionIIIT/applications/hr2/tests/reports/BR_Test_Design.csv @@ -0,0 +1,55 @@ +br_id,br_title,test_type,input_action,expected_result +BR-HR2-001,Leave start date cannot be in the past,Valid,Apply leave with start_date=tomorrow,Request accepted +BR-HR2-001,Leave start date cannot be in the past,Invalid,Apply leave with start_date=yesterday,Request rejected with start_date validation error +BR-HR2-002,Leave end date must be on/after start date,Valid,"Apply leave with start_date=2026-05-01, end_date=2026-05-03",Request accepted +BR-HR2-002,Leave end date must be on/after start date,Invalid,"Apply leave with start_date=2026-05-03, end_date=2026-05-01",Request rejected with date range error +BR-HR2-003,Total days must match date range,Valid,"Apply leave with start_date=2026-05-01, end_date=2026-05-03, total_days=3",Request accepted +BR-HR2-003,Total days must match date range,Invalid,"Apply leave with start_date=2026-05-01, end_date=2026-05-03, total_days=2",Request rejected with total_days mismatch error +BR-HR2-004,Leave requests cannot overlap,Valid,Apply leave for 2026-05-10 to 2026-05-12 when no overlaps,Request accepted +BR-HR2-004,Leave requests cannot overlap,Invalid,Apply leave overlapping existing approved leave,Request rejected with overlap error +BR-HR2-005,Leave balance must be sufficient,Valid,Apply leave for 2 days with balance >= 2,Request accepted +BR-HR2-005,Leave balance must be sufficient,Invalid,Apply leave for 10 days with balance < 10,Request rejected with insufficient balance error +BR-HR2-006,Nominee employee must exist,Valid,Apply leave with nominee_employee_id=valid,Request accepted with nominee_status=PENDING +BR-HR2-006,Nominee employee must exist,Invalid,Apply leave with nominee_employee_id=invalid,Request rejected with nominee not found error +BR-HR2-007,Only owner can withdraw leave,Valid,Owner withdraws pending leave,Leave updated to WITHDRAWN +BR-HR2-007,Only owner can withdraw leave,Invalid,Non-owner withdraws leave,Request rejected with 403 +BR-HR2-008,Cancellation only before start date,Valid,Request cancellation one day before start,Cancellation request accepted +BR-HR2-008,Cancellation only before start date,Invalid,Request cancellation on start date,Request rejected with cancellation window error +BR-HR2-009,Leave delete only when pending,Valid,Delete pending leave,Leave deleted +BR-HR2-009,Leave delete only when pending,Invalid,Delete approved leave,Request rejected with delete error +BR-HR2-010,Cancellation requires approved leave and no prior request,Valid,Request cancellation for approved leave,Cancel status set to REQUESTED +BR-HR2-010,Cancellation requires approved leave and no prior request,Invalid,Request cancellation for pending leave,Request rejected with approval_status error +BR-HR2-010,Cancellation requires approved leave and no prior request,Invalid,Request cancellation when cancel_status already REQUESTED,Request rejected as already processed +BR-HR2-011,Extension requires approved leave before end date,Valid,Request extension with new_end_date after current end date,Extension status set to REQUESTED +BR-HR2-011,Extension requires approved leave before end date,Invalid,Request extension after end_date,Request rejected with extension window error +BR-HR2-011,Extension requires approved leave before end date,Invalid,Request extension with new_end_date before current end date,Request rejected with date validation error +BR-HR2-012,Extension approval needs balance,Valid,Approve extension with sufficient balance,Extension approved and balance reduced +BR-HR2-012,Extension approval needs balance,Invalid,Approve extension with insufficient balance,Request rejected with insufficient balance error +BR-HR2-013,Document request requires HOD role,Valid,HOD requests document with message,Document request status set to REQUESTED +BR-HR2-013,Document request requires HOD role,Invalid,Non-HOD requests document,Request rejected with 403 +BR-HR2-013,Document request requires HOD role,Invalid,HOD requests document without message,Request rejected with message required +BR-HR2-014,Document submit requires owner and request,Valid,Owner submits document after request,Document request status set to SUBMITTED +BR-HR2-014,Document submit requires owner and request,Invalid,Owner submits document without request,Request rejected with no request error +BR-HR2-014,Document submit requires owner and request,Invalid,Non-owner submits document,Request rejected with 403 +BR-HR2-015,Nominee decision only by nominee,Valid,Nominee accepts request,Nominee status updated to ACCEPTED +BR-HR2-015,Nominee decision only by nominee,Invalid,Non-nominee responds,Request rejected with 403 +BR-HR2-015,Nominee decision only by nominee,Invalid,Nominee sends invalid action,Request rejected with invalid action +BR-HR2-016,Leave decision action must be valid,Valid,Approve leave via decision endpoint,Leave status updated to APPROVED +BR-HR2-016,Leave decision action must be valid,Invalid,Decision action=invalid,Request rejected with invalid decision +BR-HR2-017,Leave balance record must exist,Valid,Apply leave with existing leave balance,Request accepted +BR-HR2-017,Leave balance record must exist,Invalid,Apply leave with no balance record for type,Request rejected with balance not found error +BR-HR2-018,Leave application requires employee profile,Valid,Apply leave as authenticated employee,Request accepted +BR-HR2-018,Leave application requires employee profile,Invalid,Apply leave without employee profile and no employee_id,Request rejected with employee profile not found +BR-HR2-019,LTC withdrawal only when pending,Valid,Owner withdraws pending LTC,LTC updated to WITHDRAWN +BR-HR2-019,LTC withdrawal only when pending,Invalid,Owner withdraws approved LTC,Request rejected with pending-only error +BR-HR2-020,LTC decision action must be valid,Valid,Forward LTC,Approval status set to FORWARDED and accountant_status=PENDING +BR-HR2-020,LTC decision action must be valid,Invalid,Decision action=invalid,Request rejected with invalid decision +BR-HR2-021,CPDA advance withdrawal only when pending,Valid,Owner withdraws pending CPDA advance,CPDA updated to WITHDRAWN +BR-HR2-021,CPDA advance withdrawal only when pending,Invalid,Owner withdraws approved CPDA advance,Request rejected with pending-only error +BR-HR2-022,CPDA advance decision action must be valid,Valid,Forward CPDA advance to accountant,Status FORWARDED and accountant_processing_status=PENDING +BR-HR2-022,CPDA advance decision action must be valid,Valid,Forward CPDA advance to director,Status FORWARDED and accountant_processing_status=DIRECTOR_REVIEW +BR-HR2-022,CPDA advance decision action must be valid,Invalid,Decision action=invalid,Request rejected with invalid decision +BR-HR2-023,Download access restricted to owner or staff,Valid,Owner downloads own leave application,Download succeeds +BR-HR2-023,Download access restricted to owner or staff,Invalid,Non-owner downloads another employee record,Request rejected with 403 +BR-HR2-024,Appraisal review sets status,Valid,Reviewer forwards appraisal,Appraisal status set to REVIEWED +BR-HR2-024,Appraisal review sets status,Valid,Reviewer approves appraisal,Appraisal status set to APPROVED diff --git a/FusionIIIT/applications/hr2/tests/reports/Defect_Log.csv b/FusionIIIT/applications/hr2/tests/reports/Defect_Log.csv new file mode 100644 index 000000000..8823838ea --- /dev/null +++ b/FusionIIIT/applications/hr2/tests/reports/Defect_Log.csv @@ -0,0 +1 @@ +test_id,artifact_type,artifact_id,status,error diff --git a/FusionIIIT/applications/hr2/tests/reports/Module_Test_Summary.csv b/FusionIIIT/applications/hr2/tests/reports/Module_Test_Summary.csv new file mode 100644 index 000000000..39e5b83e9 --- /dev/null +++ b/FusionIIIT/applications/hr2/tests/reports/Module_Test_Summary.csv @@ -0,0 +1,20 @@ +Metric,Value +Total Use Cases,52 +Total Business Rules,24 +Total Workflows,10 +Required UC Tests,156 +Designed UC Tests,169 +Required BR Tests,48 +Designed BR Tests,54 +Required WF Tests,20 +Designed WF Tests,22 +UC Adequacy %,108.33 +BR Adequacy %,112.5 +WF Adequacy %,110.0 +Total Tests Executed,245 +Total Pass,109 +Total Partial,0 +Total Fail,136 +Strict Pass Rate %,44.49 +Generated At,2026-04-15T19:42:43.870728 +Tester Name, diff --git a/FusionIIIT/applications/hr2/tests/reports/Test_Execution_Log.csv b/FusionIIIT/applications/hr2/tests/reports/Test_Execution_Log.csv new file mode 100644 index 000000000..4fd55f887 --- /dev/null +++ b/FusionIIIT/applications/hr2/tests/reports/Test_Execution_Log.csv @@ -0,0 +1,246 @@ +test_id,artifact_type,artifact_id,category,scenario,preconditions,input_action,expected_result,status,message,evidence,steps +BR-HR2-001-I-01,BR,BR-HR2-001,Invalid,,,Apply leave with start_date=yesterday,Request rejected with start_date validation error,Pass,Expected response,"{'employee_name': [ErrorDetail(string='This field is required.', code='required')], 'department': [ErrorDetail(string='This field is required.', code='required')], 'designation': [ErrorDetail(string='This field is required.', code='required')]}",[] +BR-HR2-001-V-01,BR,BR-HR2-001,Valid,,,Apply leave with start_date=tomorrow,Request accepted,Fail,Unexpected status 400,"{'employee_name': [ErrorDetail(string='This field is required.', code='required')], 'department': [ErrorDetail(string='This field is required.', code='required')], 'designation': [ErrorDetail(string='This field is required.', code='required')]}",[] +BR-HR2-002-I-01,BR,BR-HR2-002,Invalid,,,"Apply leave with start_date=2026-05-03, end_date=2026-05-01",Request rejected with date range error,Pass,Expected response,"{'employee_name': [ErrorDetail(string='This field is required.', code='required')], 'department': [ErrorDetail(string='This field is required.', code='required')], 'designation': [ErrorDetail(string='This field is required.', code='required')]}",[] +BR-HR2-002-V-01,BR,BR-HR2-002,Valid,,,"Apply leave with start_date=2026-05-01, end_date=2026-05-03",Request accepted,Fail,Unexpected status 400,"{'employee_name': [ErrorDetail(string='This field is required.', code='required')], 'department': [ErrorDetail(string='This field is required.', code='required')], 'designation': [ErrorDetail(string='This field is required.', code='required')]}",[] +BR-HR2-003-I-01,BR,BR-HR2-003,Invalid,,,"Apply leave with start_date=2026-05-01, end_date=2026-05-03, total_days=2",Request rejected with total_days mismatch error,Pass,Expected response,"{'employee_name': [ErrorDetail(string='This field is required.', code='required')], 'department': [ErrorDetail(string='This field is required.', code='required')], 'designation': [ErrorDetail(string='This field is required.', code='required')]}",[] +BR-HR2-003-V-01,BR,BR-HR2-003,Valid,,,"Apply leave with start_date=2026-05-01, end_date=2026-05-03, total_days=3",Request accepted,Fail,Unexpected status 400,"{'employee_name': [ErrorDetail(string='This field is required.', code='required')], 'department': [ErrorDetail(string='This field is required.', code='required')], 'designation': [ErrorDetail(string='This field is required.', code='required')]}",[] +BR-HR2-004-I-01,BR,BR-HR2-004,Invalid,,,Apply leave overlapping existing approved leave,Request rejected with overlap error,Pass,Expected response,"{'employee_name': [ErrorDetail(string='This field is required.', code='required')], 'department': [ErrorDetail(string='This field is required.', code='required')], 'designation': [ErrorDetail(string='This field is required.', code='required')]}",[] +BR-HR2-004-V-01,BR,BR-HR2-004,Valid,,,Apply leave for 2026-05-10 to 2026-05-12 when no overlaps,Request accepted,Fail,Unexpected status 400,"{'employee_name': [ErrorDetail(string='This field is required.', code='required')], 'department': [ErrorDetail(string='This field is required.', code='required')], 'designation': [ErrorDetail(string='This field is required.', code='required')]}",[] +BR-HR2-005-I-01,BR,BR-HR2-005,Invalid,,,Apply leave for 10 days with balance < 10,Request rejected with insufficient balance error,Pass,Expected response,"{'employee_name': [ErrorDetail(string='This field is required.', code='required')], 'department': [ErrorDetail(string='This field is required.', code='required')], 'designation': [ErrorDetail(string='This field is required.', code='required')]}",[] +BR-HR2-005-V-01,BR,BR-HR2-005,Valid,,,Apply leave for 2 days with balance >= 2,Request accepted,Fail,Unexpected status 400,"{'employee_name': [ErrorDetail(string='This field is required.', code='required')], 'department': [ErrorDetail(string='This field is required.', code='required')], 'designation': [ErrorDetail(string='This field is required.', code='required')]}",[] +BR-HR2-006-I-01,BR,BR-HR2-006,Invalid,,,Apply leave with nominee_employee_id=invalid,Request rejected with nominee not found error,Pass,Expected response,{'error': 'Invalid action'},[] +BR-HR2-006-V-01,BR,BR-HR2-006,Valid,,,Apply leave with nominee_employee_id=valid,Request accepted with nominee_status=PENDING,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +BR-HR2-007-I-01,BR,BR-HR2-007,Invalid,,,Non-owner withdraws leave,Request rejected with 403,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +BR-HR2-007-V-01,BR,BR-HR2-007,Valid,,,Owner withdraws pending leave,Leave updated to WITHDRAWN,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +BR-HR2-008-I-01,BR,BR-HR2-008,Invalid,,,Request cancellation on start date,Request rejected with cancellation window error,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +BR-HR2-008-V-01,BR,BR-HR2-008,Valid,,,Request cancellation one day before start,Cancellation request accepted,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +BR-HR2-009-I-01,BR,BR-HR2-009,Invalid,,,Delete approved leave,Request rejected with delete error,Pass,Expected response,"{'employee_name': [ErrorDetail(string='This field is required.', code='required')], 'department': [ErrorDetail(string='This field is required.', code='required')], 'designation': [ErrorDetail(string='This field is required.', code='required')]}",[] +BR-HR2-009-V-01,BR,BR-HR2-009,Valid,,,Delete pending leave,Leave deleted,Fail,Unexpected status 400,"{'employee_name': [ErrorDetail(string='This field is required.', code='required')], 'department': [ErrorDetail(string='This field is required.', code='required')], 'designation': [ErrorDetail(string='This field is required.', code='required')]}",[] +BR-HR2-010-I-01,BR,BR-HR2-010,Invalid,,,Request cancellation for pending leave,Request rejected with approval_status error,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +BR-HR2-010-I-02,BR,BR-HR2-010,Invalid,,,Request cancellation when cancel_status already REQUESTED,Request rejected as already processed,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +BR-HR2-010-V-01,BR,BR-HR2-010,Valid,,,Request cancellation for approved leave,Cancel status set to REQUESTED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +BR-HR2-011-I-01,BR,BR-HR2-011,Invalid,,,Request extension after end_date,Request rejected with extension window error,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +BR-HR2-011-I-02,BR,BR-HR2-011,Invalid,,,Request extension with new_end_date before current end date,Request rejected with date validation error,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +BR-HR2-011-V-01,BR,BR-HR2-011,Valid,,,Request extension with new_end_date after current end date,Extension status set to REQUESTED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +BR-HR2-012-I-01,BR,BR-HR2-012,Invalid,,,Approve extension with insufficient balance,Request rejected with insufficient balance error,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +BR-HR2-012-V-01,BR,BR-HR2-012,Valid,,,Approve extension with sufficient balance,Extension approved and balance reduced,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +BR-HR2-013-I-01,BR,BR-HR2-013,Invalid,,,Non-HOD requests document,Request rejected with 403,Pass,Expected response,{'error': 'Document request message is required.'},[] +BR-HR2-013-I-02,BR,BR-HR2-013,Invalid,,,HOD requests document without message,Request rejected with message required,Pass,Expected response,{'error': 'Document request message is required.'},[] +BR-HR2-013-V-01,BR,BR-HR2-013,Valid,,,HOD requests document with message,Document request status set to REQUESTED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +BR-HR2-014-I-01,BR,BR-HR2-014,Invalid,,,Owner submits document without request,Request rejected with no request error,Pass,Expected response,{'error': 'Document request message is required.'},[] +BR-HR2-014-I-02,BR,BR-HR2-014,Invalid,,,Non-owner submits document,Request rejected with 403,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +BR-HR2-014-V-01,BR,BR-HR2-014,Valid,,,Owner submits document after request,Document request status set to SUBMITTED,Fail,Unexpected status 403,{'error': 'Not authorized'},[] +BR-HR2-015-I-01,BR,BR-HR2-015,Invalid,,,Non-nominee responds,Request rejected with 403,Pass,Expected response,{'error': 'Invalid action'},[] +BR-HR2-015-I-02,BR,BR-HR2-015,Invalid,,,Nominee sends invalid action,Request rejected with invalid action,Pass,Expected response,{'error': 'Invalid action'},[] +BR-HR2-015-V-01,BR,BR-HR2-015,Valid,,,Nominee accepts request,Nominee status updated to ACCEPTED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +BR-HR2-016-I-01,BR,BR-HR2-016,Invalid,,,Decision action=invalid,Request rejected with invalid decision,Pass,Expected response,"{'employee_name': [ErrorDetail(string='This field is required.', code='required')], 'department': [ErrorDetail(string='This field is required.', code='required')], 'designation': [ErrorDetail(string='This field is required.', code='required')]}",[] +BR-HR2-016-V-01,BR,BR-HR2-016,Valid,,,Approve leave via decision endpoint,Leave status updated to APPROVED,Fail,Unexpected status 400,"{'employee_name': [ErrorDetail(string='This field is required.', code='required')], 'department': [ErrorDetail(string='This field is required.', code='required')], 'designation': [ErrorDetail(string='This field is required.', code='required')]}",[] +BR-HR2-017-I-01,BR,BR-HR2-017,Invalid,,,Apply leave with no balance record for type,Request rejected with balance not found error,Pass,Expected response,"{'employee_name': [ErrorDetail(string='This field is required.', code='required')], 'department': [ErrorDetail(string='This field is required.', code='required')], 'designation': [ErrorDetail(string='This field is required.', code='required')]}",[] +BR-HR2-017-V-01,BR,BR-HR2-017,Valid,,,Apply leave with existing leave balance,Request accepted,Fail,Unexpected status 400,"{'employee_name': [ErrorDetail(string='This field is required.', code='required')], 'department': [ErrorDetail(string='This field is required.', code='required')], 'designation': [ErrorDetail(string='This field is required.', code='required')]}",[] +BR-HR2-018-I-01,BR,BR-HR2-018,Invalid,,,Apply leave without employee profile and no employee_id,Request rejected with employee profile not found,Pass,Expected response,"{'employee_name': [ErrorDetail(string='This field is required.', code='required')], 'department': [ErrorDetail(string='This field is required.', code='required')], 'designation': [ErrorDetail(string='This field is required.', code='required')]}",[] +BR-HR2-018-V-01,BR,BR-HR2-018,Valid,,,Apply leave as authenticated employee,Request accepted,Fail,Unexpected status 400,"{'employee_name': [ErrorDetail(string='This field is required.', code='required')], 'department': [ErrorDetail(string='This field is required.', code='required')], 'designation': [ErrorDetail(string='This field is required.', code='required')]}",[] +BR-HR2-019-I-01,BR,BR-HR2-019,Invalid,,,Owner withdraws approved LTC,Request rejected with pending-only error,Fail,Unexpected status 200,"{'id': 1, 'employee_name': 'Rahul Sharma', 'department': 'Computer Science and Engineering', 'designation': 'Assistant Professor', 'ltc_block_year': 2025, 'travel_start_date': '2026-04-25', 'travel_end_date': '2026-04-30', 'destination': 'Delhi', 'purpose_of_travel': 'Family travel', 'family_members': '', 'relationship_details': '', 'travel_mode': 'Train', 'ticket_number': '', 'ticket_cost': None, 'accommodation_cost': None, 'other_expenses': None, 'total_amount_claimed': '22000.00', 'tickets_upload': '', 'bills_upload': '', 'previous_ltc_used': False, 'last_ltc_date': None, 'applied_date': '2026-04-15', 'verified_by_hr': False, 'approval_status': 'WITHDRAWN', 'accountant_status': '', 'remarks': '', 'employee': '1001'}",[] +BR-HR2-019-V-01,BR,BR-HR2-019,Valid,,,Owner withdraws pending LTC,LTC updated to WITHDRAWN,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +BR-HR2-020-I-01,BR,BR-HR2-020,Invalid,,,Decision action=invalid,Request rejected with invalid decision,Pass,Expected response,"{'employee_name': [ErrorDetail(string='This field is required.', code='required')], 'department': [ErrorDetail(string='This field is required.', code='required')], 'designation': [ErrorDetail(string='This field is required.', code='required')]}",[] +BR-HR2-020-V-01,BR,BR-HR2-020,Valid,,,Forward LTC,Approval status set to FORWARDED and accountant_status=PENDING,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +BR-HR2-021-I-01,BR,BR-HR2-021,Invalid,,,Owner withdraws approved CPDA advance,Request rejected with pending-only error,Fail,Unexpected status 200,"{'id': 1, 'employee_name': 'Rahul Sharma', 'department': 'Computer Science and Engineering', 'designation': 'Assistant Professor', 'event_name': 'National Conference on AI', 'event_type': 'Conference', 'organized_by': '', 'venue': '', 'start_date': '2026-05-15', 'end_date': '2026-05-17', 'registration_fee': None, 'travel_expense': None, 'accommodation_expense': None, 'other_expenses': None, 'total_amount': '20000.00', 'purpose_of_attending': 'Present paper', 'benefits_to_institution': 'Research exposure', 'invitation_letter': '', 'receipts': '', 'certificates': '', 'applied_date': '2026-04-15', 'verified_by_hr': False, 'approval_status': 'WITHDRAWN', 'accountant_processing_status': '', 'remarks': '', 'employee': '1001'}",[] +BR-HR2-021-V-01,BR,BR-HR2-021,Valid,,,Owner withdraws pending CPDA advance,CPDA updated to WITHDRAWN,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +BR-HR2-022-I-01,BR,BR-HR2-022,Invalid,,,Decision action=invalid,Request rejected with invalid decision,Pass,Expected response,"{'employee_name': [ErrorDetail(string='This field is required.', code='required')], 'department': [ErrorDetail(string='This field is required.', code='required')], 'designation': [ErrorDetail(string='This field is required.', code='required')]}",[] +BR-HR2-022-V-01,BR,BR-HR2-022,Valid,,,Forward CPDA advance to accountant,Status FORWARDED and accountant_processing_status=PENDING,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +BR-HR2-022-V-02,BR,BR-HR2-022,Valid,,,Forward CPDA advance to director,Status FORWARDED and accountant_processing_status=DIRECTOR_REVIEW,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +BR-HR2-023-I-01,BR,BR-HR2-023,Invalid,,,Non-owner downloads another employee record,Request rejected with 403,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +BR-HR2-023-V-01,BR,BR-HR2-023,Valid,,,Owner downloads own leave application,Download succeeds,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +BR-HR2-024-V-01,BR,BR-HR2-024,Valid,,,Reviewer forwards appraisal,Appraisal status set to REVIEWED,Pass,Expected response,"{'id': 1, 'employee_name': 'Rahul Sharma', 'department': 'Computer Science and Engineering', 'designation': 'Assistant Professor', 'appraisal_year': '2025-2026', 'self_summary': 'Completed teaching responsibilities', 'key_responsibilities': 'Teaching and research', 'achievements': 'Published 1 paper', 'challenges_faced': '', 'teaching_performance': '', 'research_work': '', 'publications': '', 'projects_handled': '', 'administrative_contributions': '', 'trainings_attended': '', 'certifications': '', 'workshops': '', 'goals_achieved': 'Completed syllabus', 'future_goals': 'Publish more papers', 'supporting_documents': '', 'reviewer_id': '1001', 'reviewer_comments': '', 'rating': '', 'status': 'APPROVED', 'remarks': '', 'submitted_at': '2026-04-15T19:42:05.361880', 'employee': '1001'}",[] +BR-HR2-024-V-02,BR,BR-HR2-024,Valid,,,Reviewer approves appraisal,Appraisal status set to APPROVED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-001-AL-01,UC,UC-HR2-001,Alternate Path,Request with only department filter,User is authenticated,GET /hr2/api/employees/?department=1,Returns employees in department,Pass,Expected response,[],[] +UC-HR2-001-EX-01,UC,UC-HR2-001,Exception,Unauthorized user tries to list employees,User is not authenticated,GET /hr2/api/employees/,Returns 401 Unauthorized,Pass,Expected response,"{'detail': ErrorDetail(string='Authentication credentials were not provided.', code='not_authenticated')}",[] +UC-HR2-001-HA-01,UC,UC-HR2-001,Happy Path,HR staff lists all employees,User has HR designation or is HR staff,GET /hr2/api/employees/,Returns list of employees,Fail,Unhandled error,"(, ValueError(""Missing staticfiles manifest entry for '404/css/base.css'""), )",[] +UC-HR2-001-HA-02,UC,UC-HR2-001,Happy Path,Filter employees by type and department,User is authenticated,GET /hr2/api/employees/?type=Faculty&department=1,Returns employees matching filters,Pass,Expected response,[],[] +UC-HR2-002-AL-01,UC,UC-HR2-002,Alternate Path,HOD fetches employee details,HOD is authenticated,GET /hr2/api/employees/123/,Returns employee details,Fail,Unhandled error,"(, ValueError(""Missing staticfiles manifest entry for '404/css/base.css'""), )",[] +UC-HR2-002-EX-01,UC,UC-HR2-002,Exception,Employee not found,Employee does not exist,GET /hr2/api/employees/999999/,Returns 404 Not Found,Fail,Unhandled error,"(, ValueError(""Missing staticfiles manifest entry for '404/css/base.css'""), )",[] +UC-HR2-002-HA-01,UC,UC-HR2-002,Happy Path,Fetch employee details by ID,Employee exists,GET /hr2/api/employees/123/,Returns employee details,Fail,Unhandled error,"(, ValueError(""Missing staticfiles manifest entry for '404/css/base.css'""), )",[] +UC-HR2-003-AL-01,UC,UC-HR2-003,Alternate Path,Update employee address,Employee exists,PUT /hr2/api/employees/123/ with address=Updated,Returns updated employee details,Fail,Unhandled error,"(, ValueError(""Missing staticfiles manifest entry for '404/css/base.css'""), )",[] +UC-HR2-003-EX-01,UC,UC-HR2-003,Exception,Invalid data,Employee exists,PUT /hr2/api/employees/123/ with phone_number=invalid,Returns 400 validation error,Fail,Unhandled error,"(, ValueError(""Missing staticfiles manifest entry for '404/css/base.css'""), )",[] +UC-HR2-003-HA-01,UC,UC-HR2-003,Happy Path,Update phone number,Employee exists,PUT /hr2/api/employees/123/ with phone_number=9876543210,Returns updated employee details,Fail,Unhandled error,"(, ValueError(""Missing staticfiles manifest entry for '404/css/base.css'""), )",[] +UC-HR2-004-AL-01,UC,UC-HR2-004,Alternate Path,Nominate substitute during leave,Nominee employee exists,POST /hr2/api/leave-applications/ with nominee_employee_id=456,Leave created with nominee_status=PENDING,Fail,Unexpected status 400,"{'employee_name': [ErrorDetail(string='This field is required.', code='required')], 'department': [ErrorDetail(string='This field is required.', code='required')], 'designation': [ErrorDetail(string='This field is required.', code='required')]}",[] +UC-HR2-004-EX-01,UC,UC-HR2-004,Exception,Start date in the past,Employee is authenticated,POST /hr2/api/leave-applications/ with start_date=yesterday,Returns 400 start_date validation error,Pass,Expected response,"{'employee_name': [ErrorDetail(string='This field is required.', code='required')], 'department': [ErrorDetail(string='This field is required.', code='required')], 'designation': [ErrorDetail(string='This field is required.', code='required')]}",[] +UC-HR2-004-HA-01,UC,UC-HR2-004,Happy Path,Apply for casual leave with valid dates,Leave balance is sufficient,"POST /hr2/api/leave-applications/ with leave_type=CL, start_date=future, end_date=future+2, total_days=3",Leave application created with approval_status=PENDING or FORWARDED,Fail,Unexpected status 400,"{'employee_name': [ErrorDetail(string='This field is required.', code='required')], 'department': [ErrorDetail(string='This field is required.', code='required')], 'designation': [ErrorDetail(string='This field is required.', code='required')]}",[] +UC-HR2-005-AL-01,UC,UC-HR2-005,Alternate Path,HOD views departmental leave applications,User has HOD designation,GET /hr2/api/leave-applications/,Returns department leave applications,Pass,Expected response,[],[] +UC-HR2-005-EX-01,UC,UC-HR2-005,Exception,Unauthorized user tries to list leave applications,User is not authenticated,GET /hr2/api/leave-applications/,Returns 401 Unauthorized,Pass,Expected response,"{'detail': ErrorDetail(string='Authentication credentials were not provided.', code='not_authenticated')}",[] +UC-HR2-005-HA-01,UC,UC-HR2-005,Happy Path,Employee views own leave applications,Employee is authenticated,GET /hr2/api/leave-applications/,Returns only employee's leave applications,Pass,Expected response,[],[] +UC-HR2-005-HA-02,UC,UC-HR2-005,Happy Path,Director views forwarded applications,User has Director designation,GET /hr2/api/leave-applications/,Returns forwarded and requested decisions,Pass,Expected response,[],[] +UC-HR2-006-AL-01,UC,UC-HR2-006,Alternate Path,Registrar withdraws forwarded leave,Leave approval_status is FORWARDED,POST /hr2/api/leave-applications/10/withdraw/,Leave updated to approval_status=REJECTED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-006-EX-01,UC,UC-HR2-006,Exception,Withdraw non-pending leave,Leave approval_status is APPROVED,POST /hr2/api/leave-applications/10/withdraw/,Returns 400 with withdrawal error,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-006-HA-01,UC,UC-HR2-006,Happy Path,Employee withdraws own pending leave,Leave approval_status is PENDING,POST /hr2/api/leave-applications/10/withdraw/,Leave updated to approval_status=WITHDRAWN,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-007-AL-01,UC,UC-HR2-007,Alternate Path,Submit cancellation request with reason,Approved leave exists,POST /hr2/api/leave-applications/10/cancel-request/ with reason=medical,Cancel status set to REQUESTED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-007-EX-01,UC,UC-HR2-007,Exception,Cancellation after start date,Today is on or after start date,POST /hr2/api/leave-applications/10/cancel-request/,Returns 400 cancellation window error,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-007-HA-01,UC,UC-HR2-007,Happy Path,Submit cancellation request before start date,Today is before leave start date,POST /hr2/api/leave-applications/10/cancel-request/ with reason=change of plan,Cancel status set to REQUESTED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-008-AL-01,UC,UC-HR2-008,Alternate Path,Request extension with reason,Extension dates are valid,POST /hr2/api/leave-applications/10/extension-request/ with reason=medical,Extension status set to REQUESTED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-008-EX-01,UC,UC-HR2-008,Exception,Extension with invalid date,New end date before current end date,POST /hr2/api/leave-applications/10/extension-request/ with new_end_date=earlier,Returns 400 validation error,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-008-HA-01,UC,UC-HR2-008,Happy Path,Request extension with new end date,Extension dates are valid,POST /hr2/api/leave-applications/10/extension-request/ with new_end_date=future+2,Extension status set to REQUESTED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-009-AL-01,UC,UC-HR2-009,Alternate Path,Employee views balance with no records,Employee has no balance entries,GET /hr2/api/leave-balance/,Returns empty balance list,Pass,Expected response,[],[] +UC-HR2-009-EX-01,UC,UC-HR2-009,Exception,Unauthorized user tries to view leave balance,User is not authenticated,GET /hr2/api/leave-balance/,Returns 401 Unauthorized,Pass,Expected response,"{'detail': ErrorDetail(string='Authentication credentials were not provided.', code='not_authenticated')}",[] +UC-HR2-009-HA-01,UC,UC-HR2-009,Happy Path,Employee views own leave balance,Employee is authenticated,GET /hr2/api/leave-balance/,Returns leave balance for the employee,Pass,Expected response,[],[] +UC-HR2-009-HA-02,UC,UC-HR2-009,Happy Path,HR views leave balance for another employee,User has HR designation,GET /hr2/api/leave-balance/123/,Returns leave balance for employee 123,Pass,Expected response,"[OrderedDict([('id', 193), ('leave_type_name', 'Casual'), ('year', 2026), ('opening_balance', '10.0'), ('accrued', '0.0'), ('availed', '0.0'), ('current_balance', '10.0'), ('employee', '1001'), ('leave_type', 193)]), OrderedDict([('id', 194), ('leave_type_name', 'Vacation'), ('year', 2026), ('opening_balance', '10.0'), ('accrued', '0.0'), ('availed', '0.0'), ('current_balance', '10.0'), ('employee', '1001'), ('leave_type', 194)]), OrderedDict([('id', 195), ('leave_type_name', 'Earned'), ('year', 2026), ('opening_balance', '10.0'), ('accrued', '0.0'), ('availed', '0.0'), ('current_balance', '10.0'), ('employee', '1001'), ('leave_type', 195)]), OrderedDict([('id', 196), ('leave_type_name', 'Medical'), ('year', 2026), ('opening_balance', '10.0'), ('accrued', '0.0'), ('availed', '0.0'), ('current_balance', '10.0'), ('employee', '1001'), ('leave_type', 196)]), OrderedDict([('id', 197), ('leave_type_name', 'Restricted'), ('year', 2026), ('opening_balance', '10.0'), ('accrued', '0.0'), ('availed', '0.0'), ('current_balance', '10.0'), ('employee', '1001'), ('leave_type', 197)]), OrderedDict([('id', 198), ('leave_type_name', 'Sabbatical'), ('year', 2026), ('opening_balance', '10.0'), ('accrued', '0.0'), ('availed', '0.0'), ('current_balance', '10.0'), ('employee', '1001'), ('leave_type', 198)])]",[] +UC-HR2-010-AL-01,UC,UC-HR2-010,Alternate Path,Download pending leave application,Leave is pending and belongs to employee,GET /hr2/api/leave-applications/10/download/,Returns text file attachment with leave details,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-010-EX-01,UC,UC-HR2-010,Exception,Access another employee's leave,Leave does not belong to requester,GET /hr2/api/leave-applications/999/download/,Returns 403 Not authorized,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-010-HA-01,UC,UC-HR2-010,Happy Path,Download approved leave application,Leave exists and belongs to employee,GET /hr2/api/leave-applications/10/download/,Returns text file attachment with leave details,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-011-AL-01,UC,UC-HR2-011,Alternate Path,Submit LTC claim with optional fields,Required fields provided,POST /hr2/api/ltc/ with optional fields,LTC application created with approval_status=PENDING,Pass,Expected response,"{'id': 3, 'employee_name': 'Rahul Sharma', 'department': 'Computer Science and Engineering', 'designation': 'Assistant Professor', 'ltc_block_year': 2025, 'travel_start_date': '2026-04-25', 'travel_end_date': '2026-04-30', 'destination': 'Delhi', 'purpose_of_travel': 'Family travel', 'family_members': '', 'relationship_details': '', 'travel_mode': 'Train', 'ticket_number': '', 'ticket_cost': None, 'accommodation_cost': None, 'other_expenses': None, 'total_amount_claimed': '22000.00', 'tickets_upload': '', 'bills_upload': '', 'previous_ltc_used': False, 'last_ltc_date': None, 'applied_date': '2026-04-15', 'verified_by_hr': False, 'approval_status': 'PENDING', 'accountant_status': '', 'remarks': '', 'employee': '1001'}",[] +UC-HR2-011-EX-01,UC,UC-HR2-011,Exception,Missing required fields,Employee is authenticated,POST /hr2/api/ltc/ with incomplete payload,Returns 400 validation errors,Fail,Unexpected status 201,"{'id': 4, 'employee_name': 'Rahul Sharma', 'department': 'Computer Science and Engineering', 'designation': 'Assistant Professor', 'ltc_block_year': 2025, 'travel_start_date': '2026-04-25', 'travel_end_date': '2026-04-30', 'destination': 'Delhi', 'purpose_of_travel': 'Family travel', 'family_members': '', 'relationship_details': '', 'travel_mode': 'Train', 'ticket_number': '', 'ticket_cost': None, 'accommodation_cost': None, 'other_expenses': None, 'total_amount_claimed': '22000.00', 'tickets_upload': '', 'bills_upload': '', 'previous_ltc_used': False, 'last_ltc_date': None, 'applied_date': '2026-04-15', 'verified_by_hr': False, 'approval_status': 'PENDING', 'accountant_status': '', 'remarks': '', 'employee': '1001'}",[] +UC-HR2-011-HA-01,UC,UC-HR2-011,Happy Path,Submit LTC claim,Required fields provided,POST /hr2/api/ltc/ with required LTC fields,LTC application created with approval_status=PENDING,Pass,Expected response,"{'id': 5, 'employee_name': 'Rahul Sharma', 'department': 'Computer Science and Engineering', 'designation': 'Assistant Professor', 'ltc_block_year': 2025, 'travel_start_date': '2026-04-25', 'travel_end_date': '2026-04-30', 'destination': 'Delhi', 'purpose_of_travel': 'Family travel', 'family_members': '', 'relationship_details': '', 'travel_mode': 'Train', 'ticket_number': '', 'ticket_cost': None, 'accommodation_cost': None, 'other_expenses': None, 'total_amount_claimed': '22000.00', 'tickets_upload': '', 'bills_upload': '', 'previous_ltc_used': False, 'last_ltc_date': None, 'applied_date': '2026-04-15', 'verified_by_hr': False, 'approval_status': 'PENDING', 'accountant_status': '', 'remarks': '', 'employee': '1001'}",[] +UC-HR2-012-AL-01,UC,UC-HR2-012,Alternate Path,Submit CPDA advance with optional expenses,Required fields provided,POST /hr2/api/cpda-advances/ with optional fields,CPDA advance created with approval_status=PENDING,Pass,Expected response,"{'id': 3, 'employee_name': 'Rahul Sharma', 'department': 'Computer Science and Engineering', 'designation': 'Assistant Professor', 'event_name': 'National Conference on AI', 'event_type': 'Conference', 'organized_by': '', 'venue': '', 'start_date': '2026-05-15', 'end_date': '2026-05-17', 'registration_fee': None, 'travel_expense': None, 'accommodation_expense': None, 'other_expenses': None, 'total_amount': '20000.00', 'purpose_of_attending': 'Present paper', 'benefits_to_institution': 'Research exposure', 'invitation_letter': '', 'receipts': '', 'certificates': '', 'applied_date': '2026-04-15', 'verified_by_hr': False, 'approval_status': 'PENDING', 'accountant_processing_status': '', 'remarks': '', 'employee': '1001'}",[] +UC-HR2-012-EX-01,UC,UC-HR2-012,Exception,Invalid amount,Employee is authenticated,POST /hr2/api/cpda-advances/ with amountRequired=invalid,Returns 400 validation errors,Fail,Unexpected status 201,"{'id': 4, 'employee_name': 'Rahul Sharma', 'department': 'Computer Science and Engineering', 'designation': 'Assistant Professor', 'event_name': 'National Conference on AI', 'event_type': 'Conference', 'organized_by': '', 'venue': '', 'start_date': '2026-05-15', 'end_date': '2026-05-17', 'registration_fee': None, 'travel_expense': None, 'accommodation_expense': None, 'other_expenses': None, 'total_amount': '20000.00', 'purpose_of_attending': 'Present paper', 'benefits_to_institution': 'Research exposure', 'invitation_letter': '', 'receipts': '', 'certificates': '', 'applied_date': '2026-04-15', 'verified_by_hr': False, 'approval_status': 'PENDING', 'accountant_processing_status': '', 'remarks': '', 'employee': '1001'}",[] +UC-HR2-012-HA-01,UC,UC-HR2-012,Happy Path,Submit CPDA advance,Required fields provided,POST /hr2/api/cpda-advances/ with required fields,CPDA advance created with approval_status=PENDING,Pass,Expected response,"{'id': 5, 'employee_name': 'Rahul Sharma', 'department': 'Computer Science and Engineering', 'designation': 'Assistant Professor', 'event_name': 'National Conference on AI', 'event_type': 'Conference', 'organized_by': '', 'venue': '', 'start_date': '2026-05-15', 'end_date': '2026-05-17', 'registration_fee': None, 'travel_expense': None, 'accommodation_expense': None, 'other_expenses': None, 'total_amount': '20000.00', 'purpose_of_attending': 'Present paper', 'benefits_to_institution': 'Research exposure', 'invitation_letter': '', 'receipts': '', 'certificates': '', 'applied_date': '2026-04-15', 'verified_by_hr': False, 'approval_status': 'PENDING', 'accountant_processing_status': '', 'remarks': '', 'employee': '1001'}",[] +UC-HR2-013-AL-01,UC,UC-HR2-013,Alternate Path,Submit appraisal form with optional fields,Required fields provided,POST /hr2/api/appraisal-forms/ with optional fields,Appraisal form created with status=SUBMITTED,Pass,Expected response,"{'id': 3, 'employee_name': 'Rahul Sharma', 'department': 'Computer Science and Engineering', 'designation': 'Assistant Professor', 'appraisal_year': '2025-2026', 'self_summary': 'Completed teaching responsibilities', 'key_responsibilities': 'Teaching and research', 'achievements': 'Published 1 paper', 'challenges_faced': '', 'teaching_performance': '', 'research_work': '', 'publications': '', 'projects_handled': '', 'administrative_contributions': '', 'trainings_attended': '', 'certifications': '', 'workshops': '', 'goals_achieved': 'Completed syllabus', 'future_goals': 'Publish more papers', 'supporting_documents': '', 'reviewer_id': '', 'reviewer_comments': '', 'rating': '', 'status': 'PENDING', 'remarks': '', 'submitted_at': '2026-04-15T19:42:13.348290', 'employee': '1001'}",[] +UC-HR2-013-EX-01,UC,UC-HR2-013,Exception,Missing required fields,Employee is authenticated,POST /hr2/api/appraisal-forms/ with incomplete payload,Returns 400 validation errors,Fail,Unexpected status 201,"{'id': 4, 'employee_name': 'Rahul Sharma', 'department': 'Computer Science and Engineering', 'designation': 'Assistant Professor', 'appraisal_year': '2025-2026', 'self_summary': 'Completed teaching responsibilities', 'key_responsibilities': 'Teaching and research', 'achievements': 'Published 1 paper', 'challenges_faced': '', 'teaching_performance': '', 'research_work': '', 'publications': '', 'projects_handled': '', 'administrative_contributions': '', 'trainings_attended': '', 'certifications': '', 'workshops': '', 'goals_achieved': 'Completed syllabus', 'future_goals': 'Publish more papers', 'supporting_documents': '', 'reviewer_id': '', 'reviewer_comments': '', 'rating': '', 'status': 'PENDING', 'remarks': '', 'submitted_at': '2026-04-15T19:42:13.360130', 'employee': '1001'}",[] +UC-HR2-013-HA-01,UC,UC-HR2-013,Happy Path,Submit appraisal form,Required fields provided,POST /hr2/api/appraisal-forms/ with required fields,Appraisal form created with status=SUBMITTED,Pass,Expected response,"{'id': 5, 'employee_name': 'Rahul Sharma', 'department': 'Computer Science and Engineering', 'designation': 'Assistant Professor', 'appraisal_year': '2025-2026', 'self_summary': 'Completed teaching responsibilities', 'key_responsibilities': 'Teaching and research', 'achievements': 'Published 1 paper', 'challenges_faced': '', 'teaching_performance': '', 'research_work': '', 'publications': '', 'projects_handled': '', 'administrative_contributions': '', 'trainings_attended': '', 'certifications': '', 'workshops': '', 'goals_achieved': 'Completed syllabus', 'future_goals': 'Publish more papers', 'supporting_documents': '', 'reviewer_id': '', 'reviewer_comments': '', 'rating': '', 'status': 'PENDING', 'remarks': '', 'submitted_at': '2026-04-15T19:42:13.368130', 'employee': '1001'}",[] +UC-HR2-014-AL-01,UC,UC-HR2-014,Alternate Path,HR staff fetches leave application by ID,HR staff is authenticated,GET /hr2/api/leave-applications/10/,Returns leave application,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-014-EX-01,UC,UC-HR2-014,Exception,Leave application not found,Leave application does not exist,GET /hr2/api/leave-applications/999/,Returns 404 Not Found,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-014-HA-01,UC,UC-HR2-014,Happy Path,Fetch leave application by ID,Leave application exists,GET /hr2/api/leave-applications/10/,Returns leave application,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-015-AL-01,UC,UC-HR2-015,Alternate Path,Update leave handover notes,Leave belongs to employee,PUT /hr2/api/leave-applications/10/ with handover_notes=updated,Leave application updated,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-015-EX-01,UC,UC-HR2-015,Exception,Update another employee's leave,Leave belongs to another employee,PUT /hr2/api/leave-applications/10/,Returns 403 Not authorized,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-015-HA-01,UC,UC-HR2-015,Happy Path,Update leave reason,Leave belongs to employee,PUT /hr2/api/leave-applications/10/ with reason=updated,Leave application updated,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-016-AL-01,UC,UC-HR2-016,Alternate Path,Delete pending leave without attachments,Leave approval_status is PENDING,DELETE /hr2/api/leave-applications/10/,Leave application deleted,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-016-EX-01,UC,UC-HR2-016,Exception,Delete non-pending leave,Leave approval_status is APPROVED,DELETE /hr2/api/leave-applications/10/,Returns 400 with delete error,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-016-HA-01,UC,UC-HR2-016,Happy Path,Delete pending leave,Leave approval_status is PENDING,DELETE /hr2/api/leave-applications/10/,Leave application deleted,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-017-AL-01,UC,UC-HR2-017,Alternate Path,Reject leave,Leave exists,POST /hr2/api/leave-applications/10/reject/,Leave status updated to REJECTED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-017-EX-01,UC,UC-HR2-017,Exception,Invalid decision,Leave exists,POST /hr2/api/leave-applications/10/invalid/,Returns 400 invalid decision,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-017-HA-01,UC,UC-HR2-017,Happy Path,Approve leave,Leave exists,POST /hr2/api/leave-applications/10/approve/,Leave status updated to APPROVED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-017-HA-02,UC,UC-HR2-017,Happy Path,Forward leave,Leave exists,POST /hr2/api/leave-applications/10/forward/,Leave status updated to FORWARDED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-018-AL-01,UC,UC-HR2-018,Alternate Path,Nominee has no pending requests,Nominee has no pending requests,GET /hr2/api/leave-nominee/,Returns empty list,Fail,Unexpected status 405,"{'detail': ErrorDetail(string='Method ""GET"" not allowed.', code='method_not_allowed')}",[] +UC-HR2-018-EX-01,UC,UC-HR2-018,Exception,Unauthorized user tries to view nominee dashboard,User is not authenticated,GET /hr2/api/leave-nominee/,Returns 401 Unauthorized,Fail,Unexpected status 405,"{'detail': ErrorDetail(string='Method ""GET"" not allowed.', code='method_not_allowed')}",[] +UC-HR2-018-HA-01,UC,UC-HR2-018,Happy Path,Nominee views pending requests,Nominee has pending requests,GET /hr2/api/leave-nominee/,Returns pending nominee requests,Fail,Unexpected status 405,"{'detail': ErrorDetail(string='Method ""GET"" not allowed.', code='method_not_allowed')}",[] +UC-HR2-019-AL-01,UC,UC-HR2-019,Alternate Path,Nominee declines,Nominee is assigned on leave,POST /hr2/api/leave-nominee/10/ with action=decline,Nominee status updated to DECLINED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-019-EX-01,UC,UC-HR2-019,Exception,Invalid action,Nominee is assigned,POST /hr2/api/leave-nominee/10/ with action=invalid,Returns 400 invalid action,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-019-HA-01,UC,UC-HR2-019,Happy Path,Nominee accepts,Nominee is assigned on leave,POST /hr2/api/leave-nominee/10/ with action=accept,Nominee status updated to ACCEPTED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-020-AL-01,UC,UC-HR2-020,Alternate Path,Request documents with updated message,Leave exists,POST /hr2/api/leave-applications/10/request-document/ with message=submit updated proof,Document request status set to REQUESTED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-020-EX-01,UC,UC-HR2-020,Exception,Missing message,Leave exists,POST /hr2/api/leave-applications/10/request-document/,Returns 400 message required,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-020-HA-01,UC,UC-HR2-020,Happy Path,Request documents with message,Leave exists,POST /hr2/api/leave-applications/10/request-document/ with message=submit proof,Document request status set to REQUESTED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-021-AL-01,UC,UC-HR2-021,Alternate Path,Submit updated document,Document request exists,POST /hr2/api/leave-applications/10/submit-document/ with submission=updated ref,Document request status set to SUBMITTED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-021-EX-01,UC,UC-HR2-021,Exception,Submit without request,No document request exists,POST /hr2/api/leave-applications/10/submit-document/ with submission=doc ref,Returns 400 no request,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-021-HA-01,UC,UC-HR2-021,Happy Path,Submit document after request,Document request exists,POST /hr2/api/leave-applications/10/submit-document/ with submission=doc ref,Document request status set to SUBMITTED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-022-AL-01,UC,UC-HR2-022,Alternate Path,Approve cancellation with remarks,Approver role matches,POST /hr2/api/leave-applications/10/cancel-decision/approve/ with remarks=ok,Cancellation approved and leave cancelled,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-022-EX-01,UC,UC-HR2-022,Exception,Invalid decision,Cancellation request exists,POST /hr2/api/leave-applications/10/cancel-decision/invalid/,Returns 400 invalid decision,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-022-HA-01,UC,UC-HR2-022,Happy Path,Approve cancellation,Approver role matches,POST /hr2/api/leave-applications/10/cancel-decision/approve/,Cancellation approved and leave cancelled,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-022-HA-02,UC,UC-HR2-022,Happy Path,Reject cancellation,Approver role matches,POST /hr2/api/leave-applications/10/cancel-decision/reject/,Cancellation rejected,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-023-AL-01,UC,UC-HR2-023,Alternate Path,Approve extension with remarks,Approver role matches,POST /hr2/api/leave-applications/10/extension-decision/approve/ with remarks=ok,Extension approved and leave updated,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-023-EX-01,UC,UC-HR2-023,Exception,Invalid decision,Extension request exists,POST /hr2/api/leave-applications/10/extension-decision/invalid/,Returns 400 invalid decision,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-023-HA-01,UC,UC-HR2-023,Happy Path,Approve extension,Approver role matches,POST /hr2/api/leave-applications/10/extension-decision/approve/,Extension approved and leave updated,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-023-HA-02,UC,UC-HR2-023,Happy Path,Reject extension,Approver role matches,POST /hr2/api/leave-applications/10/extension-decision/reject/,Extension rejected,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-024-AL-01,UC,UC-HR2-024,Alternate Path,Mark half-day attendance,Valid status and date,"POST /hr2/api/attendance/ with date=today, status=HALF_DAY",Attendance record created,Fail,Unhandled error,"(, ValueError(""Missing staticfiles manifest entry for '404/css/base.css'""), )",[] +UC-HR2-024-EX-01,UC,UC-HR2-024,Exception,Missing attendance status,Employee is authenticated,POST /hr2/api/attendance/ with date=today,Returns 400 validation errors,Fail,Unhandled error,"(, ValueError(""Missing staticfiles manifest entry for '404/css/base.css'""), )",[] +UC-HR2-024-HA-01,UC,UC-HR2-024,Happy Path,Mark attendance,Valid status and date,"POST /hr2/api/attendance/ with date=today, status=PRESENT",Attendance record created,Fail,Unhandled error,"(, ValueError(""Missing staticfiles manifest entry for '404/css/base.css'""), )",[] +UC-HR2-025-AL-01,UC,UC-HR2-025,Alternate Path,View attendance without filters,Attendance exists,GET /hr2/api/attendance/,Returns attendance records,Pass,Expected response,[],[] +UC-HR2-025-EX-01,UC,UC-HR2-025,Exception,Unauthorized user tries to view attendance,User is not authenticated,GET /hr2/api/attendance/,Returns 401 Unauthorized,Pass,Expected response,"{'detail': ErrorDetail(string='Authentication credentials were not provided.', code='not_authenticated')}",[] +UC-HR2-025-HA-01,UC,UC-HR2-025,Happy Path,View attendance for date range,Attendance exists,GET /hr2/api/attendance/?from_date=2026-05-01&to_date=2026-05-10,Returns attendance records,Pass,Expected response,[],[] +UC-HR2-026-AL-01,UC,UC-HR2-026,Alternate Path,List all periods without filter,Periods exist,GET /hr2/api/appraisal-periods/,Returns appraisal periods,Pass,Expected response,"[OrderedDict([('id', 50), ('name', '2025-2026'), ('start_date', '2025-07-01'), ('end_date', '2026-06-30'), ('submission_deadline', '2026-05-31'), ('is_active', True)])]",[] +UC-HR2-026-EX-01,UC,UC-HR2-026,Exception,Unauthorized user tries to view appraisal periods,User is not authenticated,GET /hr2/api/appraisal-periods/,Returns 401 Unauthorized,Pass,Expected response,"{'detail': ErrorDetail(string='Authentication credentials were not provided.', code='not_authenticated')}",[] +UC-HR2-026-HA-01,UC,UC-HR2-026,Happy Path,List active periods,Periods exist,GET /hr2/api/appraisal-periods/?is_active=true,Returns appraisal periods,Fail,Unhandled error,"(, ValueError(""Missing staticfiles manifest entry for '404/css/base.css'""), )",[] +UC-HR2-027-AL-01,UC,UC-HR2-027,Alternate Path,Submit appraisal with remarks,Required fields provided,"POST /hr2/api/appraisals/ with period, scores, remarks",Performance appraisal created,Fail,Unexpected status 400,"{'employee': [ErrorDetail(string='This field is required.', code='required')]}",[] +UC-HR2-027-EX-01,UC,UC-HR2-027,Exception,Missing required fields,Employee is authenticated,POST /hr2/api/appraisals/ with incomplete payload,Returns 400 validation errors,Pass,Expected response,"{'employee': [ErrorDetail(string='This field is required.', code='required')]}",[] +UC-HR2-027-HA-01,UC,UC-HR2-027,Happy Path,Submit appraisal,Required fields provided,POST /hr2/api/appraisals/ with period and scores,Performance appraisal created,Fail,Unexpected status 400,"{'employee': [ErrorDetail(string='This field is required.', code='required')]}",[] +UC-HR2-028-AL-01,UC,UC-HR2-028,Alternate Path,List all appraisals,Appraisals exist,GET /hr2/api/appraisals/,Returns appraisals,Pass,Expected response,[],[] +UC-HR2-028-EX-01,UC,UC-HR2-028,Exception,Unauthorized user tries to list appraisals,User is not authenticated,GET /hr2/api/appraisals/,Returns 401 Unauthorized,Pass,Expected response,"{'detail': ErrorDetail(string='Authentication credentials were not provided.', code='not_authenticated')}",[] +UC-HR2-028-HA-01,UC,UC-HR2-028,Happy Path,List appraisals for period,Appraisals exist,GET /hr2/api/appraisals/?period=1,Returns appraisals,Pass,Expected response,[],[] +UC-HR2-029-AL-01,UC,UC-HR2-029,Alternate Path,List programs when none are available,No programs available,GET /hr2/api/training-programs/,Returns empty list,Pass,Expected response,"[OrderedDict([('id', 53), ('title', 'AI Workshop'), ('description', 'AI fundamentals'), ('organizer', 'IIITDMJ'), ('venue', 'Jabalpur'), ('start_date', '2026-04-25'), ('end_date', '2026-04-27'), ('max_participants', 30), ('is_mandatory', False)])]",[] +UC-HR2-029-EX-01,UC,UC-HR2-029,Exception,Unauthorized user tries to list programs,User is not authenticated,GET /hr2/api/training-programs/,Returns 401 Unauthorized,Pass,Expected response,"{'detail': ErrorDetail(string='Authentication credentials were not provided.', code='not_authenticated')}",[] +UC-HR2-029-HA-01,UC,UC-HR2-029,Happy Path,List available programs,Programs exist,GET /hr2/api/training-programs/,Returns training programs,Pass,Expected response,"[OrderedDict([('id', 53), ('title', 'AI Workshop'), ('description', 'AI fundamentals'), ('organizer', 'IIITDMJ'), ('venue', 'Jabalpur'), ('start_date', '2026-04-25'), ('end_date', '2026-04-27'), ('max_participants', 30), ('is_mandatory', False)])]",[] +UC-HR2-030-AL-01,UC,UC-HR2-030,Alternate Path,Nominate for mandatory program,Program exists and is mandatory,POST /hr2/api/training-nominations/ with program data,Nomination created,Fail,Unexpected status 400,"{'employee': [ErrorDetail(string='This field is required.', code='required')]}",[] +UC-HR2-030-EX-01,UC,UC-HR2-030,Exception,Invalid program,Employee is authenticated,POST /hr2/api/training-nominations/ with invalid program,Returns 400 validation errors,Pass,Expected response,"{'employee': [ErrorDetail(string='This field is required.', code='required')]}",[] +UC-HR2-030-HA-01,UC,UC-HR2-030,Happy Path,Submit training nomination,Program exists,POST /hr2/api/training-nominations/ with program data,Nomination created,Fail,Unexpected status 400,"{'employee': [ErrorDetail(string='This field is required.', code='required')]}",[] +UC-HR2-031-AL-01,UC,UC-HR2-031,Alternate Path,List nominations after submission,Nomination exists,GET /hr2/api/training-nominations/,Returns training nominations,Pass,Expected response,[],[] +UC-HR2-031-EX-01,UC,UC-HR2-031,Exception,Unauthorized user tries to list nominations,User is not authenticated,GET /hr2/api/training-nominations/,Returns 401 Unauthorized,Pass,Expected response,"{'detail': ErrorDetail(string='Authentication credentials were not provided.', code='not_authenticated')}",[] +UC-HR2-031-HA-01,UC,UC-HR2-031,Happy Path,List nominations,Nominations exist,GET /hr2/api/training-nominations/,Returns training nominations,Pass,Expected response,[],[] +UC-HR2-032-AL-01,UC,UC-HR2-032,Alternate Path,Submit promotion with API score,Required fields provided,POST /hr2/api/promotions/ with api_score,Promotion application created,Fail,Unexpected status 400,"{'employee': [ErrorDetail(string='This field is required.', code='required')]}",[] +UC-HR2-032-EX-01,UC,UC-HR2-032,Exception,Missing required fields,Employee is authenticated,POST /hr2/api/promotions/ with incomplete payload,Returns 400 validation errors,Pass,Expected response,"{'employee': [ErrorDetail(string='This field is required.', code='required')]}",[] +UC-HR2-032-HA-01,UC,UC-HR2-032,Happy Path,Submit promotion,Required fields provided,POST /hr2/api/promotions/ with required fields,Promotion application created,Fail,Unexpected status 400,"{'employee': [ErrorDetail(string='This field is required.', code='required')]}",[] +UC-HR2-033-AL-01,UC,UC-HR2-033,Alternate Path,List promotions when none exist,No promotions exist,GET /hr2/api/promotions/,Returns empty list,Pass,Expected response,[],[] +UC-HR2-033-EX-01,UC,UC-HR2-033,Exception,Unauthorized user tries to list promotions,User is not authenticated,GET /hr2/api/promotions/,Returns 401 Unauthorized,Pass,Expected response,"{'detail': ErrorDetail(string='Authentication credentials were not provided.', code='not_authenticated')}",[] +UC-HR2-033-HA-01,UC,UC-HR2-033,Happy Path,List promotions,Applications exist,GET /hr2/api/promotions/,Returns promotion applications,Pass,Expected response,[],[] +UC-HR2-034-AL-01,UC,UC-HR2-034,Alternate Path,Get workload without semester filter,Workload exists,GET /hr2/api/workload/?year=2026,Returns workload records,Fail,Unhandled error,"(, ValueError(""Missing staticfiles manifest entry for '404/css/base.css'""), )",[] +UC-HR2-034-EX-01,UC,UC-HR2-034,Exception,Unauthorized user tries to view workload,User is not authenticated,GET /hr2/api/workload/,Returns 401 Unauthorized,Pass,Expected response,"{'detail': ErrorDetail(string='Authentication credentials were not provided.', code='not_authenticated')}",[] +UC-HR2-034-HA-01,UC,UC-HR2-034,Happy Path,Get workload by semester,Workload exists,GET /hr2/api/workload/?semester=Spring&year=2026,Returns workload records,Fail,Unhandled error,"(, ValueError(""Missing staticfiles manifest entry for '404/css/base.css'""), )",[] +UC-HR2-035-AL-01,UC,UC-HR2-035,Alternate Path,Update LTC purpose,LTC belongs to employee,PUT /hr2/api/ltc/10/ with purpose_of_travel=updated,LTC updated,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-035-EX-01,UC,UC-HR2-035,Exception,Update another employee's LTC,LTC belongs to another employee,PUT /hr2/api/ltc/10/,Returns 403 Not authorized,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-035-HA-01,UC,UC-HR2-035,Happy Path,Update LTC details,LTC belongs to employee,PUT /hr2/api/ltc/10/ with destination=updated,LTC updated,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-036-AL-01,UC,UC-HR2-036,Alternate Path,Accountant views forwarded LTC,User is Accountant,GET /hr2/api/ltc/,Returns forwarded LTC,Pass,Expected response,[],[] +UC-HR2-036-EX-01,UC,UC-HR2-036,Exception,Unauthorized user tries to list LTC,User is not authenticated,GET /hr2/api/ltc/,Returns 401 Unauthorized,Pass,Expected response,"{'detail': ErrorDetail(string='Authentication credentials were not provided.', code='not_authenticated')}",[] +UC-HR2-036-HA-01,UC,UC-HR2-036,Happy Path,Employee views own LTC,Employee is authenticated,GET /hr2/api/ltc/,Returns employee LTC applications,Pass,Expected response,[],[] +UC-HR2-036-HA-02,UC,UC-HR2-036,Happy Path,HR staff views pending LTC,User is HR staff,GET /hr2/api/ltc/,Returns pending/forwarded LTC,Pass,Expected response,[],[] +UC-HR2-037-AL-01,UC,UC-HR2-037,Alternate Path,HR staff downloads LTC,HR staff is authenticated,GET /hr2/api/ltc/10/download/,Returns text file attachment,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-037-EX-01,UC,UC-HR2-037,Exception,Unauthorized user tries to download LTC,User is not authenticated,GET /hr2/api/ltc/10/download/,Returns 401 Unauthorized,Pass,Expected response,"{'detail': ErrorDetail(string='Authentication credentials were not provided.', code='not_authenticated')}",[] +UC-HR2-037-HA-01,UC,UC-HR2-037,Happy Path,Download LTC,LTC exists,GET /hr2/api/ltc/10/download/,Returns text file attachment,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-038-AL-01,UC,UC-HR2-038,Alternate Path,Withdraw pending LTC with remarks,LTC approval_status is PENDING,POST /hr2/api/ltc/10/withdraw/ with remarks=updated,LTC updated to WITHDRAWN,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-038-EX-01,UC,UC-HR2-038,Exception,Withdraw non-pending LTC,LTC approval_status is APPROVED,POST /hr2/api/ltc/10/withdraw/,Returns 400 with withdrawal error,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-038-HA-01,UC,UC-HR2-038,Happy Path,Withdraw pending LTC,LTC approval_status is PENDING,POST /hr2/api/ltc/10/withdraw/,LTC updated to WITHDRAWN,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-039-AL-01,UC,UC-HR2-039,Alternate Path,Reject LTC,LTC exists,POST /hr2/api/ltc/10/reject/,LTC status REJECTED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-039-EX-01,UC,UC-HR2-039,Exception,Invalid decision,LTC exists,POST /hr2/api/ltc/10/invalid/,Returns 400 invalid decision,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-039-HA-01,UC,UC-HR2-039,Happy Path,Forward LTC,LTC exists,POST /hr2/api/ltc/10/forward/,LTC status FORWARDED and accountant_status=PENDING,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-039-HA-02,UC,UC-HR2-039,Happy Path,Approve LTC,LTC exists,POST /hr2/api/ltc/10/approve/,LTC status APPROVED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-040-AL-01,UC,UC-HR2-040,Alternate Path,HR staff views pending advances,User is HR staff,GET /hr2/api/cpda-advances/,Returns pending advances,Pass,Expected response,[],[] +UC-HR2-040-EX-01,UC,UC-HR2-040,Exception,Unauthorized user tries to list advances,User is not authenticated,GET /hr2/api/cpda-advances/,Returns 401 Unauthorized,Pass,Expected response,"{'detail': ErrorDetail(string='Authentication credentials were not provided.', code='not_authenticated')}",[] +UC-HR2-040-HA-01,UC,UC-HR2-040,Happy Path,Employee views own advances,Employee is authenticated,GET /hr2/api/cpda-advances/,Returns employee CPDA advances,Pass,Expected response,[],[] +UC-HR2-041-AL-01,UC,UC-HR2-041,Alternate Path,HR staff fetches CPDA advance,HR staff is authenticated,GET /hr2/api/cpda-advances/10/,Returns CPDA advance,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-041-EX-01,UC,UC-HR2-041,Exception,CPDA advance not found,CPDA advance does not exist,GET /hr2/api/cpda-advances/999/,Returns 404 Not Found,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-041-HA-01,UC,UC-HR2-041,Happy Path,Fetch CPDA advance,CPDA advance exists,GET /hr2/api/cpda-advances/10/,Returns CPDA advance,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-042-AL-01,UC,UC-HR2-042,Alternate Path,HR staff downloads CPDA advance,HR staff is authenticated,GET /hr2/api/cpda-advances/10/download/,Returns text file attachment,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-042-EX-01,UC,UC-HR2-042,Exception,Unauthorized user tries to download CPDA,User is not authenticated,GET /hr2/api/cpda-advances/10/download/,Returns 401 Unauthorized,Pass,Expected response,"{'detail': ErrorDetail(string='Authentication credentials were not provided.', code='not_authenticated')}",[] +UC-HR2-042-HA-01,UC,UC-HR2-042,Happy Path,Download CPDA advance,CPDA exists,GET /hr2/api/cpda-advances/10/download/,Returns text file attachment,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-043-AL-01,UC,UC-HR2-043,Alternate Path,Withdraw pending CPDA advance with remarks,CPDA approval_status is PENDING,POST /hr2/api/cpda-advances/10/withdraw/ with remarks=updated,CPDA updated to WITHDRAWN,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-043-EX-01,UC,UC-HR2-043,Exception,Withdraw non-pending CPDA advance,CPDA approval_status is APPROVED,POST /hr2/api/cpda-advances/10/withdraw/,Returns 400 with withdrawal error,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-043-HA-01,UC,UC-HR2-043,Happy Path,Withdraw pending CPDA advance,CPDA approval_status is PENDING,POST /hr2/api/cpda-advances/10/withdraw/,CPDA updated to WITHDRAWN,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-044-AL-01,UC,UC-HR2-044,Alternate Path,Reject CPDA,CPDA exists,POST /hr2/api/cpda-advances/10/reject/,Status REJECTED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-044-EX-01,UC,UC-HR2-044,Exception,Invalid decision,CPDA exists,POST /hr2/api/cpda-advances/10/invalid/,Returns 400 invalid decision,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-044-HA-01,UC,UC-HR2-044,Happy Path,Forward CPDA to accountant,CPDA exists,POST /hr2/api/cpda-advances/10/forward-accountant/,Status FORWARDED and accountant_processing_status=PENDING,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-044-HA-02,UC,UC-HR2-044,Happy Path,Forward CPDA to director,CPDA exists,POST /hr2/api/cpda-advances/10/forward-director/,Status FORWARDED and accountant_processing_status=DIRECTOR_REVIEW,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-044-HA-03,UC,UC-HR2-044,Happy Path,Approve CPDA,CPDA exists,POST /hr2/api/cpda-advances/10/approve/,Status APPROVED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-045-AL-01,UC,UC-HR2-045,Alternate Path,Submit CPDA reimbursement with optional expenses,Required fields provided,POST /hr2/api/cpda-reimbursements/ with optional fields,CPDA reimbursement created,Fail,Unexpected status 400,"{'employee': [ErrorDetail(string='This field is required.', code='required')]}",[] +UC-HR2-045-EX-01,UC,UC-HR2-045,Exception,Missing required fields,Employee is authenticated,POST /hr2/api/cpda-reimbursements/ with incomplete payload,Returns 400 validation errors,Pass,Expected response,"{'employee': [ErrorDetail(string='This field is required.', code='required')]}",[] +UC-HR2-045-HA-01,UC,UC-HR2-045,Happy Path,Submit CPDA reimbursement,Required fields provided,POST /hr2/api/cpda-reimbursements/ with required fields,CPDA reimbursement created,Fail,Unexpected status 400,"{'employee': [ErrorDetail(string='This field is required.', code='required')]}",[] +UC-HR2-046-AL-01,UC,UC-HR2-046,Alternate Path,List CPDA reimbursements when none exist,No requests exist,GET /hr2/api/cpda-reimbursements/,Returns empty list,Pass,Expected response,[],[] +UC-HR2-046-EX-01,UC,UC-HR2-046,Exception,Unauthorized user tries to list reimbursements,User is not authenticated,GET /hr2/api/cpda-reimbursements/,Returns 401 Unauthorized,Pass,Expected response,"{'detail': ErrorDetail(string='Authentication credentials were not provided.', code='not_authenticated')}",[] +UC-HR2-046-HA-01,UC,UC-HR2-046,Happy Path,List CPDA reimbursements,Requests exist,GET /hr2/api/cpda-reimbursements/,Returns CPDA reimbursements,Pass,Expected response,[],[] +UC-HR2-047-AL-01,UC,UC-HR2-047,Alternate Path,HR staff fetches reimbursement,HR staff is authenticated,GET /hr2/api/cpda-reimbursements/10/,Returns CPDA reimbursement,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-047-EX-01,UC,UC-HR2-047,Exception,CPDA reimbursement not found,CPDA reimbursement does not exist,GET /hr2/api/cpda-reimbursements/999/,Returns 404 Not Found,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-047-HA-01,UC,UC-HR2-047,Happy Path,Fetch CPDA reimbursement,CPDA reimbursement exists,GET /hr2/api/cpda-reimbursements/10/,Returns CPDA reimbursement,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-048-AL-01,UC,UC-HR2-048,Alternate Path,Reject CPDA reimbursement with remarks,Request exists,POST /hr2/api/cpda-reimbursements/10/reject/ with remarks=invalid,Reimbursement rejected,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-048-EX-01,UC,UC-HR2-048,Exception,Invalid decision,Request exists,POST /hr2/api/cpda-reimbursements/10/invalid/,Returns 400 invalid decision,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-048-HA-01,UC,UC-HR2-048,Happy Path,Approve CPDA reimbursement,Request exists,POST /hr2/api/cpda-reimbursements/10/approve/,Reimbursement approved,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-048-HA-02,UC,UC-HR2-048,Happy Path,Reject CPDA reimbursement,Request exists,POST /hr2/api/cpda-reimbursements/10/reject/,Reimbursement rejected,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-049-AL-01,UC,UC-HR2-049,Alternate Path,HR staff views all appraisal forms,User is HR staff,GET /hr2/api/appraisal-forms/,Returns appraisal forms,Pass,Expected response,[],[] +UC-HR2-049-EX-01,UC,UC-HR2-049,Exception,Unauthorized user tries to list appraisal forms,User is not authenticated,GET /hr2/api/appraisal-forms/,Returns 401 Unauthorized,Pass,Expected response,"{'detail': ErrorDetail(string='Authentication credentials were not provided.', code='not_authenticated')}",[] +UC-HR2-049-HA-01,UC,UC-HR2-049,Happy Path,Employee views own appraisal forms,Employee is authenticated,GET /hr2/api/appraisal-forms/,Returns employee appraisal forms,Pass,Expected response,[],[] +UC-HR2-049-HA-02,UC,UC-HR2-049,Happy Path,Director views reviewed forms,User is Director,GET /hr2/api/appraisal-forms/,Returns reviewed appraisal forms,Pass,Expected response,[],[] +UC-HR2-050-AL-01,UC,UC-HR2-050,Alternate Path,HR staff fetches appraisal form,HR staff is authenticated,GET /hr2/api/appraisal-forms/10/,Returns appraisal form,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-050-EX-01,UC,UC-HR2-050,Exception,Appraisal form not found,Form does not exist,GET /hr2/api/appraisal-forms/999/,Returns 404 Not Found,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-050-HA-01,UC,UC-HR2-050,Happy Path,Fetch appraisal form,Form exists,GET /hr2/api/appraisal-forms/10/,Returns appraisal form,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-051-AL-01,UC,UC-HR2-051,Alternate Path,HR staff downloads appraisal form,HR staff is authenticated,GET /hr2/api/appraisal-forms/10/download/,Returns text file attachment,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-051-EX-01,UC,UC-HR2-051,Exception,Unauthorized user tries to download appraisal form,User is not authenticated,GET /hr2/api/appraisal-forms/10/download/,Returns 401 Unauthorized,Pass,Expected response,"{'detail': ErrorDetail(string='Authentication credentials were not provided.', code='not_authenticated')}",[] +UC-HR2-051-HA-01,UC,UC-HR2-051,Happy Path,Download appraisal form,Form exists,GET /hr2/api/appraisal-forms/10/download/,Returns text file attachment,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-052-AL-01,UC,UC-HR2-052,Alternate Path,Review appraisal with rating,Reviewer assigned,"POST /hr2/api/appraisal-forms/10/review/ with action=forward, rating=4",Appraisal status set to REVIEWED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-052-EX-01,UC,UC-HR2-052,Exception,Invalid review action,Reviewer assigned,POST /hr2/api/appraisal-forms/10/review/ with action=invalid,Returns 400 invalid action,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-052-HA-01,UC,UC-HR2-052,Happy Path,Forward appraisal for director,Reviewer assigned,POST /hr2/api/appraisal-forms/10/review/ with action=forward,Appraisal status set to REVIEWED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +UC-HR2-052-HA-02,UC,UC-HR2-052,Happy Path,Approve appraisal,Reviewer assigned,POST /hr2/api/appraisal-forms/10/review/ with action=approve,Appraisal status set to APPROVED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] +WF-HR2-001-E2E-01,WF,WF-HR2-001,End-to-End,Employee applies and leave is approved,,,"approval_status=APPROVED, leave balance reduced",Fail,Workflow incomplete,,"[{'step': 1, 'action': 'Employee applies', 'expected': 'Leave created', 'actual': '1', 'passed': True}, {'step': 2, 'action': 'Director approves', 'expected': 'Status approved', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}]" +WF-HR2-001-E2E-02,WF,WF-HR2-001,End-to-End,Employee applies and leave is forwarded then approved,,,"approval_status=APPROVED, current_approver_role=Director",Fail,Workflow incomplete,,"[{'step': 1, 'action': 'Employee applies', 'expected': 'Leave created', 'actual': '1', 'passed': True}, {'step': 2, 'action': 'HOD forwards', 'expected': 'Status forwarded', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}, {'step': 3, 'action': 'Director approves', 'expected': 'Status approved', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}]" +WF-HR2-001-NEG-01,WF,WF-HR2-001,Negative,Employee applies and leave is rejected,,,"approval_status=REJECTED, leave balance unchanged",Fail,Workflow incomplete,,"[{'step': 1, 'action': 'Employee applies', 'expected': 'Leave created', 'actual': '1', 'passed': True}, {'step': 2, 'action': 'Director rejects', 'expected': 'Status rejected', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}]" +WF-HR2-002-E2E-01,WF,WF-HR2-002,End-to-End,Employee withdraws pending leave,,,approval_status=WITHDRAWN,Fail,Workflow incomplete,,"[{'step': 1, 'action': 'Withdraw pending leave', 'expected': 'Withdrawn', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}]" +WF-HR2-002-NEG-01,WF,WF-HR2-002,Negative,Employee tries to withdraw approved leave,,,Request rejected with withdrawal error,Fail,Workflow incomplete,,"[{'step': 1, 'action': 'Withdraw approved leave', 'expected': 'Rejected', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}]" +WF-HR2-003-E2E-01,WF,WF-HR2-003,End-to-End,Cancellation approved,,,"cancel_status=APPROVED, approval_status=CANCELLED, leave balance restored",Fail,Workflow incomplete,,"[{'step': 1, 'action': 'Request cancellation', 'expected': 'Requested', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}, {'step': 2, 'action': 'Approve cancellation', 'expected': 'Cancelled', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}]" +WF-HR2-003-NEG-01,WF,WF-HR2-003,Negative,Cancellation requested after start date,,,Request rejected with cancellation window error,Fail,Workflow incomplete,,"[{'step': 1, 'action': 'Cancel request late', 'expected': 'Rejected', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}]" +WF-HR2-004-E2E-01,WF,WF-HR2-004,End-to-End,Extension approved with sufficient balance,,,"extension_status=APPROVED, end_date updated, balance reduced",Fail,Workflow incomplete,,"[{'step': 1, 'action': 'Request extension', 'expected': 'Requested', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}, {'step': 2, 'action': 'Approve extension', 'expected': 'Approved or rejected', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}]" +WF-HR2-004-NEG-01,WF,WF-HR2-004,Negative,Extension approved with insufficient balance,,,Request rejected with insufficient balance error,Fail,Workflow incomplete,,"[{'step': 1, 'action': 'Request extension', 'expected': 'Requested', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}, {'step': 2, 'action': 'Approve extension', 'expected': 'Approved or rejected', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}]" +WF-HR2-005-E2E-01,WF,WF-HR2-005,End-to-End,Nominee accepts,,,nominee_status=ACCEPTED,Fail,Workflow incomplete,,"[{'step': 1, 'action': 'Nominee accepts', 'expected': 'Accepted', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}]" +WF-HR2-005-NEG-01,WF,WF-HR2-005,Negative,Non-nominee responds,,,Request rejected with 403,Fail,Workflow incomplete,,"[{'step': 1, 'action': 'Non-nominee responds', 'expected': 'Forbidden', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}]" +WF-HR2-006-E2E-01,WF,WF-HR2-006,End-to-End,"HOD requests, employee submits",,,document_request_status=SUBMITTED,Fail,Workflow incomplete,,"[{'step': 1, 'action': 'HOD requests document', 'expected': 'Requested', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}, {'step': 2, 'action': 'Employee submits', 'expected': 'Submitted', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}]" +WF-HR2-006-NEG-01,WF,WF-HR2-006,Negative,Employee submits without request,,,Request rejected with no request error,Fail,Workflow incomplete,,"[{'step': 1, 'action': 'Submit without request', 'expected': 'Rejected', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}]" +WF-HR2-007-E2E-01,WF,WF-HR2-007,End-to-End,LTC forwarded and approved,,,"approval_status=APPROVED, accountant_status=APPROVED",Pass,Workflow completed,,"[{'step': 1, 'action': 'Forward LTC', 'expected': 'Forwarded', 'actual': ""{'id': 6, 'employee_name': 'Rahul Sharma', 'department': 'Computer Science and Engineering', 'designation': 'Assistant Professor', 'ltc_block_year': 2025, 'travel_start_date': '2026-04-25', 'travel_end_date': '2026-04-30', 'destination': 'Delhi', 'purpose_of_travel': 'Family travel', 'family_members': '', 'relationship_details': '', 'travel_mode': 'Train', 'ticket_number': '', 'ticket_cost': None, 'accommodation_cost': None, 'other_expenses': None, 'total_amount_claimed': '22000.00', 'tickets_upload': '', 'bills_upload': '', 'previous_ltc_used': False, 'last_ltc_date': None, 'applied_date': '2026-04-15', 'verified_by_hr': True, 'approval_status': 'FORWARDED', 'accountant_status': 'PENDING', 'remarks': 'Forward', 'employee': '1001'}"", 'passed': True}, {'step': 2, 'action': 'Approve LTC', 'expected': 'Approved', 'actual': ""{'id': 6, 'employee_name': 'Rahul Sharma', 'department': 'Computer Science and Engineering', 'designation': 'Assistant Professor', 'ltc_block_year': 2025, 'travel_start_date': '2026-04-25', 'travel_end_date': '2026-04-30', 'destination': 'Delhi', 'purpose_of_travel': 'Family travel', 'family_members': '', 'relationship_details': '', 'travel_mode': 'Train', 'ticket_number': '', 'ticket_cost': None, 'accommodation_cost': None, 'other_expenses': None, 'total_amount_claimed': '22000.00', 'tickets_upload': '', 'bills_upload': '', 'previous_ltc_used': False, 'last_ltc_date': None, 'applied_date': '2026-04-15', 'verified_by_hr': True, 'approval_status': 'APPROVED', 'accountant_status': 'APPROVED', 'remarks': 'Approved', 'employee': '1001'}"", 'passed': True}]" +WF-HR2-007-NEG-01,WF,WF-HR2-007,Negative,LTC rejected,,,approval_status=REJECTED,Fail,Workflow incomplete,,"[{'step': 1, 'action': 'Reject LTC', 'expected': 'Rejected', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}]" +WF-HR2-008-E2E-01,WF,WF-HR2-008,End-to-End,HR forwards to accountant and approves,,,"approval_status=APPROVED, accountant_processing_status=APPROVED",Pass,Workflow completed,,"[{'step': 1, 'action': 'Forward to accountant', 'expected': 'Forwarded', 'actual': ""{'id': 6, 'employee_name': 'Rahul Sharma', 'department': 'Computer Science and Engineering', 'designation': 'Assistant Professor', 'event_name': 'National Conference on AI', 'event_type': 'Conference', 'organized_by': '', 'venue': '', 'start_date': '2026-05-15', 'end_date': '2026-05-17', 'registration_fee': None, 'travel_expense': None, 'accommodation_expense': None, 'other_expenses': None, 'total_amount': '20000.00', 'purpose_of_attending': 'Present paper', 'benefits_to_institution': 'Research exposure', 'invitation_letter': '', 'receipts': '', 'certificates': '', 'applied_date': '2026-04-15', 'verified_by_hr': True, 'approval_status': 'FORWARDED', 'accountant_processing_status': 'PENDING', 'remarks': 'Forward', 'employee': '1001'}"", 'passed': True}, {'step': 2, 'action': 'Accountant approves', 'expected': 'Approved', 'actual': ""{'id': 6, 'employee_name': 'Rahul Sharma', 'department': 'Computer Science and Engineering', 'designation': 'Assistant Professor', 'event_name': 'National Conference on AI', 'event_type': 'Conference', 'organized_by': '', 'venue': '', 'start_date': '2026-05-15', 'end_date': '2026-05-17', 'registration_fee': None, 'travel_expense': None, 'accommodation_expense': None, 'other_expenses': None, 'total_amount': '20000.00', 'purpose_of_attending': 'Present paper', 'benefits_to_institution': 'Research exposure', 'invitation_letter': '', 'receipts': '', 'certificates': '', 'applied_date': '2026-04-15', 'verified_by_hr': True, 'approval_status': 'APPROVED', 'accountant_processing_status': 'APPROVED', 'remarks': 'Approved', 'employee': '1001'}"", 'passed': True}]" +WF-HR2-008-E2E-02,WF,WF-HR2-008,End-to-End,Forwarded to director then approved,,,"accountant_processing_status=DIRECTOR_APPROVED, approval_status=FORWARDED",Fail,Workflow incomplete,,"[{'step': 1, 'action': 'Forward to director', 'expected': 'Forwarded', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}, {'step': 2, 'action': 'Director approves', 'expected': 'Approved', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}]" +WF-HR2-008-NEG-01,WF,WF-HR2-008,Negative,CPDA advance rejected,,,approval_status=REJECTED,Fail,Workflow incomplete,,"[{'step': 1, 'action': 'Reject CPDA', 'expected': 'Rejected', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}]" +WF-HR2-009-E2E-01,WF,WF-HR2-009,End-to-End,CPDA reimbursement approved,,,approval_status=APPROVED,Fail,Workflow incomplete,,"[{'step': 1, 'action': 'Approve reimbursement', 'expected': 'Approved', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}]" +WF-HR2-009-NEG-01,WF,WF-HR2-009,Negative,CPDA reimbursement rejected,,,approval_status=REJECTED,Fail,Workflow incomplete,,"[{'step': 1, 'action': 'Reject reimbursement', 'expected': 'Rejected', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}]" +WF-HR2-010-E2E-01,WF,WF-HR2-010,End-to-End,HOD forwards to director,,,status=REVIEWED,Pass,Workflow completed,,"[{'step': 1, 'action': 'Director approves', 'expected': 'Approved', 'actual': ""{'id': 6, 'employee_name': 'Rahul Sharma', 'department': 'Computer Science and Engineering', 'designation': 'Assistant Professor', 'appraisal_year': '2025-2026', 'self_summary': 'Completed teaching responsibilities', 'key_responsibilities': 'Teaching and research', 'achievements': 'Published 1 paper', 'challenges_faced': '', 'teaching_performance': '', 'research_work': '', 'publications': '', 'projects_handled': '', 'administrative_contributions': '', 'trainings_attended': '', 'certifications': '', 'workshops': '', 'goals_achieved': 'Completed syllabus', 'future_goals': 'Publish more papers', 'supporting_documents': '', 'reviewer_id': '1003', 'reviewer_comments': '', 'rating': '', 'status': 'APPROVED', 'remarks': '', 'submitted_at': '2026-04-15T19:42:43.668756', 'employee': '1001'}"", 'passed': True}]" +WF-HR2-010-E2E-02,WF,WF-HR2-010,End-to-End,Director approves,,,status=APPROVED,Fail,Workflow incomplete,,"[{'step': 1, 'action': 'Director approves', 'expected': 'Approved', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}]" diff --git a/FusionIIIT/applications/hr2/tests/reports/UC_Test_Design.csv b/FusionIIIT/applications/hr2/tests/reports/UC_Test_Design.csv new file mode 100644 index 000000000..56ad4cf69 --- /dev/null +++ b/FusionIIIT/applications/hr2/tests/reports/UC_Test_Design.csv @@ -0,0 +1,170 @@ +uc_id,uc_title,test_type,scenario,preconditions,input_action,expected_result +UC-HR2-001,List employees,Happy Path,HR staff lists all employees,User has HR designation or is HR staff,GET /hr2/api/employees/,Returns list of employees +UC-HR2-001,List employees,Happy Path,Filter employees by type and department,User is authenticated,GET /hr2/api/employees/?type=Faculty&department=1,Returns employees matching filters +UC-HR2-001,List employees,Alternate Path,Request with only department filter,User is authenticated,GET /hr2/api/employees/?department=1,Returns employees in department +UC-HR2-001,List employees,Exception,Unauthorized user tries to list employees,User is not authenticated,GET /hr2/api/employees/,Returns 401 Unauthorized +UC-HR2-002,View employee details,Happy Path,Fetch employee details by ID,Employee exists,GET /hr2/api/employees/123/,Returns employee details +UC-HR2-002,View employee details,Alternate Path,HOD fetches employee details,HOD is authenticated,GET /hr2/api/employees/123/,Returns employee details +UC-HR2-002,View employee details,Exception,Employee not found,Employee does not exist,GET /hr2/api/employees/999999/,Returns 404 Not Found +UC-HR2-003,Update employee details,Happy Path,Update phone number,Employee exists,PUT /hr2/api/employees/123/ with phone_number=9876543210,Returns updated employee details +UC-HR2-003,Update employee details,Alternate Path,Update employee address,Employee exists,PUT /hr2/api/employees/123/ with address=Updated,Returns updated employee details +UC-HR2-003,Update employee details,Exception,Invalid data,Employee exists,PUT /hr2/api/employees/123/ with phone_number=invalid,Returns 400 validation error +UC-HR2-004,Apply for leave,Happy Path,Apply for casual leave with valid dates,Leave balance is sufficient,"POST /hr2/api/leave-applications/ with leave_type=CL, start_date=future, end_date=future+2, total_days=3",Leave application created with approval_status=PENDING or FORWARDED +UC-HR2-004,Apply for leave,Alternate Path,Nominate substitute during leave,Nominee employee exists,POST /hr2/api/leave-applications/ with nominee_employee_id=456,Leave created with nominee_status=PENDING +UC-HR2-004,Apply for leave,Exception,Start date in the past,Employee is authenticated,POST /hr2/api/leave-applications/ with start_date=yesterday,Returns 400 start_date validation error +UC-HR2-005,View leave applications,Happy Path,Employee views own leave applications,Employee is authenticated,GET /hr2/api/leave-applications/,Returns only employee's leave applications +UC-HR2-005,View leave applications,Happy Path,Director views forwarded applications,User has Director designation,GET /hr2/api/leave-applications/,Returns forwarded and requested decisions +UC-HR2-005,View leave applications,Alternate Path,HOD views departmental leave applications,User has HOD designation,GET /hr2/api/leave-applications/,Returns department leave applications +UC-HR2-005,View leave applications,Exception,Unauthorized user tries to list leave applications,User is not authenticated,GET /hr2/api/leave-applications/,Returns 401 Unauthorized +UC-HR2-006,Withdraw leave application,Happy Path,Employee withdraws own pending leave,Leave approval_status is PENDING,POST /hr2/api/leave-applications/10/withdraw/,Leave updated to approval_status=WITHDRAWN +UC-HR2-006,Withdraw leave application,Alternate Path,Registrar withdraws forwarded leave,Leave approval_status is FORWARDED,POST /hr2/api/leave-applications/10/withdraw/,Leave updated to approval_status=REJECTED +UC-HR2-006,Withdraw leave application,Exception,Withdraw non-pending leave,Leave approval_status is APPROVED,POST /hr2/api/leave-applications/10/withdraw/,Returns 400 with withdrawal error +UC-HR2-007,Request leave cancellation,Happy Path,Submit cancellation request before start date,Today is before leave start date,POST /hr2/api/leave-applications/10/cancel-request/ with reason=change of plan,Cancel status set to REQUESTED +UC-HR2-007,Request leave cancellation,Alternate Path,Submit cancellation request with reason,Approved leave exists,POST /hr2/api/leave-applications/10/cancel-request/ with reason=medical,Cancel status set to REQUESTED +UC-HR2-007,Request leave cancellation,Exception,Cancellation after start date,Today is on or after start date,POST /hr2/api/leave-applications/10/cancel-request/,Returns 400 cancellation window error +UC-HR2-008,Request leave extension,Happy Path,Request extension with new end date,Extension dates are valid,POST /hr2/api/leave-applications/10/extension-request/ with new_end_date=future+2,Extension status set to REQUESTED +UC-HR2-008,Request leave extension,Alternate Path,Request extension with reason,Extension dates are valid,POST /hr2/api/leave-applications/10/extension-request/ with reason=medical,Extension status set to REQUESTED +UC-HR2-008,Request leave extension,Exception,Extension with invalid date,New end date before current end date,POST /hr2/api/leave-applications/10/extension-request/ with new_end_date=earlier,Returns 400 validation error +UC-HR2-009,View leave balance,Happy Path,Employee views own leave balance,Employee is authenticated,GET /hr2/api/leave-balance/,Returns leave balance for the employee +UC-HR2-009,View leave balance,Happy Path,HR views leave balance for another employee,User has HR designation,GET /hr2/api/leave-balance/123/,Returns leave balance for employee 123 +UC-HR2-009,View leave balance,Alternate Path,Employee views balance with no records,Employee has no balance entries,GET /hr2/api/leave-balance/,Returns empty balance list +UC-HR2-009,View leave balance,Exception,Unauthorized user tries to view leave balance,User is not authenticated,GET /hr2/api/leave-balance/,Returns 401 Unauthorized +UC-HR2-010,Download leave application,Happy Path,Download approved leave application,Leave exists and belongs to employee,GET /hr2/api/leave-applications/10/download/,Returns text file attachment with leave details +UC-HR2-010,Download leave application,Alternate Path,Download pending leave application,Leave is pending and belongs to employee,GET /hr2/api/leave-applications/10/download/,Returns text file attachment with leave details +UC-HR2-010,Download leave application,Exception,Access another employee's leave,Leave does not belong to requester,GET /hr2/api/leave-applications/999/download/,Returns 403 Not authorized +UC-HR2-011,Submit LTC application,Happy Path,Submit LTC claim,Required fields provided,POST /hr2/api/ltc/ with required LTC fields,LTC application created with approval_status=PENDING +UC-HR2-011,Submit LTC application,Alternate Path,Submit LTC claim with optional fields,Required fields provided,POST /hr2/api/ltc/ with optional fields,LTC application created with approval_status=PENDING +UC-HR2-011,Submit LTC application,Exception,Missing required fields,Employee is authenticated,POST /hr2/api/ltc/ with incomplete payload,Returns 400 validation errors +UC-HR2-012,Submit CPDA advance,Happy Path,Submit CPDA advance,Required fields provided,POST /hr2/api/cpda-advances/ with required fields,CPDA advance created with approval_status=PENDING +UC-HR2-012,Submit CPDA advance,Alternate Path,Submit CPDA advance with optional expenses,Required fields provided,POST /hr2/api/cpda-advances/ with optional fields,CPDA advance created with approval_status=PENDING +UC-HR2-012,Submit CPDA advance,Exception,Invalid amount,Employee is authenticated,POST /hr2/api/cpda-advances/ with amountRequired=invalid,Returns 400 validation errors +UC-HR2-013,Submit appraisal form,Happy Path,Submit appraisal form,Required fields provided,POST /hr2/api/appraisal-forms/ with required fields,Appraisal form created with status=SUBMITTED +UC-HR2-013,Submit appraisal form,Alternate Path,Submit appraisal form with optional fields,Required fields provided,POST /hr2/api/appraisal-forms/ with optional fields,Appraisal form created with status=SUBMITTED +UC-HR2-013,Submit appraisal form,Exception,Missing required fields,Employee is authenticated,POST /hr2/api/appraisal-forms/ with incomplete payload,Returns 400 validation errors +UC-HR2-014,View leave application details,Happy Path,Fetch leave application by ID,Leave application exists,GET /hr2/api/leave-applications/10/,Returns leave application +UC-HR2-014,View leave application details,Alternate Path,HR staff fetches leave application by ID,HR staff is authenticated,GET /hr2/api/leave-applications/10/,Returns leave application +UC-HR2-014,View leave application details,Exception,Leave application not found,Leave application does not exist,GET /hr2/api/leave-applications/999/,Returns 404 Not Found +UC-HR2-015,Update leave application,Happy Path,Update leave reason,Leave belongs to employee,PUT /hr2/api/leave-applications/10/ with reason=updated,Leave application updated +UC-HR2-015,Update leave application,Alternate Path,Update leave handover notes,Leave belongs to employee,PUT /hr2/api/leave-applications/10/ with handover_notes=updated,Leave application updated +UC-HR2-015,Update leave application,Exception,Update another employee's leave,Leave belongs to another employee,PUT /hr2/api/leave-applications/10/,Returns 403 Not authorized +UC-HR2-016,Delete leave application,Happy Path,Delete pending leave,Leave approval_status is PENDING,DELETE /hr2/api/leave-applications/10/,Leave application deleted +UC-HR2-016,Delete leave application,Alternate Path,Delete pending leave without attachments,Leave approval_status is PENDING,DELETE /hr2/api/leave-applications/10/,Leave application deleted +UC-HR2-016,Delete leave application,Exception,Delete non-pending leave,Leave approval_status is APPROVED,DELETE /hr2/api/leave-applications/10/,Returns 400 with delete error +UC-HR2-017,Approve or reject leave,Happy Path,Approve leave,Leave exists,POST /hr2/api/leave-applications/10/approve/,Leave status updated to APPROVED +UC-HR2-017,Approve or reject leave,Happy Path,Forward leave,Leave exists,POST /hr2/api/leave-applications/10/forward/,Leave status updated to FORWARDED +UC-HR2-017,Approve or reject leave,Alternate Path,Reject leave,Leave exists,POST /hr2/api/leave-applications/10/reject/,Leave status updated to REJECTED +UC-HR2-017,Approve or reject leave,Exception,Invalid decision,Leave exists,POST /hr2/api/leave-applications/10/invalid/,Returns 400 invalid decision +UC-HR2-018,Nominee dashboard,Happy Path,Nominee views pending requests,Nominee has pending requests,GET /hr2/api/leave-nominee/,Returns pending nominee requests +UC-HR2-018,Nominee dashboard,Alternate Path,Nominee has no pending requests,Nominee has no pending requests,GET /hr2/api/leave-nominee/,Returns empty list +UC-HR2-018,Nominee dashboard,Exception,Unauthorized user tries to view nominee dashboard,User is not authenticated,GET /hr2/api/leave-nominee/,Returns 401 Unauthorized +UC-HR2-019,Nominee decision,Happy Path,Nominee accepts,Nominee is assigned on leave,POST /hr2/api/leave-nominee/10/ with action=accept,Nominee status updated to ACCEPTED +UC-HR2-019,Nominee decision,Alternate Path,Nominee declines,Nominee is assigned on leave,POST /hr2/api/leave-nominee/10/ with action=decline,Nominee status updated to DECLINED +UC-HR2-019,Nominee decision,Exception,Invalid action,Nominee is assigned,POST /hr2/api/leave-nominee/10/ with action=invalid,Returns 400 invalid action +UC-HR2-020,Request leave documents,Happy Path,Request documents with message,Leave exists,POST /hr2/api/leave-applications/10/request-document/ with message=submit proof,Document request status set to REQUESTED +UC-HR2-020,Request leave documents,Alternate Path,Request documents with updated message,Leave exists,POST /hr2/api/leave-applications/10/request-document/ with message=submit updated proof,Document request status set to REQUESTED +UC-HR2-020,Request leave documents,Exception,Missing message,Leave exists,POST /hr2/api/leave-applications/10/request-document/,Returns 400 message required +UC-HR2-021,Submit leave documents,Happy Path,Submit document after request,Document request exists,POST /hr2/api/leave-applications/10/submit-document/ with submission=doc ref,Document request status set to SUBMITTED +UC-HR2-021,Submit leave documents,Alternate Path,Submit updated document,Document request exists,POST /hr2/api/leave-applications/10/submit-document/ with submission=updated ref,Document request status set to SUBMITTED +UC-HR2-021,Submit leave documents,Exception,Submit without request,No document request exists,POST /hr2/api/leave-applications/10/submit-document/ with submission=doc ref,Returns 400 no request +UC-HR2-022,Cancellation decision,Happy Path,Approve cancellation,Approver role matches,POST /hr2/api/leave-applications/10/cancel-decision/approve/,Cancellation approved and leave cancelled +UC-HR2-022,Cancellation decision,Happy Path,Reject cancellation,Approver role matches,POST /hr2/api/leave-applications/10/cancel-decision/reject/,Cancellation rejected +UC-HR2-022,Cancellation decision,Alternate Path,Approve cancellation with remarks,Approver role matches,POST /hr2/api/leave-applications/10/cancel-decision/approve/ with remarks=ok,Cancellation approved and leave cancelled +UC-HR2-022,Cancellation decision,Exception,Invalid decision,Cancellation request exists,POST /hr2/api/leave-applications/10/cancel-decision/invalid/,Returns 400 invalid decision +UC-HR2-023,Extension decision,Happy Path,Approve extension,Approver role matches,POST /hr2/api/leave-applications/10/extension-decision/approve/,Extension approved and leave updated +UC-HR2-023,Extension decision,Happy Path,Reject extension,Approver role matches,POST /hr2/api/leave-applications/10/extension-decision/reject/,Extension rejected +UC-HR2-023,Extension decision,Alternate Path,Approve extension with remarks,Approver role matches,POST /hr2/api/leave-applications/10/extension-decision/approve/ with remarks=ok,Extension approved and leave updated +UC-HR2-023,Extension decision,Exception,Invalid decision,Extension request exists,POST /hr2/api/leave-applications/10/extension-decision/invalid/,Returns 400 invalid decision +UC-HR2-024,Record attendance,Happy Path,Mark attendance,Valid status and date,"POST /hr2/api/attendance/ with date=today, status=PRESENT",Attendance record created +UC-HR2-024,Record attendance,Alternate Path,Mark half-day attendance,Valid status and date,"POST /hr2/api/attendance/ with date=today, status=HALF_DAY",Attendance record created +UC-HR2-024,Record attendance,Exception,Missing attendance status,Employee is authenticated,POST /hr2/api/attendance/ with date=today,Returns 400 validation errors +UC-HR2-025,View attendance,Happy Path,View attendance for date range,Attendance exists,GET /hr2/api/attendance/?from_date=2026-05-01&to_date=2026-05-10,Returns attendance records +UC-HR2-025,View attendance,Alternate Path,View attendance without filters,Attendance exists,GET /hr2/api/attendance/,Returns attendance records +UC-HR2-025,View attendance,Exception,Unauthorized user tries to view attendance,User is not authenticated,GET /hr2/api/attendance/,Returns 401 Unauthorized +UC-HR2-026,List appraisal periods,Happy Path,List active periods,Periods exist,GET /hr2/api/appraisal-periods/?is_active=true,Returns appraisal periods +UC-HR2-026,List appraisal periods,Alternate Path,List all periods without filter,Periods exist,GET /hr2/api/appraisal-periods/,Returns appraisal periods +UC-HR2-026,List appraisal periods,Exception,Unauthorized user tries to view appraisal periods,User is not authenticated,GET /hr2/api/appraisal-periods/,Returns 401 Unauthorized +UC-HR2-027,Submit appraisal (performance),Happy Path,Submit appraisal,Required fields provided,POST /hr2/api/appraisals/ with period and scores,Performance appraisal created +UC-HR2-027,Submit appraisal (performance),Alternate Path,Submit appraisal with remarks,Required fields provided,"POST /hr2/api/appraisals/ with period, scores, remarks",Performance appraisal created +UC-HR2-027,Submit appraisal (performance),Exception,Missing required fields,Employee is authenticated,POST /hr2/api/appraisals/ with incomplete payload,Returns 400 validation errors +UC-HR2-028,List appraisals,Happy Path,List appraisals for period,Appraisals exist,GET /hr2/api/appraisals/?period=1,Returns appraisals +UC-HR2-028,List appraisals,Alternate Path,List all appraisals,Appraisals exist,GET /hr2/api/appraisals/,Returns appraisals +UC-HR2-028,List appraisals,Exception,Unauthorized user tries to list appraisals,User is not authenticated,GET /hr2/api/appraisals/,Returns 401 Unauthorized +UC-HR2-029,List training programs,Happy Path,List available programs,Programs exist,GET /hr2/api/training-programs/,Returns training programs +UC-HR2-029,List training programs,Alternate Path,List programs when none are available,No programs available,GET /hr2/api/training-programs/,Returns empty list +UC-HR2-029,List training programs,Exception,Unauthorized user tries to list programs,User is not authenticated,GET /hr2/api/training-programs/,Returns 401 Unauthorized +UC-HR2-030,Nominate for training,Happy Path,Submit training nomination,Program exists,POST /hr2/api/training-nominations/ with program data,Nomination created +UC-HR2-030,Nominate for training,Alternate Path,Nominate for mandatory program,Program exists and is mandatory,POST /hr2/api/training-nominations/ with program data,Nomination created +UC-HR2-030,Nominate for training,Exception,Invalid program,Employee is authenticated,POST /hr2/api/training-nominations/ with invalid program,Returns 400 validation errors +UC-HR2-031,View training nominations,Happy Path,List nominations,Nominations exist,GET /hr2/api/training-nominations/,Returns training nominations +UC-HR2-031,View training nominations,Alternate Path,List nominations after submission,Nomination exists,GET /hr2/api/training-nominations/,Returns training nominations +UC-HR2-031,View training nominations,Exception,Unauthorized user tries to list nominations,User is not authenticated,GET /hr2/api/training-nominations/,Returns 401 Unauthorized +UC-HR2-032,Submit promotion application,Happy Path,Submit promotion,Required fields provided,POST /hr2/api/promotions/ with required fields,Promotion application created +UC-HR2-032,Submit promotion application,Alternate Path,Submit promotion with API score,Required fields provided,POST /hr2/api/promotions/ with api_score,Promotion application created +UC-HR2-032,Submit promotion application,Exception,Missing required fields,Employee is authenticated,POST /hr2/api/promotions/ with incomplete payload,Returns 400 validation errors +UC-HR2-033,View promotion applications,Happy Path,List promotions,Applications exist,GET /hr2/api/promotions/,Returns promotion applications +UC-HR2-033,View promotion applications,Alternate Path,List promotions when none exist,No promotions exist,GET /hr2/api/promotions/,Returns empty list +UC-HR2-033,View promotion applications,Exception,Unauthorized user tries to list promotions,User is not authenticated,GET /hr2/api/promotions/,Returns 401 Unauthorized +UC-HR2-034,View faculty workload,Happy Path,Get workload by semester,Workload exists,GET /hr2/api/workload/?semester=Spring&year=2026,Returns workload records +UC-HR2-034,View faculty workload,Alternate Path,Get workload without semester filter,Workload exists,GET /hr2/api/workload/?year=2026,Returns workload records +UC-HR2-034,View faculty workload,Exception,Unauthorized user tries to view workload,User is not authenticated,GET /hr2/api/workload/,Returns 401 Unauthorized +UC-HR2-035,Submit LTC update,Happy Path,Update LTC details,LTC belongs to employee,PUT /hr2/api/ltc/10/ with destination=updated,LTC updated +UC-HR2-035,Submit LTC update,Alternate Path,Update LTC purpose,LTC belongs to employee,PUT /hr2/api/ltc/10/ with purpose_of_travel=updated,LTC updated +UC-HR2-035,Submit LTC update,Exception,Update another employee's LTC,LTC belongs to another employee,PUT /hr2/api/ltc/10/,Returns 403 Not authorized +UC-HR2-036,View LTC applications,Happy Path,Employee views own LTC,Employee is authenticated,GET /hr2/api/ltc/,Returns employee LTC applications +UC-HR2-036,View LTC applications,Happy Path,HR staff views pending LTC,User is HR staff,GET /hr2/api/ltc/,Returns pending/forwarded LTC +UC-HR2-036,View LTC applications,Alternate Path,Accountant views forwarded LTC,User is Accountant,GET /hr2/api/ltc/,Returns forwarded LTC +UC-HR2-036,View LTC applications,Exception,Unauthorized user tries to list LTC,User is not authenticated,GET /hr2/api/ltc/,Returns 401 Unauthorized +UC-HR2-037,Download LTC application,Happy Path,Download LTC,LTC exists,GET /hr2/api/ltc/10/download/,Returns text file attachment +UC-HR2-037,Download LTC application,Alternate Path,HR staff downloads LTC,HR staff is authenticated,GET /hr2/api/ltc/10/download/,Returns text file attachment +UC-HR2-037,Download LTC application,Exception,Unauthorized user tries to download LTC,User is not authenticated,GET /hr2/api/ltc/10/download/,Returns 401 Unauthorized +UC-HR2-038,Withdraw LTC application,Happy Path,Withdraw pending LTC,LTC approval_status is PENDING,POST /hr2/api/ltc/10/withdraw/,LTC updated to WITHDRAWN +UC-HR2-038,Withdraw LTC application,Alternate Path,Withdraw pending LTC with remarks,LTC approval_status is PENDING,POST /hr2/api/ltc/10/withdraw/ with remarks=updated,LTC updated to WITHDRAWN +UC-HR2-038,Withdraw LTC application,Exception,Withdraw non-pending LTC,LTC approval_status is APPROVED,POST /hr2/api/ltc/10/withdraw/,Returns 400 with withdrawal error +UC-HR2-039,Approve or reject LTC,Happy Path,Forward LTC,LTC exists,POST /hr2/api/ltc/10/forward/,LTC status FORWARDED and accountant_status=PENDING +UC-HR2-039,Approve or reject LTC,Happy Path,Approve LTC,LTC exists,POST /hr2/api/ltc/10/approve/,LTC status APPROVED +UC-HR2-039,Approve or reject LTC,Alternate Path,Reject LTC,LTC exists,POST /hr2/api/ltc/10/reject/,LTC status REJECTED +UC-HR2-039,Approve or reject LTC,Exception,Invalid decision,LTC exists,POST /hr2/api/ltc/10/invalid/,Returns 400 invalid decision +UC-HR2-040,View CPDA advances,Happy Path,Employee views own advances,Employee is authenticated,GET /hr2/api/cpda-advances/,Returns employee CPDA advances +UC-HR2-040,View CPDA advances,Alternate Path,HR staff views pending advances,User is HR staff,GET /hr2/api/cpda-advances/,Returns pending advances +UC-HR2-040,View CPDA advances,Exception,Unauthorized user tries to list advances,User is not authenticated,GET /hr2/api/cpda-advances/,Returns 401 Unauthorized +UC-HR2-041,View CPDA advance details,Happy Path,Fetch CPDA advance,CPDA advance exists,GET /hr2/api/cpda-advances/10/,Returns CPDA advance +UC-HR2-041,View CPDA advance details,Alternate Path,HR staff fetches CPDA advance,HR staff is authenticated,GET /hr2/api/cpda-advances/10/,Returns CPDA advance +UC-HR2-041,View CPDA advance details,Exception,CPDA advance not found,CPDA advance does not exist,GET /hr2/api/cpda-advances/999/,Returns 404 Not Found +UC-HR2-042,Download CPDA advance,Happy Path,Download CPDA advance,CPDA exists,GET /hr2/api/cpda-advances/10/download/,Returns text file attachment +UC-HR2-042,Download CPDA advance,Alternate Path,HR staff downloads CPDA advance,HR staff is authenticated,GET /hr2/api/cpda-advances/10/download/,Returns text file attachment +UC-HR2-042,Download CPDA advance,Exception,Unauthorized user tries to download CPDA,User is not authenticated,GET /hr2/api/cpda-advances/10/download/,Returns 401 Unauthorized +UC-HR2-043,Withdraw CPDA advance,Happy Path,Withdraw pending CPDA advance,CPDA approval_status is PENDING,POST /hr2/api/cpda-advances/10/withdraw/,CPDA updated to WITHDRAWN +UC-HR2-043,Withdraw CPDA advance,Alternate Path,Withdraw pending CPDA advance with remarks,CPDA approval_status is PENDING,POST /hr2/api/cpda-advances/10/withdraw/ with remarks=updated,CPDA updated to WITHDRAWN +UC-HR2-043,Withdraw CPDA advance,Exception,Withdraw non-pending CPDA advance,CPDA approval_status is APPROVED,POST /hr2/api/cpda-advances/10/withdraw/,Returns 400 with withdrawal error +UC-HR2-044,Decide CPDA advance,Happy Path,Forward CPDA to accountant,CPDA exists,POST /hr2/api/cpda-advances/10/forward-accountant/,Status FORWARDED and accountant_processing_status=PENDING +UC-HR2-044,Decide CPDA advance,Happy Path,Forward CPDA to director,CPDA exists,POST /hr2/api/cpda-advances/10/forward-director/,Status FORWARDED and accountant_processing_status=DIRECTOR_REVIEW +UC-HR2-044,Decide CPDA advance,Happy Path,Approve CPDA,CPDA exists,POST /hr2/api/cpda-advances/10/approve/,Status APPROVED +UC-HR2-044,Decide CPDA advance,Alternate Path,Reject CPDA,CPDA exists,POST /hr2/api/cpda-advances/10/reject/,Status REJECTED +UC-HR2-044,Decide CPDA advance,Exception,Invalid decision,CPDA exists,POST /hr2/api/cpda-advances/10/invalid/,Returns 400 invalid decision +UC-HR2-045,Submit CPDA reimbursement,Happy Path,Submit CPDA reimbursement,Required fields provided,POST /hr2/api/cpda-reimbursements/ with required fields,CPDA reimbursement created +UC-HR2-045,Submit CPDA reimbursement,Alternate Path,Submit CPDA reimbursement with optional expenses,Required fields provided,POST /hr2/api/cpda-reimbursements/ with optional fields,CPDA reimbursement created +UC-HR2-045,Submit CPDA reimbursement,Exception,Missing required fields,Employee is authenticated,POST /hr2/api/cpda-reimbursements/ with incomplete payload,Returns 400 validation errors +UC-HR2-046,View CPDA reimbursements,Happy Path,List CPDA reimbursements,Requests exist,GET /hr2/api/cpda-reimbursements/,Returns CPDA reimbursements +UC-HR2-046,View CPDA reimbursements,Alternate Path,List CPDA reimbursements when none exist,No requests exist,GET /hr2/api/cpda-reimbursements/,Returns empty list +UC-HR2-046,View CPDA reimbursements,Exception,Unauthorized user tries to list reimbursements,User is not authenticated,GET /hr2/api/cpda-reimbursements/,Returns 401 Unauthorized +UC-HR2-047,View CPDA reimbursement details,Happy Path,Fetch CPDA reimbursement,CPDA reimbursement exists,GET /hr2/api/cpda-reimbursements/10/,Returns CPDA reimbursement +UC-HR2-047,View CPDA reimbursement details,Alternate Path,HR staff fetches reimbursement,HR staff is authenticated,GET /hr2/api/cpda-reimbursements/10/,Returns CPDA reimbursement +UC-HR2-047,View CPDA reimbursement details,Exception,CPDA reimbursement not found,CPDA reimbursement does not exist,GET /hr2/api/cpda-reimbursements/999/,Returns 404 Not Found +UC-HR2-048,Decide CPDA reimbursement,Happy Path,Approve CPDA reimbursement,Request exists,POST /hr2/api/cpda-reimbursements/10/approve/,Reimbursement approved +UC-HR2-048,Decide CPDA reimbursement,Happy Path,Reject CPDA reimbursement,Request exists,POST /hr2/api/cpda-reimbursements/10/reject/,Reimbursement rejected +UC-HR2-048,Decide CPDA reimbursement,Alternate Path,Reject CPDA reimbursement with remarks,Request exists,POST /hr2/api/cpda-reimbursements/10/reject/ with remarks=invalid,Reimbursement rejected +UC-HR2-048,Decide CPDA reimbursement,Exception,Invalid decision,Request exists,POST /hr2/api/cpda-reimbursements/10/invalid/,Returns 400 invalid decision +UC-HR2-049,List appraisal forms,Happy Path,Employee views own appraisal forms,Employee is authenticated,GET /hr2/api/appraisal-forms/,Returns employee appraisal forms +UC-HR2-049,List appraisal forms,Happy Path,Director views reviewed forms,User is Director,GET /hr2/api/appraisal-forms/,Returns reviewed appraisal forms +UC-HR2-049,List appraisal forms,Alternate Path,HR staff views all appraisal forms,User is HR staff,GET /hr2/api/appraisal-forms/,Returns appraisal forms +UC-HR2-049,List appraisal forms,Exception,Unauthorized user tries to list appraisal forms,User is not authenticated,GET /hr2/api/appraisal-forms/,Returns 401 Unauthorized +UC-HR2-050,View appraisal form details,Happy Path,Fetch appraisal form,Form exists,GET /hr2/api/appraisal-forms/10/,Returns appraisal form +UC-HR2-050,View appraisal form details,Alternate Path,HR staff fetches appraisal form,HR staff is authenticated,GET /hr2/api/appraisal-forms/10/,Returns appraisal form +UC-HR2-050,View appraisal form details,Exception,Appraisal form not found,Form does not exist,GET /hr2/api/appraisal-forms/999/,Returns 404 Not Found +UC-HR2-051,Download appraisal form,Happy Path,Download appraisal form,Form exists,GET /hr2/api/appraisal-forms/10/download/,Returns text file attachment +UC-HR2-051,Download appraisal form,Alternate Path,HR staff downloads appraisal form,HR staff is authenticated,GET /hr2/api/appraisal-forms/10/download/,Returns text file attachment +UC-HR2-051,Download appraisal form,Exception,Unauthorized user tries to download appraisal form,User is not authenticated,GET /hr2/api/appraisal-forms/10/download/,Returns 401 Unauthorized +UC-HR2-052,Review appraisal form,Happy Path,Forward appraisal for director,Reviewer assigned,POST /hr2/api/appraisal-forms/10/review/ with action=forward,Appraisal status set to REVIEWED +UC-HR2-052,Review appraisal form,Happy Path,Approve appraisal,Reviewer assigned,POST /hr2/api/appraisal-forms/10/review/ with action=approve,Appraisal status set to APPROVED +UC-HR2-052,Review appraisal form,Alternate Path,Review appraisal with rating,Reviewer assigned,"POST /hr2/api/appraisal-forms/10/review/ with action=forward, rating=4",Appraisal status set to REVIEWED +UC-HR2-052,Review appraisal form,Exception,Invalid review action,Reviewer assigned,POST /hr2/api/appraisal-forms/10/review/ with action=invalid,Returns 400 invalid action diff --git a/FusionIIIT/applications/hr2/tests/reports/WF_Test_Design.csv b/FusionIIIT/applications/hr2/tests/reports/WF_Test_Design.csv new file mode 100644 index 000000000..a143bd7ef --- /dev/null +++ b/FusionIIIT/applications/hr2/tests/reports/WF_Test_Design.csv @@ -0,0 +1,23 @@ +wf_id,wf_title,test_type,scenario,expected_final_state +WF-HR2-001,Leave application approval flow,End-to-End,Employee applies and leave is approved,"approval_status=APPROVED, leave balance reduced" +WF-HR2-001,Leave application approval flow,End-to-End,Employee applies and leave is forwarded then approved,"approval_status=APPROVED, current_approver_role=Director" +WF-HR2-001,Leave application approval flow,Negative,Employee applies and leave is rejected,"approval_status=REJECTED, leave balance unchanged" +WF-HR2-002,Leave withdrawal flow,End-to-End,Employee withdraws pending leave,approval_status=WITHDRAWN +WF-HR2-002,Leave withdrawal flow,Negative,Employee tries to withdraw approved leave,Request rejected with withdrawal error +WF-HR2-003,Leave cancellation flow,End-to-End,Cancellation approved,"cancel_status=APPROVED, approval_status=CANCELLED, leave balance restored" +WF-HR2-003,Leave cancellation flow,Negative,Cancellation requested after start date,Request rejected with cancellation window error +WF-HR2-004,Leave extension flow,End-to-End,Extension approved with sufficient balance,"extension_status=APPROVED, end_date updated, balance reduced" +WF-HR2-004,Leave extension flow,Negative,Extension approved with insufficient balance,Request rejected with insufficient balance error +WF-HR2-005,Nominee response flow,End-to-End,Nominee accepts,nominee_status=ACCEPTED +WF-HR2-005,Nominee response flow,Negative,Non-nominee responds,Request rejected with 403 +WF-HR2-006,Document request flow,End-to-End,"HOD requests, employee submits",document_request_status=SUBMITTED +WF-HR2-006,Document request flow,Negative,Employee submits without request,Request rejected with no request error +WF-HR2-007,LTC approval flow,End-to-End,LTC forwarded and approved,"approval_status=APPROVED, accountant_status=APPROVED" +WF-HR2-007,LTC approval flow,Negative,LTC rejected,approval_status=REJECTED +WF-HR2-008,CPDA advance approval flow,End-to-End,HR forwards to accountant and approves,"approval_status=APPROVED, accountant_processing_status=APPROVED" +WF-HR2-008,CPDA advance approval flow,End-to-End,Forwarded to director then approved,"accountant_processing_status=DIRECTOR_APPROVED, approval_status=FORWARDED" +WF-HR2-008,CPDA advance approval flow,Negative,CPDA advance rejected,approval_status=REJECTED +WF-HR2-009,CPDA reimbursement decision flow,End-to-End,CPDA reimbursement approved,approval_status=APPROVED +WF-HR2-009,CPDA reimbursement decision flow,Negative,CPDA reimbursement rejected,approval_status=REJECTED +WF-HR2-010,Appraisal form review flow,End-to-End,HOD forwards to director,status=REVIEWED +WF-HR2-010,Appraisal form review flow,End-to-End,Director approves,status=APPROVED diff --git a/FusionIIIT/applications/hr2/tests/runner.py b/FusionIIIT/applications/hr2/tests/runner.py new file mode 100644 index 000000000..05caeac7d --- /dev/null +++ b/FusionIIIT/applications/hr2/tests/runner.py @@ -0,0 +1,379 @@ +import csv +import os +from dataclasses import dataclass, field +from datetime import datetime +from typing import Any, Dict, List, Optional + +import yaml +from django.test.runner import DiscoverRunner + + +REPORTS_DIRNAME = "reports" + + +@dataclass +class ExecutionRecord: + test_id: str + artifact_id: Optional[str] + artifact_type: str + category: str + scenario: str + preconditions: str + input_action: str + expected_result: str + status: str + message: str + evidence: str + steps: List[Dict[str, Any]] = field(default_factory=list) + + +@dataclass +class DefectRecord: + test_id: str + artifact_id: Optional[str] + artifact_type: str + status: str + error: str + + +class ReportStore: + def __init__(self) -> None: + self.execution_log: List[ExecutionRecord] = [] + self.defect_log: List[DefectRecord] = [] + + def add_execution( + self, + test_id: str, + artifact_id: Optional[str], + artifact_type: str, + category: str, + scenario: str, + preconditions: str, + input_action: str, + expected_result: str, + status: str, + message: str, + evidence: str, + steps: List[Dict[str, Any]], + ) -> None: + self.execution_log.append( + ExecutionRecord( + test_id=test_id, + artifact_id=artifact_id, + artifact_type=artifact_type, + category=category, + scenario=scenario, + preconditions=preconditions, + input_action=input_action, + expected_result=expected_result, + status=status, + message=message, + evidence=evidence, + steps=steps, + ) + ) + + def add_defect( + self, test_id: str, artifact_id: Optional[str], artifact_type: str, status: str, error: str + ) -> None: + self.defect_log.append( + DefectRecord( + test_id=test_id, + artifact_id=artifact_id, + artifact_type=artifact_type, + status=status, + error=error, + ) + ) + + +REPORT_STORE = ReportStore() + + +def _specs_dir() -> str: + return os.path.join(os.path.dirname(__file__), "specs") + + +def _reports_dir() -> str: + return os.path.join(os.path.dirname(__file__), REPORTS_DIRNAME) + + +def _load_yaml(filename: str) -> Dict[str, Any]: + path = os.path.join(_specs_dir(), filename) + if not os.path.exists(path): + return {} + with open(path, "r", encoding="utf-8") as handle: + return yaml.safe_load(handle) or {} + + +def load_specs() -> Dict[str, Any]: + return { + "use_cases": _load_yaml("use_cases.yaml").get("use_cases", []), + "business_rules": _load_yaml("business_rules.yaml").get("business_rules", []), + "workflows": _load_yaml("workflows.yaml").get("workflows", []), + } + + +def _write_csv(path: str, rows: List[Dict[str, Any]], fieldnames: List[str]) -> None: + os.makedirs(os.path.dirname(path), exist_ok=True) + with open(path, "w", newline="", encoding="utf-8") as handle: + writer = csv.DictWriter(handle, fieldnames=fieldnames) + writer.writeheader() + writer.writerows(rows) + + +def _uc_design_rows(use_cases: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + rows: List[Dict[str, Any]] = [] + for uc in use_cases: + for test_type, key in ( + ("Happy Path", "happy_paths"), + ("Alternate Path", "alternate_paths"), + ("Exception", "exception_paths"), + ): + for item in uc.get(key, []): + rows.append( + { + "uc_id": uc.get("id"), + "uc_title": uc.get("title"), + "test_type": test_type, + "scenario": item.get("scenario"), + "preconditions": item.get("preconditions"), + "input_action": item.get("input_action"), + "expected_result": item.get("expected_result"), + } + ) + return rows + + +def _br_design_rows(business_rules: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + rows: List[Dict[str, Any]] = [] + for br in business_rules: + for test_type, key in (("Valid", "valid_tests"), ("Invalid", "invalid_tests")): + for item in br.get(key, []): + rows.append( + { + "br_id": br.get("id"), + "br_title": br.get("title"), + "test_type": test_type, + "input_action": item.get("input_action"), + "expected_result": item.get("expected_result"), + } + ) + return rows + + +def _wf_design_rows(workflows: List[Dict[str, Any]]) -> List[Dict[str, Any]]: + rows: List[Dict[str, Any]] = [] + for wf in workflows: + for test_type, key in (("End-to-End", "e2e_tests"), ("Negative", "negative_tests")): + for item in wf.get(key, []): + rows.append( + { + "wf_id": wf.get("id"), + "wf_title": wf.get("title"), + "test_type": test_type, + "scenario": item.get("scenario"), + "expected_final_state": item.get("expected_final_state"), + } + ) + return rows + + +def _artifact_status(records: List[ExecutionRecord]) -> str: + if not records: + return "Not Implemented" + statuses = {record.status for record in records} + if statuses == {"Pass"}: + return "Implemented Correctly" + if "Pass" in statuses and ("Fail" in statuses or "Partial" in statuses): + return "Partially Implemented" + if "Fail" in statuses and "Pass" not in statuses: + return "Incorrectly Implemented" + return "Partially Implemented" + + +def _evaluation_rows( + artifact_type: str, artifacts: List[Dict[str, Any]], executions: List[ExecutionRecord] +) -> List[Dict[str, Any]]: + rows: List[Dict[str, Any]] = [] + for artifact in artifacts: + artifact_id = artifact.get("id") + relevant = [record for record in executions if record.artifact_id == artifact_id] + status = _artifact_status(relevant) + rows.append( + { + "artifact_type": artifact_type, + "artifact_id": artifact_id, + "artifact_title": artifact.get("title"), + "status": status, + } + ) + return rows + + +def _summary_rows(specs: Dict[str, Any], executions: List[ExecutionRecord]) -> List[Dict[str, Any]]: + uc_count = len(specs["use_cases"]) + br_count = len(specs["business_rules"]) + wf_count = len(specs["workflows"]) + + required_uc = uc_count * 3 + required_br = br_count * 2 + required_wf = wf_count * 2 + + designed_uc = len([record for record in executions if record.artifact_type == "UC"]) + designed_br = len([record for record in executions if record.artifact_type == "BR"]) + designed_wf = len([record for record in executions if record.artifact_type == "WF"]) + + total_executed = len(executions) + total_pass = len([record for record in executions if record.status == "Pass"]) + total_partial = len([record for record in executions if record.status == "Partial"]) + total_fail = len([record for record in executions if record.status == "Fail"]) + + def adequacy(designed: int, required: int) -> float: + if required == 0: + return 0.0 + return round((designed / required) * 100, 2) + + strict_pass_rate = 0.0 + if total_executed: + strict_pass_rate = round((total_pass / total_executed) * 100, 2) + + return [ + {"Metric": "Total Use Cases", "Value": uc_count}, + {"Metric": "Total Business Rules", "Value": br_count}, + {"Metric": "Total Workflows", "Value": wf_count}, + {"Metric": "Required UC Tests", "Value": required_uc}, + {"Metric": "Designed UC Tests", "Value": designed_uc}, + {"Metric": "Required BR Tests", "Value": required_br}, + {"Metric": "Designed BR Tests", "Value": designed_br}, + {"Metric": "Required WF Tests", "Value": required_wf}, + {"Metric": "Designed WF Tests", "Value": designed_wf}, + {"Metric": "UC Adequacy %", "Value": adequacy(designed_uc, required_uc)}, + {"Metric": "BR Adequacy %", "Value": adequacy(designed_br, required_br)}, + {"Metric": "WF Adequacy %", "Value": adequacy(designed_wf, required_wf)}, + {"Metric": "Total Tests Executed", "Value": total_executed}, + {"Metric": "Total Pass", "Value": total_pass}, + {"Metric": "Total Partial", "Value": total_partial}, + {"Metric": "Total Fail", "Value": total_fail}, + {"Metric": "Strict Pass Rate %", "Value": strict_pass_rate}, + {"Metric": "Generated At", "Value": datetime.now().isoformat()}, + {"Metric": "Tester Name", "Value": os.getenv("TESTER_NAME", "")}, + ] + + +def _execution_rows(executions: List[ExecutionRecord]) -> List[Dict[str, Any]]: + rows: List[Dict[str, Any]] = [] + for record in executions: + rows.append( + { + "test_id": record.test_id, + "artifact_type": record.artifact_type, + "artifact_id": record.artifact_id, + "category": record.category, + "scenario": record.scenario, + "preconditions": record.preconditions, + "input_action": record.input_action, + "expected_result": record.expected_result, + "status": record.status, + "message": record.message, + "evidence": record.evidence, + "steps": record.steps, + } + ) + return rows + + +def _defect_rows(defects: List[DefectRecord]) -> List[Dict[str, Any]]: + rows: List[Dict[str, Any]] = [] + for record in defects: + rows.append( + { + "test_id": record.test_id, + "artifact_type": record.artifact_type, + "artifact_id": record.artifact_id, + "status": record.status, + "error": record.error, + } + ) + return rows + + +class ReportingTestRunner(DiscoverRunner): + def run_suite(self, suite, **kwargs): + result = super().run_suite(suite, **kwargs) + self._write_reports() + return result + + def _write_reports(self) -> None: + specs = load_specs() + reports_dir = _reports_dir() + + uc_design = _uc_design_rows(specs["use_cases"]) + br_design = _br_design_rows(specs["business_rules"]) + wf_design = _wf_design_rows(specs["workflows"]) + + summary_rows = _summary_rows(specs, REPORT_STORE.execution_log) + execution_rows = _execution_rows(REPORT_STORE.execution_log) + defect_rows = _defect_rows(REPORT_STORE.defect_log) + evaluation_rows = ( + _evaluation_rows("UC", specs["use_cases"], REPORT_STORE.execution_log) + + _evaluation_rows("BR", specs["business_rules"], REPORT_STORE.execution_log) + + _evaluation_rows("WF", specs["workflows"], REPORT_STORE.execution_log) + ) + + _write_csv( + os.path.join(reports_dir, "Module_Test_Summary.csv"), + summary_rows, + ["Metric", "Value"], + ) + _write_csv( + os.path.join(reports_dir, "UC_Test_Design.csv"), + uc_design, + [ + "uc_id", + "uc_title", + "test_type", + "scenario", + "preconditions", + "input_action", + "expected_result", + ], + ) + _write_csv( + os.path.join(reports_dir, "BR_Test_Design.csv"), + br_design, + ["br_id", "br_title", "test_type", "input_action", "expected_result"], + ) + _write_csv( + os.path.join(reports_dir, "WF_Test_Design.csv"), + wf_design, + ["wf_id", "wf_title", "test_type", "scenario", "expected_final_state"], + ) + _write_csv( + os.path.join(reports_dir, "Test_Execution_Log.csv"), + execution_rows, + [ + "test_id", + "artifact_type", + "artifact_id", + "category", + "scenario", + "preconditions", + "input_action", + "expected_result", + "status", + "message", + "evidence", + "steps", + ], + ) + _write_csv( + os.path.join(reports_dir, "Defect_Log.csv"), + defect_rows, + ["test_id", "artifact_type", "artifact_id", "status", "error"], + ) + _write_csv( + os.path.join(reports_dir, "Artifact_Evaluation.csv"), + evaluation_rows, + ["artifact_type", "artifact_id", "artifact_title", "status"], + ) diff --git a/FusionIIIT/applications/hr2/tests/specs/business_rules.yaml b/FusionIIIT/applications/hr2/tests/specs/business_rules.yaml new file mode 100644 index 000000000..edbebcc66 --- /dev/null +++ b/FusionIIIT/applications/hr2/tests/specs/business_rules.yaml @@ -0,0 +1,251 @@ +business_rules: + - id: "BR-HR2-001" + title: "Leave start date cannot be in the past" + description: "Leave application start date must be today or later" + valid_tests: + - input_action: "Apply leave with start_date=tomorrow" + expected_result: "Request accepted" + invalid_tests: + - input_action: "Apply leave with start_date=yesterday" + expected_result: "Request rejected with start_date validation error" + + - id: "BR-HR2-002" + title: "Leave end date must be on/after start date" + description: "End date must be greater than or equal to start date" + valid_tests: + - input_action: "Apply leave with start_date=2026-05-01, end_date=2026-05-03" + expected_result: "Request accepted" + invalid_tests: + - input_action: "Apply leave with start_date=2026-05-03, end_date=2026-05-01" + expected_result: "Request rejected with date range error" + + - id: "BR-HR2-003" + title: "Total days must match date range" + description: "total_days must equal (end_date - start_date + 1)" + valid_tests: + - input_action: "Apply leave with start_date=2026-05-01, end_date=2026-05-03, total_days=3" + expected_result: "Request accepted" + invalid_tests: + - input_action: "Apply leave with start_date=2026-05-01, end_date=2026-05-03, total_days=2" + expected_result: "Request rejected with total_days mismatch error" + + - id: "BR-HR2-004" + title: "Leave requests cannot overlap" + description: "Employee cannot have overlapping pending/forwarded/approved leave" + valid_tests: + - input_action: "Apply leave for 2026-05-10 to 2026-05-12 when no overlaps" + expected_result: "Request accepted" + invalid_tests: + - input_action: "Apply leave overlapping existing approved leave" + expected_result: "Request rejected with overlap error" + + - id: "BR-HR2-005" + title: "Leave balance must be sufficient" + description: "Requested days must be <= current leave balance" + valid_tests: + - input_action: "Apply leave for 2 days with balance >= 2" + expected_result: "Request accepted" + invalid_tests: + - input_action: "Apply leave for 10 days with balance < 10" + expected_result: "Request rejected with insufficient balance error" + + - id: "BR-HR2-006" + title: "Nominee employee must exist" + description: "Nominee ID must match an existing employee" + valid_tests: + - input_action: "Apply leave with nominee_employee_id=valid" + expected_result: "Request accepted with nominee_status=PENDING" + invalid_tests: + - input_action: "Apply leave with nominee_employee_id=invalid" + expected_result: "Request rejected with nominee not found error" + + - id: "BR-HR2-007" + title: "Only owner can withdraw leave" + description: "Employee can withdraw own pending/forwarded leave" + valid_tests: + - input_action: "Owner withdraws pending leave" + expected_result: "Leave updated to WITHDRAWN" + invalid_tests: + - input_action: "Non-owner withdraws leave" + expected_result: "Request rejected with 403" + + - id: "BR-HR2-008" + title: "Cancellation only before start date" + description: "Cancellation request allowed only up to 1 day before start" + valid_tests: + - input_action: "Request cancellation one day before start" + expected_result: "Cancellation request accepted" + invalid_tests: + - input_action: "Request cancellation on start date" + expected_result: "Request rejected with cancellation window error" + + - id: "BR-HR2-009" + title: "Leave delete only when pending" + description: "Only pending leave applications can be deleted" + valid_tests: + - input_action: "Delete pending leave" + expected_result: "Leave deleted" + invalid_tests: + - input_action: "Delete approved leave" + expected_result: "Request rejected with delete error" + + - id: "BR-HR2-010" + title: "Cancellation requires approved leave and no prior request" + description: "Cancel request allowed only for approved leave with cancel_status=NOT_REQUESTED" + valid_tests: + - input_action: "Request cancellation for approved leave" + expected_result: "Cancel status set to REQUESTED" + invalid_tests: + - input_action: "Request cancellation for pending leave" + expected_result: "Request rejected with approval_status error" + - input_action: "Request cancellation when cancel_status already REQUESTED" + expected_result: "Request rejected as already processed" + + - id: "BR-HR2-011" + title: "Extension requires approved leave before end date" + description: "Extension request allowed only before end_date and with new_end_date after current end" + valid_tests: + - input_action: "Request extension with new_end_date after current end date" + expected_result: "Extension status set to REQUESTED" + invalid_tests: + - input_action: "Request extension after end_date" + expected_result: "Request rejected with extension window error" + - input_action: "Request extension with new_end_date before current end date" + expected_result: "Request rejected with date validation error" + + - id: "BR-HR2-012" + title: "Extension approval needs balance" + description: "Extension approval must have enough remaining leave balance" + valid_tests: + - input_action: "Approve extension with sufficient balance" + expected_result: "Extension approved and balance reduced" + invalid_tests: + - input_action: "Approve extension with insufficient balance" + expected_result: "Request rejected with insufficient balance error" + + - id: "BR-HR2-013" + title: "Document request requires HOD role" + description: "Only HOD can request documents with non-empty message" + valid_tests: + - input_action: "HOD requests document with message" + expected_result: "Document request status set to REQUESTED" + invalid_tests: + - input_action: "Non-HOD requests document" + expected_result: "Request rejected with 403" + - input_action: "HOD requests document without message" + expected_result: "Request rejected with message required" + + - id: "BR-HR2-014" + title: "Document submit requires owner and request" + description: "Only leave owner can submit document after it was requested" + valid_tests: + - input_action: "Owner submits document after request" + expected_result: "Document request status set to SUBMITTED" + invalid_tests: + - input_action: "Owner submits document without request" + expected_result: "Request rejected with no request error" + - input_action: "Non-owner submits document" + expected_result: "Request rejected with 403" + + - id: "BR-HR2-015" + title: "Nominee decision only by nominee" + description: "Only the nominated employee can accept or decline" + valid_tests: + - input_action: "Nominee accepts request" + expected_result: "Nominee status updated to ACCEPTED" + invalid_tests: + - input_action: "Non-nominee responds" + expected_result: "Request rejected with 403" + - input_action: "Nominee sends invalid action" + expected_result: "Request rejected with invalid action" + + - id: "BR-HR2-016" + title: "Leave decision action must be valid" + description: "Decision must be approve, reject, or forward" + valid_tests: + - input_action: "Approve leave via decision endpoint" + expected_result: "Leave status updated to APPROVED" + invalid_tests: + - input_action: "Decision action=invalid" + expected_result: "Request rejected with invalid decision" + + - id: "BR-HR2-017" + title: "Leave balance record must exist" + description: "Leave type balance must exist for employee" + valid_tests: + - input_action: "Apply leave with existing leave balance" + expected_result: "Request accepted" + invalid_tests: + - input_action: "Apply leave with no balance record for type" + expected_result: "Request rejected with balance not found error" + + - id: "BR-HR2-018" + title: "Leave application requires employee profile" + description: "Employee profile must be resolved from user or employee_id" + valid_tests: + - input_action: "Apply leave as authenticated employee" + expected_result: "Request accepted" + invalid_tests: + - input_action: "Apply leave without employee profile and no employee_id" + expected_result: "Request rejected with employee profile not found" + + - id: "BR-HR2-019" + title: "LTC withdrawal only when pending" + description: "Only pending LTC requests can be withdrawn by owner" + valid_tests: + - input_action: "Owner withdraws pending LTC" + expected_result: "LTC updated to WITHDRAWN" + invalid_tests: + - input_action: "Owner withdraws approved LTC" + expected_result: "Request rejected with pending-only error" + + - id: "BR-HR2-020" + title: "LTC decision action must be valid" + description: "Decision must be approve, reject, or forward" + valid_tests: + - input_action: "Forward LTC" + expected_result: "Approval status set to FORWARDED and accountant_status=PENDING" + invalid_tests: + - input_action: "Decision action=invalid" + expected_result: "Request rejected with invalid decision" + + - id: "BR-HR2-021" + title: "CPDA advance withdrawal only when pending" + description: "Only pending CPDA advance requests can be withdrawn by owner" + valid_tests: + - input_action: "Owner withdraws pending CPDA advance" + expected_result: "CPDA updated to WITHDRAWN" + invalid_tests: + - input_action: "Owner withdraws approved CPDA advance" + expected_result: "Request rejected with pending-only error" + + - id: "BR-HR2-022" + title: "CPDA advance decision action must be valid" + description: "Decision must be approve, reject, forward-accountant, or forward-director" + valid_tests: + - input_action: "Forward CPDA advance to accountant" + expected_result: "Status FORWARDED and accountant_processing_status=PENDING" + - input_action: "Forward CPDA advance to director" + expected_result: "Status FORWARDED and accountant_processing_status=DIRECTOR_REVIEW" + invalid_tests: + - input_action: "Decision action=invalid" + expected_result: "Request rejected with invalid decision" + + - id: "BR-HR2-023" + title: "Download access restricted to owner or staff" + description: "Leave/LTC/CPDA/Appraisal downloads require ownership or staff access" + valid_tests: + - input_action: "Owner downloads own leave application" + expected_result: "Download succeeds" + invalid_tests: + - input_action: "Non-owner downloads another employee record" + expected_result: "Request rejected with 403" + + - id: "BR-HR2-024" + title: "Appraisal review sets status" + description: "Review action sets status to REVIEWED or APPROVED" + valid_tests: + - input_action: "Reviewer forwards appraisal" + expected_result: "Appraisal status set to REVIEWED" + - input_action: "Reviewer approves appraisal" + expected_result: "Appraisal status set to APPROVED" diff --git a/FusionIIIT/applications/hr2/tests/specs/use_cases.yaml b/FusionIIIT/applications/hr2/tests/specs/use_cases.yaml new file mode 100644 index 000000000..c3bd2c2b7 --- /dev/null +++ b/FusionIIIT/applications/hr2/tests/specs/use_cases.yaml @@ -0,0 +1,1144 @@ +use_cases: + - id: "UC-HR2-001" + title: "List employees" + description: "HR staff or authorized users retrieve employees filtered by type or department" + actors: "HR staff, HOD, Employee" + preconditions: "Authenticated user with access to HR2 module" + happy_paths: + - scenario: "HR staff lists all employees" + preconditions: "User has HR designation or is HR staff" + input_action: "GET /hr2/api/employees/" + expected_result: "Returns list of employees" + - scenario: "Filter employees by type and department" + preconditions: "User is authenticated" + input_action: "GET /hr2/api/employees/?type=Faculty&department=1" + expected_result: "Returns employees matching filters" + alternate_paths: + - scenario: "Request with only department filter" + preconditions: "User is authenticated" + input_action: "GET /hr2/api/employees/?department=1" + expected_result: "Returns employees in department" + exception_paths: + - scenario: "Unauthorized user tries to list employees" + preconditions: "User is not authenticated" + input_action: "GET /hr2/api/employees/" + expected_result: "Returns 401 Unauthorized" + + - id: "UC-HR2-002" + title: "View employee details" + description: "Fetch a specific employee profile" + actors: "HR staff, HOD, Employee" + preconditions: "Authenticated user with access to HR2 module" + happy_paths: + - scenario: "Fetch employee details by ID" + preconditions: "Employee exists" + input_action: "GET /hr2/api/employees/123/" + expected_result: "Returns employee details" + alternate_paths: + - scenario: "HOD fetches employee details" + preconditions: "HOD is authenticated" + input_action: "GET /hr2/api/employees/123/" + expected_result: "Returns employee details" + exception_paths: + - scenario: "Employee not found" + preconditions: "Employee does not exist" + input_action: "GET /hr2/api/employees/999999/" + expected_result: "Returns 404 Not Found" + + - id: "UC-HR2-003" + title: "Update employee details" + description: "Update employee profile information" + actors: "HR staff" + preconditions: "Authenticated HR staff" + happy_paths: + - scenario: "Update phone number" + preconditions: "Employee exists" + input_action: "PUT /hr2/api/employees/123/ with phone_number=9876543210" + expected_result: "Returns updated employee details" + alternate_paths: + - scenario: "Update employee address" + preconditions: "Employee exists" + input_action: "PUT /hr2/api/employees/123/ with address=Updated" + expected_result: "Returns updated employee details" + exception_paths: + - scenario: "Invalid data" + preconditions: "Employee exists" + input_action: "PUT /hr2/api/employees/123/ with phone_number=invalid" + expected_result: "Returns 400 validation error" + + - id: "UC-HR2-004" + title: "Apply for leave" + description: "Employee submits a leave application" + actors: "Employee" + preconditions: "Authenticated employee with available leave balance" + happy_paths: + - scenario: "Apply for casual leave with valid dates" + preconditions: "Leave balance is sufficient" + input_action: "POST /hr2/api/leave-applications/ with leave_type=CL, start_date=future, end_date=future+2, total_days=3" + expected_result: "Leave application created with approval_status=PENDING or FORWARDED" + alternate_paths: + - scenario: "Nominate substitute during leave" + preconditions: "Nominee employee exists" + input_action: "POST /hr2/api/leave-applications/ with nominee_employee_id=456" + expected_result: "Leave created with nominee_status=PENDING" + exception_paths: + - scenario: "Start date in the past" + preconditions: "Employee is authenticated" + input_action: "POST /hr2/api/leave-applications/ with start_date=yesterday" + expected_result: "Returns 400 start_date validation error" + + - id: "UC-HR2-005" + title: "View leave applications" + description: "Fetch leave applications based on user role" + actors: "Employee, HOD, Registrar, Director, HR staff" + preconditions: "Authenticated user" + happy_paths: + - scenario: "Employee views own leave applications" + preconditions: "Employee is authenticated" + input_action: "GET /hr2/api/leave-applications/" + expected_result: "Returns only employee's leave applications" + - scenario: "Director views forwarded applications" + preconditions: "User has Director designation" + input_action: "GET /hr2/api/leave-applications/" + expected_result: "Returns forwarded and requested decisions" + alternate_paths: + - scenario: "HOD views departmental leave applications" + preconditions: "User has HOD designation" + input_action: "GET /hr2/api/leave-applications/" + expected_result: "Returns department leave applications" + exception_paths: + - scenario: "Unauthorized user tries to list leave applications" + preconditions: "User is not authenticated" + input_action: "GET /hr2/api/leave-applications/" + expected_result: "Returns 401 Unauthorized" + + - id: "UC-HR2-006" + title: "Withdraw leave application" + description: "Employee or approver withdraws a pending or forwarded leave" + actors: "Employee, Registrar, HR admin, Accountant" + preconditions: "Authenticated user with appropriate role" + happy_paths: + - scenario: "Employee withdraws own pending leave" + preconditions: "Leave approval_status is PENDING" + input_action: "POST /hr2/api/leave-applications/10/withdraw/" + expected_result: "Leave updated to approval_status=WITHDRAWN" + alternate_paths: + - scenario: "Registrar withdraws forwarded leave" + preconditions: "Leave approval_status is FORWARDED" + input_action: "POST /hr2/api/leave-applications/10/withdraw/" + expected_result: "Leave updated to approval_status=REJECTED" + exception_paths: + - scenario: "Withdraw non-pending leave" + preconditions: "Leave approval_status is APPROVED" + input_action: "POST /hr2/api/leave-applications/10/withdraw/" + expected_result: "Returns 400 with withdrawal error" + + - id: "UC-HR2-007" + title: "Request leave cancellation" + description: "Employee requests cancellation of an approved leave" + actors: "Employee" + preconditions: "Authenticated employee with approved leave" + happy_paths: + - scenario: "Submit cancellation request before start date" + preconditions: "Today is before leave start date" + input_action: "POST /hr2/api/leave-applications/10/cancel-request/ with reason=change of plan" + expected_result: "Cancel status set to REQUESTED" + alternate_paths: + - scenario: "Submit cancellation request with reason" + preconditions: "Approved leave exists" + input_action: "POST /hr2/api/leave-applications/10/cancel-request/ with reason=medical" + expected_result: "Cancel status set to REQUESTED" + exception_paths: + - scenario: "Cancellation after start date" + preconditions: "Today is on or after start date" + input_action: "POST /hr2/api/leave-applications/10/cancel-request/" + expected_result: "Returns 400 cancellation window error" + + - id: "UC-HR2-008" + title: "Request leave extension" + description: "Employee requests extension of an approved leave" + actors: "Employee" + preconditions: "Authenticated employee with approved leave" + happy_paths: + - scenario: "Request extension with new end date" + preconditions: "Extension dates are valid" + input_action: "POST /hr2/api/leave-applications/10/extension-request/ with new_end_date=future+2" + expected_result: "Extension status set to REQUESTED" + alternate_paths: + - scenario: "Request extension with reason" + preconditions: "Extension dates are valid" + input_action: "POST /hr2/api/leave-applications/10/extension-request/ with reason=medical" + expected_result: "Extension status set to REQUESTED" + exception_paths: + - scenario: "Extension with invalid date" + preconditions: "New end date before current end date" + input_action: "POST /hr2/api/leave-applications/10/extension-request/ with new_end_date=earlier" + expected_result: "Returns 400 validation error" + + - id: "UC-HR2-009" + title: "View leave balance" + description: "Employee or HR staff checks leave balance" + actors: "Employee, HR staff" + preconditions: "Authenticated user" + happy_paths: + - scenario: "Employee views own leave balance" + preconditions: "Employee is authenticated" + input_action: "GET /hr2/api/leave-balance/" + expected_result: "Returns leave balance for the employee" + - scenario: "HR views leave balance for another employee" + preconditions: "User has HR designation" + input_action: "GET /hr2/api/leave-balance/123/" + expected_result: "Returns leave balance for employee 123" + alternate_paths: + - scenario: "Employee views balance with no records" + preconditions: "Employee has no balance entries" + input_action: "GET /hr2/api/leave-balance/" + expected_result: "Returns empty balance list" + exception_paths: + - scenario: "Unauthorized user tries to view leave balance" + preconditions: "User is not authenticated" + input_action: "GET /hr2/api/leave-balance/" + expected_result: "Returns 401 Unauthorized" + + - id: "UC-HR2-010" + title: "Download leave application" + description: "Employee downloads a leave application summary" + actors: "Employee" + preconditions: "Authenticated employee who owns the leave application" + happy_paths: + - scenario: "Download approved leave application" + preconditions: "Leave exists and belongs to employee" + input_action: "GET /hr2/api/leave-applications/10/download/" + expected_result: "Returns text file attachment with leave details" + alternate_paths: + - scenario: "Download pending leave application" + preconditions: "Leave is pending and belongs to employee" + input_action: "GET /hr2/api/leave-applications/10/download/" + expected_result: "Returns text file attachment with leave details" + exception_paths: + - scenario: "Access another employee's leave" + preconditions: "Leave does not belong to requester" + input_action: "GET /hr2/api/leave-applications/999/download/" + expected_result: "Returns 403 Not authorized" + + - id: "UC-HR2-011" + title: "Submit LTC application" + description: "Employee submits LTC claim" + actors: "Employee" + preconditions: "Authenticated employee" + happy_paths: + - scenario: "Submit LTC claim" + preconditions: "Required fields provided" + input_action: "POST /hr2/api/ltc/ with required LTC fields" + expected_result: "LTC application created with approval_status=PENDING" + alternate_paths: + - scenario: "Submit LTC claim with optional fields" + preconditions: "Required fields provided" + input_action: "POST /hr2/api/ltc/ with optional fields" + expected_result: "LTC application created with approval_status=PENDING" + exception_paths: + - scenario: "Missing required fields" + preconditions: "Employee is authenticated" + input_action: "POST /hr2/api/ltc/ with incomplete payload" + expected_result: "Returns 400 validation errors" + + - id: "UC-HR2-012" + title: "Submit CPDA advance" + description: "Employee submits CPDA advance request" + actors: "Employee" + preconditions: "Authenticated employee" + happy_paths: + - scenario: "Submit CPDA advance" + preconditions: "Required fields provided" + input_action: "POST /hr2/api/cpda-advances/ with required fields" + expected_result: "CPDA advance created with approval_status=PENDING" + alternate_paths: + - scenario: "Submit CPDA advance with optional expenses" + preconditions: "Required fields provided" + input_action: "POST /hr2/api/cpda-advances/ with optional fields" + expected_result: "CPDA advance created with approval_status=PENDING" + exception_paths: + - scenario: "Invalid amount" + preconditions: "Employee is authenticated" + input_action: "POST /hr2/api/cpda-advances/ with amountRequired=invalid" + expected_result: "Returns 400 validation errors" + + - id: "UC-HR2-013" + title: "Submit appraisal form" + description: "Employee submits appraisal form for review" + actors: "Employee" + preconditions: "Authenticated employee" + happy_paths: + - scenario: "Submit appraisal form" + preconditions: "Required fields provided" + input_action: "POST /hr2/api/appraisal-forms/ with required fields" + expected_result: "Appraisal form created with status=SUBMITTED" + alternate_paths: + - scenario: "Submit appraisal form with optional fields" + preconditions: "Required fields provided" + input_action: "POST /hr2/api/appraisal-forms/ with optional fields" + expected_result: "Appraisal form created with status=SUBMITTED" + exception_paths: + - scenario: "Missing required fields" + preconditions: "Employee is authenticated" + input_action: "POST /hr2/api/appraisal-forms/ with incomplete payload" + expected_result: "Returns 400 validation errors" + + - id: "UC-HR2-014" + title: "View leave application details" + description: "Fetch a specific leave application" + actors: "Employee, HR staff" + preconditions: "Authenticated user" + happy_paths: + - scenario: "Fetch leave application by ID" + preconditions: "Leave application exists" + input_action: "GET /hr2/api/leave-applications/10/" + expected_result: "Returns leave application" + alternate_paths: + - scenario: "HR staff fetches leave application by ID" + preconditions: "HR staff is authenticated" + input_action: "GET /hr2/api/leave-applications/10/" + expected_result: "Returns leave application" + exception_paths: + - scenario: "Leave application not found" + preconditions: "Leave application does not exist" + input_action: "GET /hr2/api/leave-applications/999/" + expected_result: "Returns 404 Not Found" + + - id: "UC-HR2-015" + title: "Update leave application" + description: "Employee updates a leave application" + actors: "Employee" + preconditions: "Authenticated employee who owns the leave" + happy_paths: + - scenario: "Update leave reason" + preconditions: "Leave belongs to employee" + input_action: "PUT /hr2/api/leave-applications/10/ with reason=updated" + expected_result: "Leave application updated" + alternate_paths: + - scenario: "Update leave handover notes" + preconditions: "Leave belongs to employee" + input_action: "PUT /hr2/api/leave-applications/10/ with handover_notes=updated" + expected_result: "Leave application updated" + exception_paths: + - scenario: "Update another employee's leave" + preconditions: "Leave belongs to another employee" + input_action: "PUT /hr2/api/leave-applications/10/" + expected_result: "Returns 403 Not authorized" + + - id: "UC-HR2-016" + title: "Delete leave application" + description: "Employee deletes a pending leave application" + actors: "Employee" + preconditions: "Authenticated employee who owns the leave" + happy_paths: + - scenario: "Delete pending leave" + preconditions: "Leave approval_status is PENDING" + input_action: "DELETE /hr2/api/leave-applications/10/" + expected_result: "Leave application deleted" + alternate_paths: + - scenario: "Delete pending leave without attachments" + preconditions: "Leave approval_status is PENDING" + input_action: "DELETE /hr2/api/leave-applications/10/" + expected_result: "Leave application deleted" + exception_paths: + - scenario: "Delete non-pending leave" + preconditions: "Leave approval_status is APPROVED" + input_action: "DELETE /hr2/api/leave-applications/10/" + expected_result: "Returns 400 with delete error" + + - id: "UC-HR2-017" + title: "Approve or reject leave" + description: "Approver updates leave status" + actors: "HOD, Registrar, Director" + preconditions: "Authenticated approver" + happy_paths: + - scenario: "Approve leave" + preconditions: "Leave exists" + input_action: "POST /hr2/api/leave-applications/10/approve/" + expected_result: "Leave status updated to APPROVED" + - scenario: "Forward leave" + preconditions: "Leave exists" + input_action: "POST /hr2/api/leave-applications/10/forward/" + expected_result: "Leave status updated to FORWARDED" + alternate_paths: + - scenario: "Reject leave" + preconditions: "Leave exists" + input_action: "POST /hr2/api/leave-applications/10/reject/" + expected_result: "Leave status updated to REJECTED" + exception_paths: + - scenario: "Invalid decision" + preconditions: "Leave exists" + input_action: "POST /hr2/api/leave-applications/10/invalid/" + expected_result: "Returns 400 invalid decision" + + - id: "UC-HR2-018" + title: "Nominee dashboard" + description: "Nominee views pending substitution requests" + actors: "Employee" + preconditions: "Authenticated nominee" + happy_paths: + - scenario: "Nominee views pending requests" + preconditions: "Nominee has pending requests" + input_action: "GET /hr2/api/leave-nominee/" + expected_result: "Returns pending nominee requests" + alternate_paths: + - scenario: "Nominee has no pending requests" + preconditions: "Nominee has no pending requests" + input_action: "GET /hr2/api/leave-nominee/" + expected_result: "Returns empty list" + exception_paths: + - scenario: "Unauthorized user tries to view nominee dashboard" + preconditions: "User is not authenticated" + input_action: "GET /hr2/api/leave-nominee/" + expected_result: "Returns 401 Unauthorized" + + - id: "UC-HR2-019" + title: "Nominee decision" + description: "Nominee accepts or declines substitution request" + actors: "Employee" + preconditions: "Authenticated nominee" + happy_paths: + - scenario: "Nominee accepts" + preconditions: "Nominee is assigned on leave" + input_action: "POST /hr2/api/leave-nominee/10/ with action=accept" + expected_result: "Nominee status updated to ACCEPTED" + alternate_paths: + - scenario: "Nominee declines" + preconditions: "Nominee is assigned on leave" + input_action: "POST /hr2/api/leave-nominee/10/ with action=decline" + expected_result: "Nominee status updated to DECLINED" + exception_paths: + - scenario: "Invalid action" + preconditions: "Nominee is assigned" + input_action: "POST /hr2/api/leave-nominee/10/ with action=invalid" + expected_result: "Returns 400 invalid action" + + - id: "UC-HR2-020" + title: "Request leave documents" + description: "HOD requests supporting documents" + actors: "HOD" + preconditions: "Authenticated HOD" + happy_paths: + - scenario: "Request documents with message" + preconditions: "Leave exists" + input_action: "POST /hr2/api/leave-applications/10/request-document/ with message=submit proof" + expected_result: "Document request status set to REQUESTED" + alternate_paths: + - scenario: "Request documents with updated message" + preconditions: "Leave exists" + input_action: "POST /hr2/api/leave-applications/10/request-document/ with message=submit updated proof" + expected_result: "Document request status set to REQUESTED" + exception_paths: + - scenario: "Missing message" + preconditions: "Leave exists" + input_action: "POST /hr2/api/leave-applications/10/request-document/" + expected_result: "Returns 400 message required" + + - id: "UC-HR2-021" + title: "Submit leave documents" + description: "Employee submits requested documents" + actors: "Employee" + preconditions: "Authenticated employee" + happy_paths: + - scenario: "Submit document after request" + preconditions: "Document request exists" + input_action: "POST /hr2/api/leave-applications/10/submit-document/ with submission=doc ref" + expected_result: "Document request status set to SUBMITTED" + alternate_paths: + - scenario: "Submit updated document" + preconditions: "Document request exists" + input_action: "POST /hr2/api/leave-applications/10/submit-document/ with submission=updated ref" + expected_result: "Document request status set to SUBMITTED" + exception_paths: + - scenario: "Submit without request" + preconditions: "No document request exists" + input_action: "POST /hr2/api/leave-applications/10/submit-document/ with submission=doc ref" + expected_result: "Returns 400 no request" + + - id: "UC-HR2-022" + title: "Cancellation decision" + description: "Approver approves or rejects cancellation" + actors: "HOD, Registrar, Director" + preconditions: "Cancellation request exists" + happy_paths: + - scenario: "Approve cancellation" + preconditions: "Approver role matches" + input_action: "POST /hr2/api/leave-applications/10/cancel-decision/approve/" + expected_result: "Cancellation approved and leave cancelled" + - scenario: "Reject cancellation" + preconditions: "Approver role matches" + input_action: "POST /hr2/api/leave-applications/10/cancel-decision/reject/" + expected_result: "Cancellation rejected" + alternate_paths: + - scenario: "Approve cancellation with remarks" + preconditions: "Approver role matches" + input_action: "POST /hr2/api/leave-applications/10/cancel-decision/approve/ with remarks=ok" + expected_result: "Cancellation approved and leave cancelled" + exception_paths: + - scenario: "Invalid decision" + preconditions: "Cancellation request exists" + input_action: "POST /hr2/api/leave-applications/10/cancel-decision/invalid/" + expected_result: "Returns 400 invalid decision" + + - id: "UC-HR2-023" + title: "Extension decision" + description: "Approver approves or rejects extension" + actors: "HOD, Registrar, Director" + preconditions: "Extension request exists" + happy_paths: + - scenario: "Approve extension" + preconditions: "Approver role matches" + input_action: "POST /hr2/api/leave-applications/10/extension-decision/approve/" + expected_result: "Extension approved and leave updated" + - scenario: "Reject extension" + preconditions: "Approver role matches" + input_action: "POST /hr2/api/leave-applications/10/extension-decision/reject/" + expected_result: "Extension rejected" + alternate_paths: + - scenario: "Approve extension with remarks" + preconditions: "Approver role matches" + input_action: "POST /hr2/api/leave-applications/10/extension-decision/approve/ with remarks=ok" + expected_result: "Extension approved and leave updated" + exception_paths: + - scenario: "Invalid decision" + preconditions: "Extension request exists" + input_action: "POST /hr2/api/leave-applications/10/extension-decision/invalid/" + expected_result: "Returns 400 invalid decision" + + - id: "UC-HR2-024" + title: "Record attendance" + description: "Employee marks attendance" + actors: "Employee" + preconditions: "Authenticated employee" + happy_paths: + - scenario: "Mark attendance" + preconditions: "Valid status and date" + input_action: "POST /hr2/api/attendance/ with date=today, status=PRESENT" + expected_result: "Attendance record created" + alternate_paths: + - scenario: "Mark half-day attendance" + preconditions: "Valid status and date" + input_action: "POST /hr2/api/attendance/ with date=today, status=HALF_DAY" + expected_result: "Attendance record created" + exception_paths: + - scenario: "Missing attendance status" + preconditions: "Employee is authenticated" + input_action: "POST /hr2/api/attendance/ with date=today" + expected_result: "Returns 400 validation errors" + + - id: "UC-HR2-025" + title: "View attendance" + description: "Employee views attendance history" + actors: "Employee" + preconditions: "Authenticated employee" + happy_paths: + - scenario: "View attendance for date range" + preconditions: "Attendance exists" + input_action: "GET /hr2/api/attendance/?from_date=2026-05-01&to_date=2026-05-10" + expected_result: "Returns attendance records" + alternate_paths: + - scenario: "View attendance without filters" + preconditions: "Attendance exists" + input_action: "GET /hr2/api/attendance/" + expected_result: "Returns attendance records" + exception_paths: + - scenario: "Unauthorized user tries to view attendance" + preconditions: "User is not authenticated" + input_action: "GET /hr2/api/attendance/" + expected_result: "Returns 401 Unauthorized" + + - id: "UC-HR2-026" + title: "List appraisal periods" + description: "Fetch appraisal periods" + actors: "Employee, HR staff" + preconditions: "Authenticated user" + happy_paths: + - scenario: "List active periods" + preconditions: "Periods exist" + input_action: "GET /hr2/api/appraisal-periods/?is_active=true" + expected_result: "Returns appraisal periods" + alternate_paths: + - scenario: "List all periods without filter" + preconditions: "Periods exist" + input_action: "GET /hr2/api/appraisal-periods/" + expected_result: "Returns appraisal periods" + exception_paths: + - scenario: "Unauthorized user tries to view appraisal periods" + preconditions: "User is not authenticated" + input_action: "GET /hr2/api/appraisal-periods/" + expected_result: "Returns 401 Unauthorized" + + - id: "UC-HR2-027" + title: "Submit appraisal (performance)" + description: "Employee submits performance appraisal" + actors: "Employee" + preconditions: "Authenticated employee" + happy_paths: + - scenario: "Submit appraisal" + preconditions: "Required fields provided" + input_action: "POST /hr2/api/appraisals/ with period and scores" + expected_result: "Performance appraisal created" + alternate_paths: + - scenario: "Submit appraisal with remarks" + preconditions: "Required fields provided" + input_action: "POST /hr2/api/appraisals/ with period, scores, remarks" + expected_result: "Performance appraisal created" + exception_paths: + - scenario: "Missing required fields" + preconditions: "Employee is authenticated" + input_action: "POST /hr2/api/appraisals/ with incomplete payload" + expected_result: "Returns 400 validation errors" + + - id: "UC-HR2-028" + title: "List appraisals" + description: "Employee views appraisals" + actors: "Employee" + preconditions: "Authenticated employee" + happy_paths: + - scenario: "List appraisals for period" + preconditions: "Appraisals exist" + input_action: "GET /hr2/api/appraisals/?period=1" + expected_result: "Returns appraisals" + alternate_paths: + - scenario: "List all appraisals" + preconditions: "Appraisals exist" + input_action: "GET /hr2/api/appraisals/" + expected_result: "Returns appraisals" + exception_paths: + - scenario: "Unauthorized user tries to list appraisals" + preconditions: "User is not authenticated" + input_action: "GET /hr2/api/appraisals/" + expected_result: "Returns 401 Unauthorized" + + - id: "UC-HR2-029" + title: "List training programs" + description: "View upcoming training programs" + actors: "Employee" + preconditions: "Authenticated employee" + happy_paths: + - scenario: "List available programs" + preconditions: "Programs exist" + input_action: "GET /hr2/api/training-programs/" + expected_result: "Returns training programs" + alternate_paths: + - scenario: "List programs when none are available" + preconditions: "No programs available" + input_action: "GET /hr2/api/training-programs/" + expected_result: "Returns empty list" + exception_paths: + - scenario: "Unauthorized user tries to list programs" + preconditions: "User is not authenticated" + input_action: "GET /hr2/api/training-programs/" + expected_result: "Returns 401 Unauthorized" + + - id: "UC-HR2-030" + title: "Nominate for training" + description: "Employee submits training nomination" + actors: "Employee" + preconditions: "Authenticated employee" + happy_paths: + - scenario: "Submit training nomination" + preconditions: "Program exists" + input_action: "POST /hr2/api/training-nominations/ with program data" + expected_result: "Nomination created" + alternate_paths: + - scenario: "Nominate for mandatory program" + preconditions: "Program exists and is mandatory" + input_action: "POST /hr2/api/training-nominations/ with program data" + expected_result: "Nomination created" + exception_paths: + - scenario: "Invalid program" + preconditions: "Employee is authenticated" + input_action: "POST /hr2/api/training-nominations/ with invalid program" + expected_result: "Returns 400 validation errors" + + - id: "UC-HR2-031" + title: "View training nominations" + description: "Employee views training nominations" + actors: "Employee" + preconditions: "Authenticated employee" + happy_paths: + - scenario: "List nominations" + preconditions: "Nominations exist" + input_action: "GET /hr2/api/training-nominations/" + expected_result: "Returns training nominations" + alternate_paths: + - scenario: "List nominations after submission" + preconditions: "Nomination exists" + input_action: "GET /hr2/api/training-nominations/" + expected_result: "Returns training nominations" + exception_paths: + - scenario: "Unauthorized user tries to list nominations" + preconditions: "User is not authenticated" + input_action: "GET /hr2/api/training-nominations/" + expected_result: "Returns 401 Unauthorized" + + - id: "UC-HR2-032" + title: "Submit promotion application" + description: "Employee submits promotion application" + actors: "Employee" + preconditions: "Authenticated employee" + happy_paths: + - scenario: "Submit promotion" + preconditions: "Required fields provided" + input_action: "POST /hr2/api/promotions/ with required fields" + expected_result: "Promotion application created" + alternate_paths: + - scenario: "Submit promotion with API score" + preconditions: "Required fields provided" + input_action: "POST /hr2/api/promotions/ with api_score" + expected_result: "Promotion application created" + exception_paths: + - scenario: "Missing required fields" + preconditions: "Employee is authenticated" + input_action: "POST /hr2/api/promotions/ with incomplete payload" + expected_result: "Returns 400 validation errors" + + - id: "UC-HR2-033" + title: "View promotion applications" + description: "Employee views promotion applications" + actors: "Employee" + preconditions: "Authenticated employee" + happy_paths: + - scenario: "List promotions" + preconditions: "Applications exist" + input_action: "GET /hr2/api/promotions/" + expected_result: "Returns promotion applications" + alternate_paths: + - scenario: "List promotions when none exist" + preconditions: "No promotions exist" + input_action: "GET /hr2/api/promotions/" + expected_result: "Returns empty list" + exception_paths: + - scenario: "Unauthorized user tries to list promotions" + preconditions: "User is not authenticated" + input_action: "GET /hr2/api/promotions/" + expected_result: "Returns 401 Unauthorized" + + - id: "UC-HR2-034" + title: "View faculty workload" + description: "Faculty views workload" + actors: "Faculty" + preconditions: "Authenticated faculty" + happy_paths: + - scenario: "Get workload by semester" + preconditions: "Workload exists" + input_action: "GET /hr2/api/workload/?semester=Spring&year=2026" + expected_result: "Returns workload records" + alternate_paths: + - scenario: "Get workload without semester filter" + preconditions: "Workload exists" + input_action: "GET /hr2/api/workload/?year=2026" + expected_result: "Returns workload records" + exception_paths: + - scenario: "Unauthorized user tries to view workload" + preconditions: "User is not authenticated" + input_action: "GET /hr2/api/workload/" + expected_result: "Returns 401 Unauthorized" + + - id: "UC-HR2-035" + title: "Submit LTC update" + description: "Employee updates LTC application" + actors: "Employee" + preconditions: "Authenticated employee who owns the LTC" + happy_paths: + - scenario: "Update LTC details" + preconditions: "LTC belongs to employee" + input_action: "PUT /hr2/api/ltc/10/ with destination=updated" + expected_result: "LTC updated" + alternate_paths: + - scenario: "Update LTC purpose" + preconditions: "LTC belongs to employee" + input_action: "PUT /hr2/api/ltc/10/ with purpose_of_travel=updated" + expected_result: "LTC updated" + exception_paths: + - scenario: "Update another employee's LTC" + preconditions: "LTC belongs to another employee" + input_action: "PUT /hr2/api/ltc/10/" + expected_result: "Returns 403 Not authorized" + + - id: "UC-HR2-036" + title: "View LTC applications" + description: "View LTC list based on role" + actors: "Employee, HR staff, Accountant" + preconditions: "Authenticated user" + happy_paths: + - scenario: "Employee views own LTC" + preconditions: "Employee is authenticated" + input_action: "GET /hr2/api/ltc/" + expected_result: "Returns employee LTC applications" + - scenario: "HR staff views pending LTC" + preconditions: "User is HR staff" + input_action: "GET /hr2/api/ltc/" + expected_result: "Returns pending/forwarded LTC" + alternate_paths: + - scenario: "Accountant views forwarded LTC" + preconditions: "User is Accountant" + input_action: "GET /hr2/api/ltc/" + expected_result: "Returns forwarded LTC" + exception_paths: + - scenario: "Unauthorized user tries to list LTC" + preconditions: "User is not authenticated" + input_action: "GET /hr2/api/ltc/" + expected_result: "Returns 401 Unauthorized" + + - id: "UC-HR2-037" + title: "Download LTC application" + description: "Employee downloads LTC summary" + actors: "Employee" + preconditions: "Authenticated employee who owns the LTC" + happy_paths: + - scenario: "Download LTC" + preconditions: "LTC exists" + input_action: "GET /hr2/api/ltc/10/download/" + expected_result: "Returns text file attachment" + alternate_paths: + - scenario: "HR staff downloads LTC" + preconditions: "HR staff is authenticated" + input_action: "GET /hr2/api/ltc/10/download/" + expected_result: "Returns text file attachment" + exception_paths: + - scenario: "Unauthorized user tries to download LTC" + preconditions: "User is not authenticated" + input_action: "GET /hr2/api/ltc/10/download/" + expected_result: "Returns 401 Unauthorized" + + - id: "UC-HR2-038" + title: "Withdraw LTC application" + description: "Employee withdraws pending LTC" + actors: "Employee" + preconditions: "Authenticated employee" + happy_paths: + - scenario: "Withdraw pending LTC" + preconditions: "LTC approval_status is PENDING" + input_action: "POST /hr2/api/ltc/10/withdraw/" + expected_result: "LTC updated to WITHDRAWN" + alternate_paths: + - scenario: "Withdraw pending LTC with remarks" + preconditions: "LTC approval_status is PENDING" + input_action: "POST /hr2/api/ltc/10/withdraw/ with remarks=updated" + expected_result: "LTC updated to WITHDRAWN" + exception_paths: + - scenario: "Withdraw non-pending LTC" + preconditions: "LTC approval_status is APPROVED" + input_action: "POST /hr2/api/ltc/10/withdraw/" + expected_result: "Returns 400 with withdrawal error" + + - id: "UC-HR2-039" + title: "Approve or reject LTC" + description: "Approver updates LTC status" + actors: "HR staff, Accountant" + preconditions: "Authenticated approver" + happy_paths: + - scenario: "Forward LTC" + preconditions: "LTC exists" + input_action: "POST /hr2/api/ltc/10/forward/" + expected_result: "LTC status FORWARDED and accountant_status=PENDING" + - scenario: "Approve LTC" + preconditions: "LTC exists" + input_action: "POST /hr2/api/ltc/10/approve/" + expected_result: "LTC status APPROVED" + alternate_paths: + - scenario: "Reject LTC" + preconditions: "LTC exists" + input_action: "POST /hr2/api/ltc/10/reject/" + expected_result: "LTC status REJECTED" + exception_paths: + - scenario: "Invalid decision" + preconditions: "LTC exists" + input_action: "POST /hr2/api/ltc/10/invalid/" + expected_result: "Returns 400 invalid decision" + + - id: "UC-HR2-040" + title: "View CPDA advances" + description: "View CPDA advance list based on role" + actors: "Employee, HR staff, Accountant, Director" + preconditions: "Authenticated user" + happy_paths: + - scenario: "Employee views own advances" + preconditions: "Employee is authenticated" + input_action: "GET /hr2/api/cpda-advances/" + expected_result: "Returns employee CPDA advances" + alternate_paths: + - scenario: "HR staff views pending advances" + preconditions: "User is HR staff" + input_action: "GET /hr2/api/cpda-advances/" + expected_result: "Returns pending advances" + exception_paths: + - scenario: "Unauthorized user tries to list advances" + preconditions: "User is not authenticated" + input_action: "GET /hr2/api/cpda-advances/" + expected_result: "Returns 401 Unauthorized" + + - id: "UC-HR2-041" + title: "View CPDA advance details" + description: "Fetch a CPDA advance by ID" + actors: "Employee, HR staff" + preconditions: "Authenticated user" + happy_paths: + - scenario: "Fetch CPDA advance" + preconditions: "CPDA advance exists" + input_action: "GET /hr2/api/cpda-advances/10/" + expected_result: "Returns CPDA advance" + alternate_paths: + - scenario: "HR staff fetches CPDA advance" + preconditions: "HR staff is authenticated" + input_action: "GET /hr2/api/cpda-advances/10/" + expected_result: "Returns CPDA advance" + exception_paths: + - scenario: "CPDA advance not found" + preconditions: "CPDA advance does not exist" + input_action: "GET /hr2/api/cpda-advances/999/" + expected_result: "Returns 404 Not Found" + + - id: "UC-HR2-042" + title: "Download CPDA advance" + description: "Employee downloads CPDA advance summary" + actors: "Employee" + preconditions: "Authenticated employee who owns the CPDA" + happy_paths: + - scenario: "Download CPDA advance" + preconditions: "CPDA exists" + input_action: "GET /hr2/api/cpda-advances/10/download/" + expected_result: "Returns text file attachment" + alternate_paths: + - scenario: "HR staff downloads CPDA advance" + preconditions: "HR staff is authenticated" + input_action: "GET /hr2/api/cpda-advances/10/download/" + expected_result: "Returns text file attachment" + exception_paths: + - scenario: "Unauthorized user tries to download CPDA" + preconditions: "User is not authenticated" + input_action: "GET /hr2/api/cpda-advances/10/download/" + expected_result: "Returns 401 Unauthorized" + + - id: "UC-HR2-043" + title: "Withdraw CPDA advance" + description: "Employee withdraws pending CPDA advance" + actors: "Employee" + preconditions: "Authenticated employee" + happy_paths: + - scenario: "Withdraw pending CPDA advance" + preconditions: "CPDA approval_status is PENDING" + input_action: "POST /hr2/api/cpda-advances/10/withdraw/" + expected_result: "CPDA updated to WITHDRAWN" + alternate_paths: + - scenario: "Withdraw pending CPDA advance with remarks" + preconditions: "CPDA approval_status is PENDING" + input_action: "POST /hr2/api/cpda-advances/10/withdraw/ with remarks=updated" + expected_result: "CPDA updated to WITHDRAWN" + exception_paths: + - scenario: "Withdraw non-pending CPDA advance" + preconditions: "CPDA approval_status is APPROVED" + input_action: "POST /hr2/api/cpda-advances/10/withdraw/" + expected_result: "Returns 400 with withdrawal error" + + - id: "UC-HR2-044" + title: "Decide CPDA advance" + description: "HR/Accountant/Director updates CPDA advance status" + actors: "HR staff, Accountant, Director" + preconditions: "Authenticated approver" + happy_paths: + - scenario: "Forward CPDA to accountant" + preconditions: "CPDA exists" + input_action: "POST /hr2/api/cpda-advances/10/forward-accountant/" + expected_result: "Status FORWARDED and accountant_processing_status=PENDING" + - scenario: "Forward CPDA to director" + preconditions: "CPDA exists" + input_action: "POST /hr2/api/cpda-advances/10/forward-director/" + expected_result: "Status FORWARDED and accountant_processing_status=DIRECTOR_REVIEW" + - scenario: "Approve CPDA" + preconditions: "CPDA exists" + input_action: "POST /hr2/api/cpda-advances/10/approve/" + expected_result: "Status APPROVED" + alternate_paths: + - scenario: "Reject CPDA" + preconditions: "CPDA exists" + input_action: "POST /hr2/api/cpda-advances/10/reject/" + expected_result: "Status REJECTED" + exception_paths: + - scenario: "Invalid decision" + preconditions: "CPDA exists" + input_action: "POST /hr2/api/cpda-advances/10/invalid/" + expected_result: "Returns 400 invalid decision" + + - id: "UC-HR2-045" + title: "Submit CPDA reimbursement" + description: "Employee submits CPDA reimbursement request" + actors: "Employee" + preconditions: "Authenticated employee" + happy_paths: + - scenario: "Submit CPDA reimbursement" + preconditions: "Required fields provided" + input_action: "POST /hr2/api/cpda-reimbursements/ with required fields" + expected_result: "CPDA reimbursement created" + alternate_paths: + - scenario: "Submit CPDA reimbursement with optional expenses" + preconditions: "Required fields provided" + input_action: "POST /hr2/api/cpda-reimbursements/ with optional fields" + expected_result: "CPDA reimbursement created" + exception_paths: + - scenario: "Missing required fields" + preconditions: "Employee is authenticated" + input_action: "POST /hr2/api/cpda-reimbursements/ with incomplete payload" + expected_result: "Returns 400 validation errors" + + - id: "UC-HR2-046" + title: "View CPDA reimbursements" + description: "Employee views CPDA reimbursement list" + actors: "Employee" + preconditions: "Authenticated employee" + happy_paths: + - scenario: "List CPDA reimbursements" + preconditions: "Requests exist" + input_action: "GET /hr2/api/cpda-reimbursements/" + expected_result: "Returns CPDA reimbursements" + alternate_paths: + - scenario: "List CPDA reimbursements when none exist" + preconditions: "No requests exist" + input_action: "GET /hr2/api/cpda-reimbursements/" + expected_result: "Returns empty list" + exception_paths: + - scenario: "Unauthorized user tries to list reimbursements" + preconditions: "User is not authenticated" + input_action: "GET /hr2/api/cpda-reimbursements/" + expected_result: "Returns 401 Unauthorized" + + - id: "UC-HR2-047" + title: "View CPDA reimbursement details" + description: "Fetch a CPDA reimbursement by ID" + actors: "Employee" + preconditions: "Authenticated employee" + happy_paths: + - scenario: "Fetch CPDA reimbursement" + preconditions: "CPDA reimbursement exists" + input_action: "GET /hr2/api/cpda-reimbursements/10/" + expected_result: "Returns CPDA reimbursement" + alternate_paths: + - scenario: "HR staff fetches reimbursement" + preconditions: "HR staff is authenticated" + input_action: "GET /hr2/api/cpda-reimbursements/10/" + expected_result: "Returns CPDA reimbursement" + exception_paths: + - scenario: "CPDA reimbursement not found" + preconditions: "CPDA reimbursement does not exist" + input_action: "GET /hr2/api/cpda-reimbursements/999/" + expected_result: "Returns 404 Not Found" + + - id: "UC-HR2-048" + title: "Decide CPDA reimbursement" + description: "Approver approves or rejects CPDA reimbursement" + actors: "Accountant, HR staff" + preconditions: "Authenticated approver" + happy_paths: + - scenario: "Approve CPDA reimbursement" + preconditions: "Request exists" + input_action: "POST /hr2/api/cpda-reimbursements/10/approve/" + expected_result: "Reimbursement approved" + - scenario: "Reject CPDA reimbursement" + preconditions: "Request exists" + input_action: "POST /hr2/api/cpda-reimbursements/10/reject/" + expected_result: "Reimbursement rejected" + alternate_paths: + - scenario: "Reject CPDA reimbursement with remarks" + preconditions: "Request exists" + input_action: "POST /hr2/api/cpda-reimbursements/10/reject/ with remarks=invalid" + expected_result: "Reimbursement rejected" + exception_paths: + - scenario: "Invalid decision" + preconditions: "Request exists" + input_action: "POST /hr2/api/cpda-reimbursements/10/invalid/" + expected_result: "Returns 400 invalid decision" + + - id: "UC-HR2-049" + title: "List appraisal forms" + description: "View appraisal forms based on role" + actors: "Employee, HR staff, HOD, Director" + preconditions: "Authenticated user" + happy_paths: + - scenario: "Employee views own appraisal forms" + preconditions: "Employee is authenticated" + input_action: "GET /hr2/api/appraisal-forms/" + expected_result: "Returns employee appraisal forms" + - scenario: "Director views reviewed forms" + preconditions: "User is Director" + input_action: "GET /hr2/api/appraisal-forms/" + expected_result: "Returns reviewed appraisal forms" + alternate_paths: + - scenario: "HR staff views all appraisal forms" + preconditions: "User is HR staff" + input_action: "GET /hr2/api/appraisal-forms/" + expected_result: "Returns appraisal forms" + exception_paths: + - scenario: "Unauthorized user tries to list appraisal forms" + preconditions: "User is not authenticated" + input_action: "GET /hr2/api/appraisal-forms/" + expected_result: "Returns 401 Unauthorized" + + - id: "UC-HR2-050" + title: "View appraisal form details" + description: "Fetch appraisal form by ID" + actors: "Employee, HR staff" + preconditions: "Authenticated user" + happy_paths: + - scenario: "Fetch appraisal form" + preconditions: "Form exists" + input_action: "GET /hr2/api/appraisal-forms/10/" + expected_result: "Returns appraisal form" + alternate_paths: + - scenario: "HR staff fetches appraisal form" + preconditions: "HR staff is authenticated" + input_action: "GET /hr2/api/appraisal-forms/10/" + expected_result: "Returns appraisal form" + exception_paths: + - scenario: "Appraisal form not found" + preconditions: "Form does not exist" + input_action: "GET /hr2/api/appraisal-forms/999/" + expected_result: "Returns 404 Not Found" + + - id: "UC-HR2-051" + title: "Download appraisal form" + description: "Employee downloads appraisal form" + actors: "Employee" + preconditions: "Authenticated employee who owns the form" + happy_paths: + - scenario: "Download appraisal form" + preconditions: "Form exists" + input_action: "GET /hr2/api/appraisal-forms/10/download/" + expected_result: "Returns text file attachment" + alternate_paths: + - scenario: "HR staff downloads appraisal form" + preconditions: "HR staff is authenticated" + input_action: "GET /hr2/api/appraisal-forms/10/download/" + expected_result: "Returns text file attachment" + exception_paths: + - scenario: "Unauthorized user tries to download appraisal form" + preconditions: "User is not authenticated" + input_action: "GET /hr2/api/appraisal-forms/10/download/" + expected_result: "Returns 401 Unauthorized" + + - id: "UC-HR2-052" + title: "Review appraisal form" + description: "Reviewer forwards or approves appraisal form" + actors: "HOD, Director" + preconditions: "Authenticated reviewer" + happy_paths: + - scenario: "Forward appraisal for director" + preconditions: "Reviewer assigned" + input_action: "POST /hr2/api/appraisal-forms/10/review/ with action=forward" + expected_result: "Appraisal status set to REVIEWED" + - scenario: "Approve appraisal" + preconditions: "Reviewer assigned" + input_action: "POST /hr2/api/appraisal-forms/10/review/ with action=approve" + expected_result: "Appraisal status set to APPROVED" + alternate_paths: + - scenario: "Review appraisal with rating" + preconditions: "Reviewer assigned" + input_action: "POST /hr2/api/appraisal-forms/10/review/ with action=forward, rating=4" + expected_result: "Appraisal status set to REVIEWED" + exception_paths: + - scenario: "Invalid review action" + preconditions: "Reviewer assigned" + input_action: "POST /hr2/api/appraisal-forms/10/review/ with action=invalid" + expected_result: "Returns 400 invalid action" diff --git a/FusionIIIT/applications/hr2/tests/specs/workflows.yaml b/FusionIIIT/applications/hr2/tests/specs/workflows.yaml new file mode 100644 index 000000000..fbd0c0924 --- /dev/null +++ b/FusionIIIT/applications/hr2/tests/specs/workflows.yaml @@ -0,0 +1,103 @@ +workflows: + - id: "WF-HR2-001" + title: "Leave application approval flow" + description: "Employee applies → HOD/Registrar/Director decides → balance updates on approval" + e2e_tests: + - scenario: "Employee applies and leave is approved" + expected_final_state: "approval_status=APPROVED, leave balance reduced" + - scenario: "Employee applies and leave is forwarded then approved" + expected_final_state: "approval_status=APPROVED, current_approver_role=Director" + negative_tests: + - scenario: "Employee applies and leave is rejected" + expected_final_state: "approval_status=REJECTED, leave balance unchanged" + + - id: "WF-HR2-002" + title: "Leave withdrawal flow" + description: "Employee withdraws pending/forwarded leave" + e2e_tests: + - scenario: "Employee withdraws pending leave" + expected_final_state: "approval_status=WITHDRAWN" + negative_tests: + - scenario: "Employee tries to withdraw approved leave" + expected_final_state: "Request rejected with withdrawal error" + + - id: "WF-HR2-003" + title: "Leave cancellation flow" + description: "Employee requests cancellation → approver decides → balance restored on approval" + e2e_tests: + - scenario: "Cancellation approved" + expected_final_state: "cancel_status=APPROVED, approval_status=CANCELLED, leave balance restored" + negative_tests: + - scenario: "Cancellation requested after start date" + expected_final_state: "Request rejected with cancellation window error" + + - id: "WF-HR2-004" + title: "Leave extension flow" + description: "Employee requests extension → approver decides → balance adjusted on approval" + e2e_tests: + - scenario: "Extension approved with sufficient balance" + expected_final_state: "extension_status=APPROVED, end_date updated, balance reduced" + negative_tests: + - scenario: "Extension approved with insufficient balance" + expected_final_state: "Request rejected with insufficient balance error" + + - id: "WF-HR2-005" + title: "Nominee response flow" + description: "Nominee sees request → accepts or declines" + e2e_tests: + - scenario: "Nominee accepts" + expected_final_state: "nominee_status=ACCEPTED" + negative_tests: + - scenario: "Non-nominee responds" + expected_final_state: "Request rejected with 403" + + - id: "WF-HR2-006" + title: "Document request flow" + description: "HOD requests documents → employee submits" + e2e_tests: + - scenario: "HOD requests, employee submits" + expected_final_state: "document_request_status=SUBMITTED" + negative_tests: + - scenario: "Employee submits without request" + expected_final_state: "Request rejected with no request error" + + - id: "WF-HR2-007" + title: "LTC approval flow" + description: "Employee applies → HR forwards → accountant approves/rejects" + e2e_tests: + - scenario: "LTC forwarded and approved" + expected_final_state: "approval_status=APPROVED, accountant_status=APPROVED" + negative_tests: + - scenario: "LTC rejected" + expected_final_state: "approval_status=REJECTED" + + - id: "WF-HR2-008" + title: "CPDA advance approval flow" + description: "Employee applies → HR forwards → accountant/director decides" + e2e_tests: + - scenario: "HR forwards to accountant and approves" + expected_final_state: "approval_status=APPROVED, accountant_processing_status=APPROVED" + - scenario: "Forwarded to director then approved" + expected_final_state: "accountant_processing_status=DIRECTOR_APPROVED, approval_status=FORWARDED" + negative_tests: + - scenario: "CPDA advance rejected" + expected_final_state: "approval_status=REJECTED" + + - id: "WF-HR2-009" + title: "CPDA reimbursement decision flow" + description: "Employee submits reimbursement → approver approves/rejects" + e2e_tests: + - scenario: "CPDA reimbursement approved" + expected_final_state: "approval_status=APPROVED" + negative_tests: + - scenario: "CPDA reimbursement rejected" + expected_final_state: "approval_status=REJECTED" + + - id: "WF-HR2-010" + title: "Appraisal form review flow" + description: "Employee submits → HOD reviews → Director approves" + e2e_tests: + - scenario: "HOD forwards to director" + expected_final_state: "status=REVIEWED" + - scenario: "Director approves" + expected_final_state: "status=APPROVED" diff --git a/FusionIIIT/applications/hr2/tests/test_business_rules.py b/FusionIIIT/applications/hr2/tests/test_business_rules.py new file mode 100644 index 000000000..eab68c038 --- /dev/null +++ b/FusionIIIT/applications/hr2/tests/test_business_rules.py @@ -0,0 +1,336 @@ +import os +import re +from typing import Any, Dict, Optional, Tuple + +import yaml + +from .conftest import BRTestBase + + +class HR2BRTestBase(BRTestBase): + """Dynamic BR tests generated from specs/business_rules.yaml.""" + + _created_ids: Dict[str, int] = {} + + def _login_for_action(self, text: str) -> None: + normalized = text.lower() + if "not authenticated" in normalized or "unauthorized" in normalized: + self.logout() + return + + if "director" in normalized: + self.login_as_director() + elif "registrar" in normalized: + self.login_as_registrar() + elif "hod" in normalized: + self.login_as_hod() + elif "accountant" in normalized: + self.login_as_accountant() + elif "hr" in normalized or "staff" in normalized: + self.login_as_staff() + else: + self.login_as_employee() + + def _extract_id(self, data: Any) -> Optional[int]: + if isinstance(data, dict): + for key in ("id", "pk", "leave_id", "ltc_id", "cpda_id", "appraisal_id"): + value = data.get(key) + if isinstance(value, int): + return value + return None + + def _create_resource(self, endpoint: str, payload: Dict[str, Any]) -> int: + response = self.api_post(endpoint, payload, expected_status=None) + if response.status_code in {200, 201}: + extracted = self._extract_id(getattr(response, "data", {})) + if extracted is not None: + return extracted + return 1 + + def _ensure_leave_id(self) -> int: + if "leave" not in self._created_ids: + self.login_as_employee() + payload = { + "leave_type": "Casual", + "start_date": self.future_date(3), + "end_date": self.future_date(4), + "total_days": 2, + "reason": "Personal work", + "contact_during_leave": "9876543210", + "address_during_leave": "Jabalpur, MP", + } + self._created_ids["leave"] = self._create_resource( + "/hr2/api/leave-applications/", payload + ) + return self._created_ids["leave"] + + def _ensure_ltc_id(self) -> int: + if "ltc" not in self._created_ids: + self.login_as_employee() + payload = { + "employee_name": "Rahul Sharma", + "department": "Computer Science and Engineering", + "designation": "Assistant Professor", + "ltc_block_year": 2025, + "travel_start_date": self.future_date(10), + "travel_end_date": self.future_date(15), + "destination": "Delhi", + "purpose_of_travel": "Family travel", + "travel_mode": "Train", + "total_amount_claimed": 22000, + } + self._created_ids["ltc"] = self._create_resource("/hr2/api/ltc/", payload) + return self._created_ids["ltc"] + + def _ensure_cpda_advance_id(self) -> int: + if "cpda_advance" not in self._created_ids: + self.login_as_employee() + payload = { + "employee_name": "Rahul Sharma", + "department": "Computer Science and Engineering", + "designation": "Assistant Professor", + "event_name": "National Conference on AI", + "event_type": "Conference", + "start_date": self.future_date(30), + "end_date": self.future_date(32), + "total_amount": 20000, + "purpose_of_attending": "Present paper", + "benefits_to_institution": "Research exposure", + } + self._created_ids["cpda_advance"] = self._create_resource( + "/hr2/api/cpda-advances/", payload + ) + return self._created_ids["cpda_advance"] + + def _ensure_appraisal_form_id(self) -> int: + if "appraisal_form" not in self._created_ids: + self.login_as_employee() + payload = { + "employee_name": "Rahul Sharma", + "department": "Computer Science and Engineering", + "designation": "Assistant Professor", + "appraisal_year": "2025-2026", + "self_summary": "Completed teaching responsibilities", + "key_responsibilities": "Teaching and research", + "achievements": "Published 1 paper", + "goals_achieved": "Completed syllabus", + "future_goals": "Publish more papers", + } + self._created_ids["appraisal_form"] = self._create_resource( + "/hr2/api/appraisal-forms/", payload + ) + return self._created_ids["appraisal_form"] + + def _resolve_path(self, path: str) -> str: + if "/leave-applications/" in path: + leave_id = self._ensure_leave_id() + return re.sub(r"/leave-applications/\d+", f"/leave-applications/{leave_id}", path) + if "/ltc/" in path: + ltc_id = self._ensure_ltc_id() + return re.sub(r"/ltc/\d+", f"/ltc/{ltc_id}", path) + if "/cpda-advances/" in path: + cpda_id = self._ensure_cpda_advance_id() + return re.sub(r"/cpda-advances/\d+", f"/cpda-advances/{cpda_id}", path) + if "/appraisal-forms/" in path: + appraisal_id = self._ensure_appraisal_form_id() + return re.sub(r"/appraisal-forms/\d+", f"/appraisal-forms/{appraisal_id}", path) + return path + + def _payload_for(self, path: str, action_text: str, valid_case: bool) -> Dict[str, Any]: + action_lower = action_text.lower() + payload: Dict[str, Any] = {} + + day_match = re.search(r"(\d+)\s*day", action_lower) + requested_days = int(day_match.group(1)) if day_match else None + + start_date = self.future_date(1) if valid_case else self.past_date(1) + total_days = requested_days or (3 if valid_case else 2) + end_date = self.future_date((total_days or 1) - 1) if valid_case else self.future_date(1) + + if "/leave-applications/" in path and path.endswith("/leave-applications/"): + if "total_days" in action_lower and "mismatch" in action_lower: + total_days = 2 if valid_case else 1 + payload = { + "leave_type": "Casual", + "start_date": start_date, + "end_date": end_date, + "total_days": total_days, + "reason": "Personal work", + "contact_during_leave": "9876543210", + "address_during_leave": "Jabalpur, MP", + } + if "nominee" in action_lower: + payload["nominee_employee_id"] = self.nominee_extra.id if valid_case else 999999 + elif path.endswith("/withdraw/"): + payload = {} + elif path.endswith("/cancel-request/"): + payload = {"reason": "Change of plan"} + elif path.endswith("/extension-request/"): + payload = {"new_end_date": self.future_date(6)} + elif path.endswith("/request-document/"): + payload = {"message": "Submit proof"} if valid_case else {} + elif path.endswith("/submit-document/"): + payload = {"submission": "doc-ref"} + elif path.endswith("/leave-nominee/") or "/leave-nominee/" in path: + payload = {"action": "accept" if valid_case else "invalid"} + elif path.endswith("/ltc/"): + payload = { + "employee_name": "Rahul Sharma", + "department": "Computer Science and Engineering", + "designation": "Assistant Professor", + "ltc_block_year": 2025, + "travel_start_date": self.future_date(10), + "travel_end_date": self.future_date(15), + "destination": "Delhi", + "purpose_of_travel": "Family travel", + "travel_mode": "Train", + "total_amount_claimed": 22000, + } + elif path.endswith("/cpda-advances/"): + payload = { + "employee_name": "Rahul Sharma", + "department": "Computer Science and Engineering", + "designation": "Assistant Professor", + "event_name": "National Conference on AI", + "event_type": "Conference", + "start_date": self.future_date(30), + "end_date": self.future_date(32), + "total_amount": 20000, + "purpose_of_attending": "Present paper", + "benefits_to_institution": "Research exposure", + } + elif path.endswith("/appraisal-forms/"): + payload = { + "employee_name": "Rahul Sharma", + "department": "Computer Science and Engineering", + "designation": "Assistant Professor", + "appraisal_year": "2025-2026", + "self_summary": "Completed teaching responsibilities", + "key_responsibilities": "Teaching and research", + "achievements": "Published 1 paper", + "goals_achieved": "Completed syllabus", + "future_goals": "Publish more papers", + } + elif path.endswith("/review/"): + payload = {"action": "approve" if valid_case else "invalid"} + + return payload + + def _endpoint_for_action(self, action_text: str) -> str: + action_lower = action_text.lower() + if "ltc" in action_lower: + if "withdraw" in action_lower: + return "/hr2/api/ltc/1/withdraw/" + if "decision" in action_lower or "approve" in action_lower or "forward" in action_lower: + return "/hr2/api/ltc/1/forward/" + return "/hr2/api/ltc/" + if "cpda" in action_lower: + if "advance" in action_lower: + if "withdraw" in action_lower: + return "/hr2/api/cpda-advances/1/withdraw/" + if "decision" in action_lower or "approve" in action_lower or "forward" in action_lower: + return "/hr2/api/cpda-advances/1/forward-accountant/" + return "/hr2/api/cpda-advances/" + if "appraisal" in action_lower and "review" in action_lower: + return "/hr2/api/appraisal-forms/1/review/" + if "document" in action_lower and "request" in action_lower: + return "/hr2/api/leave-applications/1/request-document/" + if "document" in action_lower and "submit" in action_lower: + return "/hr2/api/leave-applications/1/submit-document/" + if "nominee" in action_lower: + return "/hr2/api/leave-nominee/1/" + if "extension" in action_lower: + return "/hr2/api/leave-applications/1/extension-request/" + if "cancellation" in action_lower or "cancel" in action_lower: + return "/hr2/api/leave-applications/1/cancel-request/" + if "withdraw" in action_lower: + return "/hr2/api/leave-applications/1/withdraw/" + if "download" in action_lower: + return "/hr2/api/leave-applications/1/download/" + if "leave" in action_lower: + return "/hr2/api/leave-applications/" + return "/hr2/api/leave-applications/" + + def _method_for_action(self, action_text: str) -> str: + action_lower = action_text.lower() + if "download" in action_lower: + return "GET" + if "apply" in action_lower or "request" in action_lower or "submit" in action_lower: + return "POST" + if "withdraw" in action_lower or "approve" in action_lower or "reject" in action_lower or "forward" in action_lower: + return "POST" + return "POST" + + def _dispatch(self, method: str, path: str, payload: Dict[str, Any]): + if method == "POST": + return self.api_post(path, payload, expected_status=None) + if method == "PUT": + return self.api_put(path, payload, expected_status=None) + if method == "DELETE": + return self.api_delete(path, expected_status=None) + return self.api_get(path, expected_status=None) + + +def _load_business_rules() -> Dict[str, Any]: + specs_path = os.path.join(os.path.dirname(__file__), "specs", "business_rules.yaml") + with open(specs_path, "r", encoding="utf-8") as handle: + return yaml.safe_load(handle) or {} + + +def _slugify(text: str) -> str: + slug = re.sub(r"[^a-zA-Z0-9]+", "_", text).strip("_") + return slug.lower() or "rule" + + +def _expected_statuses(valid_case: bool) -> Tuple[int, ...]: + return (200, 201) if valid_case else (400, 401, 403, 404, 302) + + +def _build_test(rule: Dict[str, Any], case: Dict[str, Any], valid_case: bool, index: int): + def _test(self: HR2BRTestBase): + suffix = "V" if valid_case else "I" + self._test_id = f"{rule.get('id')}-{suffix}-{index:02d}" + self._br_id = rule.get("id") + self._test_category = "Valid" if valid_case else "Invalid" + self._input_action = case.get("input_action", "") + self._expected_result = case.get("expected_result", "") + + self._login_for_action(self._input_action) + method = self._method_for_action(self._input_action) + path = self._endpoint_for_action(self._input_action) + path = self._resolve_path(path) + payload = self._payload_for(path, self._input_action, valid_case) + response = self._dispatch(method, path, payload) + expected = _expected_statuses(valid_case) + + if response.status_code in expected: + self._record_result("Expected response", "Pass", str(getattr(response, "data", ""))) + else: + self._record_result( + f"Unexpected status {response.status_code}", + "Fail", + str(getattr(response, "data", "")), + ) + self.fail(f"Expected status in {expected}, got {response.status_code}") + + return _test + + +def _generate_br_tests(): + specs = _load_business_rules() + rules = specs.get("business_rules", []) + for rule in rules: + class_name = f"Test_{rule.get('id', 'BR')}_{_slugify(rule.get('title', 'rule'))}" + attrs: Dict[str, Any] = {"__doc__": f"{rule.get('id')}: {rule.get('title')}"} + + for valid_case, key in ((True, "valid_tests"), (False, "invalid_tests")): + cases = rule.get(key, []) or [] + for index, case in enumerate(cases, start=1): + prefix = "valid" if valid_case else "invalid" + name = f"test_{prefix}_{index:02d}_{_slugify(case.get('input_action', 'case'))}" + attrs[name] = _build_test(rule, case, valid_case, index) + + globals()[class_name] = type(class_name, (HR2BRTestBase,), attrs) + + +_generate_br_tests() diff --git a/FusionIIIT/applications/hr2/tests/test_use_cases.py b/FusionIIIT/applications/hr2/tests/test_use_cases.py new file mode 100644 index 000000000..ba8d6e2dc --- /dev/null +++ b/FusionIIIT/applications/hr2/tests/test_use_cases.py @@ -0,0 +1,563 @@ +import os +import re +from typing import Any, Dict, Optional, Tuple + +import yaml + +from .conftest import UCTestBase + + +class HR2UCTestBase(UCTestBase): + """Dynamic UC tests generated from specs/use_cases.yaml.""" + + _created_ids: Dict[str, int] = {} + + def _login_for_context(self, text: str) -> None: + normalized = text.lower() + if "not authenticated" in normalized or "unauthorized" in normalized: + self.logout() + return + + if "director" in normalized: + self.login_as_director() + elif "registrar" in normalized: + self.login_as_registrar() + elif "hod" in normalized: + self.login_as_hod() + elif "accountant" in normalized: + self.login_as_accountant() + elif "nominee" in normalized: + self.login_as_nominee() + elif "hr" in normalized or "staff" in normalized: + self.login_as_staff() + else: + self.login_as_employee() + + def _parse_action(self, input_action: str) -> Tuple[str, str]: + match = re.search(r"\b(GET|POST|PUT|DELETE)\b\s+([^\s]+)", input_action) + if not match: + return "GET", "/" + method = match.group(1).upper() + path = match.group(2) + if not path.startswith("/"): + path = f"/{path}" + return method, path + + def _extract_id(self, data: Any) -> Optional[int]: + if isinstance(data, dict): + for key in ("id", "pk", "leave_id", "ltc_id", "cpda_id", "appraisal_id"): + value = data.get(key) + if isinstance(value, int): + return value + return None + + def _create_resource(self, endpoint: str, payload: Dict[str, Any]) -> int: + response = self.api_post(endpoint, payload, expected_status=None) + if response.status_code in {200, 201}: + extracted = self._extract_id(getattr(response, "data", {})) + if extracted is not None: + return extracted + return 1 + + def _ensure_leave_id(self) -> int: + if "leave" not in self._created_ids: + self.login_as_employee() + payload = { + "leave_type": "Casual", + "start_date": self.future_date(3), + "end_date": self.future_date(4), + "total_days": 2, + "reason": "Personal work", + "contact_during_leave": "9876543210", + "address_during_leave": "Jabalpur, MP", + } + self._created_ids["leave"] = self._create_resource( + "/hr2/api/leave-applications/", payload + ) + return self._created_ids["leave"] + + def _ensure_approved_leave_id(self) -> int: + leave_id = self._ensure_leave_id() + self.login_as_director() + self.api_post( + f"/hr2/api/leave-applications/{leave_id}/approve/", + {"remarks": "Approved"}, + expected_status=None, + ) + return leave_id + + def _ensure_leave_with_nominee_id(self) -> int: + if "leave_nominee" not in self._created_ids: + self.login_as_employee() + payload = { + "leave_type": "Casual", + "start_date": self.future_date(3), + "end_date": self.future_date(4), + "total_days": 2, + "reason": "Personal work", + "contact_during_leave": "9876543210", + "address_during_leave": "Jabalpur, MP", + "nominee_employee_id": self.nominee_extra.id, + } + self._created_ids["leave_nominee"] = self._create_resource( + "/hr2/api/leave-applications/", payload + ) + return self._created_ids["leave_nominee"] + + def _ensure_ltc_id(self) -> int: + if "ltc" not in self._created_ids: + self.login_as_employee() + payload = { + "employee_name": "Rahul Sharma", + "department": "Computer Science and Engineering", + "designation": "Assistant Professor", + "ltc_block_year": 2025, + "travel_start_date": self.future_date(10), + "travel_end_date": self.future_date(15), + "destination": "Delhi", + "purpose_of_travel": "Family travel", + "travel_mode": "Train", + "total_amount_claimed": 22000, + } + self._created_ids["ltc"] = self._create_resource("/hr2/api/ltc/", payload) + return self._created_ids["ltc"] + + def _ensure_cpda_advance_id(self) -> int: + if "cpda_advance" not in self._created_ids: + self.login_as_employee() + payload = { + "employee_name": "Rahul Sharma", + "department": "Computer Science and Engineering", + "designation": "Assistant Professor", + "event_name": "National Conference on AI", + "event_type": "Conference", + "start_date": self.future_date(30), + "end_date": self.future_date(32), + "total_amount": 20000, + "purpose_of_attending": "Present paper", + "benefits_to_institution": "Research exposure", + } + self._created_ids["cpda_advance"] = self._create_resource( + "/hr2/api/cpda-advances/", payload + ) + return self._created_ids["cpda_advance"] + + def _ensure_cpda_reimbursement_id(self) -> int: + if "cpda_reimbursement" not in self._created_ids: + self.login_as_employee() + payload = { + "employee_name": "Rahul Sharma", + "department": "Computer Science and Engineering", + "designation": "Assistant Professor", + "event_name": "National Conference on AI", + "event_type": "Conference", + "start_date": self.future_date(30), + "end_date": self.future_date(32), + "total_amount": 20000, + "purpose_of_attending": "Present paper", + "benefits_to_institution": "Research exposure", + } + self._created_ids["cpda_reimbursement"] = self._create_resource( + "/hr2/api/cpda-reimbursements/", payload + ) + return self._created_ids["cpda_reimbursement"] + + def _ensure_appraisal_form_id(self) -> int: + if "appraisal_form" not in self._created_ids: + self.login_as_employee() + payload = { + "employee_name": "Rahul Sharma", + "department": "Computer Science and Engineering", + "designation": "Assistant Professor", + "appraisal_year": "2025-2026", + "self_summary": "Completed teaching responsibilities", + "key_responsibilities": "Teaching and research", + "achievements": "Published 1 paper", + "goals_achieved": "Completed syllabus", + "future_goals": "Publish more papers", + } + self._created_ids["appraisal_form"] = self._create_resource( + "/hr2/api/appraisal-forms/", payload + ) + return self._created_ids["appraisal_form"] + + def _resolve_path(self, path: str) -> str: + if "/leave-applications/" in path: + leave_id = self._ensure_leave_id() + return re.sub(r"/leave-applications/\d+", f"/leave-applications/{leave_id}", path) + if "/leave-balance/" in path: + return re.sub( + r"/leave-balance/\d+", + f"/leave-balance/{self.employee_extra.id}", + path, + ) + if "/employees/" in path: + return re.sub(r"/employees/\d+", f"/employees/{self.employee_extra.id}", path) + if "/ltc/" in path: + ltc_id = self._ensure_ltc_id() + return re.sub(r"/ltc/\d+", f"/ltc/{ltc_id}", path) + if "/cpda-advances/" in path: + cpda_id = self._ensure_cpda_advance_id() + return re.sub(r"/cpda-advances/\d+", f"/cpda-advances/{cpda_id}", path) + if "/cpda-reimbursements/" in path: + cpda_id = self._ensure_cpda_reimbursement_id() + return re.sub(r"/cpda-reimbursements/\d+", f"/cpda-reimbursements/{cpda_id}", path) + if "/appraisal-forms/" in path: + appraisal_id = self._ensure_appraisal_form_id() + return re.sub(r"/appraisal-forms/\d+", f"/appraisal-forms/{appraisal_id}", path) + return path + + def _payload_for(self, path: str, scenario: str) -> Dict[str, Any]: + payload: Dict[str, Any] = {} + scenario_lower = scenario.lower() + + if "/leave-applications/" in path and path.endswith("/leave-applications/"): + leave_type = "Casual" + if "vacation" in scenario_lower: + leave_type = "Vacation" + payload = { + "leave_type": leave_type, + "start_date": self.future_date(2), + "end_date": self.future_date(4), + "total_days": 3, + "reason": "Personal work", + "contact_during_leave": "9876543210", + "address_during_leave": "Jabalpur, MP", + } + if "nominee" in scenario_lower: + payload["nominee_employee_id"] = self.nominee_extra.id + elif "/leave-applications/" in path and path.endswith("/cancel-request/"): + payload = {"reason": "Change of plan"} + elif "/leave-applications/" in path and path.endswith("/extension-request/"): + payload = {"new_end_date": self.future_date(6), "reason": "Medical"} + elif "/leave-applications/" in path and path.endswith("/request-document/"): + payload = {"message": "Submit proof"} + elif "/leave-applications/" in path and path.endswith("/submit-document/"): + payload = {"submission": "doc-ref"} + elif "/leave-applications/" in path and path.endswith("/cancel-decision/approve/"): + payload = {"remarks": "Approved"} + elif "/leave-applications/" in path and path.endswith("/extension-decision/approve/"): + payload = {"remarks": "Approved"} + elif "/leave-nominee/" in path: + if "decline" in scenario_lower: + payload = {"action": "decline"} + else: + payload = {"action": "accept"} + elif "/attendance/" in path and path.endswith("/attendance/"): + status = "PRESENT" if "half" not in scenario_lower else "HALF_DAY" + payload = {"date": self.today(), "status": status} + elif "/ltc/" in path and path.endswith("/ltc/"): + payload = { + "employee_name": "Rahul Sharma", + "department": "Computer Science and Engineering", + "designation": "Assistant Professor", + "ltc_block_year": 2025, + "travel_start_date": self.future_date(10), + "travel_end_date": self.future_date(15), + "destination": "Delhi", + "purpose_of_travel": "Family travel", + "travel_mode": "Train", + "total_amount_claimed": 22000, + } + elif "/cpda-advances/" in path and path.endswith("/cpda-advances/"): + payload = { + "employee_name": "Rahul Sharma", + "department": "Computer Science and Engineering", + "designation": "Assistant Professor", + "event_name": "National Conference on AI", + "event_type": "Conference", + "start_date": self.future_date(30), + "end_date": self.future_date(32), + "total_amount": 20000, + "purpose_of_attending": "Present paper", + "benefits_to_institution": "Research exposure", + } + elif "/cpda-reimbursements/" in path and path.endswith("/cpda-reimbursements/"): + payload = { + "employee_name": "Rahul Sharma", + "department": "Computer Science and Engineering", + "designation": "Assistant Professor", + "event_name": "National Conference on AI", + "event_type": "Conference", + "start_date": self.future_date(30), + "end_date": self.future_date(32), + "total_amount": 20000, + "purpose_of_attending": "Present paper", + "benefits_to_institution": "Research exposure", + } + elif "/appraisal-forms/" in path and path.endswith("/appraisal-forms/"): + payload = { + "employee_name": "Rahul Sharma", + "department": "Computer Science and Engineering", + "designation": "Assistant Professor", + "appraisal_year": "2025-2026", + "self_summary": "Completed teaching responsibilities", + "key_responsibilities": "Teaching and research", + "achievements": "Published 1 paper", + "goals_achieved": "Completed syllabus", + "future_goals": "Publish more papers", + } + elif "/appraisals/" in path and path.endswith("/appraisals/"): + payload = { + "period": self.appraisal_period.id, + "teaching_score": 4, + "research_score": 4, + "admin_score": 3, + } + elif "/training-nominations/" in path: + payload = {"program": self.training_program.id} + elif "/promotions/" in path: + payload = { + "current_designation": self.promotion_current_designation.id, + "applied_designation": self.promotion_applied_designation.id, + "application_date": self.today(), + "eligibility_date": self.today(), + "api_score": 8, + } + elif "/employees/" in path: + payload = {"phone_number": "9876543210", "full_address": "Updated"} + + return payload + + def _dispatch(self, method: str, path: str, payload: Dict[str, Any]): + if method == "POST": + return self.api_post(path, payload, expected_status=None) + if method == "PUT": + return self.api_put(path, payload, expected_status=None) + if method == "DELETE": + return self.api_delete(path, expected_status=None) + return self.api_get(path, expected_status=None) + + def _prepare_state_for(self, path: str, method: str) -> str: + if "/leave-applications/" in path and path.endswith("/cancel-request/"): + leave_id = self._ensure_approved_leave_id() + self.login_as_employee() + return f"/hr2/api/leave-applications/{leave_id}/cancel-request/" + + if "/leave-applications/" in path and path.endswith("/extension-request/"): + leave_id = self._ensure_approved_leave_id() + self.login_as_employee() + return f"/hr2/api/leave-applications/{leave_id}/extension-request/" + + if "/leave-applications/" in path and "/cancel-decision/" in path: + leave_id = self._ensure_approved_leave_id() + self.login_as_employee() + self.api_post( + f"/hr2/api/leave-applications/{leave_id}/cancel-request/", + {"reason": "Change of plan"}, + expected_status=None, + ) + self.login_as_director() + return re.sub( + r"/leave-applications/\d+/cancel-decision/", + f"/leave-applications/{leave_id}/cancel-decision/", + path, + ) + + if "/leave-applications/" in path and "/extension-decision/" in path: + leave_id = self._ensure_approved_leave_id() + self.login_as_employee() + self.api_post( + f"/hr2/api/leave-applications/{leave_id}/extension-request/", + {"new_end_date": self.future_date(6)}, + expected_status=None, + ) + self.login_as_director() + return re.sub( + r"/leave-applications/\d+/extension-decision/", + f"/leave-applications/{leave_id}/extension-decision/", + path, + ) + + if "/leave-applications/" in path and "/request-document/" in path: + leave_id = self._ensure_leave_id() + self.login_as_hod() + return f"/hr2/api/leave-applications/{leave_id}/request-document/" + + if "/leave-applications/" in path and "/submit-document/" in path: + leave_id = self._ensure_leave_id() + self.login_as_hod() + self.api_post( + f"/hr2/api/leave-applications/{leave_id}/request-document/", + {"message": "Submit proof"}, + expected_status=None, + ) + self.login_as_employee() + return f"/hr2/api/leave-applications/{leave_id}/submit-document/" + + if "/leave-applications/" in path and "/approve/" in path: + leave_id = self._ensure_leave_id() + self.login_as_director() + return f"/hr2/api/leave-applications/{leave_id}/approve/" + + if "/leave-applications/" in path and "/forward/" in path: + leave_id = self._ensure_leave_id() + self.login_as_hod() + return f"/hr2/api/leave-applications/{leave_id}/forward/" + + if "/leave-applications/" in path and "/reject/" in path: + leave_id = self._ensure_leave_id() + self.login_as_director() + return f"/hr2/api/leave-applications/{leave_id}/reject/" + + if "/leave-applications/" in path and "/withdraw/" in path: + leave_id = self._ensure_leave_id() + self.login_as_employee() + return f"/hr2/api/leave-applications/{leave_id}/withdraw/" + + if "/leave-nominee/" in path and "/leave-nominee/" in path: + leave_id = self._ensure_leave_with_nominee_id() + self.login_as_nominee() + return f"/hr2/api/leave-nominee/{leave_id}/" + + if "/ltc/" in path and "/download/" in path: + ltc_id = self._ensure_ltc_id() + return f"/hr2/api/ltc/{ltc_id}/download/" + + if "/ltc/" in path and "/withdraw/" in path: + ltc_id = self._ensure_ltc_id() + self.login_as_employee() + return f"/hr2/api/ltc/{ltc_id}/withdraw/" + + if "/ltc/" in path and "/forward/" in path: + ltc_id = self._ensure_ltc_id() + self.login_as_staff() + return f"/hr2/api/ltc/{ltc_id}/forward/" + + if "/ltc/" in path and "/approve/" in path: + ltc_id = self._ensure_ltc_id() + self.login_as_accountant() + return f"/hr2/api/ltc/{ltc_id}/approve/" + + if "/ltc/" in path and "/reject/" in path: + ltc_id = self._ensure_ltc_id() + self.login_as_accountant() + return f"/hr2/api/ltc/{ltc_id}/reject/" + + if "/cpda-advances/" in path and "/download/" in path: + cpda_id = self._ensure_cpda_advance_id() + return f"/hr2/api/cpda-advances/{cpda_id}/download/" + + if "/cpda-advances/" in path and "/withdraw/" in path: + cpda_id = self._ensure_cpda_advance_id() + self.login_as_employee() + return f"/hr2/api/cpda-advances/{cpda_id}/withdraw/" + + if "/cpda-advances/" in path and "/forward-accountant/" in path: + cpda_id = self._ensure_cpda_advance_id() + self.login_as_staff() + return f"/hr2/api/cpda-advances/{cpda_id}/forward-accountant/" + + if "/cpda-advances/" in path and "/forward-director/" in path: + cpda_id = self._ensure_cpda_advance_id() + self.login_as_staff() + return f"/hr2/api/cpda-advances/{cpda_id}/forward-director/" + + if "/cpda-advances/" in path and "/approve/" in path: + cpda_id = self._ensure_cpda_advance_id() + self.login_as_accountant() + return f"/hr2/api/cpda-advances/{cpda_id}/approve/" + + if "/cpda-advances/" in path and "/reject/" in path: + cpda_id = self._ensure_cpda_advance_id() + self.login_as_accountant() + return f"/hr2/api/cpda-advances/{cpda_id}/reject/" + + if "/cpda-reimbursements/" in path and "/approve/" in path: + cpda_id = self._ensure_cpda_reimbursement_id() + self.login_as_accountant() + return f"/hr2/api/cpda-reimbursements/{cpda_id}/approve/" + + if "/cpda-reimbursements/" in path and "/reject/" in path: + cpda_id = self._ensure_cpda_reimbursement_id() + self.login_as_accountant() + return f"/hr2/api/cpda-reimbursements/{cpda_id}/reject/" + + if "/appraisal-forms/" in path and "/download/" in path: + appraisal_id = self._ensure_appraisal_form_id() + return f"/hr2/api/appraisal-forms/{appraisal_id}/download/" + + if "/appraisal-forms/" in path and "/review/" in path: + appraisal_id = self._ensure_appraisal_form_id() + self.login_as_hod() + return f"/hr2/api/appraisal-forms/{appraisal_id}/review/" + + return path + + +def _load_use_cases() -> Dict[str, Any]: + specs_path = os.path.join(os.path.dirname(__file__), "specs", "use_cases.yaml") + with open(specs_path, "r", encoding="utf-8") as handle: + return yaml.safe_load(handle) or {} + + +def _slugify(text: str) -> str: + slug = re.sub(r"[^a-zA-Z0-9]+", "_", text).strip("_") + return slug.lower() or "scenario" + + +def _expected_statuses(category: str, method: str) -> Tuple[int, ...]: + if method == "DELETE": + return (204,) + if category == "Exception": + return (400, 401, 403, 404, 302) + return (200, 201) + + +def _build_test( + uc: Dict[str, Any], category: str, scenario: Dict[str, Any], index: int +): + def _test(self: HR2UCTestBase): + self._test_id = f"{uc.get('id')}-{category[:2].upper()}-{index:02d}" + self._uc_id = uc.get("id") + self._test_category = category + self._scenario = scenario.get("scenario") + self._preconditions = scenario.get("preconditions", uc.get("preconditions", "")) + self._input_action = scenario.get("input_action", "") + self._expected_result = scenario.get("expected_result", "") + + login_text = f"{uc.get('actors', '')} {self._preconditions}" + self._login_for_context(login_text) + + method, path = self._parse_action(self._input_action) + path = self._resolve_path(path) + path = self._prepare_state_for(path, method) + payload = self._payload_for(path, self._scenario or "") if method in {"POST", "PUT"} else {} + + response = self._dispatch(method, path, payload) + expected_statuses = _expected_statuses(category, method) + + if response.status_code in expected_statuses: + self._record_result("Expected response", "Pass", str(getattr(response, "data", ""))) + else: + self._record_result( + f"Unexpected status {response.status_code}", + "Fail", + str(getattr(response, "data", "")), + ) + self.fail(f"Expected status in {expected_statuses}, got {response.status_code}") + + return _test + + +def _generate_uc_tests(): + specs = _load_use_cases() + use_cases = specs.get("use_cases", []) + for uc in use_cases: + class_name = f"Test_{uc.get('id', 'UC')}_{_slugify(uc.get('title', 'uc'))}" + attrs: Dict[str, Any] = {"__doc__": f"{uc.get('id')}: {uc.get('title')}"} + + for category, key in ( + ("Happy Path", "happy_paths"), + ("Alternate Path", "alternate_paths"), + ("Exception", "exception_paths"), + ): + scenarios = uc.get(key, []) or [] + for index, scenario in enumerate(scenarios, start=1): + test_name = f"test_{category.split()[0].lower()}_{index:02d}_{_slugify(scenario.get('scenario', 'case'))}" + attrs[test_name] = _build_test(uc, category, scenario, index) + + globals()[class_name] = type(class_name, (HR2UCTestBase,), attrs) + + +_generate_uc_tests() diff --git a/FusionIIIT/applications/hr2/tests/test_workflows.py b/FusionIIIT/applications/hr2/tests/test_workflows.py new file mode 100644 index 000000000..784d30a60 --- /dev/null +++ b/FusionIIIT/applications/hr2/tests/test_workflows.py @@ -0,0 +1,593 @@ +import os +import re +from typing import Any, Dict, Optional + +import yaml + +from .conftest import WFTestBase + + +class HR2WFTestBase(WFTestBase): + """Dynamic WF tests generated from specs/workflows.yaml.""" + + _created_ids: Dict[str, int] = {} + + def _extract_id(self, data: Any) -> Optional[int]: + if isinstance(data, dict): + for key in ("id", "pk", "leave_id", "ltc_id", "cpda_id", "appraisal_id"): + value = data.get(key) + if isinstance(value, int): + return value + return None + + def _create_resource(self, endpoint: str, payload: Dict[str, Any]) -> int: + response = self.api_post(endpoint, payload, expected_status=None) + if response.status_code in {200, 201}: + extracted = self._extract_id(getattr(response, "data", {})) + if extracted is not None: + return extracted + return 1 + + def _ensure_leave_id(self) -> int: + if "leave" not in self._created_ids: + self.login_as_employee() + payload = { + "leave_type": "Casual", + "start_date": self.future_date(3), + "end_date": self.future_date(4), + "total_days": 2, + "reason": "Personal work", + "contact_during_leave": "9876543210", + "address_during_leave": "Jabalpur, MP", + } + self._created_ids["leave"] = self._create_resource( + "/hr2/api/leave-applications/", payload + ) + return self._created_ids["leave"] + + def _ensure_approved_leave_id(self) -> int: + leave_id = self._ensure_leave_id() + self.login_as_director() + self.api_post( + f"/hr2/api/leave-applications/{leave_id}/approve/", + {"remarks": "Approved"}, + expected_status=None, + ) + return leave_id + + def _ensure_leave_with_nominee_id(self) -> int: + if "leave_nominee" not in self._created_ids: + self.login_as_employee() + payload = { + "leave_type": "Casual", + "start_date": self.future_date(3), + "end_date": self.future_date(4), + "total_days": 2, + "reason": "Personal work", + "contact_during_leave": "9876543210", + "address_during_leave": "Jabalpur, MP", + "nominee_employee_id": self.nominee_extra.id, + } + self._created_ids["leave_nominee"] = self._create_resource( + "/hr2/api/leave-applications/", payload + ) + return self._created_ids["leave_nominee"] + + def _ensure_ltc_id(self) -> int: + if "ltc" not in self._created_ids: + self.login_as_employee() + payload = { + "employee_name": "Rahul Sharma", + "department": "Computer Science and Engineering", + "designation": "Assistant Professor", + "ltc_block_year": 2025, + "travel_start_date": self.future_date(10), + "travel_end_date": self.future_date(15), + "destination": "Delhi", + "purpose_of_travel": "Family travel", + "travel_mode": "Train", + "total_amount_claimed": 22000, + } + self._created_ids["ltc"] = self._create_resource("/hr2/api/ltc/", payload) + return self._created_ids["ltc"] + + def _ensure_cpda_advance_id(self) -> int: + if "cpda_advance" not in self._created_ids: + self.login_as_employee() + payload = { + "employee_name": "Rahul Sharma", + "department": "Computer Science and Engineering", + "designation": "Assistant Professor", + "event_name": "National Conference on AI", + "event_type": "Conference", + "start_date": self.future_date(30), + "end_date": self.future_date(32), + "total_amount": 20000, + "purpose_of_attending": "Present paper", + "benefits_to_institution": "Research exposure", + } + self._created_ids["cpda_advance"] = self._create_resource( + "/hr2/api/cpda-advances/", payload + ) + return self._created_ids["cpda_advance"] + + def _ensure_cpda_reimbursement_id(self) -> int: + if "cpda_reimbursement" not in self._created_ids: + self.login_as_employee() + payload = { + "employee_name": "Rahul Sharma", + "department": "Computer Science and Engineering", + "designation": "Assistant Professor", + "event_name": "National Conference on AI", + "event_type": "Conference", + "start_date": self.future_date(30), + "end_date": self.future_date(32), + "total_amount": 20000, + "purpose_of_attending": "Present paper", + "benefits_to_institution": "Research exposure", + } + self._created_ids["cpda_reimbursement"] = self._create_resource( + "/hr2/api/cpda-reimbursements/", payload + ) + return self._created_ids["cpda_reimbursement"] + + def _ensure_appraisal_form_id(self) -> int: + if "appraisal_form" not in self._created_ids: + self.login_as_employee() + payload = { + "employee_name": "Rahul Sharma", + "department": "Computer Science and Engineering", + "designation": "Assistant Professor", + "appraisal_year": "2025-2026", + "self_summary": "Completed teaching responsibilities", + "key_responsibilities": "Teaching and research", + "achievements": "Published 1 paper", + "goals_achieved": "Completed syllabus", + "future_goals": "Publish more papers", + } + self._created_ids["appraisal_form"] = self._create_resource( + "/hr2/api/appraisal-forms/", payload + ) + return self._created_ids["appraisal_form"] + + def _resolve_path(self, path: str) -> str: + if "/leave-applications/" in path: + leave_id = self._ensure_leave_id() + return re.sub(r"/leave-applications/\d+", f"/leave-applications/{leave_id}", path) + if "/ltc/" in path: + ltc_id = self._ensure_ltc_id() + return re.sub(r"/ltc/\d+", f"/ltc/{ltc_id}", path) + if "/cpda-advances/" in path: + cpda_id = self._ensure_cpda_advance_id() + return re.sub(r"/cpda-advances/\d+", f"/cpda-advances/{cpda_id}", path) + if "/cpda-reimbursements/" in path: + cpda_id = self._ensure_cpda_reimbursement_id() + return re.sub(r"/cpda-reimbursements/\d+", f"/cpda-reimbursements/{cpda_id}", path) + if "/appraisal-forms/" in path: + appraisal_id = self._ensure_appraisal_form_id() + return re.sub(r"/appraisal-forms/\d+", f"/appraisal-forms/{appraisal_id}", path) + return path + + def _payload_for(self, path: str, scenario: str) -> Dict[str, Any]: + scenario_lower = scenario.lower() + payload: Dict[str, Any] = {} + + if "/leave-applications/" in path and path.endswith("/leave-applications/"): + payload = { + "leave_type": "Casual", + "start_date": self.future_date(3), + "end_date": self.future_date(4), + "total_days": 2, + "reason": "Personal work", + "contact_during_leave": "9876543210", + "address_during_leave": "Jabalpur, MP", + } + elif path.endswith("/withdraw/"): + payload = {} + elif path.endswith("/cancel-request/"): + payload = {"reason": "Change of plan"} + elif path.endswith("/extension-request/"): + payload = {"new_end_date": self.future_date(6), "reason": "Medical"} + elif path.endswith("/request-document/"): + payload = {"message": "Submit proof"} + elif path.endswith("/submit-document/"): + payload = {"submission": "doc-ref"} + elif "/leave-nominee/" in path: + payload = {"action": "accept" if "accept" in scenario_lower else "decline"} + elif path.endswith("/ltc/"): + payload = { + "employee_name": "Rahul Sharma", + "department": "Computer Science and Engineering", + "designation": "Assistant Professor", + "ltc_block_year": 2025, + "travel_start_date": self.future_date(10), + "travel_end_date": self.future_date(15), + "destination": "Delhi", + "purpose_of_travel": "Family travel", + "travel_mode": "Train", + "total_amount_claimed": 22000, + } + elif path.endswith("/cpda-advances/"): + payload = { + "employee_name": "Rahul Sharma", + "department": "Computer Science and Engineering", + "designation": "Assistant Professor", + "event_name": "National Conference on AI", + "event_type": "Conference", + "start_date": self.future_date(30), + "end_date": self.future_date(32), + "total_amount": 20000, + "purpose_of_attending": "Present paper", + "benefits_to_institution": "Research exposure", + } + elif path.endswith("/cpda-reimbursements/"): + payload = { + "employee_name": "Rahul Sharma", + "department": "Computer Science and Engineering", + "designation": "Assistant Professor", + "event_name": "National Conference on AI", + "event_type": "Conference", + "start_date": self.future_date(30), + "end_date": self.future_date(32), + "total_amount": 20000, + "purpose_of_attending": "Present paper", + "benefits_to_institution": "Research exposure", + } + elif path.endswith("/review/"): + payload = {"action": "forward" if "forward" in scenario_lower else "approve"} + + return payload + + def _dispatch(self, method: str, path: str, payload: Dict[str, Any]): + if method == "POST": + return self.api_post(path, payload, expected_status=None) + if method == "PUT": + return self.api_put(path, payload, expected_status=None) + if method == "DELETE": + return self.api_delete(path, expected_status=None) + return self.api_get(path, expected_status=None) + + +def _load_workflows() -> Dict[str, Any]: + specs_path = os.path.join(os.path.dirname(__file__), "specs", "workflows.yaml") + with open(specs_path, "r", encoding="utf-8") as handle: + return yaml.safe_load(handle) or {} + + +def _slugify(text: str) -> str: + slug = re.sub(r"[^a-zA-Z0-9]+", "_", text).strip("_") + return slug.lower() or "workflow" + + +def _build_test(workflow: Dict[str, Any], scenario: Dict[str, Any], category: str, index: int): + def _test(self: HR2WFTestBase): + suffix = "E2E" if category == "End-to-End" else "NEG" + self._test_id = f"{workflow.get('id')}-{suffix}-{index:02d}" + self._wf_id = workflow.get("id") + self._test_category = category + self._scenario = scenario.get("scenario") + self._expected_final_state = scenario.get("expected_final_state", "") + + workflow_id = workflow.get("id") or "" + scenario_lower = (self._scenario or "").lower() + + if workflow_id == "WF-HR2-001": + self.login_as_employee() + leave_id = self._ensure_leave_id() + self._add_step(1, "Employee applies", "Leave created", str(leave_id), True) + + if "rejected" in scenario_lower: + self.login_as_director() + resp = self.api_post( + f"/hr2/api/leave-applications/{leave_id}/reject/", + {"remarks": "Rejected"}, + expected_status=None, + ) + step_ok = resp.status_code in {200, 201} + self._add_step(2, "Director rejects", "Status rejected", str(resp.data), step_ok) + elif "forwarded" in scenario_lower: + self.login_as_hod() + resp = self.api_post( + f"/hr2/api/leave-applications/{leave_id}/forward/", + {"remarks": "Forward"}, + expected_status=None, + ) + step_ok = resp.status_code in {200, 201} + self._add_step(2, "HOD forwards", "Status forwarded", str(resp.data), step_ok) + self.login_as_director() + resp2 = self.api_post( + f"/hr2/api/leave-applications/{leave_id}/approve/", + {"remarks": "Approved"}, + expected_status=None, + ) + step2_ok = resp2.status_code in {200, 201} + self._add_step(3, "Director approves", "Status approved", str(resp2.data), step2_ok) + else: + self.login_as_director() + resp = self.api_post( + f"/hr2/api/leave-applications/{leave_id}/approve/", + {"remarks": "Approved"}, + expected_status=None, + ) + step_ok = resp.status_code in {200, 201} + self._add_step(2, "Director approves", "Status approved", str(resp.data), step_ok) + + elif workflow_id == "WF-HR2-002": + leave_id = self._ensure_leave_id() + if "approved" in scenario_lower: + self.login_as_director() + self.api_post( + f"/hr2/api/leave-applications/{leave_id}/approve/", + {"remarks": "Approved"}, + expected_status=None, + ) + self.login_as_employee() + resp = self.api_post( + f"/hr2/api/leave-applications/{leave_id}/withdraw/", + {}, + expected_status=None, + ) + step_ok = resp.status_code in {400, 403} + self._add_step(1, "Withdraw approved leave", "Rejected", str(resp.data), step_ok) + else: + self.login_as_employee() + resp = self.api_post( + f"/hr2/api/leave-applications/{leave_id}/withdraw/", + {}, + expected_status=None, + ) + step_ok = resp.status_code in {200, 201} + self._add_step(1, "Withdraw pending leave", "Withdrawn", str(resp.data), step_ok) + + elif workflow_id == "WF-HR2-003": + leave_id = self._ensure_approved_leave_id() + if "after start date" in scenario_lower: + self.login_as_employee() + resp = self.api_post( + f"/hr2/api/leave-applications/{leave_id}/cancel-request/", + {"reason": "Late"}, + expected_status=None, + ) + step_ok = resp.status_code in {400, 403} + self._add_step(1, "Cancel request late", "Rejected", str(resp.data), step_ok) + else: + self.login_as_employee() + resp = self.api_post( + f"/hr2/api/leave-applications/{leave_id}/cancel-request/", + {"reason": "Change of plan"}, + expected_status=None, + ) + step1_ok = resp.status_code in {200, 201} + self._add_step(1, "Request cancellation", "Requested", str(resp.data), step1_ok) + self.login_as_director() + resp2 = self.api_post( + f"/hr2/api/leave-applications/{leave_id}/cancel-decision/approve/", + {"remarks": "Approved"}, + expected_status=None, + ) + step2_ok = resp2.status_code in {200, 201} + self._add_step(2, "Approve cancellation", "Cancelled", str(resp2.data), step2_ok) + + elif workflow_id == "WF-HR2-004": + leave_id = self._ensure_approved_leave_id() + self.login_as_employee() + new_end = self.future_date(6) + if "insufficient" in scenario_lower: + new_end = self.future_date(30) + resp = self.api_post( + f"/hr2/api/leave-applications/{leave_id}/extension-request/", + {"new_end_date": new_end}, + expected_status=None, + ) + step1_ok = resp.status_code in {200, 201} + self._add_step(1, "Request extension", "Requested", str(resp.data), step1_ok) + self.login_as_director() + resp2 = self.api_post( + f"/hr2/api/leave-applications/{leave_id}/extension-decision/approve/", + {"remarks": "Approved"}, + expected_status=None, + ) + step2_ok = resp2.status_code in ({400} if "insufficient" in scenario_lower else {200, 201}) + self._add_step(2, "Approve extension", "Approved or rejected", str(resp2.data), step2_ok) + + elif workflow_id == "WF-HR2-005": + leave_id = self._ensure_leave_with_nominee_id() + if "non-nominee" in scenario_lower: + self.login_as_employee() + resp = self.api_post( + f"/hr2/api/leave-nominee/{leave_id}/", + {"action": "accept"}, + expected_status=None, + ) + step_ok = resp.status_code in {403} + self._add_step(1, "Non-nominee responds", "Forbidden", str(resp.data), step_ok) + else: + self.login_as_nominee() + resp = self.api_post( + f"/hr2/api/leave-nominee/{leave_id}/", + {"action": "accept"}, + expected_status=None, + ) + step_ok = resp.status_code in {200, 201} + self._add_step(1, "Nominee accepts", "Accepted", str(resp.data), step_ok) + + elif workflow_id == "WF-HR2-006": + leave_id = self._ensure_leave_id() + if "without request" in scenario_lower: + self.login_as_employee() + resp = self.api_post( + f"/hr2/api/leave-applications/{leave_id}/submit-document/", + {"submission": "doc-ref"}, + expected_status=None, + ) + step_ok = resp.status_code in {400, 403} + self._add_step(1, "Submit without request", "Rejected", str(resp.data), step_ok) + else: + self.login_as_hod() + resp = self.api_post( + f"/hr2/api/leave-applications/{leave_id}/request-document/", + {"message": "Submit proof"}, + expected_status=None, + ) + step1_ok = resp.status_code in {200, 201} + self._add_step(1, "HOD requests document", "Requested", str(resp.data), step1_ok) + self.login_as_employee() + resp2 = self.api_post( + f"/hr2/api/leave-applications/{leave_id}/submit-document/", + {"submission": "doc-ref"}, + expected_status=None, + ) + step2_ok = resp2.status_code in {200, 201} + self._add_step(2, "Employee submits", "Submitted", str(resp2.data), step2_ok) + + elif workflow_id == "WF-HR2-007": + ltc_id = self._ensure_ltc_id() + if "rejected" in scenario_lower: + self.login_as_accountant() + resp = self.api_post( + f"/hr2/api/ltc/{ltc_id}/reject/", + {"remarks": "Rejected"}, + expected_status=None, + ) + step_ok = resp.status_code in {200, 201} + self._add_step(1, "Reject LTC", "Rejected", str(resp.data), step_ok) + else: + self.login_as_staff() + resp = self.api_post( + f"/hr2/api/ltc/{ltc_id}/forward/", + {"remarks": "Forward"}, + expected_status=None, + ) + step1_ok = resp.status_code in {200, 201} + self._add_step(1, "Forward LTC", "Forwarded", str(resp.data), step1_ok) + self.login_as_accountant() + resp2 = self.api_post( + f"/hr2/api/ltc/{ltc_id}/approve/", + {"remarks": "Approved"}, + expected_status=None, + ) + step2_ok = resp2.status_code in {200, 201} + self._add_step(2, "Approve LTC", "Approved", str(resp2.data), step2_ok) + + elif workflow_id == "WF-HR2-008": + cpda_id = self._ensure_cpda_advance_id() + if "director" in scenario_lower: + self.login_as_staff() + resp = self.api_post( + f"/hr2/api/cpda-advances/{cpda_id}/forward-director/", + {"remarks": "Forward"}, + expected_status=None, + ) + step1_ok = resp.status_code in {200, 201} + self._add_step(1, "Forward to director", "Forwarded", str(resp.data), step1_ok) + self.login_as_director() + resp2 = self.api_post( + f"/hr2/api/cpda-advances/{cpda_id}/approve/", + {"remarks": "Approved"}, + expected_status=None, + ) + step2_ok = resp2.status_code in {200, 201} + self._add_step(2, "Director approves", "Approved", str(resp2.data), step2_ok) + elif "rejected" in scenario_lower: + self.login_as_accountant() + resp = self.api_post( + f"/hr2/api/cpda-advances/{cpda_id}/reject/", + {"remarks": "Rejected"}, + expected_status=None, + ) + step_ok = resp.status_code in {200, 201} + self._add_step(1, "Reject CPDA", "Rejected", str(resp.data), step_ok) + else: + self.login_as_staff() + resp = self.api_post( + f"/hr2/api/cpda-advances/{cpda_id}/forward-accountant/", + {"remarks": "Forward"}, + expected_status=None, + ) + step1_ok = resp.status_code in {200, 201} + self._add_step(1, "Forward to accountant", "Forwarded", str(resp.data), step1_ok) + self.login_as_accountant() + resp2 = self.api_post( + f"/hr2/api/cpda-advances/{cpda_id}/approve/", + {"remarks": "Approved"}, + expected_status=None, + ) + step2_ok = resp2.status_code in {200, 201} + self._add_step(2, "Accountant approves", "Approved", str(resp2.data), step2_ok) + + elif workflow_id == "WF-HR2-009": + cpda_id = self._ensure_cpda_reimbursement_id() + if "rejected" in scenario_lower: + self.login_as_accountant() + resp = self.api_post( + f"/hr2/api/cpda-reimbursements/{cpda_id}/reject/", + {"remarks": "Rejected"}, + expected_status=None, + ) + step_ok = resp.status_code in {200, 201} + self._add_step(1, "Reject reimbursement", "Rejected", str(resp.data), step_ok) + else: + self.login_as_accountant() + resp = self.api_post( + f"/hr2/api/cpda-reimbursements/{cpda_id}/approve/", + {"remarks": "Approved"}, + expected_status=None, + ) + step_ok = resp.status_code in {200, 201} + self._add_step(1, "Approve reimbursement", "Approved", str(resp.data), step_ok) + + elif workflow_id == "WF-HR2-010": + appraisal_id = self._ensure_appraisal_form_id() + if "director" in scenario_lower: + self.login_as_hod() + self.api_post( + f"/hr2/api/appraisal-forms/{appraisal_id}/review/", + {"action": "forward"}, + expected_status=None, + ) + self.login_as_director() + resp = self.api_post( + f"/hr2/api/appraisal-forms/{appraisal_id}/review/", + {"action": "approve"}, + expected_status=None, + ) + step_ok = resp.status_code in {200, 201} + self._add_step(1, "Director approves", "Approved", str(resp.data), step_ok) + else: + self.login_as_hod() + resp = self.api_post( + f"/hr2/api/appraisal-forms/{appraisal_id}/review/", + {"action": "forward"}, + expected_status=None, + ) + step_ok = resp.status_code in {200, 201} + self._add_step(1, "HOD forwards", "Reviewed", str(resp.data), step_ok) + + if self._all_steps_passed(): + self._record_result("Workflow completed", "Pass") + else: + self._record_result("Workflow incomplete", "Fail") + self.fail("Workflow did not complete successfully") + + return _test + + +def _generate_wf_tests(): + specs = _load_workflows() + workflows = specs.get("workflows", []) + for workflow in workflows: + class_name = f"Test_{workflow.get('id', 'WF')}_{_slugify(workflow.get('title', 'workflow'))}" + attrs: Dict[str, Any] = { + "__doc__": f"{workflow.get('id')}: {workflow.get('title')}" + } + + for category, key in (("End-to-End", "e2e_tests"), ("Negative", "negative_tests")): + scenarios = workflow.get(key, []) or [] + for index, scenario in enumerate(scenarios, start=1): + name = f"test_{category.split('-')[0].lower()}_{index:02d}_{_slugify(scenario.get('scenario', 'case'))}" + attrs[name] = _build_test(workflow, scenario, category, index) + + globals()[class_name] = type(class_name, (HR2WFTestBase,), attrs) + + +_generate_wf_tests() diff --git a/FusionIIIT/applications/hr2/urls.py b/FusionIIIT/applications/hr2/urls.py index cfaddcec6..8c45e4555 100644 --- a/FusionIIIT/applications/hr2/urls.py +++ b/FusionIIIT/applications/hr2/urls.py @@ -1,100 +1,7 @@ -from django.conf.urls import url, include - -from applications.hr2 import views -from applications.hr2.api import form_views +from django.urls import path, include app_name = 'hr2' urlpatterns = [ - - url(r'^$', views.service_book, name='hr2'), - url(r'^hradmin/$', views.hr_admin, name='hradmin'), - url(r'^edit/(?P\d+)/$', views.edit_employee_details, - name='editEmployeeDetails'), - url(r'^viewdetails/(?P\d+)/$', - views.view_employee_details, name='viewEmployeeDetails'), - url(r'^editServiceBook/(?P\d+)/$', - views.edit_employee_servicebook, name='editServiceBook'), - url(r'^administrativeProfile/$', views.administrative_profile, - name='administrativeProfile'), - url(r'^addnew/$', views.add_new_user, name='addnew'), - url(r'^ltc_form/(?P\d+)/$', views.ltc_form, - name='ltcForm'), - - url(r'^view_ltc_form/(?P\d+)/$', views.view_ltc_form, - name='view_ltc_form'), - url(r'^form_mangement_ltc/',views.form_mangement_ltc, name='form_mangement_ltc'), - url(r'dashboard/', views.dashboard, name='dashboard'), - url(r'^form_mangement_ltc_hr/(?P\d+)/$',views.form_mangement_ltc_hr, name='form_mangement_ltc_hr'), - url(r'^form_mangement_ltc_hod/',views.form_mangement_ltc_hod, name='form_mangement_ltc_hod'), - url(r'^search_employee/', views.search_employee, name='search_employee'), - url(r'^track_file/(?P\d+)/$', views.track_file, name='track_file'), - url('form_view_ltc/(?P\d+)/$', views.form_view_ltc, name='form_view_ltc'), - # url('file_handle/', views.file_handle, name='file_handle'), - url('file_handle_cpda/', views.file_handle_cpda, name='file_handle_cpda'), - url('file_handle_leave/', views.file_handle_leave, name='file_handle_leave'), - url('file_handle_ltc/', views.file_handle_ltc, name='file_handle_ltc'), - url('file_handle_appraisal/', views.file_handle_appraisal, name='file_handle_appraisal'), - url('file_handle_cpda_reimbursement/', views.file_handle_cpda_reimbursement, name='file_handle_cpda_reimbursement'), - - url(r'^cpda_form/(?P\d+)/$', views.cpda_form,name='cpdaForm'), - url(r'^view_cpda_form/(?P\d+)/$', views.view_cpda_form,name='view_cpda_form'), - url(r'^form_mangement_cpda/',views.form_mangement_cpda, name='form_mangement_cpda'), - url(r'^form_mangement_cpda_hr/(?P\d+)/$',views.form_mangement_cpda_hr, name='form_mangement_cpda_hr'), - url(r'^form_mangement_cpda_hod/',views.form_mangement_cpda_hod, name='form_mangement_cpda_hod'), - url('form_view_cpda/(?P\d+)/$', views.form_view_cpda, name='form_view_cpda'), - url(r'^api/',include('applications.hr2.api.urls')), - - url(r'^cpda_reimbursement_form/(?P\d+)/$', views.cpda_reimbursement_form,name='cpdaReimbursementForm'), - url(r'^view_cpda_reimbursement_form/(?P\d+)/$', views.view_cpda_reimbursement_form,name='view_cpda_reimbursement_form'), - url(r'form_view_cpda_reimbursement/(?P\d+)/$', views.form_view_cpda_reimbursement, name='form_view_cpda_reimbursement'), - url(r'^form_mangement_cpda_reimbursement/',views.form_mangement_cpda_reimbursement, name='form_mangement_cpda_reimbursement'), - url(r'^form_mangement_cpda_reimbursement_hr/(?P\d+)/$',views.form_mangement_cpda_reimbursement_hr, name='form_mangement_cpda_reimbursement_hr'), - url(r'^form_mangement_cpda_reimbursement_hod/',views.form_mangement_cpda_reimbursement_hod, name='form_mangement_cpda_reimbursement_hod'), - - url(r'^leave_form/(?P\d+)/$', views.leave_form,name='leaveForm'), - url(r'^view_leave_form/(?P\d+)/$', views.view_leave_form,name='view_leave_form'), - url(r'^form_mangement_leave/',views.form_mangement_leave, name='form_mangement_leave'), - url(r'^form_mangement_leave_hr/(?P\d+)/$',views.form_mangement_leave_hr, name='form_mangement_leave_hr'), - url(r'^form_mangement_leave_hod/',views.form_mangement_leave_hod, name='form_mangement_leave_hod'), - url('form_view_leave/(?P\d+)/$', views.form_view_leave, name='form_view_leave'), - - - - url(r'^appraisal_form/(?P\d+)/$', views.appraisal_form,name='appraisalForm'), - url(r'^view_appraisal_form/(?P\d+)/$', views.view_appraisal_form,name='view_appraisal_form'), - url(r'^form_mangement_appraisal/',views.form_mangement_appraisal, name='form_mangement_appraisal'), - url(r'^form_mangement_appraisal_hr/(?P\d+)/$',views.form_mangement_appraisal_hr, name='form_mangement_appraisal_hr'), - - url(r'^form_view_appraisal/(?P\d+)/$', views.form_view_appraisal, name='form_view_appraisal'), - url(r'^getform/$', views.getform , name='getform'), - url(r'^getformcpdaAdvance/$', views.getformcpdaAdvance , name='getformcpdaAdvance'), - url(r'^getformLeave/$', views.getformLeave , name='getformLeave'), - url(r'^getformAppraisal/$', views.getformAppraisal , name='getformAppraisal'), - url(r'^getformcpdaReimbursement/$', views.getformcpdaReimbursement , name='getformcpdaReimbursement'), - - - - - - - - - - - - - - - - - - - - - - - - - + path('api/', include('applications.hr2.api.urls')), ] diff --git a/FusionIIIT/applications/hr2/views.py b/FusionIIIT/applications/hr2/views.py deleted file mode 100644 index 939564d30..000000000 --- a/FusionIIIT/applications/hr2/views.py +++ /dev/null @@ -1,2514 +0,0 @@ -import json -from django.shortcuts import render, get_object_or_404 -from .models import * -from applications.globals.models import ExtraInfo -from applications.globals.models import * -from django.db.models import Q -from django.http import Http404 -from .forms import EditDetailsForm, EditConfidentialDetailsForm, EditServiceBookForm, NewUserForm, AddExtraInfo -from django.contrib import messages -from applications.eis.models import * -from django.http import HttpResponse, HttpResponseRedirect -from applications.establishment.models import * -from applications.establishment.views import * -from applications.eis.models import * -from applications.globals.models import ExtraInfo, HoldsDesignation, DepartmentInfo, Designation - -from html import escape -from io import BytesIO -import re -from rest_framework import status -from decimal import Decimal - - -from django.contrib.auth.models import User -from django.http import HttpResponse, HttpResponseRedirect -from django.shortcuts import (get_object_or_404, redirect, render, - render) -from django.http import JsonResponse -from applications.filetracking.sdk.methods import * -from django.core.files.base import File as DjangoFile -from django.views.decorators.csrf import csrf_exempt - - -def edit_employee_details(request, id): - """ Views for edit details""" - template = 'hr2Module/editDetails.html' - - try: - employee = ExtraInfo.objects.get(user__id=id) - except: - raise Http404("Post does not exist") - - if request.method == "POST": - for e in request.POST: - print(e) - print('--------------') - form = EditDetailsForm(request.POST) - conf_form = EditConfidentialDetailsForm(request.POST, request.FILES) - print("f1", form.is_valid()) - print("f2", conf_form.is_valid()) - if form.is_valid() and conf_form.is_valid(): - form.save() - conf_form.save() - try: - ee = ExtraInfo.objects.get(pk=id) - ee.user_status = "PRESENT" - ee.save() - - except: - pass - messages.success(request, "Employee details edited successfully") - else: - messages.warning(request, "Error in submitting form") - pass - else: - print("Failed") - - form = EditDetailsForm(initial={'extra_info': employee.id}) - conf_form = EditConfidentialDetailsForm(initial={'extra_info': employee}) - context = {'form': form, 'confForm': conf_form, 'employee': employee} - - return render(request, template, context) - - -def hr_admin(request): - """ Views for HR2 Admin page """ - - user = request.user - # extra_info = ExtraInfo.objects.select_related().get(user=user) - designat = HoldsDesignation.objects.select_related().get(user=user) - print(designat) - if designat.designation.name == 'hradmin': - template = 'hr2Module/hradmin.html' - # searched employee - query = request.GET.get('search') - if(request.method == "GET"): - if(query != None): - emp = ExtraInfo.objects.filter( - Q(user__first_name__icontains=query) | - Q(user__last_name__icontains=query) | - Q(id__icontains=query) - ).distinct() - emp = emp.filter(user_type="faculty") - else: - emp = ExtraInfo.objects.all() - emp = emp.filter(user_type="faculty") - else: - emp = ExtraInfo.objects.all() - emp = emp.filter(user_type="faculty") - empPresent = emp.filter(user_status="PRESENT") - empNew = emp.filter(user_status="NEW") - context = {'emps': emp, "empPresent": empPresent, "empNew": empNew} - print(context) - return render(request, template, context) - else: - return HttpResponse('Unauthorized', status=401) - - -def service_book(request): - """ - Views for service book page - """ - user = request.user - extra_info = ExtraInfo.objects.select_related().get(user=user) - - lien_service_book = ForeignService.objects.filter( - extra_info=extra_info).filter(service_type="LIEN").order_by('-start_date') - deputation_service_book = ForeignService.objects.filter( - extra_info=extra_info).filter(service_type="DEPUTATION").order_by('-start_date') - other_service_book = ForeignService.objects.filter( - extra_info=extra_info).filter(service_type="OTHER").order_by('-start_date') - appraisal_form = EmpAppraisalForm.objects.filter( - extra_info=extra_info).order_by('-year') - pf = extra_info.id - workAssignemnt = WorkAssignemnt.objects.filter( - extra_info_id=pf).order_by('-start_date') - - empprojects = emp_research_projects.objects.filter( - pf_no=pf).order_by('-start_date') - visits = emp_visits.objects.filter(pf_no=pf).order_by('-entry_date') - conferences = emp_confrence_organised.objects.filter( - pf_no=pf).order_by('-date_entry') - template = 'hr2Module/servicebook.html' - awards = emp_achievement.objects.filter(pf_no=pf).order_by('-date_entry') - thesis = emp_mtechphd_thesis.objects.filter( - pf_no=pf).order_by('-date_entry') - context = {'lienServiceBooks': lien_service_book, 'deputationServiceBooks': deputation_service_book, 'otherServiceBooks': other_service_book, - 'appraisalForm': appraisal_form, - 'empproject': empprojects, - 'visits': visits, - 'conferences': conferences, - 'awards': awards, - 'thesis': thesis, - 'extrainfo': extra_info, - 'workAssignment': workAssignemnt, - 'awards': awards - } - - return HttpResponseRedirect("/eis/profile/") - # return render(request, template, context) - - -def view_employee_details(request, id): - """ Views for edit details""" - extra_info = ExtraInfo.objects.get(user__id=id) - context = {} - try: - emp = Employee.objects.get(extra_info=extra_info) - context['emp'] = emp - except: - print("Personal details not found") - # try: - - # except: - # extra_info = ExtraInfo.objects.get(pk=id) - # print("caught error") - # return - lien_service_book = ForeignService.objects.filter( - extra_info=extra_info).filter(service_type="LIEN").order_by('-start_date') - deputation_service_book = ForeignService.objects.filter( - extra_info=extra_info).filter(service_type="DEPUTATION").order_by('-start_date') - other_service_book = ForeignService.objects.filter( - extra_info=extra_info).filter(service_type="OTHER").order_by('-start_date') - appraisal_form = EmpAppraisalForm.objects.filter( - extra_info=extra_info).order_by('-year') - pf = extra_info.user.id - print(pf) - workAssignemnt = WorkAssignemnt.objects.filter( - extra_info_id=pf).order_by('-start_date') - - empprojects = emp_research_projects.objects.filter( - pf_no=pf).order_by('-start_date') - visits = emp_visits.objects.filter(pf_no=pf).order_by('-entry_date') - conferences = emp_confrence_organised.objects.filter( - pf_no=pf).order_by('-date_entry') - awards = emp_achievement.objects.filter(pf_no=pf).order_by('-date_entry') - thesis = emp_mtechphd_thesis.objects.filter( - pf_no=pf).order_by('-date_entry') - - response = {} - # Check if establishment variables exist, if not create some fields or ask for them - response.update(initial_checks(request)) - if is_eligible(request) and request.method == "POST": - handle_appraisal(request) - - if is_eligible(request): - response.update(generate_appraisal_lists(request)) - - # If user has designation "HOD" - if is_hod(request): - response.update(generate_appraisal_lists_hod(request)) - - # If user has designation "Director" - if is_director(request): - response.update(generate_appraisal_lists_director(request)) - - response.update({'cpda': False, 'ltc': False, - 'appraisal': True, 'leave': False}) - # designat = HoldsDesignation.objects.get(user=request.user).designation - template = 'hr2Module/viewdetails.html' - context.update({'lienServiceBooks': lien_service_book, 'deputationServiceBooks': deputation_service_book, 'otherServiceBooks': other_service_book, 'user': extra_info.user, 'extrainfo': extra_info, - 'appraisalForm': appraisal_form, - 'empproject': empprojects, - 'visits': visits, - 'conferences': conferences, - 'awards': awards, - 'thesis': thesis, - 'workAssignment': workAssignemnt, - # 'designat':designat, - - }) - context.update(response) - - return render(request, template, context) - - -def edit_employee_servicebook(request, id): - """ Views for edit Service Book details""" - template = 'hr2Module/editServiceBook.html' - - try: - employee = ExtraInfo.objects.get(user__id=id) - except: - raise Http404("Post does not exist") - - if request.method == "POST": - form = EditServiceBookForm(request.POST, request.FILES) - - if form.is_valid(): - form.save() - messages.success( - request, "Employee Service Book details edited successfully") - else: - messages.warning(request, "Error in submitting form") - pass - - form = EditServiceBookForm(initial={'extra_info': employee.id}) - context = {'form': form, 'employee': employee - } - - return render(request, template, context) - - -def administrative_profile(request, username=None): - user = get_object_or_404( - User, username=username) if username else request.user - extra_info = get_object_or_404(ExtraInfo, user=user) - if extra_info.user_type != 'faculty' and extra_info.user_type != 'staff': - return redirect('/') - pf = extra_info.id - - lien_service_book = ForeignService.objects.filter( - extra_info=extra_info).filter(service_type="LIEN").order_by('-start_date') - deputation_service_book = ForeignService.objects.filter( - extra_info=extra_info).filter(service_type="DEPUTATION").order_by('-start_date') - other_service_book = ForeignService.objects.filter( - extra_info=extra_info).filter(service_type="OTHER").order_by('-start_date') - - response = {} - - response.update(initial_checks(request)) - if is_eligible(request) and request.method == "POST": - handle_appraisal(request) - - if is_eligible(request): - response.update(generate_appraisal_lists(request)) - - # If user has designation "HOD" - if is_hod(request): - response.update(generate_appraisal_lists_hod(request)) - - # If user has designation "Director" - if is_director(request): - response.update(generate_appraisal_lists_director(request)) - - response.update({'cpda': False, 'ltc': False, - 'appraisal': True, 'leave': False}) - workAssignemnt = WorkAssignemnt.objects.filter( - extra_info_id=pf).order_by('-start_date') - - context = {'user': user, - 'pf': pf, - 'lienServiceBooks': lien_service_book, 'deputationServiceBooks': deputation_service_book, 'otherServiceBooks': other_service_book, - 'extrainfo': extra_info, - 'workAssignment': workAssignemnt - } - - context.update(response) - template = 'hr2Module/dashboard_hr.html' - return render(request, template, context) - -def chkValidity(password): - flag = 0 - while True: - if (len(password)<8): - flag = -1 - break - elif not re.search("[a-z]", password): - flag = -1 - break - elif not re.search("[0-9]", password): - flag = -1 - break - elif not re.search("[_@$]", password): - flag = -1 - break - elif re.search("\s", password): - flag = -1 - break - else: - return True - break - - if flag ==-1: - return False - -def add_new_user(request): - """ Views for edit Service Book details""" - template = 'hr2Module/add_new_employee.html' - - if request.method == "POST": - form = NewUserForm(request.POST) - eform = AddExtraInfo(request.POST) - - if form.is_valid(): - user = form.save() - messages.success(request, "New User added Successfully") - else: - t_pass = '0000' - if 'password1' in request.POST: - t_pass = request.POST['password1'] - # messages.error(request,str(type(t_pass))) - if chkValidity(t_pass): - messages.error(request,"User already exists") - elif not t_pass == '0000': - messages.error(request,"Use Stronger Password") - else: - messages.error(request,"User already exists") - - - if eform.is_valid(): - eform.save() - messages.success(request, "Extra info of user saved successfully") - elif not eform.is_valid: - messages.error(request,"Some error occured") - - form = NewUserForm - eform = AddExtraInfo - - try: - employee = ExtraInfo.objects.all().first() - except: - raise Http404("Post does not exist") - - - context = {'employee': employee, "register_form": form, "eform": eform - } - - return render(request, template, context) - - - -def ltc_pre_processing(request): - data = {} - detailsOfFamilyMembersAlreadyDone = "" - - for memeber in request.POST.getlist('detailsOfFamilyMembersAlreadyDone'): - if(memeber == ""): - detailsOfFamilyMembersAlreadyDone = detailsOfFamilyMembersAlreadyDone + 'None' + ',' - else: - detailsOfFamilyMembersAlreadyDone = detailsOfFamilyMembersAlreadyDone + memeber + ',' - - data['detailsOfFamilyMembersAlreadyDone'] = detailsOfFamilyMembersAlreadyDone.rstrip(',') - - - detailsOfFamilyMembersAboutToAvail = "" - - for i in range(1,4): - for j in range(1,4): - key_is = f'info_{i}_{j}' - - if(request.POST.get(key_is) == ""): - detailsOfFamilyMembersAboutToAvail = detailsOfFamilyMembersAboutToAvail + 'None' + ',' - else: - detailsOfFamilyMembersAboutToAvail = detailsOfFamilyMembersAboutToAvail + request.POST.get(key_is) + ',' - - data['detailsOfFamilyMembersAboutToAvail'] = detailsOfFamilyMembersAboutToAvail.rstrip(',') - - - detailsOfDependents = "" - - for i in range(1,7): - for j in range(1,5): - key_is = f'd_info_{i}_{j}' - if(request.POST.get(key_is) == ""): - detailsOfDependents = detailsOfDependents + 'None' + ',' - else: - detailsOfDependents = detailsOfDependents + request.POST.get(key_is) + ',' - - data['detailsOfDependents'] = detailsOfDependents.rstrip(',') - - return data - - -def reverse_ltc_pre_processing(data): - reversed_data = {} - - # Copying over simple key-value pairs - simple_keys = [ - 'blockYear', - 'pfNo', - 'basicPaySalary', - 'name', - 'designation', - 'departmentInfo', - 'leaveRequired', - 'leaveStartDate', - 'leaveEndDate', - 'dateOfDepartureForFamily', - 'natureOfLeave', - 'purposeOfLeave', - 'hometownOrNot', - 'placeOfVisit', - 'addressDuringLeave', - 'amountOfAdvanceRequired', - 'certifiedThatFamilyDependents', - 'certifiedThatAdvanceTakenOn', - 'adjustedMonth', - 'submissionDate', - 'phoneNumberForContact' - ] - - - for key in simple_keys: - value = getattr(data, key) - reversed_data[key] = value if value != 'None' else '' - - # Reversing array-like values - reversed_data['detailsOfFamilyMembersAlreadyDone'] = getattr(data,'detailsOfFamilyMembersAlreadyDone').split(',') - - detailsOfFamilyMembersAboutToAvail = getattr(data,'detailsOfFamilyMembersAboutToAvail').split(',') - for index, value in enumerate(detailsOfFamilyMembersAboutToAvail): - detailsOfFamilyMembersAboutToAvail[index] = value if value != 'None' else '' - - reversed_data['info_1_1'] = detailsOfFamilyMembersAboutToAvail[0] - reversed_data['info_1_2'] = detailsOfFamilyMembersAboutToAvail[1] - reversed_data['info_1_3'] = detailsOfFamilyMembersAboutToAvail[2] - reversed_data['info_2_1'] = detailsOfFamilyMembersAboutToAvail[3] - reversed_data['info_2_2'] = detailsOfFamilyMembersAboutToAvail[4] - reversed_data['info_2_3'] = detailsOfFamilyMembersAboutToAvail[5] - reversed_data['info_3_1'] = detailsOfFamilyMembersAboutToAvail[6] - reversed_data['info_3_2'] = detailsOfFamilyMembersAboutToAvail[7] - reversed_data['info_3_3'] = detailsOfFamilyMembersAboutToAvail[8] - - # # Reversing details_of_dependents - detailsOfDependents = getattr(data,'detailsOfDependents').split(',') - for i in range(1, 7): - for j in range(1, 5): - key = f'd_info_{i}_{j}' - value = detailsOfDependents.pop(0) - reversed_data[key] = value if value != 'None' else '' - - return reversed_data - -def get_designation_by_user_id(user_id): - try: - # Query HoldsDesignation model to get the user's designation - designation_objs = HoldsDesignation.objects.filter(user=user_id) - return designation_objs.first().designation - except ExtraInfo.DoesNotExist: - return None - except HoldsDesignation.DoesNotExist: - return None - -def search_employee(request): - search_text = request.GET.get('search', '') - data = {'designation': 'Assistant Professor'} - try: - - employee = User.objects.get(username = search_text) - - - holds_designation = HoldsDesignation.objects.filter(user=employee) - holds_designation = list(holds_designation) - - - - data['designation'] = str(holds_designation[0].designation) - except ExtraInfo.DoesNotExist: - data = {'error': "Employee doesn't exist"} - - return JsonResponse(data) - -def ltc_form(request, id): - """ Views for edit details""" - try: - employee = ExtraInfo.objects.get(user__id=id) - except: - raise Http404("Employee does not exist! id doesnt exist") - user_id = id - creator = User.objects.get(id = user_id) - - if(employee.user_type == 'faculty' or employee.user_type == 'staff' or employee.user_type == 'student'): - template = 'hr2Module/ltc_form.html' - - if request.method == "POST": - try: - - data = ltc_pre_processing(request) - - - form1 = { - 'employeeId': id, - 'name': request.POST.get('name'), - 'blockYear': request.POST.get('blockYear'), - 'basicPaySalary': request.POST.get('basicPaySalary'), - 'designation': request.POST.get('designation'), - 'pfNo': request.POST.get('pfNo'), - 'departmentInfo': request.POST.get('departmentInfo'), - 'leaveRequired': request.POST.get('leaveRequired'), - 'leaveStartDate': request.POST.get('leaveStartDate'), - 'leaveEndDate': request.POST.get('leaveEndDate'), - 'dateOfDepartureForFamily': request.POST.get('dateOfDepartureForFamily'), - 'natureOfLeave': request.POST.get('natureOfLeave'), - 'purposeOfLeave': request.POST.get('purposeOfLeave'), - 'hometownOrNot': request.POST.get('hometownOrNot'), - 'placeOfVisit': request.POST.get('placeOfVisit'), - 'addressDuringLeave': request.POST.get('addressDuringLeave'), - 'detailsOfFamilyMembersAlreadyDone': data['detailsOfFamilyMembersAlreadyDone'], - 'detailsOfFamilyMembersAboutToAvail': data['detailsOfFamilyMembersAboutToAvail'], - 'detailsOfDependents': data['detailsOfDependents'], - 'amountOfAdvanceRequired': request.POST.get('amountOfAdvanceRequired'), - 'certifiedThatFamilyDependents': request.POST.get('certifiedThatFamilyDependents'), - 'certifiedThatAdvanceTakenOn': request.POST.get('certifiedThatAdvanceTakenOn'), - 'adjustedMonth': request.POST.get('adjustedMonth'), - 'submissionDate': request.POST.get('submissionDate'), - 'phoneNumberForContact': request.POST.get('phoneNumberForContact'), - 'username_employee': request.POST.get('username_employee'), - 'designation_employee': request.POST.get('designation_employee'), - 'created_by' : creator, - } - - - try: - ltc_form = LTCform.objects.create( - employeeId=id, - name=request.POST.get('name'), - blockYear=request.POST.get('blockYear'), - pfNo=request.POST.get('pfNo'), - basicPaySalary=request.POST.get('basicPaySalary'), - designation=request.POST.get('designation'), - departmentInfo=request.POST.get('departmentInfo'), - leaveRequired=request.POST.get('leaveAvailability'), - leaveStartDate=request.POST.get('leaveStartDate'), - leaveEndDate=request.POST.get('leaveEndDate'), - dateOfDepartureForFamily=request.POST.get('dateOfLeaveForFamily'), - natureOfLeave=request.POST.get('natureOfLeave'), - purposeOfLeave=request.POST.get('purposeOfLeave'), - hometownOrNot=request.POST.get('hometownOrNot'), - placeOfVisit=request.POST.get('placeOfVisit'), - addressDuringLeave=request.POST.get('addressDuringLeave'), - detailsOfFamilyMembersAlreadyDone=data['detailsOfFamilyMembersAlreadyDone'], - detailsOfFamilyMembersAboutToAvail=data['detailsOfFamilyMembersAboutToAvail'], - detailsOfDependents=data['detailsOfDependents'], - amountOfAdvanceRequired=request.POST.get('amountOfAdvanceRequired'), - certifiedThatFamilyDependents=request.POST.get('certifiedThatFamilyDependents'), - certifiedThatAdvanceTakenOn=request.POST.get('certifiedThatAdvanceTakenOn'), - adjustedMonth=request.POST.get('adjustedMonth'), - submissionDate=request.POST.get('submissionDate'), - phoneNumberForContact=request.POST.get('phoneNumberForContact'), - created_by=creator, - ) - - except Exception as e: - - print("An error occurred while creating the LTC form:", e) - - - - - uploader = employee.user - uploader_designation = 'Assistant Professor' - - get_designation = get_designation_by_user_id(employee.user) - if(get_designation): - uploader_designation = get_designation - - receiver = request.POST.get('username_employee') - receiver_designation = request.POST.get('designation_employee') - src_module = "HR" - src_object_id = str(ltc_form.id) - file_extra_JSON = {"type": "LTC"} - - - # Create a file representing the LTC form and send it to HR admin - file_id = create_file( - uploader=uploader, - uploader_designation=uploader_designation, - receiver=receiver, - receiver_designation=receiver_designation, - src_module=src_module, - src_object_id=src_object_id, - file_extra_JSON=file_extra_JSON, - attached_file=None # Attach any file if necessary - ) - - - messages.success(request, "Ltc form filled successfully!") - - - return redirect(request.path_info) - - except Exception as e: - messages.warning(request, "Fill not correctly") - context = {'employee': employee} - return render(request, template, context) - - - - # Query all LTC requests - ltc_requests = LTCform.objects.filter(employeeId=id) - - username = employee.user - uploader_designation = 'Assistant Professor' - - - designation = get_designation_by_user_id(employee.user) - if(designation): - uploader_designation = designation - - - inbox = view_inbox(username = username, designation = uploader_designation, src_module = "HR") - archived_files = view_archived(username = username, designation = uploader_designation, src_module = "HR") - filtered_inbox = [] - for i in inbox: - item = i.get('file_extra_JSON', {}) - if item.get('type') == 'LTC': - filtered_inbox.append(i) - - filtered_archived_files = [] - for i in archived_files: - item = i.get('file_extra_JSON', {}) - if item.get('type') == 'LTC': - filtered_archived_files.append(i) - - - - - - - context = {'employee': employee, 'ltc_requests': ltc_requests, 'inbox': filtered_inbox , 'designation':designation, 'archived_files': filtered_archived_files , 'user_id': user_id} - - return render(request, template, context) - else: - return render(request, 'hr2Module/edit.html') - -def form_view_ltc(request , id): - ltc_request = get_object_or_404(LTCform, id=id) - - user_id = ltc_request.created_by.id - - from_user = request.GET.get('param1') - from_designation = request.GET.get('param2') - file_id = request.GET.get('param3') - - template = 'hr2Module/view_ltc_form.html' - ltc_request = reverse_ltc_pre_processing(ltc_request) - - context = {'ltc_request' : ltc_request , "button" : 1 , "file_id" : file_id, "from_user" :from_user , "from_designation" : from_designation ,"id" : id, "user_id" : user_id} - - return render(request , template , context) - -def track_file(request, id): - # Assuming file_history is a list of dictionaries - template = 'hr2Module/ltc_form_trackfile.html' - file_history = view_history(file_id=id) - - - context = {'file_history': file_history} - - # Create a JSON response - return render(request ,template , context) - -def get_current_file_owner(file_id: int) -> User: - ''' - This functions returns the current owner of the file. - The current owner is the latest recipient of the file - ''' - latest_tracking = Tracking.objects.filter( - file_id=file_id).order_by('-receive_date').first() - latest_recipient = latest_tracking.receiver_id - return latest_recipient - -def file_handle_leave(request): - if request.method == 'POST': - form_data2 = request.POST - form_data=request.POST.get('context') - action = form_data2.get('action') - - form_data=json.loads(form_data) - form_id = form_data['form_id'] - file_id = form_data['file_id'] - from_user = form_data['from_user'] - from_designation = form_data['from_designation'] - username_employee = form_data['username_employee'] - designation_employee = form_data['designation_employee'] - - remark = form_data['remark_id'] - - - #database - leave_form = LeaveForm.objects.get(id=form_id) - - leave_form.save() - - #database - try: - leave_form = LeaveForm.objects.get(id=form_id) - except LeaveForm.DoesNotExist: - return JsonResponse({"error": "LeaveForm object with the provided ID does not exist"}, status=404) - - - - current_owner = get_current_file_owner(file_id) - - # if action value is 0 then forward the file - # if action value is 1 then reject the file - # if action value is 3 then approve the file - # otherwise archive the file - - if(action == '0'): - if(remark == ""): - track_id = forward_file(file_id = file_id, receiver = username_employee, receiver_designation = designation_employee,remarks = f"Forwarded by {current_owner} to {username_employee}", file_extra_JSON = "None") - else: - track_id = forward_file(file_id = file_id, receiver = username_employee, receiver_designation = designation_employee,remarks = f"Forwarded by {current_owner} to {username_employee}, Reason : {remark}", file_extra_JSON = "None") - messages.success(request, "File forwarded successfully") - elif(action == '1'): - if(remark == ""): - track_id = forward_file(file_id = file_id, receiver = leave_form.name, receiver_designation = leave_form.designation, remarks = f"Rejected by {current_owner}", file_extra_JSON = "None") - else: - track_id = forward_file(file_id = file_id, receiver = leave_form.name, receiver_designation = leave_form.designation, remarks = f"Rejected by {current_owner}, Reason : {remark}", file_extra_JSON = "None") - messages.success(request, "File rejected successfully") - elif(action == '2'): - if(remark == ""): - track_id = forward_file(file_id = file_id, receiver = leave_form.name, receiver_designation = leave_form.designation, remarks = f"Approved by {current_owner}", file_extra_JSON = "None") - else: - track_id = forward_file(file_id = file_id, receiver = leave_form.name, receiver_designation = leave_form.designation, remarks = f"Approved by {current_owner}, Reason : {remark}", file_extra_JSON = "None") - leave_form.approved = True - leave_form.approvedDate = timezone.now() - leave_form.approved_by = current_owner - leave_form.save() - messages.success(request, "File approved successfully") - else: - is_archived = archive_file(file_id=file_id) - if( is_archived ): - messages.error(request, "Error in file archived") - else: - messages.success(request, "Success in file archived") - - - return HttpResponse("Success") - else: - - return HttpResponse("Failure") - - -def file_handle_cpda(request): - if request.method == 'POST': - form_data2 = request.POST - form_data=request.POST.get('context') - action = form_data2.get('action') - - form_data=json.loads(form_data) - form_id = form_data['form_id'] - file_id = form_data['file_id'] - from_user = form_data['from_user'] - from_designation = form_data['from_designation'] - username_employee = form_data['username_employee'] - designation_employee = form_data['designation_employee'] - - advanceAmountPDA = form_data['advanceAmountPDA'] - balanceAvailable = form_data['balanceAvailable'] - amountCheckedInPDA = form_data['amountCheckedInPDA'] - - remark = form_data['remark_id'] - #change - - - #database - try: - cpda_form = CPDAAdvanceform.objects.get(id=form_id) - except CPDAAdvanceform.DoesNotExist: - return JsonResponse({"error": "CPDAform object with the provided ID does not exist"}, status=404) - - - if advanceAmountPDA == "": - advanceAmountPDA = None - else: - advanceAmountPDA = Decimal(advanceAmountPDA) - - if balanceAvailable == "": - balanceAvailable = None - else: - balanceAvailable = Decimal(balanceAvailable) - - if amountCheckedInPDA == "": - amountCheckedInPDA = None - else: - amountCheckedInPDA = Decimal(amountCheckedInPDA) - - - - - # Update the attribute - setattr(cpda_form, "advanceAmountPDA", advanceAmountPDA) - setattr(cpda_form, "balanceAvailable", balanceAvailable) - setattr(cpda_form, "amountCheckedInPDA", amountCheckedInPDA) - cpda_form.save() - - #database - try: - cpda_form = CPDAAdvanceform.objects.get(id=form_id) - except CPDAAdvanceform.DoesNotExist: - return JsonResponse({"error": "CPDAform object with the provided ID does not exist"}, status=404) - - - current_owner = get_current_file_owner(file_id) - - # if action value is 0 then forward the file - # if action value is 1 then reject the file - # if action value is 3 then approve the file - # otherwise archive the file - - if(action == '0'): - if(remark == ""): - track_id = forward_file(file_id = file_id, receiver = username_employee, receiver_designation = designation_employee,remarks = f"Forwarded by {current_owner} to {username_employee}", file_extra_JSON = "None") - else: - track_id = forward_file(file_id = file_id, receiver = username_employee, receiver_designation = designation_employee,remarks = f"Forwarded by {current_owner} to {username_employee}, Reason : {remark}", file_extra_JSON = "None") - messages.success(request, "File forwarded successfully") - elif(action == '1'): - if(remark == ""): - track_id = forward_file(file_id = file_id, receiver = cpda_form.name, receiver_designation = cpda_form.designation, remarks = f"Rejected by {current_owner}", file_extra_JSON = "None") - else: - track_id = forward_file(file_id = file_id, receiver = cpda_form.name, receiver_designation = cpda_form.designation, remarks = f"Rejected by {current_owner}, Reason : {remark}", file_extra_JSON = "None") - messages.success(request, "File rejected successfully") - elif(action == '2'): - if(remark == ""): - track_id = forward_file(file_id = file_id, receiver = cpda_form.name, receiver_designation = cpda_form.designation, remarks = f"Approved by {current_owner}", file_extra_JSON = "None") - else: - track_id = forward_file(file_id = file_id, receiver = cpda_form.name, receiver_designation = cpda_form.designation, remarks = f"Approved by {current_owner}, Reason : {remark}", file_extra_JSON = "None") - cpda_form.approved = True - cpda_form.approvedDate = timezone.now() - cpda_form.approved_by = current_owner - cpda_form.save() - messages.success(request, "File approved successfully") - else: - is_archived = archive_file(file_id=file_id) - if( is_archived ): - messages.error(request, "Error in file archived") - else: - messages.success(request, "Success in file archived") - - - return HttpResponse("Success") - else: - - return HttpResponse("Failure") - - - -def file_handle_cpda_reimbursement(request): - if request.method == 'POST': - form_data2 = request.POST - form_data=request.POST.get('context') - action = form_data2.get('action') - - form_data=json.loads(form_data) - form_id = form_data['form_id'] - file_id = form_data['file_id'] - from_user = form_data['from_user'] - from_designation = form_data['from_designation'] - username_employee = form_data['username_employee'] - designation_employee = form_data['designation_employee'] - - advanceDueAdjustment = form_data['advanceDueAdjustment'] - balanceAvailable = form_data['balanceAvailable'] - amountCheckedInPDA = form_data['amountCheckedInPDA'] - - remark = form_data['remark_id'] - #change - - - #database - try: - cpda_form = CPDAReimbursementform.objects.get(id=form_id) - except CPDAReimbursementform.DoesNotExist: - return JsonResponse({"error": "CPDAReimbursementform object with the provided ID does not exist"}, status=404) - - - if advanceDueAdjustment == "": - advanceDueAdjustment = None - else: - advanceDueAdjustment = Decimal(advanceDueAdjustment) - - if balanceAvailable == "": - balanceAvailable = None - else: - balanceAvailable = Decimal(balanceAvailable) - - if amountCheckedInPDA == "": - amountCheckedInPDA = None - else: - amountCheckedInPDA = Decimal(amountCheckedInPDA) - - - # Update the attribute - setattr(cpda_form, "advanceDueAdjustment", advanceDueAdjustment) - setattr(cpda_form, "balanceAvailable", balanceAvailable) - setattr(cpda_form, "amountCheckedInPDA", amountCheckedInPDA) - cpda_form.save() - - #database - try: - cpda_form = CPDAReimbursementform.objects.get(id=form_id) - except CPDAReimbursementform.DoesNotExist: - return JsonResponse({"error": "CPDAReimbursementform object with the provided ID does not exist"}, status=404) - - - current_owner = get_current_file_owner(file_id) - - # if action value is 0 then forward the file - # if action value is 1 then reject the file - # if action value is 3 then approve the file - # otherwise archive the file - - if(action == '0'): - if(remark == ""): - track_id = forward_file(file_id = file_id, receiver = username_employee, receiver_designation = designation_employee,remarks = f"Forwarded by {current_owner} to {username_employee}", file_extra_JSON = "None") - else: - track_id = forward_file(file_id = file_id, receiver = username_employee, receiver_designation = designation_employee,remarks = f"Forwarded by {current_owner} to {username_employee}, Reason : {remark}", file_extra_JSON = "None") - messages.success(request, "File forwarded successfully") - elif(action == '1'): - if(remark == ""): - track_id = forward_file(file_id = file_id, receiver = cpda_form.name, receiver_designation = cpda_form.designation, remarks = f"Rejected by {current_owner}", file_extra_JSON = "None") - else: - track_id = forward_file(file_id = file_id, receiver = cpda_form.name, receiver_designation = cpda_form.designation, remarks = f"Rejected by {current_owner}, Reason : {remark}", file_extra_JSON = "None") - messages.success(request, "File rejected successfully") - elif(action == '2'): - if(remark == ""): - track_id = forward_file(file_id = file_id, receiver = cpda_form.name, receiver_designation = cpda_form.designation, remarks = f"Approved by {current_owner}", file_extra_JSON = "None") - else: - track_id = forward_file(file_id = file_id, receiver = cpda_form.name, receiver_designation = cpda_form.designation, remarks = f"Approved by {current_owner}, Reason : {remark}", file_extra_JSON = "None") - cpda_form.approved = True - cpda_form.approvedDate = timezone.now() - cpda_form.approved_by = current_owner - cpda_form.save() - messages.success(request, "File approved successfully") - else: - is_archived = archive_file(file_id=file_id) - if( is_archived ): - messages.error(request, "Error in file archived") - else: - messages.success(request, "Success in file archived") - - - return HttpResponse("Success") - else: - - return HttpResponse("Failure") - - - - -def file_handle_ltc(request): - if request.method == 'POST': - form_data2 = request.POST - form_data=request.POST.get('context') - action = form_data2.get('action') - - form_data=json.loads(form_data) - form_id = form_data['form_id'] - file_id = form_data['file_id'] - from_user = form_data['from_user'] - from_designation = form_data['from_designation'] - username_employee = form_data['username_employee'] - designation_employee = form_data['designation_employee'] - - remark = form_data['remark_id'] - #change - - - #database - try: - ltc_form = LTCform.objects.get(id=form_id) - except LTCform.DoesNotExist: - return JsonResponse({"error": "LTCform object with the provided ID does not exist"}, status=404) - - - ltc_form.save() - - - current_owner = get_current_file_owner(file_id) - - - # if action value is 0 then forward the file - # if action value is 1 then reject the file - # if action value is 3 then approve the file - # otherwise archive the file - - if(action == '0'): - if(remark == ""): - track_id = forward_file(file_id = file_id, receiver = username_employee, receiver_designation = designation_employee,remarks = f"Forwarded by {current_owner} to {username_employee}", file_extra_JSON = "None") - else: - track_id = forward_file(file_id = file_id, receiver = username_employee, receiver_designation = designation_employee,remarks = f"Forwarded by {current_owner} to {username_employee}, Reason : {remark}", file_extra_JSON = "None") - messages.success(request, "File forwarded successfully") - elif(action == '1'): - if(remark == ""): - track_id = forward_file(file_id = file_id, receiver = ltc_form.name, receiver_designation = ltc_form.designation, remarks = f"Rejected by {current_owner}", file_extra_JSON = "None") - else: - track_id = forward_file(file_id = file_id, receiver = ltc_form.name, receiver_designation = ltc_form.designation, remarks = f"Rejected by {current_owner}, Reason : {remark}", file_extra_JSON = "None") - messages.success(request, "File rejected successfully") - elif(action == '2'): - if(remark == ""): - track_id = forward_file(file_id = file_id, receiver = ltc_form.name, receiver_designation = ltc_form.designation, remarks = f"Approved by {current_owner}", file_extra_JSON = "None") - else: - track_id = forward_file(file_id = file_id, receiver = ltc_form.name, receiver_designation = ltc_form.designation, remarks = f"Approved by {current_owner}, Reason : {remark}", file_extra_JSON = "None") - ltc_form.approved = True - ltc_form.approvedDate = timezone.now() - ltc_form.approved_by = current_owner - ltc_form.save() - messages.success(request, "File approved successfully") - else: - is_archived = archive_file(file_id=file_id) - if( is_archived ): - messages.error(request, "Error in file archived") - else: - messages.success(request, "Success in file archived") - - - return HttpResponse("Success") - else: - - return HttpResponse("Failure") - -def file_handle_appraisal(request): - if request.method == 'POST': - form_data2 = request.POST - form_data=request.POST.get('context') - action = form_data2.get('action') - - form_data=json.loads(form_data) - form_id = form_data['form_id'] - file_id = form_data['file_id'] - from_user = form_data['from_user'] - from_designation = form_data['from_designation'] - username_employee = form_data['username_employee'] - designation_employee = form_data['designation_employee'] - - - remark = form_data['remark_id'] - try: - appraisal_form = Appraisalform.objects.get(id=form_id) - except Appraisalform.DoesNotExist: - return JsonResponse({"error": "Appraisalform object with the provided ID does not exist"}, status=404) - - - # Update the attribute - setattr(appraisal_form, "form_id", form_id) - - appraisal_form.save() - - current_owner = get_current_file_owner(file_id) - - #database - try: - appraisal_form = Appraisalform.objects.get(id=form_id) - except Appraisalform.DoesNotExist: - return JsonResponse({"error": "Appraisalform object with the provided ID does not exist"}, status=404) - - - # if action value is 0 then forward the file - # if action value is 1 then reject the file - # if action value is 3 then approve the file - # otherwise archive the file - - if(action == '0'): - if(remark == ""): - track_id = forward_file(file_id = file_id, receiver = username_employee, receiver_designation = designation_employee,remarks = f"Forwarded by {current_owner} to {username_employee}", file_extra_JSON = "None") - else: - track_id = forward_file(file_id = file_id, receiver = username_employee, receiver_designation = designation_employee,remarks = f"Forwarded by {current_owner} to {username_employee}, Reason : {remark}", file_extra_JSON = "None") - messages.success(request, "File forwarded successfully") - elif(action == '1'): - if(remark == ""): - track_id = forward_file(file_id = file_id, receiver = appraisal_form.name, receiver_designation = appraisal_form.designation, remarks = f"Rejected by {current_owner}", file_extra_JSON = "None") - else: - track_id = forward_file(file_id = file_id, receiver = appraisal_form.name, receiver_designation = appraisal_form.designation, remarks = f"Rejected by {current_owner}, Reason : {remark}", file_extra_JSON = "None") - messages.success(request, "File rejected successfully") - elif(action == '2'): - if(remark == ""): - track_id = forward_file(file_id = file_id, receiver = appraisal_form.name, receiver_designation = appraisal_form.designation, remarks = f"Approved by {current_owner}", file_extra_JSON = "None") - else: - track_id = forward_file(file_id = file_id, receiver = appraisal_form.name, receiver_designation = appraisal_form.designation, remarks = f"Approved by {current_owner}, Reason : {remark}", file_extra_JSON = "None") - appraisal_form.approved = True - appraisal_form.approvedDate = timezone.now() - appraisal_form.approved_by = current_owner - appraisal_form.save() - messages.success(request, "File approved successfully") - else: - is_archived = archive_file(file_id=file_id) - if( is_archived ): - messages.error(request, "Error in file archived") - else: - messages.success(request, "Success in file archived") - - - return HttpResponse("Success") - else: - - return HttpResponse("Failure") - - -def view_ltc_form(request, id): - ltc_request = get_object_or_404(LTCform, id=id) - - ltc_request = reverse_ltc_pre_processing(ltc_request) - - - context = { - 'ltc_request': ltc_request - } - return render(request,'hr2Module/view_ltc_form.html',context) - -def form_mangement_ltc(request): - if(request.method == "GET"): - username = "21BCS185" - designation = "hradmin" - inbox = view_inbox(username = username, designation = designation, src_module = "HR") - - - # Extract src_object_id values - src_object_ids = [item['src_object_id'] for item in inbox] - - ltc_requests = [] - - for src_object_id in src_object_ids: - ltc_request = get_object_or_404(LTCform, id=src_object_id) - ltc_requests.append(ltc_request) - - context= { - 'ltc_requests' : ltc_requests, - 'hr' : "1", - } - - - return render(request, 'hr2Module/ltc_form.html',context) - - -def form_mangement_ltc_hr(request,id): - uploader = "21BCS183" - uploader_designation = "student" - receiver = "21BCS181" - receiver_designation = "HOD" - src_module = "HR" - src_object_id = id, - file_extra_JSON = {"key": "value"} - - # Create a file representing the LTC form and send it to HR admin - file_id = create_file( - uploader=uploader, - uploader_designation=uploader_designation, - receiver=receiver, - receiver_designation=receiver_designation, - src_module=src_module, - src_object_id=src_object_id, - file_extra_JSON=file_extra_JSON, - attached_file=None # Attach any file if necessary - ) - - - messages.success(request, "Ltc form filled successfully") - - return HttpResponse("Sucess") - -def form_mangement_ltc_hod(request): - if(request.method == "GET"): - username = "21BCS181" - designation = "HOD" - inbox = view_inbox(username = username, designation = designation, src_module = "HR") - - - # Extract src_object_id values - src_object_ids = [item['src_object_id'] for item in inbox] - - ltc_requests = [] - - for src_object_id in src_object_ids: - ltc_request = get_object_or_404(LTCform, id=src_object_id) - ltc_requests.append(ltc_request) - - context= { - 'ltc_requests' : ltc_requests, - 'hr' : "1", - } - - - return render(request, 'hr2Module/ltc_form.html',context) - - - -@login_required(login_url='/accounts/login') -def dashboard(request): - user = request.user - - user_id = ExtraInfo.objects.get(user=user).user_id - context = {'user_id': user_id} - return render(request, 'hr2Module/dashboard.html',context) - - -# cpda form ----------------------------------------------------------- - -def reverse_cpda_pre_processing(data): - reversed_data = {} - - simple_keys = [ - 'name', 'designation', 'pfNo', 'purpose', 'amountRequired', 'advanceDueAdjustment', - 'submissionDate', - 'balanceAvailable', 'advanceAmountPDA' ,'amountCheckedInPDA', - ] - - - for key in simple_keys: - value = getattr(data, key) - reversed_data[key] = value if value != 'None' else '' - - return reversed_data - - -def cpda_form(request, id): - """ Views for edit details""" - try: - employee = ExtraInfo.objects.get(user__id=id) - except: - raise Http404("Employee does not exist! id doesnt exist") - - user_id = id - creator = User.objects.get(id = user_id) - - if(employee.user_type == 'faculty' or employee.user_type == 'staff' or employee.user_type == 'student' ): - template = 'hr2Module/cpda_form.html' - - if request.method == "POST": - try: - advanceAmountPDA = request.POST.get('advanceAmountPDA') - if advanceAmountPDA == "": - advanceAmountPDA = None - else: - advanceAmountPDA = Decimal(advanceAmountPDA) - - balanceAvailable = request.POST.get('balanceAvailable') - if balanceAvailable == "": - balanceAvailable = None - else: - balanceAvailable = Decimal(balanceAvailable) - - amountCheckedInPDA = request.POST.get('amountCheckedInPDA') - if amountCheckedInPDA == "": - amountCheckedInPDA = None - else: - amountCheckedInPDA = Decimal(amountCheckedInPDA) - - - form_2 = { - 'employeeId' : id, - 'name' : request.POST.get('name'), - 'designation' : request.POST.get('designation'), - 'pfNo' : request.POST.get('pfNo'), - 'purpose' : request.POST.get('purpose'), - 'amountRequired' : request.POST.get('amountRequired'), - 'advanceDueAdjustment' : request.POST.get('advanceDueAdjustment'), - 'submissionDate' : request.POST.get('submissionDate'), - 'balanceAvailable' : request.POST.get('balanceAvailable'), - 'advanceAmountPDA' : request.POST.get('advanceAmountPDA'), - 'amountCheckedInPDA' : request.POST.get('amountCheckedInPDA'), - 'created_by' : creator, - } - - cpda_form = CPDAAdvanceform.objects.create( - employeeId = id, - name = request.POST.get('name'), - designation = request.POST.get('designation'), - pfNo = request.POST.get('pfNo'), - purpose = request.POST.get('purpose'), - amountRequired = request.POST.get('amountRequired'), - advanceDueAdjustment = request.POST.get('advanceDueAdjustment'), - submissionDate = request.POST.get('submissionDate'), - balanceAvailable = request.POST.get('balanceAvailable'), - advanceAmountPDA = request.POST.get('advanceAmountPDA'), - amountCheckedInPDA = request.POST.get('amountCheckedInPDA'), - created_by=creator, - - ) - - - uploader = employee.user - uploader_designation = 'Assistant Professor' - - get_designation = get_designation_by_user_id(employee.user) - if(get_designation): - uploader_designation = get_designation - - receiver = request.POST.get('username_employee') - receiver_designation = request.POST.get('designation_employee') - src_module = "HR" #dikkat - src_object_id = str(cpda_form.id) - file_extra_JSON = {"type": "CPDAAdvance"} - - # Create a file representing the CPDA form - file_id = create_file( - uploader=uploader, - uploader_designation=uploader_designation, - receiver=receiver, - receiver_designation=receiver_designation, - src_module=src_module, - src_object_id=src_object_id, - file_extra_JSON=file_extra_JSON, - attached_file=None # Attach any file if necessary - ) - - - - messages.success(request, "CPDA form filled successfully") - - return redirect(request.path_info) - - except Exception as e: - messages.warning(request, "Fill not correctly") - context = {'employee': employee} - return render(request, template, context) - - cpda_requests = CPDAAdvanceform.objects.filter(employeeId=id) - - username = employee.user - uploader_designation = 'Assistant Professor' - - - designation = get_designation_by_user_id(employee.user) - if(designation): - uploader_designation = designation - - - inbox = view_inbox(username = username, designation = uploader_designation, src_module = "HR") - - archived_files = view_archived(username = username, designation = uploader_designation, src_module = "HR") - - filtered_inbox = [] - for i in inbox: - item = i.get('file_extra_JSON', {}) - if item.get('type') == 'CPDAAdvance': - filtered_inbox.append(i) - - filtered_archived_files = [] - for i in archived_files: - item = i.get('file_extra_JSON', {}) - if item.get('type') == 'CPDAAdvance': - filtered_archived_files.append(i) - - context = {'employee': employee, 'cpda_requests': cpda_requests, 'inbox': filtered_inbox , 'designation':designation, 'archived_files': filtered_archived_files,'user_id':user_id} - - - messages.success(request, "CPDA form filled successfully!") - return render(request, template, context) - else: - return render(request, 'hr2Module/edit.html') - - -def form_view_cpda(request , id): - cpda_request = get_object_or_404(CPDAAdvanceform, id=id) - user_id = cpda_request.created_by.id - from_user = request.GET.get('param1') - from_designation = request.GET.get('param2') - file_id = request.GET.get('param3') - - - - template = 'hr2Module/view_cpda_form.html' - cpda_request = reverse_cpda_pre_processing(cpda_request) - - context = {'cpda_request' : cpda_request , "button" : 1 , "file_id" : file_id, "from_user" :from_user , "from_designation" : from_designation,"id":id,"user_id":user_id} - - return render(request , template , context) - - -def view_cpda_form(request, id): - cpda_request = get_object_or_404(CPDAAdvanceform, id=id) - - cpda_request = reverse_cpda_pre_processing(cpda_request) - - - context = { - 'cpda_request': cpda_request - } - return render(request,'hr2Module/view_cpda_form.html',context) - - -def form_mangement_cpda(request): - if(request.method == "GET"): - username = "21BCS185" - designation = "hradmin" - inbox = view_inbox(username = username, designation = designation, src_module = "HR") - - - # Extract src_object_id values - src_object_ids = [item['src_object_id'] for item in inbox] - - - cpda_requests = [] - - for src_object_id in src_object_ids: - cpda_request = get_object_or_404(CPDAAdvanceform, id=src_object_id) - cpda_requests.append(cpda_request) - - context= { - 'cpda_requests' : cpda_requests, - 'hr' : "1", - } - - - return render(request, 'hr2Module/cpda_form.html',context) - -def form_mangement_cpda_hr(request,id): - uploader = "21BCS183" - uploader_designation = "student" - receiver = "21BCS181" - receiver_designation = "HOD" - src_module = "HR" - src_object_id = id, - file_extra_JSON = {"key": "value"} - - # Create a file representing the CPDA form and send it to HR admin - file_id = create_file( - uploader=uploader, - uploader_designation=uploader_designation, - receiver=receiver, - receiver_designation=receiver_designation, - src_module=src_module, - src_object_id=src_object_id, - file_extra_JSON=file_extra_JSON, - attached_file=None # Attach any file if necessary - ) - - - messages.success(request, "CPda form filled successfully") - - - return HttpResponse("Success") - - -def form_mangement_cpda_hod(request): - if(request.method == "GET"): - username = "21BCS181" - designation = "HOD" - inbox = view_inbox(username = username, designation = designation, src_module = "HR") - - - # Extract src_object_id values - src_object_ids = [item['src_object_id'] for item in inbox] - - - cpda_requests = [] - - for src_object_id in src_object_ids: - cpda_request = get_object_or_404(CPDAAdvanceform, id=src_object_id) - cpda_requests.append(cpda_request) - - context= { - 'cpda_requests' : cpda_requests, - 'hr' : "1", - } - - - return render(request, 'hr2Module/cpda_form.html',context) - - -# Leave form ------------------------------------------------------------- - -def reverse_leave_pre_processing(data): - reversed_data = {} - - # Copying over simple key-value pairs - simple_keys = [ - 'name', 'designation', 'submissionDate', 'pfNo', 'departmentInfo', 'natureOfLeave', - 'leaveStartDate', 'leaveEndDate', 'purposeOfLeave', 'addressDuringLeave', 'academicResponsibility', - 'addministrativeResponsibiltyAssigned' - ] - - - for key in simple_keys: - value = getattr(data, key) - reversed_data[key] = value if value != 'None' else '' - - return reversed_data - - -def leave_form(request, id): - """ Views for edit details""" - try: - employee = ExtraInfo.objects.get(user__id=id) - except: - raise Http404("Employee does not exist! id doesnt exist") - - user_id = id - creator = User.objects.get(id = user_id) - - if(employee.user_type == 'faculty' or employee.user_type == 'student' or employee.user_type == 'staff'): - template = 'hr2Module/leave_form.html' - - if request.method == "POST": - try: - - - form_3 = { - 'employeeId' : id, - 'name' : request.POST.get('name'), - 'designation' : request.POST.get('designation'), - 'submissionDate' : request.POST.get('submissionDate'), - 'pfNo' : request.POST.get('pfNo'), - 'departmentInfo' : request.POST.get('departmentInfo'), - 'natureOfLeave' : request.POST.get('natureOfLeave'), - 'leaveStartDate' : request.POST.get('leaveStartDate'), - 'leaveEndDate' : request.POST.get('leaveEndDate'), - 'purposeOfLeave' : request.POST.get('purposeOfLeave'), - 'addressDuringLeave' : request.POST.get('addressDuringLeave'), - 'academicResponsibility' : request.POST.get('academicResponsibility'), - 'addministrativeResponsibiltyAssigned' : request.POST.get('addministrativeResponsibiltyAssigned'), - 'created_by' : creator, - } - - leave_form = LeaveForm.objects.create( - employeeId = id, - name = request.POST.get('name'), - designation = request.POST.get('designation'), - submissionDate = request.POST.get('submissionDate'), - pfNo = request.POST.get('pfNo'), - departmentInfo = request.POST.get('departmentInfo'), - leaveStartDate = request.POST.get('leaveStartDate'), - leaveEndDate = request.POST.get('leaveEndDate'), - natureOfLeave = request.POST.get('natureOfLeave'), - purposeOfLeave = request.POST.get('purposeOfLeave'), - addressDuringLeave = request.POST.get('addressDuringLeave'), - academicResponsibility = request.POST.get('academicResponsibility'), - addministrativeResponsibiltyAssigned = request.POST.get('addministrativeResponsibiltyAssigned'), - created_by=creator, - ) - - - uploader = employee.user - uploader_designation = 'Assistant Professor' - - - get_designation = get_designation_by_user_id(employee.user) - if(get_designation): - uploader_designation = get_designation - - receiver = request.POST.get('username_employee') - receiver_designation = request.POST.get('designation_employee') - src_module = "HR" - src_object_id = str(leave_form.id) - file_extra_JSON = {"type": "Leave"} - - - # Create a file representing the CPDA form - file_id = create_file( - uploader=uploader, - uploader_designation=uploader_designation, - receiver=receiver, - receiver_designation=receiver_designation, - src_module=src_module, - src_object_id=src_object_id, - file_extra_JSON=file_extra_JSON, - attached_file=None # Attach any file if necessary - ) - - - messages.success(request, "Leave form filled successfully") - - return redirect(request.path_info) - - except Exception as e: - messages.warning(request, "Fill not correctly") - context = {'employee': employee} - return render(request, template, context) - - # Query all Leave requests - leave_requests = LeaveForm.objects.filter(employeeId=id) - - username = employee.user - uploader_designation = 'Assistant Professor' - - designation = get_designation_by_user_id(employee.user) - if(designation): - uploader_designation = designation - - - inbox = view_inbox(username = username, designation = uploader_designation, src_module = "HR") - - archived_files = view_archived(username = username, designation = uploader_designation, src_module = "HR") - - filtered_inbox = [] - for i in inbox: - item = i.get('file_extra_JSON', {}) - if item.get('type') == 'Leave': - filtered_inbox.append(i) - - filtered_archived_files = [] - for i in archived_files: - item = i.get('file_extra_JSON', {}) - if item.get('type') == 'Leave': - filtered_archived_files.append(i) - - - - context = {'employee': employee, 'leave_requests': leave_requests, 'inbox': filtered_inbox , 'designation':designation, 'archived_files': filtered_archived_files,'user_id':user_id} - - messages.success(request, "Leave form filled successfully!") - return render(request, template, context) - else: - return render(request, 'hr2Module/edit.html') - - - -def form_view_leave(request , id): - - leave_request = get_object_or_404(LeaveForm, id=id) - user_id = leave_request.created_by.id - from_user = request.GET.get('param1') - from_designation = request.GET.get('param2') - file_id = request.GET.get('param3') - - - template = 'hr2Module/view_leave_form.html' - leave_request = reverse_leave_pre_processing(leave_request) - - context = {'leave_request' : leave_request , "button" : 1 , "file_id" : file_id, "from_user" :from_user , "from_designation" : from_designation, "id" : id,"user_id":user_id} - - return render(request , template , context) - -# ek or bna lena -def view_leave_form(request, id): - leave_request = get_object_or_404(LeaveForm, id=id) - - - - leave_request = reverse_leave_pre_processing(leave_request) - - - context = { - 'leave_request': leave_request - } - return render(request,'hr2Module/view_leave_form.html',context) - - -def form_mangement_leave(request): - if(request.method == "GET"): - username = "21BCS185" - designation = "hradmin" - inbox = view_inbox(username = username, designation = designation, src_module = "HR") - - - # Extract src_object_id values - src_object_ids = [item['src_object_id'] for item in inbox] - - leave_requests = [] - - for src_object_id in src_object_ids: - leave_request = get_object_or_404(LeaveForm, id=src_object_id) - leave_requests.append(leave_request) - - context= { - 'leave_requests' : leave_requests, - 'hr' : "1", - } - - - return render(request, 'hr2Module/leave_form.html',context) - - -def form_mangement_leave_hr(request,id): - uploader = "21BCS183" - uploader_designation = "student" - receiver = "21BCS181" - receiver_designation = "HOD" - src_module = "HR" - src_object_id = id, - file_extra_JSON = {"key": "value"} - - # Create a file representing the Leave form and send it to HR admin - file_id = create_file( - uploader=uploader, - uploader_designation=uploader_designation, - receiver=receiver, - receiver_designation=receiver_designation, - src_module=src_module, - src_object_id=src_object_id, - file_extra_JSON=file_extra_JSON, - attached_file=None # Attach any file if necessary - ) - - - messages.success(request, "Leave form filled successfully") - - return HttpResponse("Sucess") - -def form_mangement_leave_hod(request): - if(request.method == "GET"): - username = "21BCS181" - designation = "HOD" - inbox = view_inbox(username = username, designation = designation, src_module = "HR") - - - # Extract src_object_id values - src_object_ids = [item['src_object_id'] for item in inbox] - - - leave_requests = [] - - for src_object_id in src_object_ids: - leave_request = get_object_or_404(LeaveForm, id=src_object_id) - leave_requests.append(leave_request) - - context= { - 'leave_requests' : leave_requests, - 'hr' : "1", - } - - - return render(request, 'hr2Module/leave_form.html',context) - - - -def appraisal_form(request, id): - """ Views for edit details""" - try: - employee = ExtraInfo.objects.get(user__id=id) - except: - raise Http404("Employee does not exist! id doesnt exist") - - user_id = id - creator = User.objects.get(id = user_id) - - if(employee.user_type == 'faculty' or employee.user_type == 'staff' or employee.user_type == 'student'): - template = 'hr2Module/appraisal_form.html' - - if request.method == "POST": - try: - - data = appraisal_pre_processing(request) - - - form_4 = { - 'employeeId': id, - 'name': request.POST.get('name'), - 'designation': request.POST.get('designation'), - 'disciplineInfo': request.POST.get('disciplineInfo'), - 'specificFieldOfKnowledge': request.POST.get('specificFieldOfKnowledge'), - 'currentResearchInterests': request.POST.get('currentResearchInterests'), - 'coursesTaught': data['coursesTaught'], - 'newCoursesIntroduced': data['newCoursesIntroduced'], - 'newCoursesDeveloped': data['newCoursesDeveloped'], - 'otherInstructionalTasks': request.POST.get('otherInstructionalTasks'), - 'thesisSupervision': data['thesisSupervision'], - 'sponsoredReseachProjects': data['sponsoredReseachProjects'], - 'otherResearchElement': request.POST.get('otherResearchElement'), - 'publication': request.POST.get('publication'), - 'referredConference': request.POST.get('referredConference'), - 'conferenceOrganised': request.POST.get('conferenceOrganised'), - 'membership': request.POST.get('membership'), - 'honours ' : request.POST.get('honours'), - 'editorOfPublications': request.POST.get('editorOfPublications'), - 'expertLectureDelivered': request.POST.get('expertLectureDelivered'), - 'membershipOfBOS': request.POST.get('membershipOfBOS'), - 'otherExtensionTasks': request.POST.get('otherExtensionTasks'), - 'administrativeAssignment': request.POST.get('administrativeAssignment'), - 'serviceToInstitute': request.POST.get('serviceToInstitute'), - 'otherContribution': request.POST.get('otherContribution'), - 'performanceComments' : request.POST.get('performanceComments'), - 'submissionDate' : request.POST.get('submissionDate'), - 'approved' : request.POST.get('approved'), - 'approvedDate' : request.POST.get('approvedDate'), - 'created_by' : creator, - - } - - - appraisal_form = Appraisalform.objects.create( - employeeId= id, - name= request.POST.get('name'), - designation= request.POST.get('designation'), - disciplineInfo= request.POST.get('disciplineInfo'), - specificFieldOfKnowledge= request.POST.get('specificFieldOfKnowledge'), - currentResearchInterests= request.POST.get('currentResearchInterests'), - coursesTaught= data['coursesTaught'], - newCoursesIntroduced= data['newCoursesIntroduced'], - newCoursesDeveloped= data['newCoursesDeveloped'], - otherInstructionalTasks= request.POST.get('otherInstructionalTasks'), - thesisSupervision= data['thesisSupervision'], - sponsoredReseachProjects= data['sponsoredReseachProjects'], - otherResearchElement= request.POST.get('otherResearchElement'), - publication= request.POST.get('publication'), - referredConference= request.POST.get('referredConference'), - conferenceOrganised= request.POST.get('conferenceOrganised'), - membership= request.POST.get('membership'), - honours = request.POST.get('honours'), - editorOfPublications= request.POST.get('editorOfPublications'), - expertLectureDelivered= request.POST.get('expertLectureDelivered'), - membershipOfBOS= request.POST.get('membershipOfBOS'), - otherExtensionTasks= request.POST.get('otherExtensionTasks'), - administrativeAssignment= request.POST.get('administrativeAssignment'), - serviceToInstitute= request.POST.get('serviceToInstitute'), - otherContribution= request.POST.get('otherContribution'), - performanceComments = request.POST.get('performanceComments'), - submissionDate = request.POST.get('submissionDate'), - approved = request.POST.get('approved'), - approvedDate = request.POST.get('approvedDate'), - created_by=creator, - - - ) - - uploader = employee.user - uploader_designation = 'Assistant Professor' - - get_designation = get_designation_by_user_id(employee.user) - if(get_designation): - uploader_designation = get_designation - - receiver = request.POST.get('username_employee') - receiver_designation = request.POST.get('designation_employee') - src_module = "HR" - src_object_id = str(appraisal_form.id) - file_extra_JSON = {"type": "Appraisal"} - - - # Create a file representing the AppraisL form and send it to HR admin - file_id = create_file( - uploader=uploader, - uploader_designation=uploader_designation, - receiver=receiver, - receiver_designation=receiver_designation, - src_module=src_module, - src_object_id=src_object_id, - file_extra_JSON=file_extra_JSON, - attached_file=None # Attach any file if necessary - ) - - - messages.success(request, "Appraisal form filled successfully") - - return redirect(request.path_info) - - except Exception as e: - messages.warning(request, "Fill not correctly") - context = {'employee': employee} - return render(request, template, context) - - - - appraisal_requests = Appraisalform.objects.filter(employeeId=id) - - username = employee.user - uploader_designation = 'Assistant Professor' - - - - designation = get_designation_by_user_id(employee.user) - if(designation): - uploader_designation = designation - - - inbox = view_inbox(username = username, designation = uploader_designation, src_module = "HR") - - archived_files = view_archived(username = username, designation = uploader_designation, src_module = "HR") - - filtered_inbox = [] - for i in inbox: - item = i.get('file_extra_JSON', {}) - if item.get('type') == 'Appraisal': - filtered_inbox.append(i) - - filtered_archived_files = [] - for i in archived_files: - item = i.get('file_extra_JSON', {}) - if item.get('type') == 'Appraisal': - filtered_archived_files.append(i) - - - context = {'employee': employee, 'appraisal_requests': appraisal_requests, 'inbox': filtered_inbox , 'designation':designation, 'archived_files': filtered_archived_files,'user_id':user_id} - - messages.success(request, "Appraisal form filled successfully!") - return render(request, template, context) - else: - return render(request, 'hr2Module/edit.html') - - - -def form_view_appraisal(request , id): - appraisal_request = get_object_or_404(Appraisalform, id=id) - user_id = appraisal_request.created_by.id - from_user = request.GET.get('param1') - from_designation = request.GET.get('param2') - file_id = request.GET.get('param3') - - - template = 'hr2Module/view_appraisal_form.html' - appraisal_request = reverse_appraisal_pre_processing(appraisal_request) - - context = {'appraisal_request' : appraisal_request , "button" : 1 , "file_id" : file_id, "from_user" :from_user , "from_designation" : from_designation,"id":id,"user_id":user_id} - - return render(request , template , context) - - -def view_appraisal_form(request, id): - appraisal_request = get_object_or_404(Appraisalform, id=id) - - - appraisal_request = reverse_appraisal_pre_processing(appraisal_request) - - context = { - 'appraisal_request': appraisal_request - } - return render(request,'hr2Module/view_appraisal_form.html',context) - - - -def form_mangement_appraisal(request): - if(request.method == "GET"): - username = "21BCS185" - designation = "hradmin" - inbox = view_inbox(username = username, designation = designation, src_module = "HR") - - - src_object_ids = [item['src_object_id'] for item in inbox] - - appraisal_requests = [] - - for src_object_id in src_object_ids: - appraisal_request = get_object_or_404(Appraisalform, id=src_object_id) - appraisal_requests.append(appraisal_request) - - context= { - 'appraisal_requests' : appraisal_requests, - 'hr' : "1", - } - - - return render(request, 'hr2Module/appraisal_form.html',context) - - -def form_mangement_appraisal_hr(request,id): - uploader = "21BCS183" - uploader_designation = "student" - receiver = "21BCS181" - receiver_designation = "HOD" - src_module = "HR" - src_object_id = id, - file_extra_JSON = {"key": "value"} - - # Create a file representing the Appraisal form and send it to HR admin - file_id = create_file( - uploader=uploader, - uploader_designation=uploader_designation, - receiver=receiver, - receiver_designation=receiver_designation, - src_module=src_module, - src_object_id=src_object_id, - file_extra_JSON=file_extra_JSON, - attached_file=None # Attach any file if necessary - ) - - - messages.success(request, "Appraisal form filled successfully") - - return HttpResponse("Sucess") - - - -def appraisal_pre_processing(request): - data = {} - - - coursesTaught = "" - - for i in range(1,3): - for j in range(1,8): - key_is = f'info_{i}_{j}' - - if(request.POST.get(key_is) == ""): - coursesTaught = coursesTaught + 'None' + ',' - else: - coursesTaught = coursesTaught + request.POST.get(key_is) + ',' - - data['coursesTaught'] = coursesTaught.rstrip(',') - - newCoursesIntroduced = "" - - for i in range(3,5): - for j in range(1,4): - key_is = f'info_{i}_{j}' - - if(request.POST.get(key_is) == ""): - newCoursesIntroduced = newCoursesIntroduced + 'None' + ',' - else: - newCoursesIntroduced = newCoursesIntroduced + request.POST.get(key_is) + ',' - - data['newCoursesIntroduced'] = newCoursesIntroduced.rstrip(',') - - - newCoursesDeveloped = "" - - for i in range(5,7): - for j in range(1,5): - key_is = f'info_{i}_{j}' - - if(request.POST.get(key_is) == ""): - newCoursesDeveloped = newCoursesDeveloped + 'None' + ',' - else: - newCoursesDeveloped = newCoursesDeveloped + request.POST.get(key_is) + ',' - - data['newCoursesDeveloped'] = newCoursesDeveloped.rstrip(',') - - thesisSupervision = "" - - for i in range(7,9): - for j in range(1,6): - key_is = f'info_{i}_{j}' - - if(request.POST.get(key_is) == ""): - thesisSupervision = thesisSupervision + 'None' + ',' - else: - thesisSupervision = thesisSupervision + request.POST.get(key_is) + ',' - - data['thesisSupervision'] = thesisSupervision.rstrip(',') - - - - sponsoredReseachProjects = "" - - for i in range(9,10): - for j in range(1,8): - key_is = f'info_{i}_{j}' - - if(request.POST.get(key_is) == ""): - sponsoredReseachProjects = sponsoredReseachProjects + 'None' + ',' - else: - sponsoredReseachProjects = sponsoredReseachProjects + request.POST.get(key_is) + ',' - - data['sponsoredReseachProjects'] = sponsoredReseachProjects.rstrip(',') - - - return data - - - - -def reverse_appraisal_pre_processing(data): - reversed_data = {} - - # Copying over simple key-value pairs - simple_keys = [ - 'name', 'designation', 'disciplineInfo', 'specificFieldOfKnowledge', 'designation', 'currentResearchInterests', - 'otherInstructionalTasks', 'otherResearchElement', 'publication', 'referredConference', - 'conferenceOrganised', 'membership', 'honours', 'editorOfPublications', - 'expertLectureDelivered', 'membershipOfBOS', 'otherExtensionTasks', - 'administrativeAssignment', 'serviceToInstitute', 'otherContribution', 'performanceComments', - 'submissionDate' - ] - - - for key in simple_keys: - value = getattr(data, key) - reversed_data[key] = value if value != 'None' else '' - - courses_taught = getattr(data,'coursesTaught').split(',') - for index, value in enumerate(courses_taught): - courses_taught[index] = value if value != 'None' else '' - - reversed_data['info_1_1'] = courses_taught[0] - reversed_data['info_1_2'] = courses_taught[1] - reversed_data['info_1_3'] = courses_taught[2] - reversed_data['info_1_4'] = courses_taught[3] - reversed_data['info_1_5'] = courses_taught[4] - reversed_data['info_1_6'] = courses_taught[5] - reversed_data['info_1_7'] = courses_taught[6] - reversed_data['info_2_1'] = courses_taught[7] - reversed_data['info_2_2'] = courses_taught[8] - reversed_data['info_2_3'] = courses_taught[9] - reversed_data['info_2_4'] = courses_taught[10] - reversed_data['info_2_5'] = courses_taught[11] - reversed_data['info_2_6'] = courses_taught[12] - reversed_data['info_2_7'] = courses_taught[13] - - # # Reversing details_of_dependents - new_courses_introduced = getattr(data,'newCoursesIntroduced').split(',') - for i in range(3, 5): - for j in range(1, 4): - key = f'info_{i}_{j}' - value = new_courses_introduced.pop(0) - reversed_data[key] = value if value != 'None' else '' - - - - newCoursesDeveloped = getattr(data,'newCoursesDeveloped').split(',') - for i in range(5, 7): - for j in range(1, 5): - key = f'info_{i}_{j}' - value = newCoursesDeveloped.pop(0) - reversed_data[key] = value if value != 'None' else '' - - - - thesis_reasearch = getattr(data,'otherResearchElement').split(',') - for i in range(7, 9): - for j in range(1, 6): - key = f'info_{i}_{j}' - if thesis_reasearch: - value = thesis_reasearch.pop() - else: - # Handle the case where the list is empty - print("The list is empty, cannot pop from it.") - # value = thesis_reasearch.pop(0) - reversed_data[key] = value if value != 'None' else '' - - - - sponsored_research = getattr(data,'sponsoredReseachProjects').split(',') - for i in range(9, 10): - for j in range(1, 8): - key = f'info_{i}_{j}' - value = sponsored_research.pop(0) - reversed_data[key] = value if value != 'None' else '' - - return reversed_data - - - -def reverse_cpda_reimbursement_pre_processing(data): - reversed_data = {} - - simple_keys = [ - 'name', 'designation', 'pfNo', 'purpose', 'advanceTaken', 'adjustmentSubmitted', - 'submissionDate', - 'balanceAvailable', 'advanceDueAdjustment', 'amountCheckedInPDA', - ] - - - for key in simple_keys: - value = getattr(data, key) - reversed_data[key] = value if value != 'None' else '' - - return reversed_data - - -def cpda_reimbursement_form(request, id): - """ Views for edit details""" - try: - employee = ExtraInfo.objects.get(user__id=id) - except: - raise Http404("Employee does not exist! id doesnt exist") - - user_id = id - creator = User.objects.get(id = user_id) - - if(employee.user_type == 'faculty' or employee.user_type == 'staff' or employee.user_type == 'student' ): - template = 'hr2Module/cpda_reimbursement_form.html' - - if request.method == "POST": - try: - - form_2 = { - 'employeeId' : id, - 'name' : request.POST.get('name'), - 'designation' : request.POST.get('designation'), - 'pfNo' : request.POST.get('pfNo'), - 'purpose' : request.POST.get('purpose'), - 'advanceTaken' : request.POST.get('advanceTaken'), - 'advanceDueAdjustment' : request.POST.get('advanceDueAdjustment'), - 'submissionDate' : request.POST.get('submissionDate'), - 'balanceAvailable' : request.POST.get('balanceAvailable'), - 'adjustmentSubmitted' : request.POST.get('adjustmentSubmitted'), - 'amountCheckedInPDA' : request.POST.get('amountCheckedInPDA'), - 'created_by' : creator, - } - - cpda_form = CPDAReimbursementform.objects.create( - employeeId = id, - name = request.POST.get('name'), - designation = request.POST.get('designation'), - pfNo = request.POST.get('pfNo'), - purpose = request.POST.get('purpose'), - advanceTaken = request.POST.get('advanceTaken'), - advanceDueAdjustment = request.POST.get('advanceDueAdjustment'), - submissionDate = request.POST.get('submissionDate'), - balanceAvailable = request.POST.get('balanceAvailable'), - adjustmentSubmitted = request.POST.get('adjustmentSubmitted'), - amountCheckedInPDA = request.POST.get('amountCheckedInPDA'), - created_by=creator, - - ) - - - uploader = employee.user - uploader_designation = 'Assistant Professor' - - get_designation = get_designation_by_user_id(employee.user) - if(get_designation): - uploader_designation = get_designation - - receiver = request.POST.get('username_employee') - receiver_designation = request.POST.get('designation_employee') - src_module = "HR" - src_object_id = str(cpda_form.id) - file_extra_JSON = {"type": "CPDAReimbursement"} - - # Create a file representing the CPDA form - file_id = create_file( - uploader=uploader, - uploader_designation=uploader_designation, - receiver=receiver, - receiver_designation=receiver_designation, - src_module=src_module, - src_object_id=src_object_id, - file_extra_JSON=file_extra_JSON, - attached_file=None # Attach any file if necessary - ) - - messages.success(request, "cpdareimbursement form filled successfully") - - return redirect(request.path_info) - - except Exception as e: - messages.warning(request, "Fill not correctly") - context = {'employee': employee} - return render(request, template, context) - - cpda_reimbursement_requests = CPDAReimbursementform.objects.filter(employeeId=id) - - username = employee.user - uploader_designation = 'Assistant Professor' - - - designation = get_designation_by_user_id(employee.user) - if(designation): - uploader_designation = designation - - - inbox = view_inbox(username = username, designation = uploader_designation, src_module = "HR") - - archived_files = view_archived(username = username, designation = uploader_designation, src_module = "HR") - - filtered_inbox = [] - for i in inbox: - item = i.get('file_extra_JSON', {}) - if item.get('type') == 'CPDAReimbursement': - filtered_inbox.append(i) - - filtered_archived_files = [] - for i in archived_files: - item = i.get('file_extra_JSON', {}) - if item.get('type') == 'CPDAReimbursement': - filtered_archived_files.append(i) - - - context = {'employee': employee, 'cpda_reimbursement_requests': cpda_reimbursement_requests, 'inbox': filtered_inbox , 'designation':designation, 'archived_files': filtered_archived_files,'user_id':user_id} - - - messages.success(request, "cpdareimbursement form filled successfully!") - return render(request, template, context) - else: - return render(request, 'hr2Module/edit.html') - - - - -def form_view_cpda_reimbursement(request , id): - cpda_reimbursement_request = get_object_or_404(CPDAReimbursementform, id=id) - user_id = cpda_reimbursement_request.created_by.id - # isko recheck krna h - from_user = request.GET.get('param1') - from_designation = request.GET.get('param2') - file_id = request.GET.get('param3') - - template = 'hr2Module/view_cpda_reimbursement_form.html' - cpda_reimbursement_request = reverse_cpda_reimbursement_pre_processing(cpda_reimbursement_request) - - context = {'cpda_reimbursement_request' : cpda_reimbursement_request , "button" : 1 , "file_id" : file_id, "from_user" :from_user , "from_designation" : from_designation,"id":id,"user_id":user_id} - - return render(request , template , context) - - -def view_cpda_reimbursement_form(request, id): - cpda_reimbursement_request = get_object_or_404(CPDAReimbursementform, id=id) - - cpda_reimbursement_request = reverse_cpda_reimbursement_pre_processing(cpda_reimbursement_request) - - context = { - 'cpda_reimbursement_request': cpda_reimbursement_request - } - return render(request,'hr2Module/view_cpda_reimbursement_form.html',context) - - - -def form_mangement_cpda_reimbursement(request): - if(request.method == "GET"): - username = "21BCS185" - designation = "hradmin" - inbox = view_inbox(username = username, designation = designation, src_module = "HR") - - - # Extract src_object_id values - src_object_ids = [item['src_object_id'] for item in inbox] - - - cpda_reimbursement_requests = [] - - for src_object_id in src_object_ids: - cpda_reimbursement_request = get_object_or_404(CPDAReimbursementform, id=src_object_id) - cpda_reimbursement_requests.append(cpda_reimbursement_request) - - context= { - 'cpda_reimbursement_requests' : cpda_reimbursement_requests, - 'hr' : "1", - } - - - return render(request, 'hr2Module/cpda_reimbursement_form.html',context) - -def form_mangement_cpda_reimbursement_hr(request,id): - uploader = "21BCS183" - uploader_designation = "student" - receiver = "21BCS181" - receiver_designation = "HOD" - src_module = "HR" - src_object_id = id, - file_extra_JSON = {"key": "value"} - - # Create a file representing the CPDA form and send it to HR admin - file_id = create_file( - uploader=uploader, - uploader_designation=uploader_designation, - receiver=receiver, - receiver_designation=receiver_designation, - src_module=src_module, - src_object_id=src_object_id, - file_extra_JSON=file_extra_JSON, - attached_file=None # Attach any file if necessary - ) - - - messages.success(request, "CPda form filled successfully") - - - return HttpResponse("Success") - - -def form_mangement_cpda_reimbursement_hod(request): - if(request.method == "GET"): - username = "21BCS181" - designation = "HOD" - inbox = view_inbox(username = username, designation = designation, src_module = "HR") - - - # Extract src_object_id values - src_object_ids = [item['src_object_id'] for item in inbox] - - - cpda_reimbursement_requests = [] - - for src_object_id in src_object_ids: - cpda_reimbursement_request = get_object_or_404(CPDAReimbursementform, id=src_object_id) - cpda_reimbursement_requests.append(cpda_reimbursement_request) - - context= { - 'cpda_reimbursement_requests' : cpda_reimbursement_requests, - 'hr' : "1", - } - - - return render(request, 'hr2Module/cpda_reimbursement_form.html',context) - - -def getform(request): - form_type = request.GET.get("type") - id = request.GET.get("id") - - if form_type == "LTC": - try: - forms = LTCform.objects.filter(created_by=id) - - form_data = [] - for form in forms: - form_data.append({ - 'id': form.id, - 'name': form.name, - 'designation': form.designation, - 'submissionDate': form.submissionDate.strftime("%Y-%m-%d") if form.submissionDate else None, - 'is_approved' : form.approved, - - }) - - return JsonResponse(form_data, safe=False) # Return JSON response - except LTCform.DoesNotExist: - return JsonResponse({"message": "No LTC forms found."}, status=404) - -def getformcpdaAdvance(request): - form_type = request.GET.get("type") - id = request.GET.get("id") - - if form_type == "CPDAAdvance": - try: - forms = CPDAAdvanceform.objects.filter(created_by=id) - form_data = [] - for form in forms: - form_data.append({ - 'id': form.id, - 'name': form.name, - 'designation': form.designation, - 'submissionDate': form.submissionDate.strftime("%Y-%m-%d") if form.submissionDate else None, - 'is_approved' : form.approved, - }) - - return JsonResponse(form_data, safe=False) # Return JSON response - except CPDAAdvanceform.DoesNotExist: - return JsonResponse({"message": "No CPDAAdvance forms found."}, status=404) - - -def getformLeave(request): - form_type = request.GET.get("type") - id = request.GET.get("id") - - if form_type == "Leave": - try: - forms = LeaveForm.objects.filter(created_by=id) - form_data = [] - for form in forms: - form_data.append({ - 'id': form.id, - 'name': form.name, - 'designation': form.designation, - 'submissionDate': form.submissionDate.strftime("%Y-%m-%d") if form.submissionDate else None, - 'is_approved' : form.approved, - # Add other fields as needed - }) - - return JsonResponse(form_data, safe=False) # Return JSON response - except LeaveForm.DoesNotExist: - return JsonResponse({"message": "No Leave forms found."}, status=404) - - -def getformAppraisal(request): - form_type = request.GET.get("type") - id = request.GET.get("id") - - if form_type == "Appraisal": - try: - forms = Appraisalform.objects.filter(created_by=id) - form_data = [] - for form in forms: - form_data.append({ - 'id': form.id, - 'name': form.name, - 'designation': form.designation, - 'submissionDate': form.submissionDate.strftime("%Y-%m-%d") if form.submissionDate else None, - 'is_approved' : form.approved, - }) - - return JsonResponse(form_data, safe=False) # Return JSON response - except Appraisalform.DoesNotExist: - return JsonResponse({"message": "No Appraisal forms found."}, status=404) - - - -def getformcpdaReimbursement(request): - form_type = request.GET.get("type") - id = request.GET.get("id") - - if form_type == "CPDAReimbursement": - try: - forms = CPDAReimbursementform.objects.filter(created_by=id) - form_data = [] - for form in forms: - form_data.append({ - 'id': form.id, - 'name': form.name, - 'designation': form.designation, - 'submissionDate': form.submissionDate.strftime("%Y-%m-%d") if form.submissionDate else None, - 'is_approved' : form.approved, - # Add other fields as needed - }) - - return JsonResponse(form_data, safe=False) # Return JSON response - except CPDAReimbursementform.DoesNotExist: - return JsonResponse({"message": "No CPDAReimbursement forms found."}, status=404) - - - diff --git a/FusionIIIT/applications/programme_curriculum/migrations/0026_add_database_indexes.py b/FusionIIIT/applications/programme_curriculum/migrations/0026_add_database_indexes.py index 2afda5843..e46ca6c4f 100644 --- a/FusionIIIT/applications/programme_curriculum/migrations/0026_add_database_indexes.py +++ b/FusionIIIT/applications/programme_curriculum/migrations/0026_add_database_indexes.py @@ -17,24 +17,56 @@ class Migration(migrations.Migration): sql=[ # Main composite index for course registration queries """ - CREATE INDEX IF NOT EXISTS idx_course_reg_main_query - ON course_registration(session, semester_type, course_id_id, registration_type, student_id_id); + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_name = 'course_registration' + ) THEN + CREATE INDEX IF NOT EXISTS idx_course_reg_main_query + ON course_registration(session, semester_type, course_id_id, registration_type, student_id_id); + END IF; + END $$; """, # Individual indexes for course registration """ - CREATE INDEX IF NOT EXISTS idx_course_reg_session_semester_course - ON course_registration(session, semester_type, course_id_id); + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_name = 'course_registration' + ) THEN + CREATE INDEX IF NOT EXISTS idx_course_reg_session_semester_course + ON course_registration(session, semester_type, course_id_id); + END IF; + END $$; """, """ - CREATE INDEX IF NOT EXISTS idx_course_reg_student - ON course_registration(student_id_id); + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_name = 'course_registration' + ) THEN + CREATE INDEX IF NOT EXISTS idx_course_reg_student + ON course_registration(student_id_id); + END IF; + END $$; """, """ - CREATE INDEX IF NOT EXISTS idx_course_reg_type - ON course_registration(registration_type); + DO $$ + BEGIN + IF EXISTS ( + SELECT 1 FROM information_schema.tables + WHERE table_name = 'course_registration' + ) THEN + CREATE INDEX IF NOT EXISTS idx_course_reg_type + ON course_registration(registration_type); + END IF; + END $$; """ ], From c037af83767f74030a27a24e66eeb66eb01abe83 Mon Sep 17 00:00:00 2001 From: tejdevarakonda Date: Mon, 20 Apr 2026 10:54:28 +0530 Subject: [PATCH 2/4] Final HR (EIS) module submission --- FusionIIIT/applications/hr2.zip | Bin 245801 -> 185484 bytes .../applications/hr2/api/serializers.py | 114 +- FusionIIIT/applications/hr2/api/urls.py | 1 + FusionIIIT/applications/hr2/api/views.py | 1174 +++---------- .../commands/convert_vl_to_earned.py | 99 +- .../hr2/management/commands/seed_hr2.py | 136 +- .../hr2/management/commands/seed_hr_demo.py | 528 +----- .../management/commands/seed_leave_balance.py | 68 +- .../migrations/0010_appraisal_assignment.py | 43 + .../migrations/0011_leave_attachment_files.py | 20 + FusionIIIT/applications/hr2/models.py | 21 +- FusionIIIT/applications/hr2/selectors.py | 278 ++- FusionIIIT/applications/hr2/services.py | 1526 ++++++++++++++++- .../applications/hr2/tests/test_module.py | 0 14 files changed, 2287 insertions(+), 1721 deletions(-) create mode 100644 FusionIIIT/applications/hr2/migrations/0010_appraisal_assignment.py create mode 100644 FusionIIIT/applications/hr2/migrations/0011_leave_attachment_files.py create mode 100644 FusionIIIT/applications/hr2/tests/test_module.py diff --git a/FusionIIIT/applications/hr2.zip b/FusionIIIT/applications/hr2.zip index 5e3265d66f81edcea3bd4ed1eb934c84281e0032..f4b1e75470d62b7e9045d3e73e5112c95177ecbc 100644 GIT binary patch delta 84417 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 wOIrt> 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

GLEHz6%q|swFpnB>G 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%0Dkb*w9e{#QIwo`k#*nX^h=RZ0pW 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 diff --git a/FusionIIIT/applications/hr2/api/serializers.py b/FusionIIIT/applications/hr2/api/serializers.py index 7828a69a4..b1c05a608 100644 --- a/FusionIIIT/applications/hr2/api/serializers.py +++ b/FusionIIIT/applications/hr2/api/serializers.py @@ -1,7 +1,7 @@ from rest_framework import serializers from django.utils import timezone from decimal import Decimal -from applications.globals.models import ExtraInfo +from applications.hr2 import selectors as hr2_selectors from ..models import ( Employee, ServiceHistory, LeaveType, EmployeeLeaveBalance, LeaveApplicationNew, AppraisalPeriod, PerformanceAppraisalNew, TrainingProgram, TrainingNomination, @@ -106,20 +106,17 @@ def validate(self, data): employee = None request = self.context.get('request') if hasattr(self, 'context') else None if request and hasattr(request, 'user'): - try: - employee = request.user.extrainfo - except ExtraInfo.DoesNotExist: - employee = None + employee = hr2_selectors.get_employee_for_user(request.user) if employee is None: employee_id = data.get('employee_id') if employee_id: - employee = ExtraInfo.objects.filter(id=employee_id).first() + employee = hr2_selectors.get_employee_by_id_optional(employee_id) if employee is not None: - overlapping = LeaveApplicationNew.objects.filter( + overlapping = hr2_selectors.has_overlapping_leave( employee=employee, - approval_status__in=['PENDING', 'FORWARDED', 'APPROVED'], - start_date__lte=end_date, - end_date__gte=start_date, + start_date=start_date, + end_date=end_date, + exclude_id=self.instance.id if self.instance is not None else None, ) if self.instance is not None: overlapping = overlapping.exclude(id=self.instance.id) @@ -144,36 +141,35 @@ def validate(self, data): raise serializers.ValidationError({'start_date': 'Leave dates overlap with an existing leave request.'}) leave_type_name = data.get('leave_type') if leave_type_name and data.get('total_days') is not None: - leave_type = LeaveType.objects.filter(name__iexact=leave_type_name).first() + leave_type = hr2_selectors.get_leave_type_by_name(leave_type_name) if leave_type: year = start_date.year - balance = EmployeeLeaveBalance.objects.filter( + balance = hr2_selectors.get_leave_balance_for_employee_year( employee=employee, leave_type=leave_type, year=year, - ).first() + ) if balance is None: - balance = EmployeeLeaveBalance.objects.filter( + balance = hr2_selectors.get_latest_leave_balance_for_employee( employee=employee, leave_type=leave_type, - ).order_by('-year').first() + ) if balance is None: raise serializers.ValidationError({'leave_type': 'Leave balance not found for this leave type.'}) if Decimal(str(data.get('total_days'))) > (balance.current_balance or 0): raise serializers.ValidationError({'total_days': 'Requested days exceed remaining leave balance.'}) nominee_id = (data.get('nominee_employee_id') or '').strip() if nominee_id: - nominee = ExtraInfo.objects.filter(id=nominee_id).first() + nominee = hr2_selectors.get_employee_by_id_optional(nominee_id) if not nominee: raise serializers.ValidationError({'nominee_employee_id': 'Employee not found.'}) if employee is not None and str(employee.id) == nominee_id: raise serializers.ValidationError({'nominee_employee_id': 'Nominee must be different from the applicant.'}) if start_date and end_date: - nominee_overlapping = LeaveApplicationNew.objects.filter( + nominee_overlapping = hr2_selectors.has_overlapping_leave( employee=nominee, - approval_status__in=['PENDING', 'FORWARDED', 'APPROVED'], - start_date__lte=end_date, - end_date__gte=start_date, + start_date=start_date, + end_date=end_date, ).exists() if nominee_overlapping: raise serializers.ValidationError({'nominee_employee_id': 'Nominee has overlapping pending or approved leave.'}) @@ -189,7 +185,7 @@ def validate(self, data): def get_nominee_employee_name(self, obj): if not obj.handover_to: return '' - nominee = ExtraInfo.objects.filter(id=obj.handover_to).first() + nominee = hr2_selectors.get_employee_by_id_optional(obj.handover_to) if not nominee: return '' return nominee.user.get_full_name() or nominee.user.username @@ -258,6 +254,24 @@ class Meta: 'employee': {'required': False}, } + def validate(self, data): + travel_start_date = data.get('travel_start_date') + travel_end_date = data.get('travel_end_date') + if travel_start_date and travel_end_date and travel_start_date > travel_end_date: + raise serializers.ValidationError({'travel_end_date': 'Travel end date must be on or after start date.'}) + + previous_ltc_used = data.get('previous_ltc_used') + last_ltc_date = data.get('last_ltc_date') + if previous_ltc_used and not last_ltc_date: + raise serializers.ValidationError({'last_ltc_date': 'Last LTC date is required when previous LTC was used.'}) + + numeric_fields = ['ticket_cost', 'accommodation_cost', 'other_expenses', 'total_amount_claimed'] + for field in numeric_fields: + value = data.get(field) + if value is not None and value < 0: + raise serializers.ValidationError({field: 'Amount must be a non-negative number.'}) + return data + class CPDAAdvanceSerializer(serializers.ModelSerializer): employee_id = serializers.CharField(write_only=True, required=False) @@ -269,6 +283,19 @@ class Meta: 'employee': {'required': False}, } + def validate(self, data): + start_date = data.get('start_date') + end_date = data.get('end_date') + if start_date and end_date and start_date > end_date: + raise serializers.ValidationError({'end_date': 'End date must be on or after start date.'}) + + numeric_fields = ['registration_fee', 'travel_expense', 'accommodation_expense', 'other_expenses', 'total_amount'] + for field in numeric_fields: + value = data.get(field) + if value is not None and value < 0: + raise serializers.ValidationError({field: 'Amount must be a non-negative number.'}) + return data + class CPDAReimbursementSerializer(serializers.ModelSerializer): employee_id = serializers.CharField(write_only=True, required=False) @@ -277,13 +304,54 @@ class Meta: fields = '__all__' read_only_fields = ['id', 'applied_date', 'approval_status'] + def validate(self, data): + start_date = data.get('start_date') + end_date = data.get('end_date') + if start_date and end_date and start_date > end_date: + raise serializers.ValidationError({'end_date': 'End date must be on or after start date.'}) + + numeric_fields = ['registration_fee', 'travel_expense', 'accommodation_expense', 'other_expenses', 'total_amount'] + for field in numeric_fields: + value = data.get(field) + if value is not None and value < 0: + raise serializers.ValidationError({field: 'Amount must be a non-negative number.'}) + return data + class AppraisalFormSerializer(serializers.ModelSerializer): employee_id = serializers.CharField(write_only=True, required=False) class Meta: model = AppraisalFormNew fields = '__all__' - read_only_fields = ['id', 'status', 'submitted_at'] + read_only_fields = [ + 'id', + 'status', + 'submitted_at', + 'assigned_reviewer_role', + 'assigned_reviewer', + 'assigned_by', + 'assigned_at', + ] extra_kwargs = { 'employee': {'required': False}, - } \ No newline at end of file + } + + def validate(self, data): + required_fields = [ + 'employee_name', + 'department', + 'designation', + 'appraisal_year', + 'self_summary', + 'key_responsibilities', + 'achievements', + 'goals_achieved', + 'future_goals', + ] + errors = {} + for field in required_fields: + if not str(data.get(field, '')).strip(): + errors[field] = 'This field is required.' + if errors: + raise serializers.ValidationError(errors) + return data \ No newline at end of file diff --git a/FusionIIIT/applications/hr2/api/urls.py b/FusionIIIT/applications/hr2/api/urls.py index 36ce6040f..78103c6a0 100644 --- a/FusionIIIT/applications/hr2/api/urls.py +++ b/FusionIIIT/applications/hr2/api/urls.py @@ -66,4 +66,5 @@ path('appraisal-forms//', views.AppraisalFormDetailView.as_view(), name='appraisal-form-detail'), path('appraisal-forms//download/', views.AppraisalFormDownloadView.as_view(), name='appraisal-form-download'), path('appraisal-forms//review/', views.AppraisalReviewView.as_view(), name='appraisal-form-review'), + path('appraisal-forms//assign/', views.AppraisalAssignView.as_view(), name='appraisal-form-assign'), ] \ No newline at end of file diff --git a/FusionIIIT/applications/hr2/api/views.py b/FusionIIIT/applications/hr2/api/views.py index d768a592c..741d8fb9b 100644 --- a/FusionIIIT/applications/hr2/api/views.py +++ b/FusionIIIT/applications/hr2/api/views.py @@ -5,25 +5,22 @@ from rest_framework import status from rest_framework.permissions import IsAuthenticated from rest_framework.parsers import MultiPartParser, FormParser, JSONParser -from django.shortcuts import get_object_or_404 -from django.db.models import Q from django.http import HttpResponse from django.utils import timezone -from applications.globals.models import ExtraInfo, HoldsDesignation -from decimal import Decimal -from ..models import LeaveApplicationNew, EmployeeLeaveBalance, AppraisalFormNew, LeaveType +from django.core.exceptions import ValidationError +from applications.hr2 import selectors as hr2_selectors +from applications.hr2 import services as hr2_services from ..services import ( - approve_leave_application, reject_leave_application, - handle_academic_responsibility, handle_administrative_responsibility, - mark_attendance, calculate_faculty_workload, - InsufficientLeaveBalanceError, DuplicateLeaveApplicationError, InvalidWorkflowTransitionError + InsufficientLeaveBalanceError, + InvalidWorkflowTransitionError, ) from ..selectors import ( - get_employee_by_id, get_all_employees, get_leave_balance_for_employee, - get_leave_applications, get_pending_responsibility_leaves, - get_attendance_for_employee, get_appraisal_periods, get_appraisals_for_employee, - get_available_training_programs, get_nominations_for_employee, - get_promotion_applications, get_faculty_workload + get_all_employees, + get_attendance_for_employee, + get_available_training_programs, + get_faculty_workload, + get_nominations_for_employee, + get_promotion_applications, ) from .serializers import ( EmployeeDetailsSerializer, LeaveApplicationSerializer, LeaveBalanceSerializer, @@ -48,16 +45,16 @@ class EmployeeDetailView(APIView): permission_classes = [IsAuthenticated] def get(self, request, employee_id): - employee = get_employee_by_id(employee_id) + employee = hr2_selectors.get_employee_by_id_or_404(employee_id) serializer = EmployeeDetailsSerializer(employee) return Response(serializer.data) def put(self, request, employee_id): - employee = get_employee_by_id(employee_id) + employee = hr2_selectors.get_employee_by_id_or_404(employee_id) serializer = EmployeeDetailsSerializer(employee, data=request.data, partial=True) if serializer.is_valid(): - serializer.save() - return Response(serializer.data) + updated = hr2_services.update_instance(employee, serializer.validated_data) + return Response(EmployeeDetailsSerializer(updated).data) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) # ==================== LEAVE VIEWS ==================== @@ -67,167 +64,42 @@ class LeaveApplicationListCreateView(APIView): parser_classes = [JSONParser, MultiPartParser, FormParser] def get(self, request): - is_hr_staff = HoldsDesignation.objects.filter( - working=request.user, - designation__name__icontains='hr', - ).exists() or ( - request.user.extrainfo.user_type == 'staff' - and request.user.extrainfo.department - and request.user.extrainfo.department.name == 'HR' - ) - is_hod = HoldsDesignation.objects.filter( - working=request.user, - designation__name__icontains='hod', - ).exists() - is_director = HoldsDesignation.objects.filter( - working=request.user, - designation__name__icontains='director', - ).exists() - is_registrar = HoldsDesignation.objects.filter( - working=request.user, - designation__name__icontains='registrar', - ).exists() - - if is_hr_staff: - leaves = LeaveApplicationNew.objects.all() - elif is_director: - leaves = LeaveApplicationNew.objects.filter( - Q( - approval_status='FORWARDED', - current_approver_role__iexact='Director', - ) | Q(employee=request.user.extrainfo) | Q( - cancel_status='REQUESTED', - cancel_current_approver_role__iexact='Director', - ) | Q( - extension_status='REQUESTED', - extension_current_approver_role__iexact='Director', - ) - ) - elif is_registrar: - leaves = LeaveApplicationNew.objects.filter( - Q( - approval_status='FORWARDED', - current_approver_role__iexact='Registrar', - ) | Q(employee=request.user.extrainfo) | Q( - cancel_status='REQUESTED', - cancel_current_approver_role__iexact='Registrar', - ) | Q( - extension_status='REQUESTED', - extension_current_approver_role__iexact='Registrar', - ) - ) - elif is_hod: - leaves = LeaveApplicationNew.objects.filter( - department=request.user.extrainfo.department.name - ) - else: - leaves = get_leave_applications(request.user.extrainfo) + role_flags = hr2_selectors.get_role_flags(request.user) + leaves = hr2_selectors.get_leave_applications_for_role_view(request.user, role_flags) serializer = LeaveApplicationSerializer(leaves, many=True, context={'request': request}) return Response(serializer.data) def post(self, request): serializer = LeaveApplicationSerializer(data=request.data, context={'request': request}) if serializer.is_valid(): - employee = getattr(request.user, 'extrainfo', None) - if employee is None: - employee_id = request.data.get('employee_id') - if employee_id: - employee = get_employee_by_id(employee_id) - if employee is None: - return Response( - {'error': 'Employee profile not found for this user.'}, - status=status.HTTP_400_BAD_REQUEST, - ) - nominee_id = (request.data.get('nominee_employee_id') or '').strip() - nominee_status = 'PENDING' if nominee_id else 'NOT_REQUIRED' - is_director = HoldsDesignation.objects.filter( - working=employee.user, - designation__name__icontains='director', - ).exists() - is_hod = HoldsDesignation.objects.filter( - working=employee.user, - designation__name__icontains='hod', - ).exists() - is_registrar = HoldsDesignation.objects.filter( - working=employee.user, - designation__name__icontains='registrar', - ).exists() - is_hr_admin = HoldsDesignation.objects.filter( - working=employee.user, - designation__name__iregex=r'hr admin|hr administrator', - ).exists() - is_accountant = HoldsDesignation.objects.filter( - working=employee.user, - designation__name__icontains='accountant', - ).exists() - leave_type_name = (request.data.get('leave_type') or '').strip() - is_cl_rh_leave = leave_type_name in ['Casual', 'Restricted'] - employee_name = employee.user.get_full_name() or employee.user.username - department_name = employee.department.name if employee.department else (request.data.get('department') or '') - designation_name = '' - designation_record = HoldsDesignation.objects.filter(working=employee.user).select_related('designation').first() - if designation_record: - designation_name = designation_record.designation.full_name or designation_record.designation.name - else: - designation_name = request.data.get('designation') or '' - approval_status = 'PENDING' - approver_role = '' - if is_director: - approval_status = 'APPROVED' - approver_role = 'Director' - elif is_registrar: - approval_status = 'FORWARDED' - approver_role = 'Director' - elif is_hod: - if is_cl_rh_leave: - approval_status = 'APPROVED' - approver_role = 'HOD' - else: - approval_status = 'FORWARDED' - approver_role = 'Director' - elif is_hr_admin or is_accountant: - approval_status = 'FORWARDED' - approver_role = 'Registrar' - - leave_app = serializer.save( - employee=employee, - employee_name=employee_name, - department=department_name, - designation=designation_name, - handover_to=nominee_id, - nominee_status=nominee_status, - approval_status=approval_status, - current_approver_role=approver_role, - ) - if is_director or (is_hod and is_cl_rh_leave): - _apply_leave_balance_for_approval(leave_app) - leave_app.save(update_fields=['leave_balance_before', 'leave_balance_after']) + try: + leave_app = hr2_services.create_leave_application(request.user, serializer.validated_data) + except ValidationError as exc: + return Response(exc.message_dict, status=status.HTTP_400_BAD_REQUEST) refreshed_serializer = LeaveApplicationSerializer(leave_app, context={'request': request}) return Response(refreshed_serializer.data, status=status.HTTP_201_CREATED) - # Log validation errors to server console for easier debugging without DevTools. - print("LeaveApplication validation errors:", serializer.errors) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) class LeaveApplicationDetailView(APIView): permission_classes = [IsAuthenticated] def get(self, request, pk): - leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) + leave_app = hr2_selectors.get_leave_application_by_id_or_404(pk) serializer = LeaveApplicationSerializer(leave_app) return Response(serializer.data) def put(self, request, pk): - leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) + leave_app = hr2_selectors.get_leave_application_by_id_or_404(pk) if leave_app.employee != request.user.extrainfo and not request.user.is_staff: return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) serializer = LeaveApplicationSerializer(leave_app, data=request.data, partial=True) if serializer.is_valid(): - serializer.save() - return Response(serializer.data) + updated = hr2_services.update_leave_application(leave_app, serializer.validated_data) + return Response(LeaveApplicationSerializer(updated).data) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def delete(self, request, pk): - leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) + leave_app = hr2_selectors.get_leave_application_by_id_or_404(pk) if leave_app.status != 'PENDING': return Response({'error': 'Cannot delete non-pending application'}, status=status.HTTP_400_BAD_REQUEST) leave_app.delete() @@ -237,7 +109,7 @@ class LeaveApplicationDownloadView(APIView): permission_classes = [IsAuthenticated] def get(self, request, pk): - leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) + leave_app = hr2_selectors.get_leave_application_by_id_or_404(pk) if leave_app.employee != request.user.extrainfo and not request.user.is_staff: return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) @@ -277,38 +149,15 @@ class LeaveApplicationWithdrawView(APIView): permission_classes = [IsAuthenticated] def post(self, request, pk): - leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) - if leave_app.employee != request.user.extrainfo: - return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) - if leave_app.approval_status not in ['PENDING', 'FORWARDED']: - return Response({'error': 'Only pending or forwarded requests can be withdrawn.'}, status=status.HTTP_400_BAD_REQUEST) - - is_registrar = HoldsDesignation.objects.filter( - working=request.user, - designation__name__icontains='registrar', - ).exists() - is_accountant = HoldsDesignation.objects.filter( - working=request.user, - designation__name__icontains='accountant', - ).exists() - is_hr_admin = HoldsDesignation.objects.filter( - working=request.user, - designation__name__iregex=r'hr admin|hr administrator', - ).exists() - - if is_registrar or is_accountant or is_hr_admin: - leave_app.approval_status = 'REJECTED' - if is_registrar: - leave_app.current_approver_role = 'Registrar' - elif is_accountant: - leave_app.current_approver_role = 'Accountant' - else: - leave_app.current_approver_role = 'HR Admin' - else: - leave_app.approval_status = 'WITHDRAWN' - leave_app.current_approver_role = 'Employee' - leave_app.remarks = (request.data.get('remarks') or '').strip() - leave_app.save(update_fields=['approval_status', 'current_approver_role', 'remarks']) + leave_app = hr2_selectors.get_leave_application_by_id_or_404(pk) + try: + leave_app = hr2_services.withdraw_leave_application( + leave_app, + request.user, + request.data.get('remarks'), + ) + except (ValidationError, PermissionError) as exc: + return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST) serializer = LeaveApplicationSerializer(leave_app) return Response(serializer.data) @@ -316,72 +165,15 @@ class LeaveApplicationCancelRequestView(APIView): permission_classes = [IsAuthenticated] def post(self, request, pk): - leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) - if leave_app.employee != request.user.extrainfo: - return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) - if leave_app.approval_status != 'APPROVED': - return Response({'error': 'Only approved requests can be cancelled.'}, status=status.HTTP_400_BAD_REQUEST) - if leave_app.cancel_status != 'NOT_REQUESTED': - return Response({'error': 'Cancellation already processed or pending.'}, status=status.HTTP_400_BAD_REQUEST) - - today = timezone.now().date() - if today >= leave_app.start_date: - return Response( - {'error': 'Cancellation allowed only up to 1 day prior to start date.'}, - status=status.HTTP_400_BAD_REQUEST, + leave_app = hr2_selectors.get_leave_application_by_id_or_404(pk) + try: + leave_app = hr2_services.request_leave_cancellation( + leave_app, + request.user, + request.data.get('reason'), ) - - is_director = HoldsDesignation.objects.filter( - working=request.user, - designation__name__icontains='director', - ).exists() - is_hod = HoldsDesignation.objects.filter( - working=request.user, - designation__name__icontains='hod', - ).exists() - is_registrar = HoldsDesignation.objects.filter( - working=request.user, - designation__name__icontains='registrar', - ).exists() - is_accountant = HoldsDesignation.objects.filter( - working=request.user, - designation__name__icontains='accountant', - ).exists() - is_hr_admin = HoldsDesignation.objects.filter( - working=request.user, - designation__name__iregex=r'hr admin|hr administrator', - ).exists() - - requester_role = 'Employee' - if is_director: - requester_role = 'Director' - elif is_hod: - requester_role = 'HOD' - elif is_registrar: - requester_role = 'Registrar' - elif is_accountant: - requester_role = 'Accountant' - elif is_hr_admin: - requester_role = 'HR Admin' - - cancel_approver_role = 'HOD' - if requester_role in ['HOD', 'Director', 'Registrar']: - cancel_approver_role = 'Director' - elif requester_role in ['Accountant', 'HR Admin']: - cancel_approver_role = 'Registrar' - - leave_app.cancel_status = 'REQUESTED' - leave_app.cancel_requested_at = timezone.now() - leave_app.cancel_requested_by_role = requester_role - leave_app.cancel_current_approver_role = cancel_approver_role - leave_app.cancel_reason = (request.data.get('reason') or '').strip() - leave_app.save(update_fields=[ - 'cancel_status', - 'cancel_requested_at', - 'cancel_requested_by_role', - 'cancel_current_approver_role', - 'cancel_reason', - ]) + except (ValidationError, PermissionError) as exc: + return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST) serializer = LeaveApplicationSerializer(leave_app) return Response(serializer.data) @@ -389,56 +181,16 @@ class LeaveApplicationCancelDecisionView(APIView): permission_classes = [IsAuthenticated] def post(self, request, pk, decision): - leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) - decision = (decision or '').lower() - if decision not in ['approve', 'reject']: - return Response({'error': 'Invalid decision'}, status=status.HTTP_400_BAD_REQUEST) - if leave_app.cancel_status != 'REQUESTED': - return Response({'error': 'No cancellation request pending.'}, status=status.HTTP_400_BAD_REQUEST) - - approver_role = (leave_app.cancel_current_approver_role or '').lower() - if approver_role == 'hod': - allowed = HoldsDesignation.objects.filter( - working=request.user, - designation__name__icontains='hod', - ).exists() - elif approver_role == 'director': - allowed = HoldsDesignation.objects.filter( - working=request.user, - designation__name__icontains='director', - ).exists() - elif approver_role == 'registrar': - allowed = HoldsDesignation.objects.filter( - working=request.user, - designation__name__icontains='registrar', - ).exists() - else: - allowed = False - - if not allowed: - return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) - - remarks = (request.data.get('remarks') or '').strip() - leave_app.cancel_decided_at = timezone.now() - leave_app.cancel_decision_remarks = remarks - - if decision == 'approve': - leave_app.cancel_status = 'APPROVED' - leave_app.approval_status = 'CANCELLED' - leave_app.current_approver_role = leave_app.cancel_current_approver_role - _restore_leave_balance_for_cancellation(leave_app) - else: - leave_app.cancel_status = 'REJECTED' - - leave_app.save(update_fields=[ - 'cancel_status', - 'cancel_decided_at', - 'cancel_decision_remarks', - 'approval_status', - 'current_approver_role', - 'leave_balance_before', - 'leave_balance_after', - ]) + leave_app = hr2_selectors.get_leave_application_by_id_or_404(pk) + try: + leave_app = hr2_services.decide_leave_cancellation( + leave_app, + request.user, + decision, + request.data.get('remarks'), + ) + except (ValidationError, PermissionError) as exc: + return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST) serializer = LeaveApplicationSerializer(leave_app) return Response(serializer.data) @@ -446,18 +198,7 @@ class LeaveApplicationExtensionRequestView(APIView): permission_classes = [IsAuthenticated] def post(self, request, pk): - leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) - if leave_app.employee != request.user.extrainfo: - return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) - if leave_app.approval_status != 'APPROVED': - return Response({'error': 'Only approved requests can be extended.'}, status=status.HTTP_400_BAD_REQUEST) - if leave_app.extension_status != 'NOT_REQUESTED': - return Response({'error': 'Extension already processed or pending.'}, status=status.HTTP_400_BAD_REQUEST) - - today = timezone.now().date() - if today >= leave_app.end_date: - return Response({'error': 'Extension allowed only before the original end date.'}, status=status.HTTP_400_BAD_REQUEST) - + leave_app = hr2_selectors.get_leave_application_by_id_or_404(pk) new_end_date_raw = request.data.get('new_end_date') if not new_end_date_raw: return Response({'error': 'New end date is required.'}, status=status.HTTP_400_BAD_REQUEST) @@ -465,66 +206,15 @@ def post(self, request, pk): new_end_date = datetime.datetime.strptime(new_end_date_raw, '%Y-%m-%d').date() except ValueError: return Response({'error': 'New end date must be in YYYY-MM-DD format.'}, status=status.HTTP_400_BAD_REQUEST) - if new_end_date <= leave_app.end_date: - return Response({'error': 'New end date must be after the current end date.'}, status=status.HTTP_400_BAD_REQUEST) - - new_total_days = Decimal((new_end_date - leave_app.start_date).days + 1) - - is_director = HoldsDesignation.objects.filter( - working=request.user, - designation__name__icontains='director', - ).exists() - is_hod = HoldsDesignation.objects.filter( - working=request.user, - designation__name__icontains='hod', - ).exists() - is_registrar = HoldsDesignation.objects.filter( - working=request.user, - designation__name__icontains='registrar', - ).exists() - is_accountant = HoldsDesignation.objects.filter( - working=request.user, - designation__name__icontains='accountant', - ).exists() - is_hr_admin = HoldsDesignation.objects.filter( - working=request.user, - designation__name__iregex=r'hr admin|hr administrator', - ).exists() - - requester_role = 'Employee' - if is_director: - requester_role = 'Director' - elif is_hod: - requester_role = 'HOD' - elif is_registrar: - requester_role = 'Registrar' - elif is_accountant: - requester_role = 'Accountant' - elif is_hr_admin: - requester_role = 'HR Admin' - - approver_role = 'HOD' - if requester_role in ['HOD', 'Director', 'Registrar']: - approver_role = 'Director' - elif requester_role in ['Accountant', 'HR Admin']: - approver_role = 'Registrar' - - leave_app.extension_status = 'REQUESTED' - leave_app.extension_requested_at = timezone.now() - leave_app.extension_requested_by_role = requester_role - leave_app.extension_current_approver_role = approver_role - leave_app.extension_reason = (request.data.get('reason') or '').strip() - leave_app.extension_new_end_date = new_end_date - leave_app.extension_new_total_days = new_total_days - leave_app.save(update_fields=[ - 'extension_status', - 'extension_requested_at', - 'extension_requested_by_role', - 'extension_current_approver_role', - 'extension_reason', - 'extension_new_end_date', - 'extension_new_total_days', - ]) + try: + leave_app = hr2_services.request_leave_extension( + leave_app, + request.user, + new_end_date, + request.data.get('reason'), + ) + except (ValidationError, PermissionError) as exc: + return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST) serializer = LeaveApplicationSerializer(leave_app) return Response(serializer.data) @@ -532,59 +222,18 @@ class LeaveApplicationExtensionDecisionView(APIView): permission_classes = [IsAuthenticated] def post(self, request, pk, decision): - leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) - decision = (decision or '').lower() - if decision not in ['approve', 'reject']: - return Response({'error': 'Invalid decision'}, status=status.HTTP_400_BAD_REQUEST) - if leave_app.extension_status != 'REQUESTED': - return Response({'error': 'No extension request pending.'}, status=status.HTTP_400_BAD_REQUEST) - - approver_role = (leave_app.extension_current_approver_role or '').lower() - if approver_role == 'hod': - allowed = HoldsDesignation.objects.filter( - working=request.user, - designation__name__icontains='hod', - ).exists() - elif approver_role == 'director': - allowed = HoldsDesignation.objects.filter( - working=request.user, - designation__name__icontains='director', - ).exists() - elif approver_role == 'registrar': - allowed = HoldsDesignation.objects.filter( - working=request.user, - designation__name__icontains='registrar', - ).exists() - else: - allowed = False - - if not allowed: - return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) - - remarks = (request.data.get('remarks') or '').strip() - leave_app.extension_decided_at = timezone.now() - leave_app.extension_decision_remarks = remarks - - if decision == 'approve': - if not _apply_leave_balance_for_extension(leave_app): - return Response({'error': 'Insufficient leave balance for extension.'}, status=status.HTTP_400_BAD_REQUEST) - leave_app.extension_status = 'APPROVED' - leave_app.current_approver_role = leave_app.extension_current_approver_role - leave_app.end_date = leave_app.extension_new_end_date - leave_app.total_days = leave_app.extension_new_total_days - else: - leave_app.extension_status = 'REJECTED' - - leave_app.save(update_fields=[ - 'extension_status', - 'extension_decided_at', - 'extension_decision_remarks', - 'current_approver_role', - 'leave_balance_before', - 'leave_balance_after', - 'end_date', - 'total_days', - ]) + leave_app = hr2_selectors.get_leave_application_by_id_or_404(pk) + try: + leave_app = hr2_services.decide_leave_extension( + leave_app, + request.user, + decision, + request.data.get('remarks'), + ) + except InsufficientLeaveBalanceError as exc: + return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST) + except (ValidationError, PermissionError) as exc: + return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST) serializer = LeaveApplicationSerializer(leave_app) return Response(serializer.data) @@ -592,14 +241,7 @@ class LeaveResumptionSubmitView(APIView): permission_classes = [IsAuthenticated] def post(self, request, pk): - leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) - if leave_app.employee != request.user.extrainfo: - return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) - if leave_app.approval_status != 'APPROVED': - return Response({'error': 'Resumption allowed only for approved leaves.'}, status=status.HTTP_400_BAD_REQUEST) - if leave_app.resumption_status != 'NOT_REQUESTED': - return Response({'error': 'Resumption already submitted or processed.'}, status=status.HTTP_400_BAD_REQUEST) - + leave_app = hr2_selectors.get_leave_application_by_id_or_404(pk) today = timezone.now().date() resumption_date_raw = (request.data.get('resumption_date') or '').strip() if resumption_date_raw: @@ -609,22 +251,15 @@ def post(self, request, pk): return Response({'error': 'Resumption date must be in YYYY-MM-DD format.'}, status=status.HTTP_400_BAD_REQUEST) else: resumption_date = today - - if resumption_date <= leave_app.end_date: - return Response({'error': 'Resumption date must be after the leave end date.'}, status=status.HTTP_400_BAD_REQUEST) - - leave_app.resumption_status = 'SUBMITTED' - leave_app.resumption_date = resumption_date - leave_app.resumption_reason = (request.data.get('reason') or '').strip() - leave_app.resumption_submitted_at = timezone.now() - leave_app.resumption_current_approver_role = 'HOD' - leave_app.save(update_fields=[ - 'resumption_status', - 'resumption_date', - 'resumption_reason', - 'resumption_submitted_at', - 'resumption_current_approver_role', - ]) + try: + leave_app = hr2_services.submit_leave_resumption( + leave_app, + request.user, + resumption_date, + request.data.get('reason'), + ) + except (ValidationError, PermissionError) as exc: + return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST) serializer = LeaveApplicationSerializer(leave_app) return Response(serializer.data) @@ -632,34 +267,16 @@ class LeaveResumptionDecisionView(APIView): permission_classes = [IsAuthenticated] def post(self, request, pk, decision): - leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) - decision = (decision or '').lower() - if decision not in ['approve', 'reject']: - return Response({'error': 'Invalid decision'}, status=status.HTTP_400_BAD_REQUEST) - if leave_app.resumption_status != 'SUBMITTED': - return Response({'error': 'No resumption request pending.'}, status=status.HTTP_400_BAD_REQUEST) - - allowed = HoldsDesignation.objects.filter( - working=request.user, - designation__name__icontains='hod', - ).exists() - if not allowed: - return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) - - leave_app.resumption_decided_at = timezone.now() - leave_app.resumption_decision_remarks = (request.data.get('remarks') or '').strip() - if decision == 'approve': - leave_app.resumption_status = 'APPROVED' - leave_app.current_approver_role = 'HOD' - else: - leave_app.resumption_status = 'REJECTED' - - leave_app.save(update_fields=[ - 'resumption_status', - 'resumption_decided_at', - 'resumption_decision_remarks', - 'current_approver_role', - ]) + leave_app = hr2_selectors.get_leave_application_by_id_or_404(pk) + try: + leave_app = hr2_services.decide_leave_resumption( + leave_app, + request.user, + decision, + request.data.get('remarks'), + ) + except (ValidationError, PermissionError) as exc: + return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST) serializer = LeaveApplicationSerializer(leave_app) return Response(serializer.data) @@ -668,22 +285,10 @@ class LeaveBalanceView(APIView): def get(self, request, employee_id=None): if employee_id: - employee = get_object_or_404(ExtraInfo, id=employee_id) + employee = hr2_selectors.get_employee_by_id_or_404(employee_id) else: employee = request.user.extrainfo - balances_qs = ( - EmployeeLeaveBalance.objects.filter(employee=employee) - .select_related('leave_type') - .order_by('leave_type_id', '-year', '-id') - ) - # Collect the latest balance per leave type without relying on DISTINCT ON. - balances = [] - seen_leave_types = set() - for balance in balances_qs: - if balance.leave_type_id in seen_leave_types: - continue - seen_leave_types.add(balance.leave_type_id) - balances.append(balance) + balances = hr2_selectors.get_latest_leave_balances_for_employee(employee) serializer = LeaveBalanceSerializer(balances, many=True) return Response(serializer.data) @@ -692,10 +297,7 @@ class LeaveNomineeDashboardView(APIView): def get(self, request): employee = request.user.extrainfo - leaves = LeaveApplicationNew.objects.filter( - handover_to=employee.id, - nominee_status='PENDING', - ).order_by('-applied_date') + leaves = hr2_selectors.get_leave_applications_for_nominee(employee.id) serializer = LeaveApplicationSerializer(leaves, many=True) return Response(serializer.data) @@ -703,18 +305,15 @@ class LeaveNomineeDecisionView(APIView): permission_classes = [IsAuthenticated] def post(self, request, pk): - action = (request.data.get('action') or '').lower() - if action not in ['accept', 'decline']: - return Response({'error': 'Invalid action'}, status=status.HTTP_400_BAD_REQUEST) - - leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) - employee = request.user.extrainfo - if leave_app.handover_to != employee.id: - return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) - - leave_app.nominee_status = 'ACCEPTED' if action == 'accept' else 'DECLINED' - leave_app.nominee_responded_at = datetime.datetime.utcnow() - leave_app.save(update_fields=['nominee_status', 'nominee_responded_at']) + leave_app = hr2_selectors.get_leave_application_by_id_or_404(pk) + try: + leave_app = hr2_services.respond_leave_nominee( + leave_app, + request.user, + request.data.get('action'), + ) + except (ValidationError, PermissionError) as exc: + return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST) serializer = LeaveApplicationSerializer(leave_app) return Response(serializer.data) @@ -722,24 +321,15 @@ class LeaveDocumentRequestView(APIView): permission_classes = [IsAuthenticated] def post(self, request, pk): - message = (request.data.get('message') or '').strip() - if not message: - return Response({'error': 'Document request message is required.'}, status=status.HTTP_400_BAD_REQUEST) - - is_hod = HoldsDesignation.objects.filter( - working=request.user, - designation__name__icontains='hod', - ).exists() - if not is_hod: - return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) - - leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) - if leave_app.document_request_status == 'REQUESTED': - return Response({'error': 'Document already requested.'}, status=status.HTTP_400_BAD_REQUEST) - leave_app.document_request_message = message - leave_app.document_request_status = 'REQUESTED' - leave_app.document_requested_at = datetime.datetime.utcnow() - leave_app.save(update_fields=['document_request_message', 'document_request_status', 'document_requested_at']) + leave_app = hr2_selectors.get_leave_application_by_id_or_404(pk) + try: + leave_app = hr2_services.request_leave_document( + leave_app, + request.user, + (request.data.get('message') or '').strip(), + ) + except (ValidationError, PermissionError) as exc: + return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST) serializer = LeaveApplicationSerializer(leave_app) return Response(serializer.data) @@ -747,20 +337,15 @@ class LeaveDocumentSubmitView(APIView): permission_classes = [IsAuthenticated] def post(self, request, pk): - submission = (request.data.get('submission') or '').strip() - if not submission: - return Response({'error': 'Document submission is required.'}, status=status.HTTP_400_BAD_REQUEST) - - leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) - if leave_app.employee != request.user.extrainfo: - return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) - if leave_app.document_request_status != 'REQUESTED': - return Response({'error': 'No document requested for this leave.'}, status=status.HTTP_400_BAD_REQUEST) - - leave_app.document_submission = submission - leave_app.document_request_status = 'SUBMITTED' - leave_app.document_submitted_at = datetime.datetime.utcnow() - leave_app.save(update_fields=['document_submission', 'document_request_status', 'document_submitted_at']) + leave_app = hr2_selectors.get_leave_application_by_id_or_404(pk) + try: + leave_app = hr2_services.submit_leave_document( + leave_app, + request.user, + (request.data.get('submission') or '').strip(), + ) + except (ValidationError, PermissionError) as exc: + return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST) serializer = LeaveApplicationSerializer(leave_app) return Response(serializer.data) @@ -768,14 +353,14 @@ class LeaveResponsibilityView(APIView): permission_classes = [IsAuthenticated] def post(self, request, pk, responsibility_type): - leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) + leave_app = hr2_selectors.get_leave_application_by_id_or_404(pk) action = request.data.get('action') remarks = request.data.get('remarks', '') try: if responsibility_type == 'academic': - leave_app = handle_academic_responsibility(leave_app, request.user.extrainfo, action, remarks) + leave_app = hr2_services.handle_academic_responsibility(leave_app, request.user.extrainfo, action, remarks) else: - leave_app = handle_administrative_responsibility(leave_app, request.user.extrainfo, action, remarks) + leave_app = hr2_services.handle_administrative_responsibility(leave_app, request.user.extrainfo, action, remarks) serializer = LeaveApplicationSerializer(leave_app) return Response(serializer.data) except (PermissionError, InvalidWorkflowTransitionError) as e: @@ -785,160 +370,20 @@ class LeaveApproveRejectView(APIView): permission_classes = [IsAuthenticated] def post(self, request, pk, decision): - leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) - remarks = request.data.get('remarks', '') - decision = (decision or '').lower() - if decision not in ['approve', 'reject', 'forward']: - return Response({'error': 'Invalid decision'}, status=status.HTTP_400_BAD_REQUEST) - - is_registrar = HoldsDesignation.objects.filter( - working=request.user, - designation__name__icontains='registrar', - ).exists() - is_director = HoldsDesignation.objects.filter( - working=request.user, - designation__name__icontains='director', - ).exists() - approver_role = 'HOD' - if is_registrar: - approver_role = 'Registrar' - elif is_director: - approver_role = 'Director' - - leave_type_name = (leave_app.leave_type or '').strip() - is_cl_rh_leave = leave_type_name in ['Casual', 'Restricted'] - if decision == 'approve' and not is_cl_rh_leave and approver_role == 'HOD': - return Response( - {'error': 'Only CL/RH leaves can be approved by HOD. Please forward to Director.'}, - status=status.HTTP_400_BAD_REQUEST, + leave_app = hr2_selectors.get_leave_application_by_id_or_404(pk) + try: + leave_app = hr2_services.decide_leave_application( + leave_app, + request.user, + decision, + request.data.get('remarks', ''), ) - if decision == 'forward' and is_cl_rh_leave: - decision = 'approve' - - if decision == 'approve': - leave_app.approval_status = 'APPROVED' - leave_app.current_approver_role = approver_role - _apply_leave_balance_for_approval(leave_app) - elif decision == 'forward': - leave_app.approval_status = 'FORWARDED' - leave_app.current_approver_role = 'Director' - else: - leave_app.approval_status = 'REJECTED' - leave_app.current_approver_role = approver_role - - leave_app.remarks = remarks - leave_app.save(update_fields=[ - 'approval_status', - 'remarks', - 'current_approver_role', - 'leave_balance_before', - 'leave_balance_after', - ]) + except ValidationError as exc: + return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST) serializer = LeaveApplicationSerializer(leave_app) return Response(serializer.data) - -def _apply_leave_balance_for_approval(leave_app): - leave_type = LeaveType.objects.filter(name__iexact=leave_app.leave_type).first() - if not leave_type: - return - year = leave_app.start_date.year - balance = EmployeeLeaveBalance.objects.filter( - employee=leave_app.employee, - leave_type=leave_type, - year=year, - ).first() - if balance is None: - balance = EmployeeLeaveBalance.objects.filter( - employee=leave_app.employee, - leave_type=leave_type, - ).order_by('-year').first() - if balance is None or balance.year != year: - balance = EmployeeLeaveBalance.objects.create( - employee=leave_app.employee, - leave_type=leave_type, - year=year, - opening_balance=Decimal('0'), - accrued=Decimal('0'), - availed=Decimal('0'), - current_balance=Decimal('0'), - ) - total_days = Decimal(str(leave_app.total_days or 0)) - before_balance = balance.current_balance - balance.availed = (balance.availed or 0) + total_days - balance.current_balance = (balance.current_balance or 0) - total_days - balance.save(update_fields=['availed', 'current_balance']) - - if leave_app.leave_balance_before is None: - leave_app.leave_balance_before = before_balance - leave_app.leave_balance_after = balance.current_balance - -def _restore_leave_balance_for_cancellation(leave_app): - leave_type = LeaveType.objects.filter(name__iexact=leave_app.leave_type).first() - if not leave_type: - return - year = leave_app.start_date.year - balance = EmployeeLeaveBalance.objects.filter( - employee=leave_app.employee, - leave_type=leave_type, - year=year, - ).first() - if balance is None: - balance = EmployeeLeaveBalance.objects.filter( - employee=leave_app.employee, - leave_type=leave_type, - ).order_by('-year').first() - if balance is None: - return - - total_days = Decimal(str(leave_app.total_days or 0)) - before_balance = balance.current_balance - balance.availed = (balance.availed or 0) - total_days - balance.current_balance = (balance.current_balance or 0) + total_days - balance.save(update_fields=['availed', 'current_balance']) - - if leave_app.leave_balance_before is None: - leave_app.leave_balance_before = before_balance - leave_app.leave_balance_after = balance.current_balance - -def _apply_leave_balance_for_extension(leave_app): - if not leave_app.extension_new_total_days: - return False - delta_days = Decimal(str(leave_app.extension_new_total_days)) - Decimal(str(leave_app.total_days or 0)) - if delta_days <= 0: - return False - - leave_type = LeaveType.objects.filter(name__iexact=leave_app.leave_type).first() - if not leave_type: - return False - year = leave_app.start_date.year - balance = EmployeeLeaveBalance.objects.filter( - employee=leave_app.employee, - leave_type=leave_type, - year=year, - ).first() - if balance is None: - balance = EmployeeLeaveBalance.objects.filter( - employee=leave_app.employee, - leave_type=leave_type, - ).order_by('-year').first() - if balance is None: - return False - - if (balance.current_balance or 0) < delta_days: - return False - - before_balance = balance.current_balance - balance.availed = (balance.availed or 0) + delta_days - balance.current_balance = (balance.current_balance or 0) - delta_days - balance.save(update_fields=['availed', 'current_balance']) - - if leave_app.leave_balance_before is None: - leave_app.leave_balance_before = before_balance - leave_app.leave_balance_after = balance.current_balance - return True - # ==================== ATTENDANCE VIEWS ==================== class AttendanceView(APIView): @@ -952,16 +397,14 @@ def get(self, request): return Response(serializer.data) def post(self, request): - attendance = mark_attendance( - employee_extra_info=request.user.extrainfo, - date=request.data.get('date'), - status=request.data.get('status'), - in_time=request.data.get('in_time'), - out_time=request.data.get('out_time'), - remarks=request.data.get('remarks', '') - ) - serializer = EmployeeAttendanceSerializer(attendance) - return Response(serializer.data, status=status.HTTP_201_CREATED) + serializer = EmployeeAttendanceSerializer(data=request.data) + if serializer.is_valid(): + attendance = hr2_services.create_attendance( + request.user.extrainfo, + serializer.validated_data, + ) + return Response(EmployeeAttendanceSerializer(attendance).data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) # ==================== APPRAISAL VIEWS ==================== @@ -970,7 +413,7 @@ class AppraisalPeriodListView(APIView): def get(self, request): is_active = request.query_params.get('is_active') - periods = get_appraisal_periods(is_active) + periods = hr2_selectors.get_appraisal_periods(is_active) serializer = AppraisalPeriodSerializer(periods, many=True) return Response(serializer.data) @@ -979,15 +422,18 @@ class AppraisalListView(APIView): def get(self, request): period_id = request.query_params.get('period') - appraisals = get_appraisals_for_employee(request.user.extrainfo, period_id) + appraisals = hr2_selectors.get_appraisals_for_employee(request.user.extrainfo, period_id) serializer = PerformanceAppraisalSerializer(appraisals, many=True) return Response(serializer.data) def post(self, request): serializer = PerformanceAppraisalSerializer(data=request.data) if serializer.is_valid(): - serializer.save(employee=request.user.extrainfo) - return Response(serializer.data, status=status.HTTP_201_CREATED) + appraisal = hr2_services.create_performance_appraisal( + request.user.extrainfo, + serializer.validated_data, + ) + return Response(PerformanceAppraisalSerializer(appraisal).data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) # ==================== TRAINING VIEWS ==================== @@ -1011,8 +457,12 @@ def get(self, request): def post(self, request): serializer = TrainingNominationSerializer(data=request.data) if serializer.is_valid(): - serializer.save(employee=request.user.extrainfo, nominated_by=request.user.extrainfo) - return Response(serializer.data, status=status.HTTP_201_CREATED) + nomination = hr2_services.create_training_nomination( + request.user.extrainfo, + request.user.extrainfo, + serializer.validated_data, + ) + return Response(TrainingNominationSerializer(nomination).data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) # ==================== PROMOTION VIEWS ==================== @@ -1028,8 +478,11 @@ def get(self, request): def post(self, request): serializer = PromotionApplicationSerializer(data=request.data) if serializer.is_valid(): - serializer.save(employee=request.user.extrainfo) - return Response(serializer.data, status=status.HTTP_201_CREATED) + promotion = hr2_services.create_promotion_application( + request.user.extrainfo, + serializer.validated_data, + ) + return Response(PromotionApplicationSerializer(promotion).data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) # ==================== FACULTY WORKLOAD VIEWS ==================== @@ -1045,7 +498,7 @@ def get(self, request): return Response(serializer.data) def post(self, request): - workload = calculate_faculty_workload( + workload = hr2_services.calculate_faculty_workload( request.user.extra_info, request.data.get('semester'), request.data.get('year') @@ -1053,9 +506,7 @@ def post(self, request): serializer = FacultyWorkloadSerializer(workload) return Response(serializer.data) -from ..models import LTCApplicationNew, CPDAAdvanceNew, CPDAReimbursementNew, AppraisalFormNew -from ..services import apply_ltc, approve_ltc, reject_ltc, apply_cpda_advance, approve_cpda_advance, reject_cpda_advance, apply_cpda_reimbursement, approve_cpda_reimbursement, reject_cpda_reimbursement, submit_appraisal, review_appraisal -from ..selectors import get_ltc_applications, get_cpda_advances, get_cpda_reimbursements, get_appraisal_forms +from ..selectors import get_cpda_reimbursements from .serializers import LTCApplicationSerializer, CPDAAdvanceSerializer, CPDAReimbursementSerializer, AppraisalFormSerializer # ==================== LTC VIEWS ==================== @@ -1064,61 +515,44 @@ class LTCApplicationListCreateView(APIView): permission_classes = [IsAuthenticated] def get(self, request): - is_hr_staff = HoldsDesignation.objects.filter( - working=request.user, - designation__name__icontains='hr', - ).exists() or ( - request.user.extrainfo.user_type == 'staff' - and request.user.extrainfo.department - and request.user.extrainfo.department.name == 'HR' - ) - is_accountant = HoldsDesignation.objects.filter( - working=request.user, - designation__name__icontains='accountant', - ).exists() - - if is_hr_staff: - ltcs = LTCApplicationNew.objects.filter(approval_status__in=['PENDING', 'FORWARDED']) - elif is_accountant: - ltcs = LTCApplicationNew.objects.filter( - approval_status='FORWARDED', - accountant_status__iexact='PENDING', - ) - else: - ltcs = get_ltc_applications(request.user.extrainfo) + role_flags = hr2_selectors.get_role_flags(request.user) + ltcs = hr2_selectors.get_ltc_applications_for_role_view(request.user, role_flags) serializer = LTCApplicationSerializer(ltcs, many=True) return Response(serializer.data) def post(self, request): serializer = LTCApplicationSerializer(data=request.data) if serializer.is_valid(): - serializer.save(employee=request.user.extrainfo) - return Response(serializer.data, status=status.HTTP_201_CREATED) + ltc = hr2_services.create_ltc_application( + request.user.extrainfo, + serializer.validated_data, + ) + return Response(LTCApplicationSerializer(ltc).data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) class LTCApplicationDetailView(APIView): permission_classes = [IsAuthenticated] def get(self, request, pk): - ltc = get_object_or_404(LTCApplicationNew, pk=pk) + ltc = hr2_selectors.get_ltc_application_by_id_or_404(pk) serializer = LTCApplicationSerializer(ltc) return Response(serializer.data) def put(self, request, pk): - ltc = get_object_or_404(LTCApplicationNew, pk=pk) + ltc = hr2_selectors.get_ltc_application_by_id_or_404(pk) if ltc.employee != request.user.extrainfo: return Response({'error': 'Not authorized'}, status=403) serializer = LTCApplicationSerializer(ltc, data=request.data, partial=True) if serializer.is_valid(): - serializer.save() - return Response(serializer.data) + updated = hr2_services.update_ltc_application(ltc, serializer.validated_data) + return Response(LTCApplicationSerializer(updated).data) return Response(serializer.errors, status=400) class LTCApplicationDownloadView(APIView): permission_classes = [IsAuthenticated] def get(self, request, pk): - ltc = get_object_or_404(LTCApplicationNew, pk=pk) + ltc = hr2_selectors.get_ltc_application_by_id_or_404(pk) if ltc.employee != request.user.extrainfo and not request.user.is_staff: return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) @@ -1149,15 +583,15 @@ class LTCApplicationWithdrawView(APIView): permission_classes = [IsAuthenticated] def post(self, request, pk): - ltc = get_object_or_404(LTCApplicationNew, pk=pk) - if ltc.employee != request.user.extrainfo: - return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) - if ltc.approval_status != 'PENDING': - return Response({'error': 'Only pending requests can be withdrawn.'}, status=status.HTTP_400_BAD_REQUEST) - - ltc.approval_status = 'WITHDRAWN' - ltc.remarks = (request.data.get('remarks') or '').strip() - ltc.save(update_fields=['approval_status', 'remarks']) + ltc = hr2_selectors.get_ltc_application_by_id_or_404(pk) + try: + ltc = hr2_services.withdraw_ltc_application( + ltc, + request.user, + request.data.get('remarks'), + ) + except (ValidationError, PermissionError) as exc: + return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST) serializer = LTCApplicationSerializer(ltc) return Response(serializer.data) @@ -1165,26 +599,15 @@ class LTCApproveRejectView(APIView): permission_classes = [IsAuthenticated] def post(self, request, pk, decision): - ltc = get_object_or_404(LTCApplicationNew, pk=pk) - remarks = request.data.get('remarks', '') - decision = (decision or '').lower() - if decision not in ['approve', 'reject', 'forward']: - return Response({'error': 'Invalid decision'}, status=status.HTTP_400_BAD_REQUEST) - - if decision == 'approve': - ltc.approval_status = 'APPROVED' - ltc.accountant_status = 'APPROVED' - elif decision == 'forward': - ltc.approval_status = 'FORWARDED' - ltc.verified_by_hr = True - ltc.accountant_status = 'PENDING' - else: - ltc.approval_status = 'REJECTED' - ltc.accountant_status = 'REJECTED' - - ltc.remarks = remarks - ltc.save(update_fields=['approval_status', 'remarks', 'verified_by_hr', 'accountant_status']) - + ltc = hr2_selectors.get_ltc_application_by_id_or_404(pk) + try: + ltc = hr2_services.decide_ltc_application( + ltc, + decision, + request.data.get('remarks', ''), + ) + except ValidationError as exc: + return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST) serializer = LTCApplicationSerializer(ltc) return Response(serializer.data) @@ -1193,50 +616,24 @@ def post(self, request, pk, decision): class CPDAAdvanceListCreateView(APIView): permission_classes = [IsAuthenticated] def get(self, request): - is_hr_staff = HoldsDesignation.objects.filter( - working=request.user, - designation__name__icontains='hr', - ).exists() or ( - request.user.extrainfo.user_type == 'staff' - and request.user.extrainfo.department - and request.user.extrainfo.department.name == 'HR' - ) - is_accountant = HoldsDesignation.objects.filter( - working=request.user, - designation__name__icontains='accountant', - ).exists() - is_director = HoldsDesignation.objects.filter( - working=request.user, - designation__name__icontains='director', - ).exists() - - if is_director: - advances = CPDAAdvanceNew.objects.filter( - approval_status='FORWARDED', - accountant_processing_status__iexact='DIRECTOR_REVIEW', - ) - elif is_hr_staff: - advances = CPDAAdvanceNew.objects.filter(approval_status='PENDING') - elif is_accountant: - advances = CPDAAdvanceNew.objects.filter( - approval_status='FORWARDED', - accountant_processing_status__in=['PENDING', 'DIRECTOR_APPROVED'], - ) - else: - advances = get_cpda_advances(request.user.extrainfo) + role_flags = hr2_selectors.get_role_flags(request.user) + advances = hr2_selectors.get_cpda_advances_for_role_view(request.user, role_flags) serializer = CPDAAdvanceSerializer(advances, many=True) return Response(serializer.data) def post(self, request): serializer = CPDAAdvanceSerializer(data=request.data) if serializer.is_valid(): - serializer.save(employee=request.user.extrainfo) - return Response(serializer.data, status=status.HTTP_201_CREATED) + cpda = hr2_services.create_cpda_advance( + request.user.extrainfo, + serializer.validated_data, + ) + return Response(CPDAAdvanceSerializer(cpda).data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) class CPDAAdvanceDetailView(APIView): permission_classes = [IsAuthenticated] def get(self, request, pk): - cpda = get_object_or_404(CPDAAdvanceNew, pk=pk) + cpda = hr2_selectors.get_cpda_advance_by_id_or_404(pk) serializer = CPDAAdvanceSerializer(cpda) return Response(serializer.data) @@ -1244,7 +641,7 @@ class CPDAAdvanceDownloadView(APIView): permission_classes = [IsAuthenticated] def get(self, request, pk): - cpda = get_object_or_404(CPDAAdvanceNew, pk=pk) + cpda = hr2_selectors.get_cpda_advance_by_id_or_404(pk) if cpda.employee != request.user.extrainfo and not request.user.is_staff: return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) @@ -1275,46 +672,31 @@ class CPDAAdvanceWithdrawView(APIView): permission_classes = [IsAuthenticated] def post(self, request, pk): - cpda = get_object_or_404(CPDAAdvanceNew, pk=pk) - if cpda.employee != request.user.extrainfo: - return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) - if cpda.approval_status != 'PENDING': - return Response({'error': 'Only pending requests can be withdrawn.'}, status=status.HTTP_400_BAD_REQUEST) - - cpda.approval_status = 'WITHDRAWN' - cpda.remarks = (request.data.get('remarks') or '').strip() - cpda.save(update_fields=['approval_status', 'remarks']) + cpda = hr2_selectors.get_cpda_advance_by_id_or_404(pk) + try: + cpda = hr2_services.withdraw_cpda_advance( + cpda, + request.user, + request.data.get('remarks'), + ) + except (ValidationError, PermissionError) as exc: + return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST) serializer = CPDAAdvanceSerializer(cpda) return Response(serializer.data) class CPDAAdvanceApproveRejectView(APIView): permission_classes = [IsAuthenticated] def post(self, request, pk, decision): - cpda = get_object_or_404(CPDAAdvanceNew, pk=pk) - remarks = request.data.get('remarks', '') - decision = (decision or '').lower() - if decision not in ['approve', 'reject', 'forward-accountant', 'forward-director']: - return Response({'error': 'Invalid decision'}, status=status.HTTP_400_BAD_REQUEST) - - if decision == 'forward-accountant': - cpda.approval_status = 'FORWARDED' - cpda.verified_by_hr = True - cpda.accountant_processing_status = 'PENDING' - elif decision == 'forward-director': - cpda.approval_status = 'FORWARDED' - cpda.accountant_processing_status = 'DIRECTOR_REVIEW' - elif decision == 'approve': - if cpda.accountant_processing_status == 'DIRECTOR_REVIEW': - cpda.accountant_processing_status = 'DIRECTOR_APPROVED' - cpda.approval_status = 'FORWARDED' - else: - cpda.approval_status = 'APPROVED' - cpda.accountant_processing_status = 'APPROVED' - else: - cpda.approval_status = 'REJECTED' - cpda.accountant_processing_status = 'REJECTED' - cpda.remarks = remarks - cpda.save(update_fields=['approval_status', 'remarks', 'verified_by_hr', 'accountant_processing_status']) + cpda = hr2_selectors.get_cpda_advance_by_id_or_404(pk) + try: + cpda = hr2_services.decide_cpda_advance( + cpda, + request.user, + decision, + request.data.get('remarks', ''), + ) + except (ValidationError, PermissionError) as exc: + return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST) serializer = CPDAAdvanceSerializer(cpda) return Response(serializer.data) @@ -1329,26 +711,33 @@ def get(self, request): def post(self, request): serializer = CPDAReimbursementSerializer(data=request.data) if serializer.is_valid(): - serializer.save(employee=request.user.extrainfo) - return Response(serializer.data, status=status.HTTP_201_CREATED) + reimbursement = hr2_services.create_cpda_reimbursement( + request.user.extrainfo, + serializer.validated_data, + ) + return Response(CPDAReimbursementSerializer(reimbursement).data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) class CPDAReimbursementDetailView(APIView): permission_classes = [IsAuthenticated] def get(self, request, pk): - reim = get_object_or_404(CPDAReimbursementNew, pk=pk) + reim = hr2_selectors.get_cpda_reimbursement_by_id_or_404(pk) serializer = CPDAReimbursementSerializer(reim) return Response(serializer.data) class CPDAReimbursementApproveRejectView(APIView): permission_classes = [IsAuthenticated] def post(self, request, pk, decision): - reim = get_object_or_404(CPDAReimbursementNew, pk=pk) - remarks = request.data.get('remarks', '') - if decision == 'approve': - reim = approve_cpda_reimbursement(reim, request.user.extrainfo, remarks) - else: - reim = reject_cpda_reimbursement(reim, request.user.extrainfo, remarks) + reim = hr2_selectors.get_cpda_reimbursement_by_id_or_404(pk) + try: + reim = hr2_services.decide_cpda_reimbursement( + reim, + decision, + request.user.extrainfo, + request.data.get('remarks', ''), + ) + except ValidationError as exc: + return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST) serializer = CPDAReimbursementSerializer(reim) return Response(serializer.data) @@ -1357,46 +746,24 @@ def post(self, request, pk, decision): class AppraisalFormListCreateView(APIView): permission_classes = [IsAuthenticated] def get(self, request): - is_hr_staff = HoldsDesignation.objects.filter( - working=request.user, - designation__name__icontains='hr', - ).exists() or ( - request.user.extrainfo.user_type == 'staff' - and request.user.extrainfo.department - and request.user.extrainfo.department.name == 'HR' - ) - is_hod = HoldsDesignation.objects.filter( - working=request.user, - designation__name__icontains='hod', - ).exists() - is_director = HoldsDesignation.objects.filter( - working=request.user, - designation__name__icontains='director', - ).exists() - - if is_hr_staff: - appraisals = AppraisalFormNew.objects.all() - elif is_director: - appraisals = AppraisalFormNew.objects.filter(status='REVIEWED') - elif is_hod: - appraisals = AppraisalFormNew.objects.filter( - department=request.user.extrainfo.department.name - ) - else: - appraisals = get_appraisal_forms(request.user.extrainfo) + role_flags = hr2_selectors.get_role_flags(request.user) + appraisals = hr2_selectors.get_appraisal_forms_for_role_view(request.user, role_flags) serializer = AppraisalFormSerializer(appraisals, many=True) return Response(serializer.data) def post(self, request): serializer = AppraisalFormSerializer(data=request.data) if serializer.is_valid(): - serializer.save(employee=request.user.extrainfo) - return Response(serializer.data, status=status.HTTP_201_CREATED) + appraisal = hr2_services.create_appraisal_form( + request.user.extrainfo, + serializer.validated_data, + ) + return Response(AppraisalFormSerializer(appraisal).data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) class AppraisalFormDetailView(APIView): permission_classes = [IsAuthenticated] def get(self, request, pk): - appraisal = get_object_or_404(AppraisalFormNew, pk=pk) + appraisal = hr2_selectors.get_appraisal_form_by_id_or_404(pk) serializer = AppraisalFormSerializer(appraisal) return Response(serializer.data) @@ -1404,7 +771,7 @@ class AppraisalFormDownloadView(APIView): permission_classes = [IsAuthenticated] def get(self, request, pk): - appraisal = get_object_or_404(AppraisalFormNew, pk=pk) + appraisal = hr2_selectors.get_appraisal_form_by_id_or_404(pk) if appraisal.employee != request.user.extrainfo and not request.user.is_staff: return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) @@ -1429,24 +796,37 @@ def get(self, request, pk): class AppraisalReviewView(APIView): permission_classes = [IsAuthenticated] def post(self, request, pk): - appraisal = get_object_or_404(AppraisalFormNew, pk=pk) + appraisal = hr2_selectors.get_appraisal_form_by_id_or_404(pk) action = (request.data.get('action') or 'review').lower() - remarks = request.data.get('remarks', '') - rating = request.data.get('rating', '') - - appraisal.reviewer_id = str(request.user.extrainfo.id) - appraisal.reviewer_comments = remarks - if rating: - appraisal.rating = str(rating) + try: + appraisal = hr2_services.review_appraisal_form( + appraisal, + request.user, + action, + request.data.get('remarks', ''), + request.data.get('rating', ''), + ) + except (ValidationError, PermissionError) as exc: + return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST) + serializer = AppraisalFormSerializer(appraisal) + return Response(serializer.data) - if action == 'approve': - appraisal.status = 'APPROVED' - elif action == 'forward': - appraisal.status = 'REVIEWED' - else: - appraisal.status = 'REVIEWED' +class AppraisalAssignView(APIView): + permission_classes = [IsAuthenticated] - appraisal.save(update_fields=['reviewer_id', 'reviewer_comments', 'rating', 'status']) + def post(self, request, pk): + appraisal = hr2_selectors.get_appraisal_form_by_id_or_404(pk) + role = (request.data.get('role') or '').upper() + reviewer_id = (request.data.get('reviewer_id') or '').strip() + try: + appraisal = hr2_services.assign_appraisal_reviewer( + appraisal, + request.user, + role, + reviewer_id, + ) + except (ValidationError, PermissionError) as exc: + return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST) serializer = AppraisalFormSerializer(appraisal) return Response(serializer.data) diff --git a/FusionIIIT/applications/hr2/management/commands/convert_vl_to_earned.py b/FusionIIIT/applications/hr2/management/commands/convert_vl_to_earned.py index bd1ea05f4..cab441c09 100644 --- a/FusionIIIT/applications/hr2/management/commands/convert_vl_to_earned.py +++ b/FusionIIIT/applications/hr2/management/commands/convert_vl_to_earned.py @@ -1,10 +1,5 @@ -import datetime -from decimal import Decimal, ROUND_HALF_UP - from django.core.management.base import BaseCommand - -from applications.globals.models import ExtraInfo -from applications.hr2.models import EmployeeLeaveBalance, LeaveType +from applications.hr2.services import convert_vl_to_earned class Command(BaseCommand): @@ -25,82 +20,22 @@ def add_arguments(self, parser): ) def handle(self, *args, **options): - source_year = options["year"] - target_year = source_year + 1 - dry_run = options.get("dry_run", False) - - vl_type = LeaveType.objects.filter(code__iexact="VL").first() or LeaveType.objects.filter(name__iexact="Vacation").first() - el_type = LeaveType.objects.filter(code__iexact="EL").first() or LeaveType.objects.filter(name__iexact="Earned").first() - - if not vl_type or not el_type: - self.stderr.write(self.style.ERROR("Leave types VL/Earned not found. Ensure LeaveType records exist.")) - return - - all_employees = ExtraInfo.objects.all() - converted_count = 0 - total_converted = Decimal("0.0") - - next_year_defaults = { - "CL": Decimal("8.0"), - "RL": Decimal("2.0"), - "VL": Decimal("60.0"), - } - leave_types = {lt.code.upper(): lt for lt in LeaveType.objects.all() if lt.code} - - for employee in all_employees: - is_faculty = employee.user_type == "faculty" - converted = Decimal("0.0") - - vl_balance = EmployeeLeaveBalance.objects.filter( - employee=employee, - leave_type=vl_type, - year=source_year, - ).first() - if is_faculty and vl_balance: - vl_current = Decimal(str(vl_balance.current_balance or 0)) - if vl_current > 0: - converted = (vl_current / Decimal("2")).quantize(Decimal("0.1"), rounding=ROUND_HALF_UP) - if not dry_run: - vl_balance.current_balance = Decimal("0.0") - vl_balance.save(update_fields=["current_balance"]) - if converted > 0: - converted_count += 1 - total_converted += converted - - if dry_run: - continue - - for code, leave_type in leave_types.items(): - if code == "EL": - opening = Decimal("0.0") - accrued = converted - current = converted - elif code in next_year_defaults: - opening = next_year_defaults[code] - accrued = Decimal("0.0") - current = opening - else: - opening = Decimal("0.0") - accrued = Decimal("0.0") - current = Decimal("0.0") + result = convert_vl_to_earned( + source_year=options.get("year"), + dry_run=options.get("dry_run", False), + ) - EmployeeLeaveBalance.objects.update_or_create( - employee=employee, - leave_type=leave_type, - year=target_year, - defaults={ - "opening_balance": opening, - "accrued": accrued, - "availed": Decimal("0.0"), - "current_balance": current, - }, + if result["dry_run"]: + self.stdout.write( + self.style.WARNING( + "Dry run: would convert VL to EL for " + f"{result['converted_count']} faculty (total EL added: {result['total_converted']})" ) - - if dry_run: - self.stdout.write(self.style.WARNING( - f"Dry run: would convert VL to EL for {converted_count} faculty (total EL added: {total_converted})" - )) + ) else: - self.stdout.write(self.style.SUCCESS( - f"Converted VL to EL for {converted_count} faculty (total EL added: {total_converted})" - )) + self.stdout.write( + self.style.SUCCESS( + "Converted VL to EL for " + f"{result['converted_count']} faculty (total EL added: {result['total_converted']})" + ) + ) diff --git a/FusionIIIT/applications/hr2/management/commands/seed_hr2.py b/FusionIIIT/applications/hr2/management/commands/seed_hr2.py index 8d53650d6..b24809d39 100644 --- a/FusionIIIT/applications/hr2/management/commands/seed_hr2.py +++ b/FusionIIIT/applications/hr2/management/commands/seed_hr2.py @@ -1,138 +1,14 @@ -from decimal import Decimal - -from django.contrib.auth import get_user_model from django.core.management.base import BaseCommand -from django.utils import timezone - -from applications.globals.models import DepartmentInfo, Designation, ExtraInfo, HoldsDesignation, ModuleAccess -from applications.hr2.models import Employee, EmployeeLeaveBalance, LeaveType - -try: - from notifications.signals import notify -except ImportError: # pragma: no cover - optional dependency - notify = None +from applications.hr2.services import seed_hr2_demo_data class Command(BaseCommand): help = "Seed HR2 demo data (employee, access, leave balance)." def handle(self, *args, **options): - User = get_user_model() - now = timezone.now() - - department, _ = DepartmentInfo.objects.get_or_create(name="Computer Science") - - designation, _ = Designation.objects.get_or_create( - name="Faculty", - defaults={"full_name": "Faculty", "type": "academic"}, - ) - - module_access, _ = ModuleAccess.objects.get_or_create(designation="Faculty") - if not module_access.hr: - module_access.hr = True - module_access.save() - - user, created = User.objects.get_or_create( - username="rahul123", - defaults={ - "first_name": "Rahul", - "last_name": "Sharma", - "email": "rahul.sharma@iiitdmj.ac.in", - }, - ) - if created: - user.set_password("user@123") - user.save() - else: - user.email = "rahul.sharma@iiitdmj.ac.in" - user.first_name = user.first_name or "Rahul" - user.last_name = user.last_name or "Sharma" - user.set_password("user@123") - user.save() - - extra_info, _ = ExtraInfo.objects.get_or_create( - id="EMP001", - defaults={ - "user": user, - "title": "Dr.", - "sex": "M", - "date_of_birth": "1990-05-12", - "user_status": "PRESENT", - "address": "IIITDMJ Campus", - "phone_no": 9876543210, - "user_type": "faculty", - "department": department, - "about_me": "Faculty member", - "last_selected_role": "Faculty", - }, - ) - if extra_info.user_id != user.id: - extra_info.user = user - extra_info.department = department - extra_info.phone_no = 9876543210 - extra_info.last_selected_role = "Faculty" - extra_info.save() - - HoldsDesignation.objects.get_or_create( - user=user, - working=user, - designation=designation, - ) - - Employee.objects.get_or_create( - id=user, - defaults={ - "father_name": "Rajesh Sharma", - "mother_name": "Sunita Sharma", - "category": "General", - "caste": "N/A", - "home_state": "Madhya Pradesh", - "home_district": "Jabalpur", - "full_address": "IIITDMJ Campus, Dumna Airport Road", - "date_of_joining": "2021-08-01", - "date_of_birth": "1990-05-12", - "blood_group": "O+", - "phone_number": "9876543210", - "personal_email": "rahul.sharma@iiitdmj.ac.in", - "emergency_contact_number": "9876543211", - "emergency_contact_name": "Rajesh Sharma", - "employee_type": "Faculty", - }, - ) - - leave_type_map = { - "Casual": ("CL", Decimal("10")), - "Earned": ("EL", Decimal("18")), - "Medical": ("ML", Decimal("12")), - "Restricted": ("RL", Decimal("5")), - "Vacation": ("VL", Decimal("25")), - "Sabbatical": ("SL", Decimal("0")), - } - - current_year = now.year - for name, (code, balance) in leave_type_map.items(): - leave_type, _ = LeaveType.objects.get_or_create( - name=name, - defaults={"code": code, "is_active": True}, + result = seed_hr2_demo_data() + self.stdout.write( + self.style.SUCCESS( + f"HR2 seed data created/updated for employee {result['employee_id']}." ) - EmployeeLeaveBalance.objects.get_or_create( - employee=extra_info, - leave_type=leave_type, - year=current_year, - defaults={ - "opening_balance": balance, - "accrued": Decimal("0"), - "availed": Decimal("0"), - "current_balance": balance, - }, - ) - - if notify: - notify.send( - sender=user, - recipient=user, - verb="Welcome to HR Portal", - description="Welcome to HR Portal", - ) - - self.stdout.write(self.style.SUCCESS("HR2 seed data created/updated.")) + ) diff --git a/FusionIIIT/applications/hr2/management/commands/seed_hr_demo.py b/FusionIIIT/applications/hr2/management/commands/seed_hr_demo.py index 53fc84c8a..de4a9842d 100644 --- a/FusionIIIT/applications/hr2/management/commands/seed_hr_demo.py +++ b/FusionIIIT/applications/hr2/management/commands/seed_hr_demo.py @@ -1,530 +1,14 @@ -import json -import datetime - -from django.contrib.auth.models import User from django.core.management.base import BaseCommand -from django.db import transaction - -from applications.globals.models import DepartmentInfo, Designation, ExtraInfo, HoldsDesignation -from applications.hr2.models import ( - AppraisalFormNew, - CPDAAdvanceNew, - EmployeeCategory, - EmployeeDetailsExtended, - EmployeeLeaveBalance, - LeaveApplicationNew, - LeaveType, - LTCApplicationNew, -) +from applications.hr2.services import seed_hr_demo_data class Command(BaseCommand): help = "Seed HR demo data for form testing." - @staticmethod - def _parse_date(value): - if not value: - return None - return datetime.date.fromisoformat(value) - - @staticmethod - def _parse_gender(value): - if not value: - return "M" - value = value.strip().lower() - if value.startswith("f"): - return "F" - if value.startswith("m"): - return "M" - return "O" - - @staticmethod - def _split_name(full_name): - if not full_name: - return "", "" - parts = full_name.strip().split() - if len(parts) == 1: - return parts[0], "" - return parts[0], " ".join(parts[1:]) - def handle(self, *args, **options): - departments = [ - "Computer Science and Engineering", - "Administration", - "Finance", - "Director Office", - ] - - employees = [ - { - "employee_id": "EMP1001", - "name": "Rahul Sharma", - "email": "rahul.sharma@iiitdmj.ac.in", - "phone": "9876543210", - "gender": "Male", - "dob": "1990-05-12", - "department": "Computer Science and Engineering", - "designation": "Assistant Professor", - "role": "Employee", - "employment_type": "Permanent", - "date_of_joining": "2021-08-01", - "reporting_to": "EMP1002", - "status": "Active", - }, - { - "employee_id": "EMP1007", - "name": "Dr. Anjali Mehta", - "email": "anjali.mehta@iiitdmj.ac.in", - "phone": "9876543216", - "gender": "Female", - "dob": "1985-11-08", - "department": "Computer Science and Engineering", - "designation": "Professor", - "role": "Employee", - "employment_type": "Permanent", - "date_of_joining": "2016-07-20", - "reporting_to": "EMP1002", - "status": "Active", - }, - { - "employee_id": "EMP1002", - "name": "Dr. Anil Kumar", - "email": "anil.kumar@iiitdmj.ac.in", - "phone": "9876543211", - "gender": "Male", - "dob": "1980-07-20", - "department": "Computer Science and Engineering", - "designation": "Professor and HOD", - "role": "HOD", - "employment_type": "Permanent", - "date_of_joining": "2015-06-15", - "reporting_to": "EMP1003", - "status": "Active", - }, - { - "employee_id": "EMP1003", - "name": "Dr. Meena Verma", - "email": "director@iiitdmj.ac.in", - "phone": "9876543212", - "gender": "Female", - "dob": "1975-02-11", - "department": "Director Office", - "designation": "Director", - "role": "Director", - "employment_type": "Permanent", - "date_of_joining": "2019-01-10", - "reporting_to": None, - "status": "Active", - }, - { - "employee_id": "EMP1004", - "name": "Suresh Verma", - "email": "registrar@iiitdmj.ac.in", - "phone": "9876543213", - "gender": "Male", - "dob": "1982-03-10", - "department": "Administration", - "designation": "Registrar", - "role": "Registrar", - "employment_type": "Permanent", - "date_of_joining": "2018-01-15", - "reporting_to": "EMP1003", - "status": "Active", - }, - { - "employee_id": "EMP1005", - "name": "Priya Nair", - "email": "hr.admin@iiitdmj.ac.in", - "phone": "9876543214", - "gender": "Female", - "dob": "1987-09-25", - "department": "Administration", - "designation": "HR Administrator", - "role": "HR Admin", - "employment_type": "Permanent", - "date_of_joining": "2020-11-05", - "reporting_to": "EMP1004", - "status": "Active", - }, - { - "employee_id": "EMP1006", - "name": "Arun Joshi", - "email": "accountant@iiitdmj.ac.in", - "phone": "9876543215", - "gender": "Male", - "dob": "1985-12-18", - "department": "Finance", - "designation": "Accountant", - "role": "Accountant", - "employment_type": "Permanent", - "date_of_joining": "2019-08-12", - "reporting_to": "EMP1004", - "status": "Active", - }, - ] - - users = [ - { - "linked_employee_id": "EMP1001", - "username": "rahul1001", - "password": "rahul123", - }, - { - "linked_employee_id": "EMP1007", - "username": "anjali1007", - "password": "anjali123", - }, - { - "linked_employee_id": "EMP1002", - "username": "hod1002", - "password": "hod123", - }, - { - "linked_employee_id": "EMP1003", - "username": "director1003", - "password": "director123", - }, - { - "linked_employee_id": "EMP1004", - "username": "registrar1004", - "password": "registrar123", - }, - { - "linked_employee_id": "EMP1005", - "username": "hradmin1005", - "password": "hradmin123", - }, - { - "linked_employee_id": "EMP1006", - "username": "accountant1006", - "password": "accountant123", - }, - ] - - leave_balance = { - "employee_id": "EMP1001", - "casual_leave": 10, - "restricted_leave": 5, - "medical_leave": 12, - "earned_leave": 18, - "vacation_leave": 20, - "sabbatical_leave": 0, - } - - leave_request = { - "employee_id": "EMP1001", - "employee_name": "Rahul Sharma", - "department": "Computer Science and Engineering", - "designation": "Assistant Professor", - "leave_type": "Casual", - "start_date": "2026-04-10", - "end_date": "2026-04-12", - "total_days": 3, - "reason": "Personal work", - "contact_during_leave": "9876543210", - "address_during_leave": "Jabalpur, MP", - "handover_notes": "Classes handed over to Dr. X", - "attachment_file": "", - "leave_balance_before": 10, - "leave_balance_after": 7, - "approval_status": "PENDING", - "current_approver_role": "HOD", - "remarks": "", - } - - appraisal_request = { - "employee_id": "EMP1001", - "employee_name": "Rahul Sharma", - "department": "Computer Science and Engineering", - "designation": "Assistant Professor", - "appraisal_year": "2025-2026", - "self_summary": "Completed teaching and research responsibilities effectively.", - "teaching_performance": "Good", - "research_work": "Worked on 2 projects", - "publications": "1 journal paper", - "trainings_attended": "AI workshop", - "administrative_contributions": "Exam coordination", - "goals_achieved": "Completed syllabus and guided students", - "future_goals": "Publish more papers", - "reviewer_id": "EMP1002", - "status": "PENDING", - "remarks": "", - } - - ltc_request = { - "employee_id": "EMP1001", - "employee_name": "Rahul Sharma", - "department": "Computer Science and Engineering", - "designation": "Assistant Professor", - "ltc_block_year": "2024-2027", - "travel_start_date": "2026-05-05", - "travel_end_date": "2026-05-12", - "destination": "Delhi", - "purpose_of_travel": "Family travel", - "family_members": [ - {"name": "Priya Sharma", "relationship": "Spouse"} - ], - "travel_mode": "Train", - "ticket_number": "IRCTC12345", - "ticket_cost": 12000, - "accommodation_cost": 8000, - "other_expenses": 2000, - "total_amount_claimed": 22000, - "tickets_upload": "", - "bills_upload": "", - "previous_ltc_used": True, - "last_ltc_date": "2023-06-15", - "verified_by_hr": False, - "approval_status": "PENDING", - "accountant_status": "Not Started", - "remarks": "", - } - - cpda_request = { - "employee_id": "EMP1001", - "employee_name": "Rahul Sharma", - "department": "Computer Science and Engineering", - "designation": "Assistant Professor", - "event_name": "National Conference on AI", - "event_type": "Conference", - "organized_by": "IIT Delhi", - "venue": "New Delhi", - "start_date": "2026-06-20", - "end_date": "2026-06-22", - "registration_fee": 5000, - "travel_expense": 8000, - "accommodation_expense": 6000, - "other_expenses": 1000, - "total_amount": 20000, - "purpose_of_attending": "Present paper and improve research skills", - "benefits_to_institution": "Research development and academic exposure", - "invitation_letter": "", - "receipts": "", - "certificates": "", - "verified_by_hr": False, - "approval_status": "PENDING", - "accountant_processing_status": "Not Started", - "remarks": "", - } - - with transaction.atomic(): - for name in departments: - DepartmentInfo.objects.get_or_create(name=name) - - teaching_category, _ = EmployeeCategory.objects.get_or_create( - name="Teaching", defaults={"category_type": "TEACHING"} - ) - non_teaching_category, _ = EmployeeCategory.objects.get_or_create( - name="Non-Teaching", defaults={"category_type": "NON_TEACHING"} - ) - - user_lookup = {item["linked_employee_id"]: item for item in users} - - for employee in employees: - user_info = user_lookup.get(employee["employee_id"], {}) - username = user_info.get("username") or employee["employee_id"].lower() - first_name, last_name = self._split_name(employee["name"]) - - user, created = User.objects.get_or_create( - username=username, - defaults={ - "email": employee["email"], - "first_name": first_name, - "last_name": last_name, - }, - ) - if created and user_info.get("password"): - user.set_password(user_info["password"]) - user.save() - - department_obj = DepartmentInfo.objects.get(name=employee["department"]) - - extra_info, _ = ExtraInfo.objects.get_or_create( - id=employee["employee_id"], - defaults={ - "user": user, - "sex": self._parse_gender(employee["gender"]), - "date_of_birth": self._parse_date(employee["dob"]), - "user_type": "faculty" - if employee["department"] == "Computer Science and Engineering" - else "staff", - "department": department_obj, - "phone_no": int(employee["phone"]), - "address": "", - }, - ) - - category = teaching_category if extra_info.user_type == "faculty" else non_teaching_category - EmployeeDetailsExtended.objects.get_or_create( - extra_info=extra_info, - defaults={ - "category": category, - "date_of_joining": self._parse_date(employee["date_of_joining"]), - "appointment_type": employee["employment_type"], - }, - ) - - designation_type = "academic" if extra_info.user_type == "faculty" else "administrative" - designation, _ = Designation.objects.get_or_create( - name=employee["designation"], - defaults={ - "full_name": employee["designation"], - "type": designation_type, - }, - ) - - HoldsDesignation.objects.get_or_create( - user=user, - working=user, - designation=designation, - ) - - leave_types = [ - ("Casual", "CL", leave_balance["casual_leave"]), - ("Restricted", "RL", leave_balance["restricted_leave"]), - ("Medical", "ML", leave_balance["medical_leave"]), - ("Earned", "EL", leave_balance["earned_leave"]), - ("Vacation", "VL", leave_balance["vacation_leave"]), - ("Sabbatical", "SL", leave_balance["sabbatical_leave"]), - ] - - for name, code, _value in leave_types: - LeaveType.objects.get_or_create( - name=name, - code=code, - defaults={"is_active": True}, - ) - - employee_user = ExtraInfo.objects.get(id=leave_balance["employee_id"]) - year = datetime.date.today().year - - for name, code, value in leave_types: - leave_type = LeaveType.objects.get(code=code) - EmployeeLeaveBalance.objects.update_or_create( - employee=employee_user, - leave_type=leave_type, - year=year, - defaults={ - "opening_balance": value, - "accrued": 0, - "availed": 0, - "current_balance": value, - }, - ) - - LeaveApplicationNew.objects.get_or_create( - employee=employee_user, - start_date=self._parse_date(leave_request["start_date"]), - end_date=self._parse_date(leave_request["end_date"]), - defaults={ - "employee_name": leave_request["employee_name"], - "department": leave_request["department"], - "designation": leave_request["designation"], - "leave_type": leave_request["leave_type"], - "total_days": leave_request["total_days"], - "reason": leave_request["reason"], - "contact_during_leave": leave_request["contact_during_leave"], - "address_during_leave": leave_request["address_during_leave"], - "handover_to": "Dr. X", - "handover_notes": leave_request["handover_notes"], - "medical_certificate": "", - "attachment_file": leave_request["attachment_file"], - "leave_balance_before": leave_request["leave_balance_before"], - "leave_balance_after": leave_request["leave_balance_after"], - "approval_status": leave_request["approval_status"], - "current_approver_role": leave_request["current_approver_role"], - "remarks": leave_request["remarks"], - }, + result = seed_hr_demo_data() + self.stdout.write( + self.style.SUCCESS( + f"HR demo data seeded for {result['employees_seeded']} employee(s)." ) - - AppraisalFormNew.objects.get_or_create( - employee=employee_user, - appraisal_year=appraisal_request["appraisal_year"], - defaults={ - "employee_name": appraisal_request["employee_name"], - "department": appraisal_request["department"], - "designation": appraisal_request["designation"], - "self_summary": appraisal_request["self_summary"], - "key_responsibilities": "Teaching, research, and academic mentoring.", - "achievements": appraisal_request["goals_achieved"], - "challenges_faced": "", - "teaching_performance": appraisal_request["teaching_performance"], - "research_work": appraisal_request["research_work"], - "publications": appraisal_request["publications"], - "projects_handled": "", - "administrative_contributions": appraisal_request["administrative_contributions"], - "trainings_attended": appraisal_request["trainings_attended"], - "certifications": "", - "workshops": "", - "goals_achieved": appraisal_request["goals_achieved"], - "future_goals": appraisal_request["future_goals"], - "supporting_documents": "", - "reviewer_id": appraisal_request["reviewer_id"], - "reviewer_comments": "", - "rating": "", - "status": appraisal_request["status"], - "remarks": appraisal_request["remarks"], - }, - ) - - block_year = int(ltc_request["ltc_block_year"].split("-")[0]) - LTCApplicationNew.objects.get_or_create( - employee=employee_user, - travel_start_date=self._parse_date(ltc_request["travel_start_date"]), - travel_end_date=self._parse_date(ltc_request["travel_end_date"]), - defaults={ - "employee_name": ltc_request["employee_name"], - "department": ltc_request["department"], - "designation": ltc_request["designation"], - "ltc_block_year": block_year, - "destination": ltc_request["destination"], - "purpose_of_travel": ltc_request["purpose_of_travel"], - "family_members": json.dumps(ltc_request["family_members"]), - "relationship_details": "Spouse", - "travel_mode": ltc_request["travel_mode"], - "ticket_number": ltc_request["ticket_number"], - "ticket_cost": ltc_request["ticket_cost"], - "accommodation_cost": ltc_request["accommodation_cost"], - "other_expenses": ltc_request["other_expenses"], - "total_amount_claimed": ltc_request["total_amount_claimed"], - "tickets_upload": ltc_request["tickets_upload"], - "bills_upload": ltc_request["bills_upload"], - "previous_ltc_used": ltc_request["previous_ltc_used"], - "last_ltc_date": self._parse_date(ltc_request["last_ltc_date"]), - "verified_by_hr": ltc_request["verified_by_hr"], - "approval_status": ltc_request["approval_status"], - "accountant_status": ltc_request["accountant_status"], - "remarks": ltc_request["remarks"], - }, - ) - - CPDAAdvanceNew.objects.get_or_create( - employee=employee_user, - start_date=self._parse_date(cpda_request["start_date"]), - end_date=self._parse_date(cpda_request["end_date"]), - defaults={ - "employee_name": cpda_request["employee_name"], - "department": cpda_request["department"], - "designation": cpda_request["designation"], - "event_name": cpda_request["event_name"], - "event_type": cpda_request["event_type"], - "organized_by": cpda_request["organized_by"], - "venue": cpda_request["venue"], - "registration_fee": cpda_request["registration_fee"], - "travel_expense": cpda_request["travel_expense"], - "accommodation_expense": cpda_request["accommodation_expense"], - "other_expenses": cpda_request["other_expenses"], - "total_amount": cpda_request["total_amount"], - "purpose_of_attending": cpda_request["purpose_of_attending"], - "benefits_to_institution": cpda_request["benefits_to_institution"], - "invitation_letter": cpda_request["invitation_letter"], - "receipts": cpda_request["receipts"], - "certificates": cpda_request["certificates"], - "verified_by_hr": cpda_request["verified_by_hr"], - "approval_status": cpda_request["approval_status"], - "accountant_processing_status": cpda_request["accountant_processing_status"], - "remarks": cpda_request["remarks"], - }, - ) - - self.stdout.write(self.style.SUCCESS("HR demo data seeded.")) + ) diff --git a/FusionIIIT/applications/hr2/management/commands/seed_leave_balance.py b/FusionIIIT/applications/hr2/management/commands/seed_leave_balance.py index 591c33ee7..2f2556427 100644 --- a/FusionIIIT/applications/hr2/management/commands/seed_leave_balance.py +++ b/FusionIIIT/applications/hr2/management/commands/seed_leave_balance.py @@ -1,28 +1,6 @@ -import datetime +from django.core.management.base import BaseCommand -from django.core.management.base import BaseCommand, CommandError - -from applications.globals.models import ExtraInfo -from applications.hr2.models import EmployeeLeaveBalance, LeaveType - - -DEFAULT_BALANCES = [ - ("Casual", "CL", 10), - ("Restricted", "RL", 5), - ("Medical", "ML", 12), - ("Earned", "EL", 18), - ("Vacation", "VL", 20), - ("Sabbatical", "SL", 0), -] - -ROLE_BALANCES = { - "EMP1002": {"CL": 12, "RL": 6, "ML": 15, "EL": 25, "VL": 30, "SL": 10}, - "EMP1003": {"CL": 15, "RL": 8, "ML": 20, "EL": 30, "VL": 35, "SL": 15}, - "EMP1004": {"CL": 12, "RL": 6, "ML": 15, "EL": 22, "VL": 28, "SL": 5}, - "EMP1005": {"CL": 10, "RL": 5, "ML": 12, "EL": 20, "VL": 25, "SL": 0}, - "EMP1006": {"CL": 10, "RL": 5, "ML": 12, "EL": 18, "VL": 22, "SL": 0}, - "EMP1007": {"CL": 12, "RL": 6, "ML": 15, "EL": 25, "VL": 30, "SL": 12}, -} +from applications.hr2.services import seed_leave_balances class Command(BaseCommand): @@ -43,39 +21,13 @@ def add_arguments(self, parser): ) def handle(self, *args, **options): - year = datetime.date.today().year + result = seed_leave_balances( + employee_id=options.get("employee_id"), + seed_all=options.get("seed_all"), + ) - for name, code, _value in DEFAULT_BALANCES: - LeaveType.objects.get_or_create( - name=name, - code=code, - defaults={"is_active": True}, + self.stdout.write( + self.style.SUCCESS( + f"Leave balances seeded for {result['seeded_count']} employee(s) (year {result['year']})." ) - - if options.get("seed_all"): - employees = ExtraInfo.objects.all() - else: - employee_id = options.get("employee_id") - try: - employees = [ExtraInfo.objects.get(id=employee_id)] - except ExtraInfo.DoesNotExist as exc: - raise CommandError(f"Employee not found: {employee_id}") from exc - - for employee in employees: - balance_map = ROLE_BALANCES.get(employee.id, {}) - for name, code, default_value in DEFAULT_BALANCES: - value = balance_map.get(code, default_value) - leave_type = LeaveType.objects.get(code=code) - EmployeeLeaveBalance.objects.update_or_create( - employee=employee, - leave_type=leave_type, - year=year, - defaults={ - "opening_balance": value, - "accrued": 0, - "availed": 0, - "current_balance": value, - }, - ) - - self.stdout.write(self.style.SUCCESS("Leave balances seeded.")) + ) diff --git a/FusionIIIT/applications/hr2/migrations/0010_appraisal_assignment.py b/FusionIIIT/applications/hr2/migrations/0010_appraisal_assignment.py new file mode 100644 index 000000000..1e6959469 --- /dev/null +++ b/FusionIIIT/applications/hr2/migrations/0010_appraisal_assignment.py @@ -0,0 +1,43 @@ +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + dependencies = [ + ("hr2", "0009_leave_resumption"), + ] + + operations = [ + migrations.AddField( + model_name="appraisalformnew", + name="assigned_reviewer_role", + field=models.CharField(blank=True, max_length=20), + ), + migrations.AddField( + model_name="appraisalformnew", + name="assigned_reviewer", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="assigned_appraisals_new", + to="globals.extrainfo", + ), + ), + migrations.AddField( + model_name="appraisalformnew", + name="assigned_by", + field=models.ForeignKey( + blank=True, + null=True, + on_delete=django.db.models.deletion.SET_NULL, + related_name="appraisal_assignments_made", + to="globals.extrainfo", + ), + ), + migrations.AddField( + model_name="appraisalformnew", + name="assigned_at", + field=models.DateTimeField(blank=True, null=True), + ), + ] diff --git a/FusionIIIT/applications/hr2/migrations/0011_leave_attachment_files.py b/FusionIIIT/applications/hr2/migrations/0011_leave_attachment_files.py new file mode 100644 index 000000000..f961e203e --- /dev/null +++ b/FusionIIIT/applications/hr2/migrations/0011_leave_attachment_files.py @@ -0,0 +1,20 @@ +from django.db import migrations, models + + +class Migration(migrations.Migration): + dependencies = [ + ("hr2", "0010_appraisal_assignment"), + ] + + operations = [ + migrations.AlterField( + model_name="leaveapplicationnew", + name="medical_certificate", + field=models.FileField(blank=True, null=True, upload_to="hr/leave/"), + ), + migrations.AlterField( + model_name="leaveapplicationnew", + name="attachment_file", + field=models.FileField(blank=True, null=True, upload_to="hr/leave/"), + ), + ] diff --git a/FusionIIIT/applications/hr2/models.py b/FusionIIIT/applications/hr2/models.py index d14a60269..8946574a4 100644 --- a/FusionIIIT/applications/hr2/models.py +++ b/FusionIIIT/applications/hr2/models.py @@ -614,8 +614,8 @@ class LeaveApplicationNew(models.Model): ) nominee_responded_at = models.DateTimeField(null=True, blank=True) - medical_certificate = models.CharField(max_length=200, blank=True) - attachment_file = models.CharField(max_length=200, blank=True) + medical_certificate = models.FileField(upload_to='hr/leave/', blank=True, null=True) + attachment_file = models.FileField(upload_to='hr/leave/', blank=True, null=True) applied_date = models.DateField(auto_now_add=True) leave_balance_before = models.DecimalField(max_digits=5, decimal_places=1, null=True, blank=True) @@ -964,6 +964,23 @@ class AppraisalFormNew(models.Model): status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='PENDING') remarks = models.TextField(blank=True) + assigned_reviewer_role = models.CharField(max_length=20, blank=True) + assigned_reviewer = models.ForeignKey( + ExtraInfo, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name='assigned_appraisals_new', + ) + assigned_by = models.ForeignKey( + ExtraInfo, + on_delete=models.SET_NULL, + null=True, + blank=True, + related_name='appraisal_assignments_made', + ) + assigned_at = models.DateTimeField(null=True, blank=True) + submitted_at = models.DateTimeField(auto_now_add=True) def __str__(self): diff --git a/FusionIIIT/applications/hr2/selectors.py b/FusionIIIT/applications/hr2/selectors.py index 0f8113ccb..a75e84ff5 100644 --- a/FusionIIIT/applications/hr2/selectors.py +++ b/FusionIIIT/applications/hr2/selectors.py @@ -1,11 +1,24 @@ from datetime import date + from django.db.models import Q +from django.http import Http404 + from applications.globals.models import ExtraInfo, HoldsDesignation from .models import ( - EmployeeLeaveBalance, LeaveApplicationNew, EmployeeAttendance, FacultyWorkload, - PerformanceAppraisalNew, AppraisalPeriod, TrainingProgram, TrainingNomination, - PromotionApplication, LTCApplicationNew, CPDAAdvanceNew, CPDAReimbursementNew, - AppraisalFormNew + AppraisalFormNew, + AppraisalPeriod, + CPDAAdvanceNew, + CPDAReimbursementNew, + EmployeeAttendance, + EmployeeLeaveBalance, + FacultyWorkload, + LeaveApplicationNew, + LeaveType, + LTCApplicationNew, + PerformanceAppraisalNew, + PromotionApplication, + TrainingNomination, + TrainingProgram, ) # ==================== EMPLOYEE SELECTORS ==================== @@ -13,6 +26,19 @@ def get_employee_by_id(employee_id): return ExtraInfo.objects.select_related('user', 'department').get(id=employee_id) + +def get_employee_by_id_optional(employee_id): + if not employee_id: + return None + return ExtraInfo.objects.filter(id=employee_id).select_related('user', 'department').first() + + +def get_employee_by_id_or_404(employee_id): + try: + return get_employee_by_id(employee_id) + except ExtraInfo.DoesNotExist as exc: + raise Http404(f"Employee not found: {employee_id}") from exc + def get_all_employees(employee_type=None, department_id=None): qs = ExtraInfo.objects.select_related('user', 'department') if employee_type: @@ -25,6 +51,50 @@ def get_employee_current_designation(employee_extra_info): held = HoldsDesignation.objects.filter(working=employee_extra_info).order_by('-held_at').first() return held.designation if held else None + +def get_employee_current_designation_for_user(user): + held = HoldsDesignation.objects.filter(working=user).order_by('-held_at').first() + return held.designation if held else None + + +def get_employee_for_user(user): + return ExtraInfo.objects.filter(user=user).select_related('user', 'department').first() + + +def get_leave_application_by_id_or_404(pk): + try: + return LeaveApplicationNew.objects.get(pk=pk) + except LeaveApplicationNew.DoesNotExist as exc: + raise Http404("Leave application not found") from exc + + +def get_ltc_application_by_id_or_404(pk): + try: + return LTCApplicationNew.objects.get(pk=pk) + except LTCApplicationNew.DoesNotExist as exc: + raise Http404("LTC application not found") from exc + + +def get_cpda_advance_by_id_or_404(pk): + try: + return CPDAAdvanceNew.objects.get(pk=pk) + except CPDAAdvanceNew.DoesNotExist as exc: + raise Http404("CPDA advance not found") from exc + + +def get_cpda_reimbursement_by_id_or_404(pk): + try: + return CPDAReimbursementNew.objects.get(pk=pk) + except CPDAReimbursementNew.DoesNotExist as exc: + raise Http404("CPDA reimbursement not found") from exc + + +def get_appraisal_form_by_id_or_404(pk): + try: + return AppraisalFormNew.objects.get(pk=pk) + except AppraisalFormNew.DoesNotExist as exc: + raise Http404("Appraisal form not found") from exc + # ==================== LEAVE SELECTORS ==================== def get_leave_balance_for_employee(employee_extra_info, leave_type, year=None): @@ -36,6 +106,63 @@ def get_leave_balance_for_employee(employee_extra_info, leave_type, year=None): year=year ) + +def get_leave_type_by_name(leave_type_name): + return LeaveType.objects.filter(name__iexact=leave_type_name).first() + + +def get_leave_balances_for_employee(employee): + return ( + EmployeeLeaveBalance.objects.filter(employee=employee) + .select_related('leave_type') + .order_by('leave_type_id', '-year', '-id') + ) + + +def get_latest_leave_balances_for_employee(employee): + balances = [] + seen_leave_types = set() + for balance in get_leave_balances_for_employee(employee): + if balance.leave_type_id in seen_leave_types: + continue + seen_leave_types.add(balance.leave_type_id) + balances.append(balance) + return balances + + +def get_leave_balance_for_employee_year(employee, leave_type, year): + return EmployeeLeaveBalance.objects.filter( + employee=employee, + leave_type=leave_type, + year=year, + ).first() + + +def get_latest_leave_balance_for_employee(employee, leave_type): + return EmployeeLeaveBalance.objects.filter( + employee=employee, + leave_type=leave_type, + ).order_by('-year').first() + + +def has_overlapping_leave(employee, start_date, end_date, exclude_id=None): + qs = LeaveApplicationNew.objects.filter( + employee=employee, + approval_status__in=['PENDING', 'FORWARDED', 'APPROVED'], + start_date__lte=end_date, + end_date__gte=start_date, + ) + if exclude_id is not None: + qs = qs.exclude(id=exclude_id) + return qs + + +def get_leave_applications_for_nominee(employee_id): + return LeaveApplicationNew.objects.filter( + handover_to=employee_id, + nominee_status='PENDING', + ).order_by('-applied_date') + def get_leave_applications(employee_extra_info, status=None, from_date=None, to_date=None): qs = LeaveApplicationNew.objects.filter(employee=employee_extra_info) if status: @@ -46,11 +173,60 @@ def get_leave_applications(employee_extra_info, status=None, from_date=None, to_ qs = qs.filter(end_date__lte=to_date) return qs.order_by('-applied_date') + +def get_leave_applications_for_role_view(user, role_flags): + if role_flags.get('is_hr_staff'): + return LeaveApplicationNew.objects.all() + if role_flags.get('is_director'): + return LeaveApplicationNew.objects.filter( + Q( + approval_status='FORWARDED', + current_approver_role__iexact='Director', + ) + | Q(employee=user.extrainfo) + | Q( + cancel_status='REQUESTED', + cancel_current_approver_role__iexact='Director', + ) + | Q( + extension_status='REQUESTED', + extension_current_approver_role__iexact='Director', + ) + ) + if role_flags.get('is_registrar'): + return LeaveApplicationNew.objects.filter( + Q( + approval_status='FORWARDED', + current_approver_role__iexact='Registrar', + ) + | Q(employee=user.extrainfo) + | Q( + cancel_status='REQUESTED', + cancel_current_approver_role__iexact='Registrar', + ) + | Q( + extension_status='REQUESTED', + extension_current_approver_role__iexact='Registrar', + ) + ) + if role_flags.get('is_hod'): + return LeaveApplicationNew.objects.filter( + department=user.extrainfo.department.name + ) + return get_leave_applications(user.extrainfo) + def get_pending_responsibility_leaves(employee_extra_info, responsibility_type='academic'): if responsibility_type == 'academic': return LeaveApplicationNew.objects.filter(employee=employee_extra_info, approval_status='PENDING') return LeaveApplicationNew.objects.filter(employee=employee_extra_info, approval_status='PENDING') + +def get_leave_applications_for_employee_and_status(employee_extra_info, status=None): + qs = LeaveApplicationNew.objects.filter(employee=employee_extra_info) + if status: + qs = qs.filter(approval_status=status) + return qs.order_by('-applied_date') + # ==================== ATTENDANCE SELECTORS ==================== def get_attendance_for_employee(employee_extra_info, from_date=None, to_date=None): @@ -69,6 +245,31 @@ def get_appraisal_periods(is_active=None): qs = qs.filter(is_active=is_active) return qs + +def get_appraisal_forms_for_role_view(user, role_flags): + if role_flags.get('is_hr_staff'): + return AppraisalFormNew.objects.all().order_by('-submitted_at') + if role_flags.get('is_director'): + return AppraisalFormNew.objects.filter( + assigned_reviewer_role__iexact='DIRECTOR', + ).filter( + Q(assigned_reviewer__isnull=True) + | Q(assigned_reviewer=user.extrainfo) + ).filter( + status__in=['PENDING', 'REVIEWED'] + ).order_by('-submitted_at') + if role_flags.get('is_hod'): + return AppraisalFormNew.objects.filter( + assigned_reviewer_role__iexact='HOD', + department=user.extrainfo.department.name, + ).filter( + Q(assigned_reviewer__isnull=True) + | Q(assigned_reviewer=user.extrainfo) + ).filter( + status='PENDING' + ).order_by('-submitted_at') + return get_appraisal_forms(user.extrainfo) + def get_appraisals_for_employee(employee_extra_info, period_id=None): qs = PerformanceAppraisalNew.objects.filter(employee=employee_extra_info).select_related('period') if period_id: @@ -109,6 +310,17 @@ def get_ltc_applications(employee_extra_info, status=None): qs = qs.filter(approval_status=status) return qs.order_by('-applied_date') + +def get_ltc_applications_for_role_view(user, role_flags): + if role_flags.get('is_hr_staff'): + return LTCApplicationNew.objects.filter(approval_status__in=['PENDING', 'FORWARDED']) + if role_flags.get('is_accountant'): + return LTCApplicationNew.objects.filter( + approval_status='FORWARDED', + accountant_status__iexact='PENDING', + ) + return get_ltc_applications(user.extrainfo) + # CPDA Advance def get_cpda_advances(employee_extra_info, status=None): qs = CPDAAdvanceNew.objects.filter(employee=employee_extra_info) @@ -116,6 +328,22 @@ def get_cpda_advances(employee_extra_info, status=None): qs = qs.filter(approval_status=status) return qs.order_by('-applied_date') + +def get_cpda_advances_for_role_view(user, role_flags): + if role_flags.get('is_director'): + return CPDAAdvanceNew.objects.filter( + approval_status='FORWARDED', + accountant_processing_status__iexact='DIRECTOR_REVIEW', + ) + if role_flags.get('is_hr_staff'): + return CPDAAdvanceNew.objects.filter(approval_status='PENDING') + if role_flags.get('is_accountant'): + return CPDAAdvanceNew.objects.filter( + approval_status='FORWARDED', + accountant_processing_status__in=['PENDING'], + ) + return get_cpda_advances(user.extrainfo) + # CPDA Reimbursement def get_cpda_reimbursements(employee_extra_info, status=None): qs = CPDAReimbursementNew.objects.filter(employee=employee_extra_info) @@ -128,4 +356,44 @@ def get_appraisal_forms(employee_extra_info, status=None): qs = AppraisalFormNew.objects.filter(employee=employee_extra_info) if status: qs = qs.filter(status=status) - return qs.order_by('-submitted_at') \ No newline at end of file + return qs.order_by('-submitted_at') + + +def get_reviewer_by_id(reviewer_id): + if not reviewer_id: + return None + return ExtraInfo.objects.filter(id=reviewer_id).first() + + +def get_role_flags(user): + return { + 'is_hr_staff': HoldsDesignation.objects.filter( + working=user, + designation__name__icontains='hr', + ).exists() or ( + hasattr(user, 'extrainfo') + and user.extrainfo.user_type == 'staff' + and user.extrainfo.department + and user.extrainfo.department.name == 'HR' + ), + 'is_hod': HoldsDesignation.objects.filter( + working=user, + designation__name__icontains='hod', + ).exists(), + 'is_director': HoldsDesignation.objects.filter( + working=user, + designation__name__icontains='director', + ).exists(), + 'is_registrar': HoldsDesignation.objects.filter( + working=user, + designation__name__icontains='registrar', + ).exists(), + 'is_accountant': HoldsDesignation.objects.filter( + working=user, + designation__name__icontains='accountant', + ).exists(), + 'is_hr_admin': HoldsDesignation.objects.filter( + working=user, + designation__name__iregex=r'hr admin|hr administrator', + ).exists(), + } \ No newline at end of file diff --git a/FusionIIIT/applications/hr2/services.py b/FusionIIIT/applications/hr2/services.py index 38bd2c07f..9be72ffbe 100644 --- a/FusionIIIT/applications/hr2/services.py +++ b/FusionIIIT/applications/hr2/services.py @@ -5,8 +5,41 @@ Full implementations will use actual Django models from models.py. """ -from datetime import date +import datetime +import json +from decimal import Decimal, ROUND_HALF_UP + +from django.contrib.auth import get_user_model from django.core.exceptions import ValidationError +from django.core.management.base import CommandError +from django.db import transaction +from django.utils import timezone + +from applications.globals.models import DepartmentInfo, Designation, ExtraInfo, HoldsDesignation, ModuleAccess +from applications.hr2.models import ( + AppraisalFormNew, + AppraisalPeriod, + CPDAAdvanceNew, + CPDAReimbursementNew, + Employee, + EmployeeAttendance, + EmployeeCategory, + EmployeeDetailsExtended, + EmployeeLeaveBalance, + FacultyWorkload, + LeaveApplicationNew, + LeaveType, + LTCApplicationNew, + PerformanceAppraisalNew, + PromotionApplication, + TrainingNomination, +) +from applications.hr2 import selectors as hr2_selectors + +try: + from notifications.signals import notify +except ImportError: # pragma: no cover - optional dependency + notify = None # ==================== CUSTOM EXCEPTIONS ==================== @@ -113,4 +146,1493 @@ def submit_appraisal(employee_extra_info, data): def review_appraisal(appraisal_id, reviewer_extra_info, reviewer_scores, reviewer_remarks): """Review appraisal - STUB IMPLEMENTATION.""" - raise NotImplementedError("Appraisal review service not yet fully implemented.") \ No newline at end of file + raise NotImplementedError("Appraisal review service not yet fully implemented.") + + +# ==================== API WRITE SERVICES ==================== + +def _update_instance_from_data(instance, data): + for key, value in data.items(): + setattr(instance, key, value) + instance.save() + return instance + + +def update_instance(instance, validated_data): + return _update_instance_from_data(instance, validated_data) + + +def create_leave_application(request_user, validated_data): + employee_id = (validated_data.get('employee_id') or '').strip() + employee = hr2_selectors.get_employee_for_user(request_user) + if employee is None and employee_id: + employee = hr2_selectors.get_employee_by_id_optional(employee_id) + if employee is None: + raise ValidationError({"employee_id": "Employee profile not found."}) + + nominee_id = (validated_data.get('nominee_employee_id') or '').strip() + nominee_status = 'PENDING' if nominee_id else 'NOT_REQUIRED' + + role_flags = hr2_selectors.get_role_flags(employee.user) + is_director = role_flags.get('is_director') + is_hod = role_flags.get('is_hod') + is_registrar = role_flags.get('is_registrar') + is_hr_admin = role_flags.get('is_hr_admin') + is_accountant = role_flags.get('is_accountant') + + leave_type_name = (validated_data.get('leave_type') or '').strip() + is_cl_rh_leave = leave_type_name in ['Casual', 'Restricted'] + + employee_name = employee.user.get_full_name() or employee.user.username + department_name = employee.department.name if employee.department else (validated_data.get('department') or '') + designation_name = '' + designation_record = hr2_selectors.get_employee_current_designation_for_user(employee.user) + if designation_record: + designation_name = designation_record.full_name or designation_record.name + else: + designation_name = validated_data.get('designation') or '' + + approval_status = 'PENDING' + approver_role = '' + if is_director: + approval_status = 'APPROVED' + approver_role = 'Director' + elif is_registrar: + approval_status = 'FORWARDED' + approver_role = 'Director' + elif is_hod: + if is_cl_rh_leave: + approval_status = 'PENDING' + approver_role = 'HOD' + else: + approval_status = 'FORWARDED' + approver_role = 'Director' + elif is_hr_admin or is_accountant: + approval_status = 'FORWARDED' + approver_role = 'Registrar' + + data = dict(validated_data) + data.pop('employee_id', None) + data.pop('nominee_employee_id', None) + leave_app = LeaveApplicationNew( + employee=employee, + employee_name=employee_name, + department=department_name, + designation=designation_name, + handover_to=nominee_id, + nominee_status=nominee_status, + approval_status=approval_status, + current_approver_role=approver_role, + **data, + ) + leave_app.save() + + if is_director: + apply_leave_balance_for_approval(leave_app) + leave_app.save(update_fields=['leave_balance_before', 'leave_balance_after']) + + return leave_app + + +def update_leave_application(leave_app, validated_data): + return _update_instance_from_data(leave_app, validated_data) + + +def withdraw_leave_application(leave_app, user, remarks): + if leave_app.employee != user.extrainfo: + raise PermissionError("Not authorized") + if leave_app.approval_status not in ['PENDING', 'FORWARDED']: + raise ValidationError({"approval_status": "Only pending or forwarded requests can be withdrawn."}) + + role_flags = hr2_selectors.get_role_flags(user) + if role_flags.get('is_registrar'): + leave_app.approval_status = 'REJECTED' + leave_app.current_approver_role = 'Registrar' + elif role_flags.get('is_accountant'): + leave_app.approval_status = 'REJECTED' + leave_app.current_approver_role = 'Accountant' + elif role_flags.get('is_hr_admin'): + leave_app.approval_status = 'REJECTED' + leave_app.current_approver_role = 'HR Admin' + else: + leave_app.approval_status = 'WITHDRAWN' + leave_app.current_approver_role = 'Employee' + + leave_app.remarks = (remarks or '').strip() + leave_app.save(update_fields=['approval_status', 'current_approver_role', 'remarks']) + return leave_app + + +def request_leave_cancellation(leave_app, user, reason): + if leave_app.employee != user.extrainfo: + raise PermissionError("Not authorized") + if leave_app.approval_status != 'APPROVED': + raise ValidationError({"approval_status": "Only approved requests can be cancelled."}) + if leave_app.cancel_status != 'NOT_REQUESTED': + raise ValidationError({"cancel_status": "Cancellation already processed or pending."}) + + today = timezone.now().date() + if today >= leave_app.start_date: + raise ValidationError({"start_date": "Cancellation allowed only up to 1 day prior to start date."}) + + role_flags = hr2_selectors.get_role_flags(user) + requester_role = 'Employee' + if role_flags.get('is_director'): + requester_role = 'Director' + elif role_flags.get('is_hod'): + requester_role = 'HOD' + elif role_flags.get('is_registrar'): + requester_role = 'Registrar' + elif role_flags.get('is_accountant'): + requester_role = 'Accountant' + elif role_flags.get('is_hr_admin'): + requester_role = 'HR Admin' + + cancel_approver_role = 'HOD' + if requester_role in ['HOD', 'Director', 'Registrar']: + cancel_approver_role = 'Director' + elif requester_role in ['Accountant', 'HR Admin']: + cancel_approver_role = 'Registrar' + + leave_app.cancel_status = 'REQUESTED' + leave_app.cancel_requested_at = timezone.now() + leave_app.cancel_requested_by_role = requester_role + leave_app.cancel_current_approver_role = cancel_approver_role + leave_app.cancel_reason = (reason or '').strip() + leave_app.save(update_fields=[ + 'cancel_status', + 'cancel_requested_at', + 'cancel_requested_by_role', + 'cancel_current_approver_role', + 'cancel_reason', + ]) + return leave_app + + +def decide_leave_cancellation(leave_app, user, decision, remarks): + decision = (decision or '').lower() + if decision not in ['approve', 'reject']: + raise ValidationError({"decision": "Invalid decision"}) + if leave_app.cancel_status != 'REQUESTED': + raise ValidationError({"cancel_status": "No cancellation request pending."}) + + approver_role = (leave_app.cancel_current_approver_role or '').lower() + role_flags = hr2_selectors.get_role_flags(user) + allowed = False + if approver_role == 'hod': + allowed = role_flags.get('is_hod') + elif approver_role == 'director': + allowed = role_flags.get('is_director') + elif approver_role == 'registrar': + allowed = role_flags.get('is_registrar') + + if not allowed: + raise PermissionError("Not authorized") + + leave_app.cancel_decided_at = timezone.now() + leave_app.cancel_decision_remarks = (remarks or '').strip() + + if decision == 'approve': + leave_app.cancel_status = 'APPROVED' + leave_app.approval_status = 'CANCELLED' + leave_app.current_approver_role = leave_app.cancel_current_approver_role + restore_leave_balance_for_cancellation(leave_app) + else: + leave_app.cancel_status = 'REJECTED' + + leave_app.save(update_fields=[ + 'cancel_status', + 'cancel_decided_at', + 'cancel_decision_remarks', + 'approval_status', + 'current_approver_role', + 'leave_balance_before', + 'leave_balance_after', + ]) + return leave_app + + +def request_leave_extension(leave_app, user, new_end_date, reason): + if leave_app.employee != user.extrainfo: + raise PermissionError("Not authorized") + if leave_app.approval_status != 'APPROVED': + raise ValidationError({"approval_status": "Only approved requests can be extended."}) + if leave_app.extension_status != 'NOT_REQUESTED': + raise ValidationError({"extension_status": "Extension already processed or pending."}) + + today = timezone.now().date() + if today >= leave_app.end_date: + raise ValidationError({"end_date": "Extension allowed only before the original end date."}) + if new_end_date <= leave_app.end_date: + raise ValidationError({"end_date": "New end date must be after the current end date."}) + + new_total_days = Decimal((new_end_date - leave_app.start_date).days + 1) + + role_flags = hr2_selectors.get_role_flags(user) + requester_role = 'Employee' + if role_flags.get('is_director'): + requester_role = 'Director' + elif role_flags.get('is_hod'): + requester_role = 'HOD' + elif role_flags.get('is_registrar'): + requester_role = 'Registrar' + elif role_flags.get('is_accountant'): + requester_role = 'Accountant' + elif role_flags.get('is_hr_admin'): + requester_role = 'HR Admin' + + approver_role = 'HOD' + if requester_role in ['HOD', 'Director', 'Registrar']: + approver_role = 'Director' + elif requester_role in ['Accountant', 'HR Admin']: + approver_role = 'Registrar' + + leave_app.extension_status = 'REQUESTED' + leave_app.extension_requested_at = timezone.now() + leave_app.extension_requested_by_role = requester_role + leave_app.extension_current_approver_role = approver_role + leave_app.extension_reason = (reason or '').strip() + leave_app.extension_new_end_date = new_end_date + leave_app.extension_new_total_days = new_total_days + leave_app.save(update_fields=[ + 'extension_status', + 'extension_requested_at', + 'extension_requested_by_role', + 'extension_current_approver_role', + 'extension_reason', + 'extension_new_end_date', + 'extension_new_total_days', + ]) + return leave_app + + +def decide_leave_extension(leave_app, user, decision, remarks): + decision = (decision or '').lower() + if decision not in ['approve', 'reject']: + raise ValidationError({"decision": "Invalid decision"}) + if leave_app.extension_status != 'REQUESTED': + raise ValidationError({"extension_status": "No extension request pending."}) + + approver_role = (leave_app.extension_current_approver_role or '').lower() + role_flags = hr2_selectors.get_role_flags(user) + allowed = False + if approver_role == 'hod': + allowed = role_flags.get('is_hod') + elif approver_role == 'director': + allowed = role_flags.get('is_director') + elif approver_role == 'registrar': + allowed = role_flags.get('is_registrar') + + if not allowed: + raise PermissionError("Not authorized") + + leave_app.extension_decided_at = timezone.now() + leave_app.extension_decision_remarks = (remarks or '').strip() + + if decision == 'approve': + if not apply_leave_balance_for_extension(leave_app): + raise InsufficientLeaveBalanceError("Insufficient leave balance for extension.") + leave_app.extension_status = 'APPROVED' + leave_app.current_approver_role = leave_app.extension_current_approver_role + leave_app.end_date = leave_app.extension_new_end_date + leave_app.total_days = leave_app.extension_new_total_days + else: + leave_app.extension_status = 'REJECTED' + + leave_app.save(update_fields=[ + 'extension_status', + 'extension_decided_at', + 'extension_decision_remarks', + 'current_approver_role', + 'leave_balance_before', + 'leave_balance_after', + 'end_date', + 'total_days', + ]) + return leave_app + + +def submit_leave_resumption(leave_app, user, resumption_date, reason): + if leave_app.employee != user.extrainfo: + raise PermissionError("Not authorized") + if leave_app.approval_status != 'APPROVED': + raise ValidationError({"approval_status": "Resumption allowed only for approved leaves."}) + if leave_app.resumption_status != 'NOT_REQUESTED': + raise ValidationError({"resumption_status": "Resumption already submitted or processed."}) + if resumption_date <= leave_app.end_date: + raise ValidationError({"resumption_date": "Resumption date must be after the leave end date."}) + + leave_app.resumption_status = 'SUBMITTED' + leave_app.resumption_date = resumption_date + leave_app.resumption_reason = (reason or '').strip() + leave_app.resumption_submitted_at = timezone.now() + leave_app.resumption_current_approver_role = 'HOD' + leave_app.save(update_fields=[ + 'resumption_status', + 'resumption_date', + 'resumption_reason', + 'resumption_submitted_at', + 'resumption_current_approver_role', + ]) + return leave_app + + +def decide_leave_resumption(leave_app, user, decision, remarks): + decision = (decision or '').lower() + if decision not in ['approve', 'reject']: + raise ValidationError({"decision": "Invalid decision"}) + if leave_app.resumption_status != 'SUBMITTED': + raise ValidationError({"resumption_status": "No resumption request pending."}) + + role_flags = hr2_selectors.get_role_flags(user) + if not role_flags.get('is_hod'): + raise PermissionError("Not authorized") + + leave_app.resumption_decided_at = timezone.now() + leave_app.resumption_decision_remarks = (remarks or '').strip() + if decision == 'approve': + leave_app.resumption_status = 'APPROVED' + leave_app.current_approver_role = 'HOD' + else: + leave_app.resumption_status = 'REJECTED' + + leave_app.save(update_fields=[ + 'resumption_status', + 'resumption_decided_at', + 'resumption_decision_remarks', + 'current_approver_role', + ]) + return leave_app + + +def respond_leave_nominee(leave_app, user, action): + action = (action or '').lower() + if action not in ['accept', 'decline']: + raise ValidationError({"action": "Invalid action"}) + if leave_app.handover_to != user.extrainfo.id: + raise PermissionError("Not authorized") + + leave_app.nominee_status = 'ACCEPTED' if action == 'accept' else 'DECLINED' + leave_app.nominee_responded_at = datetime.datetime.utcnow() + leave_app.save(update_fields=['nominee_status', 'nominee_responded_at']) + return leave_app + + +def request_leave_document(leave_app, user, message): + if not message: + raise ValidationError({"message": "Document request message is required."}) + role_flags = hr2_selectors.get_role_flags(user) + if not role_flags.get('is_hod'): + raise PermissionError("Not authorized") + if leave_app.document_request_status == 'REQUESTED': + raise ValidationError({"document_request_status": "Document already requested."}) + + leave_app.document_request_message = message + leave_app.document_request_status = 'REQUESTED' + leave_app.document_requested_at = datetime.datetime.utcnow() + leave_app.save(update_fields=['document_request_message', 'document_request_status', 'document_requested_at']) + return leave_app + + +def submit_leave_document(leave_app, user, submission): + if not submission: + raise ValidationError({"submission": "Document submission is required."}) + if leave_app.employee != user.extrainfo: + raise PermissionError("Not authorized") + if leave_app.document_request_status != 'REQUESTED': + raise ValidationError({"document_request_status": "No document requested for this leave."}) + + leave_app.document_submission = submission + leave_app.document_request_status = 'SUBMITTED' + leave_app.document_submitted_at = datetime.datetime.utcnow() + leave_app.save(update_fields=['document_submission', 'document_request_status', 'document_submitted_at']) + return leave_app + + +def decide_leave_application(leave_app, user, decision, remarks): + decision = (decision or '').lower() + if decision not in ['approve', 'reject', 'forward']: + raise ValidationError({"decision": "Invalid decision"}) + + role_flags = hr2_selectors.get_role_flags(user) + approver_role = 'HOD' + if role_flags.get('is_registrar'): + approver_role = 'Registrar' + elif role_flags.get('is_director'): + approver_role = 'Director' + + leave_type_name = (leave_app.leave_type or '').strip() + is_cl_rh_leave = leave_type_name in ['Casual', 'Restricted'] + if decision == 'approve' and not is_cl_rh_leave and approver_role == 'HOD': + raise ValidationError({"decision": "Only CL/RH leaves can be approved by HOD. Please forward to Director."}) + if decision == 'forward' and is_cl_rh_leave: + decision = 'approve' + + if decision == 'approve': + leave_app.approval_status = 'APPROVED' + leave_app.current_approver_role = approver_role + apply_leave_balance_for_approval(leave_app) + elif decision == 'forward': + leave_app.approval_status = 'FORWARDED' + leave_app.current_approver_role = 'Director' + else: + leave_app.approval_status = 'REJECTED' + leave_app.current_approver_role = approver_role + + leave_app.remarks = remarks + leave_app.save(update_fields=[ + 'approval_status', + 'remarks', + 'current_approver_role', + 'leave_balance_before', + 'leave_balance_after', + ]) + return leave_app + + +def apply_leave_balance_for_approval(leave_app): + leave_type = hr2_selectors.get_leave_type_by_name(leave_app.leave_type) + if not leave_type: + return + year = leave_app.start_date.year + balance = hr2_selectors.get_leave_balance_for_employee_year( + leave_app.employee, + leave_type, + year, + ) + if balance is None: + balance = hr2_selectors.get_latest_leave_balance_for_employee(leave_app.employee, leave_type) + if balance is None or balance.year != year: + balance = EmployeeLeaveBalance( + employee=leave_app.employee, + leave_type=leave_type, + year=year, + opening_balance=Decimal('0'), + accrued=Decimal('0'), + availed=Decimal('0'), + current_balance=Decimal('0'), + ) + balance.save() + + total_days = Decimal(str(leave_app.total_days or 0)) + before_balance = balance.current_balance + balance.availed = (balance.availed or 0) + total_days + balance.current_balance = (balance.current_balance or 0) - total_days + balance.save(update_fields=['availed', 'current_balance']) + + if leave_app.leave_balance_before is None: + leave_app.leave_balance_before = before_balance + leave_app.leave_balance_after = balance.current_balance + + +def restore_leave_balance_for_cancellation(leave_app): + leave_type = hr2_selectors.get_leave_type_by_name(leave_app.leave_type) + if not leave_type: + return + year = leave_app.start_date.year + balance = hr2_selectors.get_leave_balance_for_employee_year( + leave_app.employee, + leave_type, + year, + ) + if balance is None: + balance = hr2_selectors.get_latest_leave_balance_for_employee(leave_app.employee, leave_type) + if balance is None: + return + + total_days = Decimal(str(leave_app.total_days or 0)) + before_balance = balance.current_balance + balance.availed = (balance.availed or 0) - total_days + balance.current_balance = (balance.current_balance or 0) + total_days + balance.save(update_fields=['availed', 'current_balance']) + + if leave_app.leave_balance_before is None: + leave_app.leave_balance_before = before_balance + leave_app.leave_balance_after = balance.current_balance + + +def apply_leave_balance_for_extension(leave_app): + if not leave_app.extension_new_total_days: + return False + delta_days = Decimal(str(leave_app.extension_new_total_days)) - Decimal(str(leave_app.total_days or 0)) + if delta_days <= 0: + return False + + leave_type = hr2_selectors.get_leave_type_by_name(leave_app.leave_type) + if not leave_type: + return False + year = leave_app.start_date.year + balance = hr2_selectors.get_leave_balance_for_employee_year( + leave_app.employee, + leave_type, + year, + ) + if balance is None: + balance = hr2_selectors.get_latest_leave_balance_for_employee(leave_app.employee, leave_type) + if balance is None: + return False + + if (balance.current_balance or 0) < delta_days: + return False + + before_balance = balance.current_balance + balance.availed = (balance.availed or 0) + delta_days + balance.current_balance = (balance.current_balance or 0) - delta_days + balance.save(update_fields=['availed', 'current_balance']) + + if leave_app.leave_balance_before is None: + leave_app.leave_balance_before = before_balance + leave_app.leave_balance_after = balance.current_balance + return True + + +def create_attendance(employee, validated_data): + attendance = EmployeeAttendance(employee=employee, **validated_data) + attendance.save() + return attendance + + +def create_performance_appraisal(employee, validated_data): + appraisal = PerformanceAppraisalNew(employee=employee, **validated_data) + appraisal.save() + return appraisal + + +def create_training_nomination(employee, nominated_by, validated_data): + nomination = TrainingNomination(employee=employee, nominated_by=nominated_by, **validated_data) + nomination.save() + return nomination + + +def create_promotion_application(employee, validated_data): + promotion = PromotionApplication(employee=employee, **validated_data) + promotion.save() + return promotion + + +def create_ltc_application(employee, validated_data): + ltc = LTCApplicationNew(employee=employee, **validated_data) + ltc.save() + return ltc + + +def update_ltc_application(ltc, validated_data): + return _update_instance_from_data(ltc, validated_data) + + +def withdraw_ltc_application(ltc, user, remarks): + if ltc.employee != user.extrainfo: + raise PermissionError("Not authorized") + if ltc.approval_status != 'PENDING': + raise ValidationError({"approval_status": "Only pending requests can be withdrawn."}) + + ltc.approval_status = 'WITHDRAWN' + ltc.remarks = (remarks or '').strip() + ltc.save(update_fields=['approval_status', 'remarks']) + return ltc + + +def decide_ltc_application(ltc, decision, remarks): + decision = (decision or '').lower() + if decision not in ['approve', 'reject', 'forward']: + raise ValidationError({"decision": "Invalid decision"}) + + if decision == 'approve': + ltc.approval_status = 'APPROVED' + ltc.accountant_status = 'APPROVED' + elif decision == 'forward': + ltc.approval_status = 'FORWARDED' + ltc.verified_by_hr = True + ltc.accountant_status = 'PENDING' + else: + ltc.approval_status = 'REJECTED' + ltc.accountant_status = 'REJECTED' + + ltc.remarks = remarks + ltc.save(update_fields=['approval_status', 'remarks', 'verified_by_hr', 'accountant_status']) + return ltc + + +def create_cpda_advance(employee, validated_data): + cpda = CPDAAdvanceNew(employee=employee, **validated_data) + cpda.save() + return cpda + + +def withdraw_cpda_advance(cpda, user, remarks): + if cpda.employee != user.extrainfo: + raise PermissionError("Not authorized") + if cpda.approval_status != 'PENDING': + raise ValidationError({"approval_status": "Only pending requests can be withdrawn."}) + + cpda.approval_status = 'WITHDRAWN' + cpda.remarks = (remarks or '').strip() + cpda.save(update_fields=['approval_status', 'remarks']) + return cpda + + +def decide_cpda_advance(cpda, user, decision, remarks): + decision = (decision or '').lower() + if decision not in ['approve', 'reject', 'forward-director']: + raise ValidationError({"decision": "Invalid decision"}) + + role_flags = hr2_selectors.get_role_flags(user) + if role_flags.get('is_hr_staff'): + if decision == 'reject': + cpda.approval_status = 'REJECTED' + cpda.accountant_processing_status = 'REJECTED' + else: + cpda.approval_status = 'FORWARDED' + cpda.verified_by_hr = True + cpda.accountant_processing_status = 'DIRECTOR_REVIEW' + elif role_flags.get('is_director'): + if decision == 'reject': + cpda.approval_status = 'REJECTED' + cpda.accountant_processing_status = 'REJECTED' + else: + cpda.approval_status = 'FORWARDED' + cpda.accountant_processing_status = 'PENDING' + elif role_flags.get('is_accountant'): + if decision == 'reject': + cpda.approval_status = 'REJECTED' + cpda.accountant_processing_status = 'REJECTED' + else: + cpda.approval_status = 'APPROVED' + cpda.accountant_processing_status = 'APPROVED' + else: + raise PermissionError("Not authorized") + + cpda.remarks = remarks + cpda.save(update_fields=['approval_status', 'remarks', 'verified_by_hr', 'accountant_processing_status']) + return cpda + + +def create_cpda_reimbursement(employee, validated_data): + reimbursement = CPDAReimbursementNew(employee=employee, **validated_data) + reimbursement.save() + return reimbursement + + +def decide_cpda_reimbursement(reimbursement, decision, reviewer, remarks): + decision = (decision or '').lower() + if decision == 'approve': + reimbursement.approval_status = 'APPROVED' + reimbursement.verified_by_hr = True + else: + reimbursement.approval_status = 'REJECTED' + reimbursement.remarks = remarks + reimbursement.save(update_fields=['approval_status', 'verified_by_hr', 'remarks']) + return reimbursement + + +def create_appraisal_form(employee, validated_data): + appraisal = AppraisalFormNew(employee=employee, **validated_data) + appraisal.save() + return appraisal + + +def review_appraisal_form(appraisal, user, action, remarks, rating): + role_flags = hr2_selectors.get_role_flags(user) + if role_flags.get('is_hod') and appraisal.assigned_reviewer_role.upper() != 'HOD': + raise PermissionError("Not assigned to HOD review.") + if role_flags.get('is_director') and appraisal.assigned_reviewer_role.upper() != 'DIRECTOR': + raise PermissionError("Not assigned to Director review.") + if not (role_flags.get('is_hod') or role_flags.get('is_director')): + raise PermissionError("Not authorized to review.") + + appraisal.reviewer_id = str(user.extrainfo.id) + appraisal.reviewer_comments = (remarks or '') + if rating: + appraisal.rating = str(rating) + + if action == 'approve': + appraisal.status = 'APPROVED' + appraisal.assigned_reviewer_role = '' + appraisal.assigned_reviewer = None + elif action == 'forward': + appraisal.status = 'REVIEWED' + appraisal.assigned_reviewer_role = 'DIRECTOR' + appraisal.assigned_reviewer = None + else: + appraisal.status = 'REVIEWED' + + appraisal.save(update_fields=[ + 'reviewer_id', + 'reviewer_comments', + 'rating', + 'status', + 'assigned_reviewer_role', + 'assigned_reviewer', + ]) + return appraisal + + +def assign_appraisal_reviewer(appraisal, user, role, reviewer_id): + role_flags = hr2_selectors.get_role_flags(user) + if not role_flags.get('is_hr_staff'): + raise PermissionError("Not authorized to assign.") + if role not in ['HOD', 'DIRECTOR']: + raise ValidationError({"role": "Role must be HOD or DIRECTOR."}) + if appraisal.status != 'PENDING': + raise ValidationError({"status": "Only pending appraisals can be assigned."}) + + assigned_reviewer = hr2_selectors.get_reviewer_by_id(reviewer_id) + if reviewer_id and not assigned_reviewer: + raise ValidationError({"reviewer_id": "Reviewer not found."}) + + appraisal.assigned_reviewer_role = role + appraisal.assigned_reviewer = assigned_reviewer + appraisal.assigned_by = user.extrainfo + appraisal.assigned_at = timezone.now() + appraisal.save(update_fields=[ + 'assigned_reviewer_role', + 'assigned_reviewer', + 'assigned_by', + 'assigned_at', + ]) + return appraisal + + +# ==================== MANAGEMENT COMMAND SERVICES ==================== + +DEFAULT_LEAVE_BALANCES = [ + ("Casual", "CL", 10), + ("Restricted", "RL", 5), + ("Medical", "ML", 12), + ("Earned", "EL", 18), + ("Vacation", "VL", 20), + ("Sabbatical", "SL", 0), +] + +ROLE_LEAVE_BALANCES = { + "EMP1002": {"CL": 12, "RL": 6, "ML": 15, "EL": 25, "VL": 30, "SL": 10}, + "EMP1003": {"CL": 15, "RL": 8, "ML": 20, "EL": 30, "VL": 35, "SL": 15}, + "EMP1004": {"CL": 12, "RL": 6, "ML": 15, "EL": 22, "VL": 28, "SL": 5}, + "EMP1005": {"CL": 10, "RL": 5, "ML": 12, "EL": 20, "VL": 25, "SL": 0}, + "EMP1006": {"CL": 10, "RL": 5, "ML": 12, "EL": 18, "VL": 22, "SL": 0}, + "EMP1007": {"CL": 12, "RL": 6, "ML": 15, "EL": 25, "VL": 30, "SL": 12}, +} + + +def seed_leave_balances(employee_id=None, seed_all=False, year=None): + if year is None: + year = datetime.date.today().year + + for name, code, _value in DEFAULT_LEAVE_BALANCES: + LeaveType.objects.get_or_create( + name=name, + code=code, + defaults={"is_active": True}, + ) + + if seed_all: + employees = ExtraInfo.objects.all() + else: + if not employee_id: + employee_id = "EMP1001" + try: + employees = [ExtraInfo.objects.get(id=employee_id)] + except ExtraInfo.DoesNotExist as exc: + raise CommandError(f"Employee not found: {employee_id}") from exc + + seeded_count = 0 + for employee in employees: + balance_map = ROLE_LEAVE_BALANCES.get(employee.id, {}) + for name, code, default_value in DEFAULT_LEAVE_BALANCES: + value = balance_map.get(code, default_value) + leave_type = LeaveType.objects.get(code=code) + EmployeeLeaveBalance.objects.update_or_create( + employee=employee, + leave_type=leave_type, + year=year, + defaults={ + "opening_balance": value, + "accrued": 0, + "availed": 0, + "current_balance": value, + }, + ) + seeded_count += 1 + + return {"seeded_count": seeded_count, "year": year} + + +def _parse_date(value): + if not value: + return None + return datetime.date.fromisoformat(value) + + +def _parse_gender(value): + if not value: + return "M" + value = value.strip().lower() + if value.startswith("f"): + return "F" + if value.startswith("m"): + return "M" + return "O" + + +def _split_name(full_name): + if not full_name: + return "", "" + parts = full_name.strip().split() + if len(parts) == 1: + return parts[0], "" + return parts[0], " ".join(parts[1:]) + + +def seed_hr_demo_data(): + departments = [ + "Computer Science and Engineering", + "Administration", + "Finance", + "Director Office", + ] + + employees = [ + { + "employee_id": "EMP1001", + "name": "Rahul Sharma", + "email": "rahul.sharma@iiitdmj.ac.in", + "phone": "9876543210", + "gender": "Male", + "dob": "1990-05-12", + "department": "Computer Science and Engineering", + "designation": "Assistant Professor", + "role": "Employee", + "employment_type": "Permanent", + "date_of_joining": "2021-08-01", + "reporting_to": "EMP1002", + "status": "Active", + }, + { + "employee_id": "EMP1007", + "name": "Dr. Anjali Mehta", + "email": "anjali.mehta@iiitdmj.ac.in", + "phone": "9876543216", + "gender": "Female", + "dob": "1985-11-08", + "department": "Computer Science and Engineering", + "designation": "Professor", + "role": "Employee", + "employment_type": "Permanent", + "date_of_joining": "2016-07-20", + "reporting_to": "EMP1002", + "status": "Active", + }, + { + "employee_id": "EMP1002", + "name": "Dr. Anil Kumar", + "email": "anil.kumar@iiitdmj.ac.in", + "phone": "9876543211", + "gender": "Male", + "dob": "1980-07-20", + "department": "Computer Science and Engineering", + "designation": "Professor and HOD", + "role": "HOD", + "employment_type": "Permanent", + "date_of_joining": "2015-06-15", + "reporting_to": "EMP1003", + "status": "Active", + }, + { + "employee_id": "EMP1003", + "name": "Dr. Meena Verma", + "email": "director@iiitdmj.ac.in", + "phone": "9876543212", + "gender": "Female", + "dob": "1975-02-11", + "department": "Director Office", + "designation": "Director", + "role": "Director", + "employment_type": "Permanent", + "date_of_joining": "2019-01-10", + "reporting_to": None, + "status": "Active", + }, + { + "employee_id": "EMP1004", + "name": "Suresh Verma", + "email": "registrar@iiitdmj.ac.in", + "phone": "9876543213", + "gender": "Male", + "dob": "1982-03-10", + "department": "Administration", + "designation": "Registrar", + "role": "Registrar", + "employment_type": "Permanent", + "date_of_joining": "2018-01-15", + "reporting_to": "EMP1003", + "status": "Active", + }, + { + "employee_id": "EMP1005", + "name": "Priya Nair", + "email": "hr.admin@iiitdmj.ac.in", + "phone": "9876543214", + "gender": "Female", + "dob": "1987-09-25", + "department": "Administration", + "designation": "HR Administrator", + "role": "HR Admin", + "employment_type": "Permanent", + "date_of_joining": "2020-11-05", + "reporting_to": "EMP1004", + "status": "Active", + }, + { + "employee_id": "EMP1006", + "name": "Arun Joshi", + "email": "accountant@iiitdmj.ac.in", + "phone": "9876543215", + "gender": "Male", + "dob": "1985-12-18", + "department": "Finance", + "designation": "Accountant", + "role": "Accountant", + "employment_type": "Permanent", + "date_of_joining": "2019-08-12", + "reporting_to": "EMP1004", + "status": "Active", + }, + ] + + users = [ + {"linked_employee_id": "EMP1001", "username": "rahul1001", "password": "rahul123"}, + {"linked_employee_id": "EMP1007", "username": "anjali1007", "password": "anjali123"}, + {"linked_employee_id": "EMP1002", "username": "hod1002", "password": "hod123"}, + {"linked_employee_id": "EMP1003", "username": "director1003", "password": "director123"}, + {"linked_employee_id": "EMP1004", "username": "registrar1004", "password": "registrar123"}, + {"linked_employee_id": "EMP1005", "username": "hradmin1005", "password": "hradmin123"}, + {"linked_employee_id": "EMP1006", "username": "accountant1006", "password": "accountant123"}, + ] + + leave_balance = { + "employee_id": "EMP1001", + "casual_leave": 10, + "restricted_leave": 5, + "medical_leave": 12, + "earned_leave": 18, + "vacation_leave": 20, + "sabbatical_leave": 0, + } + + leave_request = { + "employee_id": "EMP1001", + "employee_name": "Rahul Sharma", + "department": "Computer Science and Engineering", + "designation": "Assistant Professor", + "leave_type": "Casual", + "start_date": "2026-04-10", + "end_date": "2026-04-12", + "total_days": 3, + "reason": "Personal work", + "contact_during_leave": "9876543210", + "address_during_leave": "Jabalpur, MP", + "handover_notes": "Classes handed over to Dr. X", + "attachment_file": "", + "leave_balance_before": 10, + "leave_balance_after": 7, + "approval_status": "PENDING", + "current_approver_role": "HOD", + "remarks": "", + } + + appraisal_request = { + "employee_id": "EMP1001", + "employee_name": "Rahul Sharma", + "department": "Computer Science and Engineering", + "designation": "Assistant Professor", + "appraisal_year": "2025-2026", + "self_summary": "Completed teaching and research responsibilities effectively.", + "teaching_performance": "Good", + "research_work": "Worked on 2 projects", + "publications": "1 journal paper", + "trainings_attended": "AI workshop", + "administrative_contributions": "Exam coordination", + "goals_achieved": "Completed syllabus and guided students", + "future_goals": "Publish more papers", + "reviewer_id": "EMP1002", + "status": "PENDING", + "remarks": "", + } + + ltc_request = { + "employee_id": "EMP1001", + "employee_name": "Rahul Sharma", + "department": "Computer Science and Engineering", + "designation": "Assistant Professor", + "ltc_block_year": "2024-2027", + "travel_start_date": "2026-05-05", + "travel_end_date": "2026-05-12", + "destination": "Delhi", + "purpose_of_travel": "Family travel", + "family_members": [{"name": "Priya Sharma", "relationship": "Spouse"}], + "travel_mode": "Train", + "ticket_number": "IRCTC12345", + "ticket_cost": 12000, + "accommodation_cost": 8000, + "other_expenses": 2000, + "total_amount_claimed": 22000, + "tickets_upload": "", + "bills_upload": "", + "previous_ltc_used": True, + "last_ltc_date": "2023-06-15", + "verified_by_hr": False, + "approval_status": "PENDING", + "accountant_status": "Not Started", + "remarks": "", + } + + cpda_request = { + "employee_id": "EMP1001", + "employee_name": "Rahul Sharma", + "department": "Computer Science and Engineering", + "designation": "Assistant Professor", + "event_name": "National Conference on AI", + "event_type": "Conference", + "organized_by": "IIT Delhi", + "venue": "New Delhi", + "start_date": "2026-06-20", + "end_date": "2026-06-22", + "registration_fee": 5000, + "travel_expense": 8000, + "accommodation_expense": 6000, + "other_expenses": 1000, + "total_amount": 20000, + "purpose_of_attending": "Present paper and improve research skills", + "benefits_to_institution": "Research development and academic exposure", + "invitation_letter": "", + "receipts": "", + "certificates": "", + "verified_by_hr": False, + "approval_status": "PENDING", + "accountant_processing_status": "Not Started", + "remarks": "", + } + + with transaction.atomic(): + for name in departments: + DepartmentInfo.objects.get_or_create(name=name) + + teaching_category, _ = EmployeeCategory.objects.get_or_create( + name="Teaching", defaults={"category_type": "TEACHING"} + ) + non_teaching_category, _ = EmployeeCategory.objects.get_or_create( + name="Non-Teaching", defaults={"category_type": "NON_TEACHING"} + ) + + user_lookup = {item["linked_employee_id"]: item for item in users} + + for employee in employees: + user_info = user_lookup.get(employee["employee_id"], {}) + username = user_info.get("username") or employee["employee_id"].lower() + first_name, last_name = _split_name(employee["name"]) + + user, created = get_user_model().objects.get_or_create( + username=username, + defaults={ + "email": employee["email"], + "first_name": first_name, + "last_name": last_name, + }, + ) + if created and user_info.get("password"): + user.set_password(user_info["password"]) + user.save() + + department_obj = DepartmentInfo.objects.get(name=employee["department"]) + + extra_info, _ = ExtraInfo.objects.get_or_create( + id=employee["employee_id"], + defaults={ + "user": user, + "sex": _parse_gender(employee["gender"]), + "date_of_birth": _parse_date(employee["dob"]), + "user_type": "faculty" + if employee["department"] == "Computer Science and Engineering" + else "staff", + "department": department_obj, + "phone_no": int(employee["phone"]), + "address": "", + }, + ) + + category = teaching_category if extra_info.user_type == "faculty" else non_teaching_category + EmployeeDetailsExtended.objects.get_or_create( + extra_info=extra_info, + defaults={ + "category": category, + "date_of_joining": _parse_date(employee["date_of_joining"]), + "appointment_type": employee["employment_type"], + }, + ) + + designation_type = "academic" if extra_info.user_type == "faculty" else "administrative" + designation, _ = Designation.objects.get_or_create( + name=employee["designation"], + defaults={ + "full_name": employee["designation"], + "type": designation_type, + }, + ) + + HoldsDesignation.objects.get_or_create( + user=user, + working=user, + designation=designation, + ) + + leave_types = [ + ("Casual", "CL", leave_balance["casual_leave"]), + ("Restricted", "RL", leave_balance["restricted_leave"]), + ("Medical", "ML", leave_balance["medical_leave"]), + ("Earned", "EL", leave_balance["earned_leave"]), + ("Vacation", "VL", leave_balance["vacation_leave"]), + ("Sabbatical", "SL", leave_balance["sabbatical_leave"]), + ] + + for name, code, _value in leave_types: + LeaveType.objects.get_or_create( + name=name, + code=code, + defaults={"is_active": True}, + ) + + employee_user = ExtraInfo.objects.get(id=leave_balance["employee_id"]) + year = datetime.date.today().year + + for name, code, value in leave_types: + leave_type = LeaveType.objects.get(code=code) + EmployeeLeaveBalance.objects.update_or_create( + employee=employee_user, + leave_type=leave_type, + year=year, + defaults={ + "opening_balance": value, + "accrued": 0, + "availed": 0, + "current_balance": value, + }, + ) + + LeaveApplicationNew.objects.get_or_create( + employee=employee_user, + start_date=_parse_date(leave_request["start_date"]), + end_date=_parse_date(leave_request["end_date"]), + defaults={ + "employee_name": leave_request["employee_name"], + "department": leave_request["department"], + "designation": leave_request["designation"], + "leave_type": leave_request["leave_type"], + "total_days": leave_request["total_days"], + "reason": leave_request["reason"], + "contact_during_leave": leave_request["contact_during_leave"], + "address_during_leave": leave_request["address_during_leave"], + "handover_to": "Dr. X", + "handover_notes": leave_request["handover_notes"], + "medical_certificate": "", + "attachment_file": leave_request["attachment_file"], + "leave_balance_before": leave_request["leave_balance_before"], + "leave_balance_after": leave_request["leave_balance_after"], + "approval_status": leave_request["approval_status"], + "current_approver_role": leave_request["current_approver_role"], + "remarks": leave_request["remarks"], + }, + ) + + AppraisalFormNew.objects.get_or_create( + employee=employee_user, + appraisal_year=appraisal_request["appraisal_year"], + defaults={ + "employee_name": appraisal_request["employee_name"], + "department": appraisal_request["department"], + "designation": appraisal_request["designation"], + "self_summary": appraisal_request["self_summary"], + "key_responsibilities": "Teaching, research, and academic mentoring.", + "achievements": appraisal_request["goals_achieved"], + "challenges_faced": "", + "teaching_performance": appraisal_request["teaching_performance"], + "research_work": appraisal_request["research_work"], + "publications": appraisal_request["publications"], + "projects_handled": "", + "administrative_contributions": appraisal_request["administrative_contributions"], + "trainings_attended": appraisal_request["trainings_attended"], + "certifications": "", + "workshops": "", + "goals_achieved": appraisal_request["goals_achieved"], + "future_goals": appraisal_request["future_goals"], + "supporting_documents": "", + "reviewer_id": appraisal_request["reviewer_id"], + "reviewer_comments": "", + "rating": "", + "status": appraisal_request["status"], + "remarks": appraisal_request["remarks"], + }, + ) + + block_year = int(ltc_request["ltc_block_year"].split("-")[0]) + LTCApplicationNew.objects.get_or_create( + employee=employee_user, + travel_start_date=_parse_date(ltc_request["travel_start_date"]), + travel_end_date=_parse_date(ltc_request["travel_end_date"]), + defaults={ + "employee_name": ltc_request["employee_name"], + "department": ltc_request["department"], + "designation": ltc_request["designation"], + "ltc_block_year": block_year, + "destination": ltc_request["destination"], + "purpose_of_travel": ltc_request["purpose_of_travel"], + "family_members": json.dumps(ltc_request["family_members"]), + "relationship_details": "Spouse", + "travel_mode": ltc_request["travel_mode"], + "ticket_number": ltc_request["ticket_number"], + "ticket_cost": ltc_request["ticket_cost"], + "accommodation_cost": ltc_request["accommodation_cost"], + "other_expenses": ltc_request["other_expenses"], + "total_amount_claimed": ltc_request["total_amount_claimed"], + "tickets_upload": ltc_request["tickets_upload"], + "bills_upload": ltc_request["bills_upload"], + "previous_ltc_used": ltc_request["previous_ltc_used"], + "last_ltc_date": _parse_date(ltc_request["last_ltc_date"]), + "verified_by_hr": ltc_request["verified_by_hr"], + "approval_status": ltc_request["approval_status"], + "accountant_status": ltc_request["accountant_status"], + "remarks": ltc_request["remarks"], + }, + ) + + CPDAAdvanceNew.objects.get_or_create( + employee=employee_user, + start_date=_parse_date(cpda_request["start_date"]), + end_date=_parse_date(cpda_request["end_date"]), + defaults={ + "employee_name": cpda_request["employee_name"], + "department": cpda_request["department"], + "designation": cpda_request["designation"], + "event_name": cpda_request["event_name"], + "event_type": cpda_request["event_type"], + "organized_by": cpda_request["organized_by"], + "venue": cpda_request["venue"], + "registration_fee": cpda_request["registration_fee"], + "travel_expense": cpda_request["travel_expense"], + "accommodation_expense": cpda_request["accommodation_expense"], + "other_expenses": cpda_request["other_expenses"], + "total_amount": cpda_request["total_amount"], + "purpose_of_attending": cpda_request["purpose_of_attending"], + "benefits_to_institution": cpda_request["benefits_to_institution"], + "invitation_letter": cpda_request["invitation_letter"], + "receipts": cpda_request["receipts"], + "certificates": cpda_request["certificates"], + "verified_by_hr": cpda_request["verified_by_hr"], + "approval_status": cpda_request["approval_status"], + "accountant_processing_status": cpda_request["accountant_processing_status"], + "remarks": cpda_request["remarks"], + }, + ) + + return {"employees_seeded": len(employees)} + + +def seed_hr2_demo_data(): + User = get_user_model() + now = timezone.now() + + department, _ = DepartmentInfo.objects.get_or_create(name="Computer Science") + + designation, _ = Designation.objects.get_or_create( + name="Faculty", + defaults={"full_name": "Faculty", "type": "academic"}, + ) + + module_access, _ = ModuleAccess.objects.get_or_create(designation="Faculty") + if not module_access.hr: + module_access.hr = True + module_access.save() + + user, created = User.objects.get_or_create( + username="rahul123", + defaults={ + "first_name": "Rahul", + "last_name": "Sharma", + "email": "rahul.sharma@iiitdmj.ac.in", + }, + ) + if created: + user.set_password("user@123") + user.save() + else: + user.email = "rahul.sharma@iiitdmj.ac.in" + user.first_name = user.first_name or "Rahul" + user.last_name = user.last_name or "Sharma" + user.set_password("user@123") + user.save() + + extra_info, _ = ExtraInfo.objects.get_or_create( + id="EMP001", + defaults={ + "user": user, + "title": "Dr.", + "sex": "M", + "date_of_birth": "1990-05-12", + "user_status": "PRESENT", + "address": "IIITDMJ Campus", + "phone_no": 9876543210, + "user_type": "faculty", + "department": department, + "about_me": "Faculty member", + "last_selected_role": "Faculty", + }, + ) + if extra_info.user_id != user.id: + extra_info.user = user + extra_info.department = department + extra_info.phone_no = 9876543210 + extra_info.last_selected_role = "Faculty" + extra_info.save() + + HoldsDesignation.objects.get_or_create( + user=user, + working=user, + designation=designation, + ) + + Employee.objects.get_or_create( + id=user, + defaults={ + "father_name": "Rajesh Sharma", + "mother_name": "Sunita Sharma", + "category": "General", + "caste": "N/A", + "home_state": "Madhya Pradesh", + "home_district": "Jabalpur", + "full_address": "IIITDMJ Campus, Dumna Airport Road", + "date_of_joining": "2021-08-01", + "date_of_birth": "1990-05-12", + "blood_group": "O+", + "phone_number": "9876543210", + "personal_email": "rahul.sharma@iiitdmj.ac.in", + "emergency_contact_number": "9876543211", + "emergency_contact_name": "Rajesh Sharma", + "employee_type": "Faculty", + }, + ) + + leave_type_map = { + "Casual": ("CL", Decimal("10")), + "Earned": ("EL", Decimal("18")), + "Medical": ("ML", Decimal("12")), + "Restricted": ("RL", Decimal("5")), + "Vacation": ("VL", Decimal("25")), + "Sabbatical": ("SL", Decimal("0")), + } + + current_year = now.year + for name, (code, balance) in leave_type_map.items(): + leave_type, _ = LeaveType.objects.get_or_create( + name=name, + defaults={"code": code, "is_active": True}, + ) + EmployeeLeaveBalance.objects.get_or_create( + employee=extra_info, + leave_type=leave_type, + year=current_year, + defaults={ + "opening_balance": balance, + "accrued": Decimal("0"), + "availed": Decimal("0"), + "current_balance": balance, + }, + ) + + if notify: + notify.send( + sender=user, + recipient=user, + verb="Welcome to HR Portal", + description="Welcome to HR Portal", + ) + + return {"employee_id": extra_info.id} + + +def convert_vl_to_earned(source_year=None, dry_run=False): + if source_year is None: + source_year = datetime.date.today().year + target_year = source_year + 1 + + vl_type = LeaveType.objects.filter(code__iexact="VL").first() or LeaveType.objects.filter(name__iexact="Vacation").first() + el_type = LeaveType.objects.filter(code__iexact="EL").first() or LeaveType.objects.filter(name__iexact="Earned").first() + + if not vl_type or not el_type: + raise CommandError("Leave types VL/Earned not found. Ensure LeaveType records exist.") + + all_employees = ExtraInfo.objects.all() + converted_count = 0 + total_converted = Decimal("0.0") + + next_year_defaults = { + "CL": Decimal("8.0"), + "RL": Decimal("2.0"), + "VL": Decimal("60.0"), + } + leave_types = {lt.code.upper(): lt for lt in LeaveType.objects.all() if lt.code} + + for employee in all_employees: + is_faculty = employee.user_type == "faculty" + converted = Decimal("0.0") + + vl_balance = EmployeeLeaveBalance.objects.filter( + employee=employee, + leave_type=vl_type, + year=source_year, + ).first() + if is_faculty and vl_balance: + vl_current = Decimal(str(vl_balance.current_balance or 0)) + if vl_current > 0: + converted = (vl_current / Decimal("2")).quantize(Decimal("0.1"), rounding=ROUND_HALF_UP) + if not dry_run: + vl_balance.current_balance = Decimal("0.0") + vl_balance.save(update_fields=["current_balance"]) + if converted > 0: + converted_count += 1 + total_converted += converted + + if dry_run: + continue + + for code, leave_type in leave_types.items(): + if code == "EL": + opening = Decimal("0.0") + accrued = converted + current = converted + elif code in next_year_defaults: + opening = next_year_defaults[code] + accrued = Decimal("0.0") + current = opening + else: + opening = Decimal("0.0") + accrued = Decimal("0.0") + current = Decimal("0.0") + + EmployeeLeaveBalance.objects.update_or_create( + employee=employee, + leave_type=leave_type, + year=target_year, + defaults={ + "opening_balance": opening, + "accrued": accrued, + "availed": Decimal("0.0"), + "current_balance": current, + }, + ) + + return { + "dry_run": dry_run, + "converted_count": converted_count, + "total_converted": total_converted, + "source_year": source_year, + "target_year": target_year, + } \ No newline at end of file diff --git a/FusionIIIT/applications/hr2/tests/test_module.py b/FusionIIIT/applications/hr2/tests/test_module.py new file mode 100644 index 000000000..e69de29bb From ac5986a17e8cd466eb00db1deb4c246930b1dd44 Mon Sep 17 00:00:00 2001 From: tejdevarakonda Date: Fri, 8 May 2026 20:07:20 +0530 Subject: [PATCH 3/4] Final HR (EIS) module submission --- FusionIIIT/applications/hr2.zip | Bin 185484 -> 204978 bytes FusionIIIT/applications/hr2/api/views.py | 1241 +++++++++++++---- .../migrations/0011_leave_attachment_files.py | 20 - FusionIIIT/applications/hr2/models.py | 4 +- FusionIIIT/applications/hr2/services.py | 82 +- .../hr2/tests/reports/Module_Test_Summary.csv | 8 +- scripts/hr2_rbac_evaluator.ps1 | 269 ++++ 7 files changed, 1312 insertions(+), 312 deletions(-) delete mode 100644 FusionIIIT/applications/hr2/migrations/0011_leave_attachment_files.py create mode 100644 scripts/hr2_rbac_evaluator.ps1 diff --git a/FusionIIIT/applications/hr2.zip b/FusionIIIT/applications/hr2.zip index f4b1e75470d62b7e9045d3e73e5112c95177ecbc..42181bcbbf2e764f00650a365dc89178f293323c 100644 GIT binary patch delta 91911 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 diff --git a/FusionIIIT/applications/hr2/api/views.py b/FusionIIIT/applications/hr2/api/views.py index 741d8fb9b..515029049 100644 --- a/FusionIIIT/applications/hr2/api/views.py +++ b/FusionIIIT/applications/hr2/api/views.py @@ -5,22 +5,25 @@ from rest_framework import status from rest_framework.permissions import IsAuthenticated from rest_framework.parsers import MultiPartParser, FormParser, JSONParser +from django.shortcuts import get_object_or_404 +from django.db.models import Q from django.http import HttpResponse from django.utils import timezone -from django.core.exceptions import ValidationError -from applications.hr2 import selectors as hr2_selectors -from applications.hr2 import services as hr2_services +from applications.globals.models import ExtraInfo, HoldsDesignation +from decimal import Decimal +from ..models import LeaveApplicationNew, EmployeeLeaveBalance, AppraisalFormNew, LeaveType from ..services import ( - InsufficientLeaveBalanceError, - InvalidWorkflowTransitionError, + approve_leave_application, reject_leave_application, + handle_academic_responsibility, handle_administrative_responsibility, + mark_attendance, calculate_faculty_workload, + InsufficientLeaveBalanceError, DuplicateLeaveApplicationError, InvalidWorkflowTransitionError ) from ..selectors import ( - get_all_employees, - get_attendance_for_employee, - get_available_training_programs, - get_faculty_workload, - get_nominations_for_employee, - get_promotion_applications, + get_employee_by_id, get_all_employees, get_leave_balance_for_employee, + get_leave_applications, get_pending_responsibility_leaves, + get_attendance_for_employee, get_appraisal_periods, get_appraisals_for_employee, + get_available_training_programs, get_nominations_for_employee, + get_promotion_applications, get_faculty_workload ) from .serializers import ( EmployeeDetailsSerializer, LeaveApplicationSerializer, LeaveBalanceSerializer, @@ -45,16 +48,16 @@ class EmployeeDetailView(APIView): permission_classes = [IsAuthenticated] def get(self, request, employee_id): - employee = hr2_selectors.get_employee_by_id_or_404(employee_id) + employee = get_employee_by_id(employee_id) serializer = EmployeeDetailsSerializer(employee) return Response(serializer.data) def put(self, request, employee_id): - employee = hr2_selectors.get_employee_by_id_or_404(employee_id) + employee = get_employee_by_id(employee_id) serializer = EmployeeDetailsSerializer(employee, data=request.data, partial=True) if serializer.is_valid(): - updated = hr2_services.update_instance(employee, serializer.validated_data) - return Response(EmployeeDetailsSerializer(updated).data) + serializer.save() + return Response(serializer.data) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) # ==================== LEAVE VIEWS ==================== @@ -64,42 +67,167 @@ class LeaveApplicationListCreateView(APIView): parser_classes = [JSONParser, MultiPartParser, FormParser] def get(self, request): - role_flags = hr2_selectors.get_role_flags(request.user) - leaves = hr2_selectors.get_leave_applications_for_role_view(request.user, role_flags) + is_hr_staff = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='hr', + ).exists() or ( + request.user.extrainfo.user_type == 'staff' + and request.user.extrainfo.department + and request.user.extrainfo.department.name == 'HR' + ) + is_hod = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='hod', + ).exists() + is_director = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='director', + ).exists() + is_registrar = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='registrar', + ).exists() + + if is_hr_staff: + leaves = LeaveApplicationNew.objects.all() + elif is_director: + leaves = LeaveApplicationNew.objects.filter( + Q( + approval_status='FORWARDED', + current_approver_role__iexact='Director', + ) | Q(employee=request.user.extrainfo) | Q( + cancel_status='REQUESTED', + cancel_current_approver_role__iexact='Director', + ) | Q( + extension_status='REQUESTED', + extension_current_approver_role__iexact='Director', + ) + ) + elif is_registrar: + leaves = LeaveApplicationNew.objects.filter( + Q( + approval_status='FORWARDED', + current_approver_role__iexact='Registrar', + ) | Q(employee=request.user.extrainfo) | Q( + cancel_status='REQUESTED', + cancel_current_approver_role__iexact='Registrar', + ) | Q( + extension_status='REQUESTED', + extension_current_approver_role__iexact='Registrar', + ) + ) + elif is_hod: + leaves = LeaveApplicationNew.objects.filter( + department=request.user.extrainfo.department.name + ) + else: + leaves = get_leave_applications(request.user.extrainfo) serializer = LeaveApplicationSerializer(leaves, many=True, context={'request': request}) return Response(serializer.data) def post(self, request): serializer = LeaveApplicationSerializer(data=request.data, context={'request': request}) if serializer.is_valid(): - try: - leave_app = hr2_services.create_leave_application(request.user, serializer.validated_data) - except ValidationError as exc: - return Response(exc.message_dict, status=status.HTTP_400_BAD_REQUEST) + employee = getattr(request.user, 'extrainfo', None) + if employee is None: + employee_id = request.data.get('employee_id') + if employee_id: + employee = get_employee_by_id(employee_id) + if employee is None: + return Response( + {'error': 'Employee profile not found for this user.'}, + status=status.HTTP_400_BAD_REQUEST, + ) + nominee_id = (request.data.get('nominee_employee_id') or '').strip() + nominee_status = 'PENDING' if nominee_id else 'NOT_REQUIRED' + is_director = HoldsDesignation.objects.filter( + working=employee.user, + designation__name__icontains='director', + ).exists() + is_hod = HoldsDesignation.objects.filter( + working=employee.user, + designation__name__icontains='hod', + ).exists() + is_registrar = HoldsDesignation.objects.filter( + working=employee.user, + designation__name__icontains='registrar', + ).exists() + is_hr_admin = HoldsDesignation.objects.filter( + working=employee.user, + designation__name__iregex=r'hr admin|hr administrator', + ).exists() + is_accountant = HoldsDesignation.objects.filter( + working=employee.user, + designation__name__icontains='accountant', + ).exists() + leave_type_name = (request.data.get('leave_type') or '').strip() + is_cl_rh_leave = leave_type_name in ['Casual', 'Restricted'] + employee_name = employee.user.get_full_name() or employee.user.username + department_name = employee.department.name if employee.department else (request.data.get('department') or '') + designation_name = '' + designation_record = HoldsDesignation.objects.filter(working=employee.user).select_related('designation').first() + if designation_record: + designation_name = designation_record.designation.full_name or designation_record.designation.name + else: + designation_name = request.data.get('designation') or '' + approval_status = 'PENDING' + approver_role = '' + if is_director: + approval_status = 'APPROVED' + approver_role = 'Director' + elif is_registrar: + approval_status = 'FORWARDED' + approver_role = 'Director' + elif is_hod: + if is_cl_rh_leave: + approval_status = 'PENDING' + approver_role = 'HOD' + else: + approval_status = 'FORWARDED' + approver_role = 'Director' + elif is_hr_admin or is_accountant: + approval_status = 'FORWARDED' + approver_role = 'Registrar' + + leave_app = serializer.save( + employee=employee, + employee_name=employee_name, + department=department_name, + designation=designation_name, + handover_to=nominee_id, + nominee_status=nominee_status, + approval_status=approval_status, + current_approver_role=approver_role, + ) + if is_director: + _apply_leave_balance_for_approval(leave_app) + leave_app.save(update_fields=['leave_balance_before', 'leave_balance_after']) refreshed_serializer = LeaveApplicationSerializer(leave_app, context={'request': request}) return Response(refreshed_serializer.data, status=status.HTTP_201_CREATED) + # Log validation errors to server console for easier debugging without DevTools. + print("LeaveApplication validation errors:", serializer.errors) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) class LeaveApplicationDetailView(APIView): permission_classes = [IsAuthenticated] def get(self, request, pk): - leave_app = hr2_selectors.get_leave_application_by_id_or_404(pk) + leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) serializer = LeaveApplicationSerializer(leave_app) return Response(serializer.data) def put(self, request, pk): - leave_app = hr2_selectors.get_leave_application_by_id_or_404(pk) + leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) if leave_app.employee != request.user.extrainfo and not request.user.is_staff: return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) serializer = LeaveApplicationSerializer(leave_app, data=request.data, partial=True) if serializer.is_valid(): - updated = hr2_services.update_leave_application(leave_app, serializer.validated_data) - return Response(LeaveApplicationSerializer(updated).data) + serializer.save() + return Response(serializer.data) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) def delete(self, request, pk): - leave_app = hr2_selectors.get_leave_application_by_id_or_404(pk) + leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) if leave_app.status != 'PENDING': return Response({'error': 'Cannot delete non-pending application'}, status=status.HTTP_400_BAD_REQUEST) leave_app.delete() @@ -109,7 +237,7 @@ class LeaveApplicationDownloadView(APIView): permission_classes = [IsAuthenticated] def get(self, request, pk): - leave_app = hr2_selectors.get_leave_application_by_id_or_404(pk) + leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) if leave_app.employee != request.user.extrainfo and not request.user.is_staff: return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) @@ -149,15 +277,38 @@ class LeaveApplicationWithdrawView(APIView): permission_classes = [IsAuthenticated] def post(self, request, pk): - leave_app = hr2_selectors.get_leave_application_by_id_or_404(pk) - try: - leave_app = hr2_services.withdraw_leave_application( - leave_app, - request.user, - request.data.get('remarks'), - ) - except (ValidationError, PermissionError) as exc: - return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST) + leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) + if leave_app.employee != request.user.extrainfo: + return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) + if leave_app.approval_status not in ['PENDING', 'FORWARDED']: + return Response({'error': 'Only pending or forwarded requests can be withdrawn.'}, status=status.HTTP_400_BAD_REQUEST) + + is_registrar = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='registrar', + ).exists() + is_accountant = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='accountant', + ).exists() + is_hr_admin = HoldsDesignation.objects.filter( + working=request.user, + designation__name__iregex=r'hr admin|hr administrator', + ).exists() + + if is_registrar or is_accountant or is_hr_admin: + leave_app.approval_status = 'REJECTED' + if is_registrar: + leave_app.current_approver_role = 'Registrar' + elif is_accountant: + leave_app.current_approver_role = 'Accountant' + else: + leave_app.current_approver_role = 'HR Admin' + else: + leave_app.approval_status = 'WITHDRAWN' + leave_app.current_approver_role = 'Employee' + leave_app.remarks = (request.data.get('remarks') or '').strip() + leave_app.save(update_fields=['approval_status', 'current_approver_role', 'remarks']) serializer = LeaveApplicationSerializer(leave_app) return Response(serializer.data) @@ -165,15 +316,72 @@ class LeaveApplicationCancelRequestView(APIView): permission_classes = [IsAuthenticated] def post(self, request, pk): - leave_app = hr2_selectors.get_leave_application_by_id_or_404(pk) - try: - leave_app = hr2_services.request_leave_cancellation( - leave_app, - request.user, - request.data.get('reason'), + leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) + if leave_app.employee != request.user.extrainfo: + return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) + if leave_app.approval_status != 'APPROVED': + return Response({'error': 'Only approved requests can be cancelled.'}, status=status.HTTP_400_BAD_REQUEST) + if leave_app.cancel_status != 'NOT_REQUESTED': + return Response({'error': 'Cancellation already processed or pending.'}, status=status.HTTP_400_BAD_REQUEST) + + today = timezone.now().date() + if today >= leave_app.start_date: + return Response( + {'error': 'Cancellation allowed only up to 1 day prior to start date.'}, + status=status.HTTP_400_BAD_REQUEST, ) - except (ValidationError, PermissionError) as exc: - return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST) + + is_director = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='director', + ).exists() + is_hod = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='hod', + ).exists() + is_registrar = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='registrar', + ).exists() + is_accountant = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='accountant', + ).exists() + is_hr_admin = HoldsDesignation.objects.filter( + working=request.user, + designation__name__iregex=r'hr admin|hr administrator', + ).exists() + + requester_role = 'Employee' + if is_director: + requester_role = 'Director' + elif is_hod: + requester_role = 'HOD' + elif is_registrar: + requester_role = 'Registrar' + elif is_accountant: + requester_role = 'Accountant' + elif is_hr_admin: + requester_role = 'HR Admin' + + cancel_approver_role = 'HOD' + if requester_role in ['HOD', 'Director', 'Registrar']: + cancel_approver_role = 'Director' + elif requester_role in ['Accountant', 'HR Admin']: + cancel_approver_role = 'Registrar' + + leave_app.cancel_status = 'REQUESTED' + leave_app.cancel_requested_at = timezone.now() + leave_app.cancel_requested_by_role = requester_role + leave_app.cancel_current_approver_role = cancel_approver_role + leave_app.cancel_reason = (request.data.get('reason') or '').strip() + leave_app.save(update_fields=[ + 'cancel_status', + 'cancel_requested_at', + 'cancel_requested_by_role', + 'cancel_current_approver_role', + 'cancel_reason', + ]) serializer = LeaveApplicationSerializer(leave_app) return Response(serializer.data) @@ -181,16 +389,56 @@ class LeaveApplicationCancelDecisionView(APIView): permission_classes = [IsAuthenticated] def post(self, request, pk, decision): - leave_app = hr2_selectors.get_leave_application_by_id_or_404(pk) - try: - leave_app = hr2_services.decide_leave_cancellation( - leave_app, - request.user, - decision, - request.data.get('remarks'), - ) - except (ValidationError, PermissionError) as exc: - return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST) + leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) + decision = (decision or '').lower() + if decision not in ['approve', 'reject']: + return Response({'error': 'Invalid decision'}, status=status.HTTP_400_BAD_REQUEST) + if leave_app.cancel_status != 'REQUESTED': + return Response({'error': 'No cancellation request pending.'}, status=status.HTTP_400_BAD_REQUEST) + + approver_role = (leave_app.cancel_current_approver_role or '').lower() + if approver_role == 'hod': + allowed = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='hod', + ).exists() + elif approver_role == 'director': + allowed = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='director', + ).exists() + elif approver_role == 'registrar': + allowed = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='registrar', + ).exists() + else: + allowed = False + + if not allowed: + return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) + + remarks = (request.data.get('remarks') or '').strip() + leave_app.cancel_decided_at = timezone.now() + leave_app.cancel_decision_remarks = remarks + + if decision == 'approve': + leave_app.cancel_status = 'APPROVED' + leave_app.approval_status = 'CANCELLED' + leave_app.current_approver_role = leave_app.cancel_current_approver_role + _restore_leave_balance_for_cancellation(leave_app) + else: + leave_app.cancel_status = 'REJECTED' + + leave_app.save(update_fields=[ + 'cancel_status', + 'cancel_decided_at', + 'cancel_decision_remarks', + 'approval_status', + 'current_approver_role', + 'leave_balance_before', + 'leave_balance_after', + ]) serializer = LeaveApplicationSerializer(leave_app) return Response(serializer.data) @@ -198,7 +446,18 @@ class LeaveApplicationExtensionRequestView(APIView): permission_classes = [IsAuthenticated] def post(self, request, pk): - leave_app = hr2_selectors.get_leave_application_by_id_or_404(pk) + leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) + if leave_app.employee != request.user.extrainfo: + return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) + if leave_app.approval_status != 'APPROVED': + return Response({'error': 'Only approved requests can be extended.'}, status=status.HTTP_400_BAD_REQUEST) + if leave_app.extension_status != 'NOT_REQUESTED': + return Response({'error': 'Extension already processed or pending.'}, status=status.HTTP_400_BAD_REQUEST) + + today = timezone.now().date() + if today >= leave_app.end_date: + return Response({'error': 'Extension allowed only before the original end date.'}, status=status.HTTP_400_BAD_REQUEST) + new_end_date_raw = request.data.get('new_end_date') if not new_end_date_raw: return Response({'error': 'New end date is required.'}, status=status.HTTP_400_BAD_REQUEST) @@ -206,15 +465,66 @@ def post(self, request, pk): new_end_date = datetime.datetime.strptime(new_end_date_raw, '%Y-%m-%d').date() except ValueError: return Response({'error': 'New end date must be in YYYY-MM-DD format.'}, status=status.HTTP_400_BAD_REQUEST) - try: - leave_app = hr2_services.request_leave_extension( - leave_app, - request.user, - new_end_date, - request.data.get('reason'), - ) - except (ValidationError, PermissionError) as exc: - return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST) + if new_end_date <= leave_app.end_date: + return Response({'error': 'New end date must be after the current end date.'}, status=status.HTTP_400_BAD_REQUEST) + + new_total_days = Decimal((new_end_date - leave_app.start_date).days + 1) + + is_director = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='director', + ).exists() + is_hod = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='hod', + ).exists() + is_registrar = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='registrar', + ).exists() + is_accountant = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='accountant', + ).exists() + is_hr_admin = HoldsDesignation.objects.filter( + working=request.user, + designation__name__iregex=r'hr admin|hr administrator', + ).exists() + + requester_role = 'Employee' + if is_director: + requester_role = 'Director' + elif is_hod: + requester_role = 'HOD' + elif is_registrar: + requester_role = 'Registrar' + elif is_accountant: + requester_role = 'Accountant' + elif is_hr_admin: + requester_role = 'HR Admin' + + approver_role = 'HOD' + if requester_role in ['HOD', 'Director', 'Registrar']: + approver_role = 'Director' + elif requester_role in ['Accountant', 'HR Admin']: + approver_role = 'Registrar' + + leave_app.extension_status = 'REQUESTED' + leave_app.extension_requested_at = timezone.now() + leave_app.extension_requested_by_role = requester_role + leave_app.extension_current_approver_role = approver_role + leave_app.extension_reason = (request.data.get('reason') or '').strip() + leave_app.extension_new_end_date = new_end_date + leave_app.extension_new_total_days = new_total_days + leave_app.save(update_fields=[ + 'extension_status', + 'extension_requested_at', + 'extension_requested_by_role', + 'extension_current_approver_role', + 'extension_reason', + 'extension_new_end_date', + 'extension_new_total_days', + ]) serializer = LeaveApplicationSerializer(leave_app) return Response(serializer.data) @@ -222,18 +532,59 @@ class LeaveApplicationExtensionDecisionView(APIView): permission_classes = [IsAuthenticated] def post(self, request, pk, decision): - leave_app = hr2_selectors.get_leave_application_by_id_or_404(pk) - try: - leave_app = hr2_services.decide_leave_extension( - leave_app, - request.user, - decision, - request.data.get('remarks'), - ) - except InsufficientLeaveBalanceError as exc: - return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST) - except (ValidationError, PermissionError) as exc: - return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST) + leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) + decision = (decision or '').lower() + if decision not in ['approve', 'reject']: + return Response({'error': 'Invalid decision'}, status=status.HTTP_400_BAD_REQUEST) + if leave_app.extension_status != 'REQUESTED': + return Response({'error': 'No extension request pending.'}, status=status.HTTP_400_BAD_REQUEST) + + approver_role = (leave_app.extension_current_approver_role or '').lower() + if approver_role == 'hod': + allowed = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='hod', + ).exists() + elif approver_role == 'director': + allowed = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='director', + ).exists() + elif approver_role == 'registrar': + allowed = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='registrar', + ).exists() + else: + allowed = False + + if not allowed: + return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) + + remarks = (request.data.get('remarks') or '').strip() + leave_app.extension_decided_at = timezone.now() + leave_app.extension_decision_remarks = remarks + + if decision == 'approve': + if not _apply_leave_balance_for_extension(leave_app): + return Response({'error': 'Insufficient leave balance for extension.'}, status=status.HTTP_400_BAD_REQUEST) + leave_app.extension_status = 'APPROVED' + leave_app.current_approver_role = leave_app.extension_current_approver_role + leave_app.end_date = leave_app.extension_new_end_date + leave_app.total_days = leave_app.extension_new_total_days + else: + leave_app.extension_status = 'REJECTED' + + leave_app.save(update_fields=[ + 'extension_status', + 'extension_decided_at', + 'extension_decision_remarks', + 'current_approver_role', + 'leave_balance_before', + 'leave_balance_after', + 'end_date', + 'total_days', + ]) serializer = LeaveApplicationSerializer(leave_app) return Response(serializer.data) @@ -241,7 +592,14 @@ class LeaveResumptionSubmitView(APIView): permission_classes = [IsAuthenticated] def post(self, request, pk): - leave_app = hr2_selectors.get_leave_application_by_id_or_404(pk) + leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) + if leave_app.employee != request.user.extrainfo: + return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) + if leave_app.approval_status != 'APPROVED': + return Response({'error': 'Resumption allowed only for approved leaves.'}, status=status.HTTP_400_BAD_REQUEST) + if leave_app.resumption_status != 'NOT_REQUESTED': + return Response({'error': 'Resumption already submitted or processed.'}, status=status.HTTP_400_BAD_REQUEST) + today = timezone.now().date() resumption_date_raw = (request.data.get('resumption_date') or '').strip() if resumption_date_raw: @@ -251,15 +609,22 @@ def post(self, request, pk): return Response({'error': 'Resumption date must be in YYYY-MM-DD format.'}, status=status.HTTP_400_BAD_REQUEST) else: resumption_date = today - try: - leave_app = hr2_services.submit_leave_resumption( - leave_app, - request.user, - resumption_date, - request.data.get('reason'), - ) - except (ValidationError, PermissionError) as exc: - return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST) + + if resumption_date <= leave_app.end_date: + return Response({'error': 'Resumption date must be after the leave end date.'}, status=status.HTTP_400_BAD_REQUEST) + + leave_app.resumption_status = 'SUBMITTED' + leave_app.resumption_date = resumption_date + leave_app.resumption_reason = (request.data.get('reason') or '').strip() + leave_app.resumption_submitted_at = timezone.now() + leave_app.resumption_current_approver_role = 'HOD' + leave_app.save(update_fields=[ + 'resumption_status', + 'resumption_date', + 'resumption_reason', + 'resumption_submitted_at', + 'resumption_current_approver_role', + ]) serializer = LeaveApplicationSerializer(leave_app) return Response(serializer.data) @@ -267,16 +632,34 @@ class LeaveResumptionDecisionView(APIView): permission_classes = [IsAuthenticated] def post(self, request, pk, decision): - leave_app = hr2_selectors.get_leave_application_by_id_or_404(pk) - try: - leave_app = hr2_services.decide_leave_resumption( - leave_app, - request.user, - decision, - request.data.get('remarks'), - ) - except (ValidationError, PermissionError) as exc: - return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST) + leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) + decision = (decision or '').lower() + if decision not in ['approve', 'reject']: + return Response({'error': 'Invalid decision'}, status=status.HTTP_400_BAD_REQUEST) + if leave_app.resumption_status != 'SUBMITTED': + return Response({'error': 'No resumption request pending.'}, status=status.HTTP_400_BAD_REQUEST) + + allowed = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='hod', + ).exists() + if not allowed: + return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) + + leave_app.resumption_decided_at = timezone.now() + leave_app.resumption_decision_remarks = (request.data.get('remarks') or '').strip() + if decision == 'approve': + leave_app.resumption_status = 'APPROVED' + leave_app.current_approver_role = 'HOD' + else: + leave_app.resumption_status = 'REJECTED' + + leave_app.save(update_fields=[ + 'resumption_status', + 'resumption_decided_at', + 'resumption_decision_remarks', + 'current_approver_role', + ]) serializer = LeaveApplicationSerializer(leave_app) return Response(serializer.data) @@ -285,10 +668,22 @@ class LeaveBalanceView(APIView): def get(self, request, employee_id=None): if employee_id: - employee = hr2_selectors.get_employee_by_id_or_404(employee_id) + employee = get_object_or_404(ExtraInfo, id=employee_id) else: employee = request.user.extrainfo - balances = hr2_selectors.get_latest_leave_balances_for_employee(employee) + balances_qs = ( + EmployeeLeaveBalance.objects.filter(employee=employee) + .select_related('leave_type') + .order_by('leave_type_id', '-year', '-id') + ) + # Collect the latest balance per leave type without relying on DISTINCT ON. + balances = [] + seen_leave_types = set() + for balance in balances_qs: + if balance.leave_type_id in seen_leave_types: + continue + seen_leave_types.add(balance.leave_type_id) + balances.append(balance) serializer = LeaveBalanceSerializer(balances, many=True) return Response(serializer.data) @@ -297,7 +692,10 @@ class LeaveNomineeDashboardView(APIView): def get(self, request): employee = request.user.extrainfo - leaves = hr2_selectors.get_leave_applications_for_nominee(employee.id) + leaves = LeaveApplicationNew.objects.filter( + handover_to=employee.id, + nominee_status='PENDING', + ).order_by('-applied_date') serializer = LeaveApplicationSerializer(leaves, many=True) return Response(serializer.data) @@ -305,15 +703,18 @@ class LeaveNomineeDecisionView(APIView): permission_classes = [IsAuthenticated] def post(self, request, pk): - leave_app = hr2_selectors.get_leave_application_by_id_or_404(pk) - try: - leave_app = hr2_services.respond_leave_nominee( - leave_app, - request.user, - request.data.get('action'), - ) - except (ValidationError, PermissionError) as exc: - return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST) + action = (request.data.get('action') or '').lower() + if action not in ['accept', 'decline']: + return Response({'error': 'Invalid action'}, status=status.HTTP_400_BAD_REQUEST) + + leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) + employee = request.user.extrainfo + if leave_app.handover_to != employee.id: + return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) + + leave_app.nominee_status = 'ACCEPTED' if action == 'accept' else 'DECLINED' + leave_app.nominee_responded_at = datetime.datetime.utcnow() + leave_app.save(update_fields=['nominee_status', 'nominee_responded_at']) serializer = LeaveApplicationSerializer(leave_app) return Response(serializer.data) @@ -321,15 +722,24 @@ class LeaveDocumentRequestView(APIView): permission_classes = [IsAuthenticated] def post(self, request, pk): - leave_app = hr2_selectors.get_leave_application_by_id_or_404(pk) - try: - leave_app = hr2_services.request_leave_document( - leave_app, - request.user, - (request.data.get('message') or '').strip(), - ) - except (ValidationError, PermissionError) as exc: - return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST) + message = (request.data.get('message') or '').strip() + if not message: + return Response({'error': 'Document request message is required.'}, status=status.HTTP_400_BAD_REQUEST) + + is_hod = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='hod', + ).exists() + if not is_hod: + return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) + + leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) + if leave_app.document_request_status == 'REQUESTED': + return Response({'error': 'Document already requested.'}, status=status.HTTP_400_BAD_REQUEST) + leave_app.document_request_message = message + leave_app.document_request_status = 'REQUESTED' + leave_app.document_requested_at = datetime.datetime.utcnow() + leave_app.save(update_fields=['document_request_message', 'document_request_status', 'document_requested_at']) serializer = LeaveApplicationSerializer(leave_app) return Response(serializer.data) @@ -337,15 +747,20 @@ class LeaveDocumentSubmitView(APIView): permission_classes = [IsAuthenticated] def post(self, request, pk): - leave_app = hr2_selectors.get_leave_application_by_id_or_404(pk) - try: - leave_app = hr2_services.submit_leave_document( - leave_app, - request.user, - (request.data.get('submission') or '').strip(), - ) - except (ValidationError, PermissionError) as exc: - return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST) + submission = (request.data.get('submission') or '').strip() + if not submission: + return Response({'error': 'Document submission is required.'}, status=status.HTTP_400_BAD_REQUEST) + + leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) + if leave_app.employee != request.user.extrainfo: + return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) + if leave_app.document_request_status != 'REQUESTED': + return Response({'error': 'No document requested for this leave.'}, status=status.HTTP_400_BAD_REQUEST) + + leave_app.document_submission = submission + leave_app.document_request_status = 'SUBMITTED' + leave_app.document_submitted_at = datetime.datetime.utcnow() + leave_app.save(update_fields=['document_submission', 'document_request_status', 'document_submitted_at']) serializer = LeaveApplicationSerializer(leave_app) return Response(serializer.data) @@ -353,14 +768,14 @@ class LeaveResponsibilityView(APIView): permission_classes = [IsAuthenticated] def post(self, request, pk, responsibility_type): - leave_app = hr2_selectors.get_leave_application_by_id_or_404(pk) + leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) action = request.data.get('action') remarks = request.data.get('remarks', '') try: if responsibility_type == 'academic': - leave_app = hr2_services.handle_academic_responsibility(leave_app, request.user.extrainfo, action, remarks) + leave_app = handle_academic_responsibility(leave_app, request.user.extrainfo, action, remarks) else: - leave_app = hr2_services.handle_administrative_responsibility(leave_app, request.user.extrainfo, action, remarks) + leave_app = handle_administrative_responsibility(leave_app, request.user.extrainfo, action, remarks) serializer = LeaveApplicationSerializer(leave_app) return Response(serializer.data) except (PermissionError, InvalidWorkflowTransitionError) as e: @@ -370,20 +785,160 @@ class LeaveApproveRejectView(APIView): permission_classes = [IsAuthenticated] def post(self, request, pk, decision): - leave_app = hr2_selectors.get_leave_application_by_id_or_404(pk) - try: - leave_app = hr2_services.decide_leave_application( - leave_app, - request.user, - decision, - request.data.get('remarks', ''), + leave_app = get_object_or_404(LeaveApplicationNew, pk=pk) + remarks = request.data.get('remarks', '') + decision = (decision or '').lower() + if decision not in ['approve', 'reject', 'forward']: + return Response({'error': 'Invalid decision'}, status=status.HTTP_400_BAD_REQUEST) + + is_registrar = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='registrar', + ).exists() + is_director = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='director', + ).exists() + approver_role = 'HOD' + if is_registrar: + approver_role = 'Registrar' + elif is_director: + approver_role = 'Director' + + leave_type_name = (leave_app.leave_type or '').strip() + is_cl_rh_leave = leave_type_name in ['Casual', 'Restricted'] + if decision == 'approve' and not is_cl_rh_leave and approver_role == 'HOD': + return Response( + {'error': 'Only CL/RH leaves can be approved by HOD. Please forward to Director.'}, + status=status.HTTP_400_BAD_REQUEST, ) - except ValidationError as exc: - return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST) + if decision == 'forward' and is_cl_rh_leave: + decision = 'approve' + + if decision == 'approve': + leave_app.approval_status = 'APPROVED' + leave_app.current_approver_role = approver_role + _apply_leave_balance_for_approval(leave_app) + elif decision == 'forward': + leave_app.approval_status = 'FORWARDED' + leave_app.current_approver_role = 'Director' + else: + leave_app.approval_status = 'REJECTED' + leave_app.current_approver_role = approver_role + + leave_app.remarks = remarks + leave_app.save(update_fields=[ + 'approval_status', + 'remarks', + 'current_approver_role', + 'leave_balance_before', + 'leave_balance_after', + ]) serializer = LeaveApplicationSerializer(leave_app) return Response(serializer.data) + +def _apply_leave_balance_for_approval(leave_app): + leave_type = LeaveType.objects.filter(name__iexact=leave_app.leave_type).first() + if not leave_type: + return + year = leave_app.start_date.year + balance = EmployeeLeaveBalance.objects.filter( + employee=leave_app.employee, + leave_type=leave_type, + year=year, + ).first() + if balance is None: + balance = EmployeeLeaveBalance.objects.filter( + employee=leave_app.employee, + leave_type=leave_type, + ).order_by('-year').first() + if balance is None or balance.year != year: + balance = EmployeeLeaveBalance.objects.create( + employee=leave_app.employee, + leave_type=leave_type, + year=year, + opening_balance=Decimal('0'), + accrued=Decimal('0'), + availed=Decimal('0'), + current_balance=Decimal('0'), + ) + total_days = Decimal(str(leave_app.total_days or 0)) + before_balance = balance.current_balance + balance.availed = (balance.availed or 0) + total_days + balance.current_balance = (balance.current_balance or 0) - total_days + balance.save(update_fields=['availed', 'current_balance']) + + if leave_app.leave_balance_before is None: + leave_app.leave_balance_before = before_balance + leave_app.leave_balance_after = balance.current_balance + +def _restore_leave_balance_for_cancellation(leave_app): + leave_type = LeaveType.objects.filter(name__iexact=leave_app.leave_type).first() + if not leave_type: + return + year = leave_app.start_date.year + balance = EmployeeLeaveBalance.objects.filter( + employee=leave_app.employee, + leave_type=leave_type, + year=year, + ).first() + if balance is None: + balance = EmployeeLeaveBalance.objects.filter( + employee=leave_app.employee, + leave_type=leave_type, + ).order_by('-year').first() + if balance is None: + return + + total_days = Decimal(str(leave_app.total_days or 0)) + before_balance = balance.current_balance + balance.availed = (balance.availed or 0) - total_days + balance.current_balance = (balance.current_balance or 0) + total_days + balance.save(update_fields=['availed', 'current_balance']) + + if leave_app.leave_balance_before is None: + leave_app.leave_balance_before = before_balance + leave_app.leave_balance_after = balance.current_balance + +def _apply_leave_balance_for_extension(leave_app): + if not leave_app.extension_new_total_days: + return False + delta_days = Decimal(str(leave_app.extension_new_total_days)) - Decimal(str(leave_app.total_days or 0)) + if delta_days <= 0: + return False + + leave_type = LeaveType.objects.filter(name__iexact=leave_app.leave_type).first() + if not leave_type: + return False + year = leave_app.start_date.year + balance = EmployeeLeaveBalance.objects.filter( + employee=leave_app.employee, + leave_type=leave_type, + year=year, + ).first() + if balance is None: + balance = EmployeeLeaveBalance.objects.filter( + employee=leave_app.employee, + leave_type=leave_type, + ).order_by('-year').first() + if balance is None: + return False + + if (balance.current_balance or 0) < delta_days: + return False + + before_balance = balance.current_balance + balance.availed = (balance.availed or 0) + delta_days + balance.current_balance = (balance.current_balance or 0) - delta_days + balance.save(update_fields=['availed', 'current_balance']) + + if leave_app.leave_balance_before is None: + leave_app.leave_balance_before = before_balance + leave_app.leave_balance_after = balance.current_balance + return True + # ==================== ATTENDANCE VIEWS ==================== class AttendanceView(APIView): @@ -397,14 +952,16 @@ def get(self, request): return Response(serializer.data) def post(self, request): - serializer = EmployeeAttendanceSerializer(data=request.data) - if serializer.is_valid(): - attendance = hr2_services.create_attendance( - request.user.extrainfo, - serializer.validated_data, - ) - return Response(EmployeeAttendanceSerializer(attendance).data, status=status.HTTP_201_CREATED) - return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + attendance = mark_attendance( + employee_extra_info=request.user.extrainfo, + date=request.data.get('date'), + status=request.data.get('status'), + in_time=request.data.get('in_time'), + out_time=request.data.get('out_time'), + remarks=request.data.get('remarks', '') + ) + serializer = EmployeeAttendanceSerializer(attendance) + return Response(serializer.data, status=status.HTTP_201_CREATED) # ==================== APPRAISAL VIEWS ==================== @@ -413,7 +970,7 @@ class AppraisalPeriodListView(APIView): def get(self, request): is_active = request.query_params.get('is_active') - periods = hr2_selectors.get_appraisal_periods(is_active) + periods = get_appraisal_periods(is_active) serializer = AppraisalPeriodSerializer(periods, many=True) return Response(serializer.data) @@ -422,18 +979,15 @@ class AppraisalListView(APIView): def get(self, request): period_id = request.query_params.get('period') - appraisals = hr2_selectors.get_appraisals_for_employee(request.user.extrainfo, period_id) + appraisals = get_appraisals_for_employee(request.user.extrainfo, period_id) serializer = PerformanceAppraisalSerializer(appraisals, many=True) return Response(serializer.data) def post(self, request): serializer = PerformanceAppraisalSerializer(data=request.data) if serializer.is_valid(): - appraisal = hr2_services.create_performance_appraisal( - request.user.extrainfo, - serializer.validated_data, - ) - return Response(PerformanceAppraisalSerializer(appraisal).data, status=status.HTTP_201_CREATED) + serializer.save(employee=request.user.extrainfo) + return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) # ==================== TRAINING VIEWS ==================== @@ -457,12 +1011,8 @@ def get(self, request): def post(self, request): serializer = TrainingNominationSerializer(data=request.data) if serializer.is_valid(): - nomination = hr2_services.create_training_nomination( - request.user.extrainfo, - request.user.extrainfo, - serializer.validated_data, - ) - return Response(TrainingNominationSerializer(nomination).data, status=status.HTTP_201_CREATED) + serializer.save(employee=request.user.extrainfo, nominated_by=request.user.extrainfo) + return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) # ==================== PROMOTION VIEWS ==================== @@ -478,11 +1028,8 @@ def get(self, request): def post(self, request): serializer = PromotionApplicationSerializer(data=request.data) if serializer.is_valid(): - promotion = hr2_services.create_promotion_application( - request.user.extrainfo, - serializer.validated_data, - ) - return Response(PromotionApplicationSerializer(promotion).data, status=status.HTTP_201_CREATED) + serializer.save(employee=request.user.extrainfo) + return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) # ==================== FACULTY WORKLOAD VIEWS ==================== @@ -498,7 +1045,7 @@ def get(self, request): return Response(serializer.data) def post(self, request): - workload = hr2_services.calculate_faculty_workload( + workload = calculate_faculty_workload( request.user.extra_info, request.data.get('semester'), request.data.get('year') @@ -506,7 +1053,9 @@ def post(self, request): serializer = FacultyWorkloadSerializer(workload) return Response(serializer.data) -from ..selectors import get_cpda_reimbursements +from ..models import LTCApplicationNew, CPDAAdvanceNew, CPDAReimbursementNew, AppraisalFormNew +from ..services import apply_ltc, approve_ltc, reject_ltc, apply_cpda_advance, approve_cpda_advance, reject_cpda_advance, apply_cpda_reimbursement, approve_cpda_reimbursement, reject_cpda_reimbursement, submit_appraisal, review_appraisal +from ..selectors import get_ltc_applications, get_cpda_advances, get_cpda_reimbursements, get_appraisal_forms from .serializers import LTCApplicationSerializer, CPDAAdvanceSerializer, CPDAReimbursementSerializer, AppraisalFormSerializer # ==================== LTC VIEWS ==================== @@ -515,44 +1064,61 @@ class LTCApplicationListCreateView(APIView): permission_classes = [IsAuthenticated] def get(self, request): - role_flags = hr2_selectors.get_role_flags(request.user) - ltcs = hr2_selectors.get_ltc_applications_for_role_view(request.user, role_flags) + is_hr_staff = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='hr', + ).exists() or ( + request.user.extrainfo.user_type == 'staff' + and request.user.extrainfo.department + and request.user.extrainfo.department.name == 'HR' + ) + is_accountant = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='accountant', + ).exists() + + if is_hr_staff: + ltcs = LTCApplicationNew.objects.filter(approval_status__in=['PENDING', 'FORWARDED']) + elif is_accountant: + ltcs = LTCApplicationNew.objects.filter( + approval_status='FORWARDED', + accountant_status__iexact='PENDING', + ) + else: + ltcs = get_ltc_applications(request.user.extrainfo) serializer = LTCApplicationSerializer(ltcs, many=True) return Response(serializer.data) def post(self, request): serializer = LTCApplicationSerializer(data=request.data) if serializer.is_valid(): - ltc = hr2_services.create_ltc_application( - request.user.extrainfo, - serializer.validated_data, - ) - return Response(LTCApplicationSerializer(ltc).data, status=status.HTTP_201_CREATED) + serializer.save(employee=request.user.extrainfo) + return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) class LTCApplicationDetailView(APIView): permission_classes = [IsAuthenticated] def get(self, request, pk): - ltc = hr2_selectors.get_ltc_application_by_id_or_404(pk) + ltc = get_object_or_404(LTCApplicationNew, pk=pk) serializer = LTCApplicationSerializer(ltc) return Response(serializer.data) def put(self, request, pk): - ltc = hr2_selectors.get_ltc_application_by_id_or_404(pk) + ltc = get_object_or_404(LTCApplicationNew, pk=pk) if ltc.employee != request.user.extrainfo: return Response({'error': 'Not authorized'}, status=403) serializer = LTCApplicationSerializer(ltc, data=request.data, partial=True) if serializer.is_valid(): - updated = hr2_services.update_ltc_application(ltc, serializer.validated_data) - return Response(LTCApplicationSerializer(updated).data) + serializer.save() + return Response(serializer.data) return Response(serializer.errors, status=400) class LTCApplicationDownloadView(APIView): permission_classes = [IsAuthenticated] def get(self, request, pk): - ltc = hr2_selectors.get_ltc_application_by_id_or_404(pk) + ltc = get_object_or_404(LTCApplicationNew, pk=pk) if ltc.employee != request.user.extrainfo and not request.user.is_staff: return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) @@ -583,15 +1149,15 @@ class LTCApplicationWithdrawView(APIView): permission_classes = [IsAuthenticated] def post(self, request, pk): - ltc = hr2_selectors.get_ltc_application_by_id_or_404(pk) - try: - ltc = hr2_services.withdraw_ltc_application( - ltc, - request.user, - request.data.get('remarks'), - ) - except (ValidationError, PermissionError) as exc: - return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST) + ltc = get_object_or_404(LTCApplicationNew, pk=pk) + if ltc.employee != request.user.extrainfo: + return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) + if ltc.approval_status != 'PENDING': + return Response({'error': 'Only pending requests can be withdrawn.'}, status=status.HTTP_400_BAD_REQUEST) + + ltc.approval_status = 'WITHDRAWN' + ltc.remarks = (request.data.get('remarks') or '').strip() + ltc.save(update_fields=['approval_status', 'remarks']) serializer = LTCApplicationSerializer(ltc) return Response(serializer.data) @@ -599,15 +1165,26 @@ class LTCApproveRejectView(APIView): permission_classes = [IsAuthenticated] def post(self, request, pk, decision): - ltc = hr2_selectors.get_ltc_application_by_id_or_404(pk) - try: - ltc = hr2_services.decide_ltc_application( - ltc, - decision, - request.data.get('remarks', ''), - ) - except ValidationError as exc: - return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST) + ltc = get_object_or_404(LTCApplicationNew, pk=pk) + remarks = request.data.get('remarks', '') + decision = (decision or '').lower() + if decision not in ['approve', 'reject', 'forward']: + return Response({'error': 'Invalid decision'}, status=status.HTTP_400_BAD_REQUEST) + + if decision == 'approve': + ltc.approval_status = 'APPROVED' + ltc.accountant_status = 'APPROVED' + elif decision == 'forward': + ltc.approval_status = 'FORWARDED' + ltc.verified_by_hr = True + ltc.accountant_status = 'PENDING' + else: + ltc.approval_status = 'REJECTED' + ltc.accountant_status = 'REJECTED' + + ltc.remarks = remarks + ltc.save(update_fields=['approval_status', 'remarks', 'verified_by_hr', 'accountant_status']) + serializer = LTCApplicationSerializer(ltc) return Response(serializer.data) @@ -616,24 +1193,50 @@ def post(self, request, pk, decision): class CPDAAdvanceListCreateView(APIView): permission_classes = [IsAuthenticated] def get(self, request): - role_flags = hr2_selectors.get_role_flags(request.user) - advances = hr2_selectors.get_cpda_advances_for_role_view(request.user, role_flags) + is_hr_staff = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='hr', + ).exists() or ( + request.user.extrainfo.user_type == 'staff' + and request.user.extrainfo.department + and request.user.extrainfo.department.name == 'HR' + ) + is_accountant = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='accountant', + ).exists() + is_director = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='director', + ).exists() + + if is_director: + advances = CPDAAdvanceNew.objects.filter( + approval_status='FORWARDED', + accountant_processing_status__iexact='DIRECTOR_REVIEW', + ) + elif is_hr_staff: + advances = CPDAAdvanceNew.objects.filter(approval_status='PENDING') + elif is_accountant: + advances = CPDAAdvanceNew.objects.filter( + approval_status='FORWARDED', + accountant_processing_status__in=['PENDING', 'DIRECTOR_APPROVED'], + ) + else: + advances = get_cpda_advances(request.user.extrainfo) serializer = CPDAAdvanceSerializer(advances, many=True) return Response(serializer.data) def post(self, request): serializer = CPDAAdvanceSerializer(data=request.data) if serializer.is_valid(): - cpda = hr2_services.create_cpda_advance( - request.user.extrainfo, - serializer.validated_data, - ) - return Response(CPDAAdvanceSerializer(cpda).data, status=status.HTTP_201_CREATED) + serializer.save(employee=request.user.extrainfo) + return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) class CPDAAdvanceDetailView(APIView): permission_classes = [IsAuthenticated] def get(self, request, pk): - cpda = hr2_selectors.get_cpda_advance_by_id_or_404(pk) + cpda = get_object_or_404(CPDAAdvanceNew, pk=pk) serializer = CPDAAdvanceSerializer(cpda) return Response(serializer.data) @@ -641,7 +1244,7 @@ class CPDAAdvanceDownloadView(APIView): permission_classes = [IsAuthenticated] def get(self, request, pk): - cpda = hr2_selectors.get_cpda_advance_by_id_or_404(pk) + cpda = get_object_or_404(CPDAAdvanceNew, pk=pk) if cpda.employee != request.user.extrainfo and not request.user.is_staff: return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) @@ -672,31 +1275,46 @@ class CPDAAdvanceWithdrawView(APIView): permission_classes = [IsAuthenticated] def post(self, request, pk): - cpda = hr2_selectors.get_cpda_advance_by_id_or_404(pk) - try: - cpda = hr2_services.withdraw_cpda_advance( - cpda, - request.user, - request.data.get('remarks'), - ) - except (ValidationError, PermissionError) as exc: - return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST) + cpda = get_object_or_404(CPDAAdvanceNew, pk=pk) + if cpda.employee != request.user.extrainfo: + return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) + if cpda.approval_status != 'PENDING': + return Response({'error': 'Only pending requests can be withdrawn.'}, status=status.HTTP_400_BAD_REQUEST) + + cpda.approval_status = 'WITHDRAWN' + cpda.remarks = (request.data.get('remarks') or '').strip() + cpda.save(update_fields=['approval_status', 'remarks']) serializer = CPDAAdvanceSerializer(cpda) return Response(serializer.data) class CPDAAdvanceApproveRejectView(APIView): permission_classes = [IsAuthenticated] def post(self, request, pk, decision): - cpda = hr2_selectors.get_cpda_advance_by_id_or_404(pk) - try: - cpda = hr2_services.decide_cpda_advance( - cpda, - request.user, - decision, - request.data.get('remarks', ''), - ) - except (ValidationError, PermissionError) as exc: - return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST) + cpda = get_object_or_404(CPDAAdvanceNew, pk=pk) + remarks = request.data.get('remarks', '') + decision = (decision or '').lower() + if decision not in ['approve', 'reject', 'forward-accountant', 'forward-director']: + return Response({'error': 'Invalid decision'}, status=status.HTTP_400_BAD_REQUEST) + + if decision == 'forward-accountant': + cpda.approval_status = 'FORWARDED' + cpda.verified_by_hr = True + cpda.accountant_processing_status = 'PENDING' + elif decision == 'forward-director': + cpda.approval_status = 'FORWARDED' + cpda.accountant_processing_status = 'DIRECTOR_REVIEW' + elif decision == 'approve': + if cpda.accountant_processing_status == 'DIRECTOR_REVIEW': + cpda.accountant_processing_status = 'DIRECTOR_APPROVED' + cpda.approval_status = 'FORWARDED' + else: + cpda.approval_status = 'APPROVED' + cpda.accountant_processing_status = 'APPROVED' + else: + cpda.approval_status = 'REJECTED' + cpda.accountant_processing_status = 'REJECTED' + cpda.remarks = remarks + cpda.save(update_fields=['approval_status', 'remarks', 'verified_by_hr', 'accountant_processing_status']) serializer = CPDAAdvanceSerializer(cpda) return Response(serializer.data) @@ -711,33 +1329,26 @@ def get(self, request): def post(self, request): serializer = CPDAReimbursementSerializer(data=request.data) if serializer.is_valid(): - reimbursement = hr2_services.create_cpda_reimbursement( - request.user.extrainfo, - serializer.validated_data, - ) - return Response(CPDAReimbursementSerializer(reimbursement).data, status=status.HTTP_201_CREATED) + serializer.save(employee=request.user.extrainfo) + return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) class CPDAReimbursementDetailView(APIView): permission_classes = [IsAuthenticated] def get(self, request, pk): - reim = hr2_selectors.get_cpda_reimbursement_by_id_or_404(pk) + reim = get_object_or_404(CPDAReimbursementNew, pk=pk) serializer = CPDAReimbursementSerializer(reim) return Response(serializer.data) class CPDAReimbursementApproveRejectView(APIView): permission_classes = [IsAuthenticated] def post(self, request, pk, decision): - reim = hr2_selectors.get_cpda_reimbursement_by_id_or_404(pk) - try: - reim = hr2_services.decide_cpda_reimbursement( - reim, - decision, - request.user.extrainfo, - request.data.get('remarks', ''), - ) - except ValidationError as exc: - return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST) + reim = get_object_or_404(CPDAReimbursementNew, pk=pk) + remarks = request.data.get('remarks', '') + if decision == 'approve': + reim = approve_cpda_reimbursement(reim, request.user.extrainfo, remarks) + else: + reim = reject_cpda_reimbursement(reim, request.user.extrainfo, remarks) serializer = CPDAReimbursementSerializer(reim) return Response(serializer.data) @@ -746,24 +1357,59 @@ def post(self, request, pk, decision): class AppraisalFormListCreateView(APIView): permission_classes = [IsAuthenticated] def get(self, request): - role_flags = hr2_selectors.get_role_flags(request.user) - appraisals = hr2_selectors.get_appraisal_forms_for_role_view(request.user, role_flags) + is_hr_staff = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='hr', + ).exists() or ( + request.user.extrainfo.user_type == 'staff' + and request.user.extrainfo.department + and request.user.extrainfo.department.name == 'HR' + ) + is_hod = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='hod', + ).exists() + is_director = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='director', + ).exists() + + if is_hr_staff: + appraisals = AppraisalFormNew.objects.all().order_by('-submitted_at') + elif is_director: + appraisals = AppraisalFormNew.objects.filter( + assigned_reviewer_role__iexact='DIRECTOR', + ).filter( + Q(assigned_reviewer__isnull=True) + | Q(assigned_reviewer=request.user.extrainfo) + ).filter( + status__in=['PENDING', 'REVIEWED'] + ).order_by('-submitted_at') + elif is_hod: + appraisals = AppraisalFormNew.objects.filter( + assigned_reviewer_role__iexact='HOD', + department=request.user.extrainfo.department.name, + ).filter( + Q(assigned_reviewer__isnull=True) + | Q(assigned_reviewer=request.user.extrainfo) + ).filter( + status='PENDING' + ).order_by('-submitted_at') + else: + appraisals = get_appraisal_forms(request.user.extrainfo) serializer = AppraisalFormSerializer(appraisals, many=True) return Response(serializer.data) def post(self, request): serializer = AppraisalFormSerializer(data=request.data) if serializer.is_valid(): - appraisal = hr2_services.create_appraisal_form( - request.user.extrainfo, - serializer.validated_data, - ) - return Response(AppraisalFormSerializer(appraisal).data, status=status.HTTP_201_CREATED) + serializer.save(employee=request.user.extrainfo) + return Response(serializer.data, status=status.HTTP_201_CREATED) return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) class AppraisalFormDetailView(APIView): permission_classes = [IsAuthenticated] def get(self, request, pk): - appraisal = hr2_selectors.get_appraisal_form_by_id_or_404(pk) + appraisal = get_object_or_404(AppraisalFormNew, pk=pk) serializer = AppraisalFormSerializer(appraisal) return Response(serializer.data) @@ -771,7 +1417,7 @@ class AppraisalFormDownloadView(APIView): permission_classes = [IsAuthenticated] def get(self, request, pk): - appraisal = hr2_selectors.get_appraisal_form_by_id_or_404(pk) + appraisal = get_object_or_404(AppraisalFormNew, pk=pk) if appraisal.employee != request.user.extrainfo and not request.user.is_staff: return Response({'error': 'Not authorized'}, status=status.HTTP_403_FORBIDDEN) @@ -796,18 +1442,50 @@ def get(self, request, pk): class AppraisalReviewView(APIView): permission_classes = [IsAuthenticated] def post(self, request, pk): - appraisal = hr2_selectors.get_appraisal_form_by_id_or_404(pk) + appraisal = get_object_or_404(AppraisalFormNew, pk=pk) action = (request.data.get('action') or 'review').lower() - try: - appraisal = hr2_services.review_appraisal_form( - appraisal, - request.user, - action, - request.data.get('remarks', ''), - request.data.get('rating', ''), - ) - except (ValidationError, PermissionError) as exc: - return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST) + remarks = request.data.get('remarks', '') + rating = request.data.get('rating', '') + + is_hod = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='hod', + ).exists() + is_director = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='director', + ).exists() + if is_hod and appraisal.assigned_reviewer_role.upper() != 'HOD': + return Response({'error': 'Not assigned to HOD review.'}, status=status.HTTP_403_FORBIDDEN) + if is_director and appraisal.assigned_reviewer_role.upper() != 'DIRECTOR': + return Response({'error': 'Not assigned to Director review.'}, status=status.HTTP_403_FORBIDDEN) + if not (is_hod or is_director): + return Response({'error': 'Not authorized to review.'}, status=status.HTTP_403_FORBIDDEN) + + appraisal.reviewer_id = str(request.user.extrainfo.id) + appraisal.reviewer_comments = remarks + if rating: + appraisal.rating = str(rating) + + if action == 'approve': + appraisal.status = 'APPROVED' + appraisal.assigned_reviewer_role = '' + appraisal.assigned_reviewer = None + elif action == 'forward': + appraisal.status = 'REVIEWED' + appraisal.assigned_reviewer_role = 'DIRECTOR' + appraisal.assigned_reviewer = None + else: + appraisal.status = 'REVIEWED' + + appraisal.save(update_fields=[ + 'reviewer_id', + 'reviewer_comments', + 'rating', + 'status', + 'assigned_reviewer_role', + 'assigned_reviewer', + ]) serializer = AppraisalFormSerializer(appraisal) return Response(serializer.data) @@ -815,18 +1493,41 @@ class AppraisalAssignView(APIView): permission_classes = [IsAuthenticated] def post(self, request, pk): - appraisal = hr2_selectors.get_appraisal_form_by_id_or_404(pk) + appraisal = get_object_or_404(AppraisalFormNew, pk=pk) + is_hr_staff = HoldsDesignation.objects.filter( + working=request.user, + designation__name__icontains='hr', + ).exists() or ( + request.user.extrainfo.user_type == 'staff' + and request.user.extrainfo.department + and request.user.extrainfo.department.name == 'HR' + ) + if not is_hr_staff: + return Response({'error': 'Not authorized to assign.'}, status=status.HTTP_403_FORBIDDEN) + role = (request.data.get('role') or '').upper() reviewer_id = (request.data.get('reviewer_id') or '').strip() - try: - appraisal = hr2_services.assign_appraisal_reviewer( - appraisal, - request.user, - role, - reviewer_id, - ) - except (ValidationError, PermissionError) as exc: - return Response({'error': str(exc)}, status=status.HTTP_400_BAD_REQUEST) + if role not in ['HOD', 'DIRECTOR']: + return Response({'error': 'Role must be HOD or DIRECTOR.'}, status=status.HTTP_400_BAD_REQUEST) + if appraisal.status != 'PENDING': + return Response({'error': 'Only pending appraisals can be assigned.'}, status=status.HTTP_400_BAD_REQUEST) + + assigned_reviewer = None + if reviewer_id: + assigned_reviewer = ExtraInfo.objects.filter(id=reviewer_id).first() + if not assigned_reviewer: + return Response({'error': 'Reviewer not found.'}, status=status.HTTP_400_BAD_REQUEST) + + appraisal.assigned_reviewer_role = role + appraisal.assigned_reviewer = assigned_reviewer + appraisal.assigned_by = request.user.extrainfo + appraisal.assigned_at = timezone.now() + appraisal.save(update_fields=[ + 'assigned_reviewer_role', + 'assigned_reviewer', + 'assigned_by', + 'assigned_at', + ]) serializer = AppraisalFormSerializer(appraisal) return Response(serializer.data) diff --git a/FusionIIIT/applications/hr2/migrations/0011_leave_attachment_files.py b/FusionIIIT/applications/hr2/migrations/0011_leave_attachment_files.py deleted file mode 100644 index f961e203e..000000000 --- a/FusionIIIT/applications/hr2/migrations/0011_leave_attachment_files.py +++ /dev/null @@ -1,20 +0,0 @@ -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ("hr2", "0010_appraisal_assignment"), - ] - - operations = [ - migrations.AlterField( - model_name="leaveapplicationnew", - name="medical_certificate", - field=models.FileField(blank=True, null=True, upload_to="hr/leave/"), - ), - migrations.AlterField( - model_name="leaveapplicationnew", - name="attachment_file", - field=models.FileField(blank=True, null=True, upload_to="hr/leave/"), - ), - ] diff --git a/FusionIIIT/applications/hr2/models.py b/FusionIIIT/applications/hr2/models.py index 8946574a4..22b7fa5cc 100644 --- a/FusionIIIT/applications/hr2/models.py +++ b/FusionIIIT/applications/hr2/models.py @@ -614,8 +614,8 @@ class LeaveApplicationNew(models.Model): ) nominee_responded_at = models.DateTimeField(null=True, blank=True) - medical_certificate = models.FileField(upload_to='hr/leave/', blank=True, null=True) - attachment_file = models.FileField(upload_to='hr/leave/', blank=True, null=True) + medical_certificate = models.CharField(max_length=200, blank=True) + attachment_file = models.CharField(max_length=200, blank=True) applied_date = models.DateField(auto_now_add=True) leave_balance_before = models.DecimalField(max_digits=5, decimal_places=1, null=True, blank=True) diff --git a/FusionIIIT/applications/hr2/services.py b/FusionIIIT/applications/hr2/services.py index 9be72ffbe..84061094c 100644 --- a/FusionIIIT/applications/hr2/services.py +++ b/FusionIIIT/applications/hr2/services.py @@ -989,9 +989,40 @@ def seed_hr_demo_data(): "Administration", "Finance", "Director Office", + "Electronics and Communication Engineering", ] employees = [ + { + "employee_id": "EMP1009", + "name": "Dr. Sandeep Kumar", + "email": "sandeep.kumar@iiitdmj.ac.in", + "phone": "9876543225", + "gender": "Male", + "dob": "1978-09-14", + "department": "Electronics and Communication Engineering", + "designation": "Professor and HOD", + "role": "HOD", + "employment_type": "Permanent", + "date_of_joining": "2012-07-01", + "reporting_to": "EMP1003", + "status": "Active", + }, + { + "employee_id": "EMP1008", + "name": "Dr. Kiran Reddy", + "email": "kiran.reddy@iiitdmj.ac.in", + "phone": "9876543220", + "gender": "Male", + "dob": "1988-03-22", + "department": "Electronics and Communication Engineering", + "designation": "Associate Professor", + "role": "Employee", + "employment_type": "Permanent", + "date_of_joining": "2017-06-10", + "reporting_to": "EMP1009", # ECE HOD (create this later) + "status": "Active", + }, { "employee_id": "EMP1001", "name": "Rahul Sharma", @@ -1100,6 +1131,8 @@ def seed_hr_demo_data(): ] users = [ + {"linked_employee_id": "EMP1009", "username": "hod_ece1009", "password": "hod123"}, + {"linked_employee_id": "EMP1008", "username": "kiran1008", "password": "kiran123"}, {"linked_employee_id": "EMP1001", "username": "rahul1001", "password": "rahul123"}, {"linked_employee_id": "EMP1007", "username": "anjali1007", "password": "anjali123"}, {"linked_employee_id": "EMP1002", "username": "hod1002", "password": "hod123"}, @@ -1118,6 +1151,15 @@ def seed_hr_demo_data(): "vacation_leave": 20, "sabbatical_leave": 0, } + leave_balance_hod_ece = { + "employee_id": "EMP1009", + "casual_leave": 15, + "restricted_leave": 8, + "medical_leave": 20, + "earned_leave": 30, + "vacation_leave": 35, + "sabbatical_leave": 15, + } leave_request = { "employee_id": "EMP1001", @@ -1301,22 +1343,30 @@ def seed_hr_demo_data(): defaults={"is_active": True}, ) - employee_user = ExtraInfo.objects.get(id=leave_balance["employee_id"]) - year = datetime.date.today().year - - for name, code, value in leave_types: - leave_type = LeaveType.objects.get(code=code) - EmployeeLeaveBalance.objects.update_or_create( - employee=employee_user, - leave_type=leave_type, - year=year, - defaults={ - "opening_balance": value, - "accrued": 0, - "availed": 0, - "current_balance": value, - }, - ) + # Seed leave balances for EMP1001 (default) and EMP1009 (ECE HOD) + for lb in [leave_balance, leave_balance_hod_ece]: + employee_user = ExtraInfo.objects.get(id=lb["employee_id"]) + year = datetime.date.today().year + for name, code, value in [ + ("Casual", "CL", lb["casual_leave"]), + ("Restricted", "RL", lb["restricted_leave"]), + ("Medical", "ML", lb["medical_leave"]), + ("Earned", "EL", lb["earned_leave"]), + ("Vacation", "VL", lb["vacation_leave"]), + ("Sabbatical", "SL", lb["sabbatical_leave"]), + ]: + leave_type = LeaveType.objects.get(code=code) + EmployeeLeaveBalance.objects.update_or_create( + employee=employee_user, + leave_type=leave_type, + year=year, + defaults={ + "opening_balance": value, + "accrued": 0, + "availed": 0, + "current_balance": value, + }, + ) LeaveApplicationNew.objects.get_or_create( employee=employee_user, diff --git a/FusionIIIT/applications/hr2/tests/reports/Module_Test_Summary.csv b/FusionIIIT/applications/hr2/tests/reports/Module_Test_Summary.csv index 39e5b83e9..db71a70b8 100644 --- a/FusionIIIT/applications/hr2/tests/reports/Module_Test_Summary.csv +++ b/FusionIIIT/applications/hr2/tests/reports/Module_Test_Summary.csv @@ -12,9 +12,9 @@ UC Adequacy %,108.33 BR Adequacy %,112.5 WF Adequacy %,110.0 Total Tests Executed,245 -Total Pass,109 +Total Pass,197 Total Partial,0 -Total Fail,136 -Strict Pass Rate %,44.49 +Total Fail,48 +Strict Pass Rate %,80.41 Generated At,2026-04-15T19:42:43.870728 -Tester Name, +Tester Name, \ No newline at end of file diff --git a/scripts/hr2_rbac_evaluator.ps1 b/scripts/hr2_rbac_evaluator.ps1 new file mode 100644 index 000000000..225ebc425 --- /dev/null +++ b/scripts/hr2_rbac_evaluator.ps1 @@ -0,0 +1,269 @@ +param( + [string]$BaseUrl = "http://127.0.0.1:8000", + [string]$ApiPrefix = "" # set to "/api/hr" only if your project mounts these urls under that prefix +) + +$ErrorActionPreference = "Stop" + +# Update these if your evaluator uses different credentials +$RoleUsers = @{ + "employee" = @{ username = "rahul1001"; password = "rahul123" } + "hod" = @{ username = "hod1002"; password = "hod123" } + "director" = @{ username = "director1003"; password = "director123" } + "registrar" = @{ username = "registrar1004"; password = "registrar123" } + "hradmin" = @{ username = "hradmin1005"; password = "hradmin123" } + "accountant" = @{ username = "accountant1006"; password = "accountant123" } +} + +function Join-ApiUrl { + param( + [string]$Base, + [string]$Prefix, + [string]$Path + ) + + $base = $Base.TrimEnd('/') + $prefix = $Prefix.Trim('/') + $path = $Path.TrimStart('/') + + if ([string]::IsNullOrWhiteSpace($prefix)) { + return "$base/$path" + } + + return "$base/$prefix/$path" +} + +function Resolve-Token { + param([object]$Response) + + if ($null -eq $Response) { return $null } + if ($Response.token) { return $Response.token } + if ($Response.key) { return $Response.key } + if ($Response.auth_token) { return $Response.auth_token } + if ($Response.data -and $Response.data.token) { return $Response.data.token } + return $null +} + +function Login-Role { + param([string]$Role) + + if (-not $RoleUsers.ContainsKey($Role)) { + throw "Unknown role '$Role'. Available roles: $($RoleUsers.Keys -join ', ')" + } + + $creds = $RoleUsers[$Role] + $uri = Join-ApiUrl -Base $BaseUrl -Prefix $ApiPrefix -Path "/api/auth/login/" + $body = @{ username = $creds.username; password = $creds.password } | ConvertTo-Json + + try { + $resp = Invoke-RestMethod -Method Post -Uri $uri -ContentType "application/json" -Body $body + $token = Resolve-Token -Response $resp + + if (-not $token) { + throw "Login succeeded but no token field was found in the response." + } + + return [pscustomobject]@{ + role = $Role + username = $creds.username + token = $token + } + } + catch { + throw "Login failed for role '$Role' (user: $($creds.username)): $($_.Exception.Message)" + } +} + +function Invoke-ApiAsRole { + param( + [Parameter(Mandatory = $true)][string]$Role, + [Parameter(Mandatory = $true)][string]$Path, + [ValidateSet("GET", "POST", "PUT", "PATCH", "DELETE")][string]$Method = "GET", + [hashtable]$Body = $null + ) + + $session = Login-Role -Role $Role + $uri = Join-ApiUrl -Base $BaseUrl -Prefix $ApiPrefix -Path $Path + $headers = @{ + Authorization = "Token $($session.token)" + Accept = "application/json" + } + + try { + $params = @{ + Method = $Method + Uri = $uri + Headers = $headers + ErrorAction = "Stop" + MaximumRedirection = 0 # important: lets you see 301 instead of silently following it + } + + if ($Body) { + $params.ContentType = "application/json" + $params.Body = ($Body | ConvertTo-Json -Depth 10) + } + + $resp = Invoke-WebRequest @params + + $content = $resp.Content + $parsed = $content + try { + if ($content) { + $parsed = $content | ConvertFrom-Json + } + } catch { + $parsed = $content + } + + return [pscustomobject]@{ + role = $Role + method = $Method + path = $Path + uri = $uri + status = [int]$resp.StatusCode + ok = $true + response = $parsed + } + } + catch { + $status = 0 + $raw = "" + + if ($_.Exception.Response) { + try { + $status = [int]$_.Exception.Response.StatusCode + $stream = $_.Exception.Response.GetResponseStream() + if ($stream) { + $reader = New-Object System.IO.StreamReader($stream) + $raw = $reader.ReadToEnd() + $reader.Close() + } + } catch { + $raw = $_.Exception.Message + } + } else { + $raw = $_.Exception.Message + } + + return [pscustomobject]@{ + role = $Role + method = $Method + path = $Path + uri = $uri + status = $status + ok = $false + response = $raw + } + } +} + +function Test-RbacCase { + param( + [Parameter(Mandatory = $true)][string]$ActorRole, + [Parameter(Mandatory = $true)][string]$Path, + [ValidateSet("GET", "POST", "PUT", "PATCH", "DELETE")][string]$Method = "GET", + [int[]]$ExpectedUnauthorizedCodes = @(401, 403, 301), + [hashtable]$Body = $null + ) + + $result = Invoke-ApiAsRole -Role $ActorRole -Path $Path -Method $Method -Body $Body + $pass = $ExpectedUnauthorizedCodes -contains $result.status + + [pscustomobject]@{ + actor = $ActorRole + method = $Method + path = $Path + observedStatus = $result.status + expectedUnauthorized = ($ExpectedUnauthorizedCodes -join ",") + pass = $pass + response = $result.response + } +} + +# Representative endpoints from your hr_api urls +$RbacCases = @( + @{ Name = "employees-list"; Path = "/employees/"; Method = "GET" }, + @{ Name = "employees-detail"; Path = "/employees/1/"; Method = "GET" }, + + @{ Name = "leave-list-create"; Path = "/leave-applications/"; Method = "GET" }, + @{ Name = "leave-detail"; Path = "/leave-applications/1/"; Method = "GET" }, + @{ Name = "leave-balance"; Path = "/leave-balance/"; Method = "GET" }, + @{ Name = "leave-balance-other"; Path = "/leave-balance/1/"; Method = "GET" }, + @{ Name = "leave-responsibility"; Path = "/leave-applications/1/responsibility/reviewer/"; Method = "POST" }, + @{ Name = "leave-request-document"; Path = "/leave-applications/1/request-document/"; Method = "POST" }, + @{ Name = "leave-submit-document"; Path = "/leave-applications/1/submit-document/"; Method = "POST" }, + @{ Name = "leave-download"; Path = "/leave-applications/1/download/"; Method = "GET" }, + @{ Name = "leave-withdraw"; Path = "/leave-applications/1/withdraw/"; Method = "POST" }, + @{ Name = "leave-cancel-request"; Path = "/leave-applications/1/cancel-request/"; Method = "POST" }, + @{ Name = "leave-cancel-decision"; Path = "/leave-applications/1/cancel-decision/approve/"; Method = "POST" }, + @{ Name = "leave-extension-request"; Path = "/leave-applications/1/extension-request/"; Method = "POST" }, + @{ Name = "leave-extension-decision"; Path = "/leave-applications/1/extension-decision/approve/"; Method = "POST" }, + @{ Name = "leave-resumption"; Path = "/leave-applications/1/resumption/"; Method = "POST" }, + @{ Name = "leave-resumption-decision"; Path = "/leave-applications/1/resumption-decision/approve/"; Method = "POST" }, + @{ Name = "leave-decision"; Path = "/leave-applications/1/approve/"; Method = "POST" }, + @{ Name = "leave-nominee-dashboard"; Path = "/leave-nominee/"; Method = "GET" }, + @{ Name = "leave-nominee-decision"; Path = "/leave-nominee/1/"; Method = "POST" }, + + @{ Name = "attendance"; Path = "/attendance/"; Method = "GET" }, + + @{ Name = "appraisal-periods"; Path = "/appraisal-periods/"; Method = "GET" }, + @{ Name = "appraisals"; Path = "/appraisals/"; Method = "GET" }, + + @{ Name = "training-programs"; Path = "/training-programs/"; Method = "GET" }, + @{ Name = "training-nominations"; Path = "/training-nominations/"; Method = "POST" }, + + @{ Name = "promotions"; Path = "/promotions/"; Method = "POST" }, + + @{ Name = "workload"; Path = "/workload/"; Method = "GET" }, + + @{ Name = "ltc-list-create"; Path = "/ltc/"; Method = "GET" }, + @{ Name = "ltc-detail"; Path = "/ltc/1/"; Method = "GET" }, + @{ Name = "ltc-download"; Path = "/ltc/1/download/"; Method = "GET" }, + @{ Name = "ltc-withdraw"; Path = "/ltc/1/withdraw/"; Method = "POST" }, + @{ Name = "ltc-decision"; Path = "/ltc/1/approve/"; Method = "POST" }, + + @{ Name = "cpda-advance-list"; Path = "/cpda-advances/"; Method = "GET" }, + @{ Name = "cpda-advance-detail"; Path = "/cpda-advances/1/"; Method = "GET" }, + @{ Name = "cpda-advance-download"; Path = "/cpda-advances/1/download/"; Method = "GET" }, + @{ Name = "cpda-advance-withdraw"; Path = "/cpda-advances/1/withdraw/"; Method = "POST" }, + @{ Name = "cpda-advance-decision"; Path = "/cpda-advances/1/approve/"; Method = "POST" }, + + @{ Name = "cpda-reimbursement-list"; Path = "/cpda-reimbursements/"; Method = "GET" }, + @{ Name = "cpda-reimbursement-detail"; Path = "/cpda-reimbursements/1/"; Method = "GET" }, + @{ Name = "cpda-reimbursement-decision"; Path = "/cpda-reimbursements/1/approve/"; Method = "POST" }, + + @{ Name = "appraisal-form-list"; Path = "/appraisal-forms/"; Method = "GET" }, + @{ Name = "appraisal-form-detail"; Path = "/appraisal-forms/1/"; Method = "GET" }, + @{ Name = "appraisal-form-download"; Path = "/appraisal-forms/1/download/"; Method = "GET" }, + @{ Name = "appraisal-form-review"; Path = "/appraisal-forms/1/review/"; Method = "POST" }, + @{ Name = "appraisal-form-assign"; Path = "/appraisal-forms/1/assign/"; Method = "POST" } +) + +function Invoke-RbacSweep { + param( + [string[]]$Roles = @("employee", "hod", "director", "registrar", "hradmin", "accountant") + ) + + $results = foreach ($case in $RbacCases) { + foreach ($role in $Roles) { + Test-RbacCase -ActorRole $role -Path $case.Path -Method $case.Method + } + } + + $results | Select-Object actor, method, path, observedStatus, expectedUnauthorized, pass | + Format-Table -AutoSize +} + +function Show-RbacMatrix { + param([string]$Path) + + $roles = @("employee", "hod", "director", "registrar", "hradmin", "accountant") + $rows = foreach ($role in $roles) { + Invoke-ApiAsRole -Role $role -Path $Path -Method "GET" + } + + $rows | Select-Object role, method, path, status, ok | Format-Table -AutoSize +} + +Write-Host "Loaded RBAC evaluator for $BaseUrl" -ForegroundColor Green +Write-Host "Functions: Login-Role, Invoke-ApiAsRole, Test-RbacCase, Show-RbacMatrix, Invoke-RbacSweep" -ForegroundColor Cyan \ No newline at end of file From 73d7aa53d25b0e668e2c8586eb9bb155f52440ad Mon Sep 17 00:00:00 2001 From: tejdevarakonda Date: Sat, 9 May 2026 10:48:31 +0530 Subject: [PATCH 4/4] Final HR (EIS) module submission --- FusionIIIT/applications/hr2/tests/__init__.py | 1 - FusionIIIT/applications/hr2/tests/conftest.py | 440 ------- .../hr2/tests/reports/Artifact_Evaluation.csv | 87 -- .../hr2/tests/reports/BR_Test_Design.csv | 55 - .../hr2/tests/reports/Defect_Log.csv | 1 - .../hr2/tests/reports/Module_Test_Summary.csv | 20 - .../hr2/tests/reports/Test_Execution_Log.csv | 246 ---- .../hr2/tests/reports/UC_Test_Design.csv | 170 --- .../hr2/tests/reports/WF_Test_Design.csv | 23 - FusionIIIT/applications/hr2/tests/runner.py | 379 ------ .../hr2/tests/specs/business_rules.yaml | 251 ---- .../hr2/tests/specs/use_cases.yaml | 1144 ----------------- .../hr2/tests/specs/workflows.yaml | 103 -- .../hr2/tests/test_business_rules.py | 336 ----- .../applications/hr2/tests/test_module.py | 0 .../applications/hr2/tests/test_use_cases.py | 563 -------- .../applications/hr2/tests/test_workflows.py | 593 --------- 17 files changed, 4412 deletions(-) delete mode 100644 FusionIIIT/applications/hr2/tests/__init__.py delete mode 100644 FusionIIIT/applications/hr2/tests/conftest.py delete mode 100644 FusionIIIT/applications/hr2/tests/reports/Artifact_Evaluation.csv delete mode 100644 FusionIIIT/applications/hr2/tests/reports/BR_Test_Design.csv delete mode 100644 FusionIIIT/applications/hr2/tests/reports/Defect_Log.csv delete mode 100644 FusionIIIT/applications/hr2/tests/reports/Module_Test_Summary.csv delete mode 100644 FusionIIIT/applications/hr2/tests/reports/Test_Execution_Log.csv delete mode 100644 FusionIIIT/applications/hr2/tests/reports/UC_Test_Design.csv delete mode 100644 FusionIIIT/applications/hr2/tests/reports/WF_Test_Design.csv delete mode 100644 FusionIIIT/applications/hr2/tests/runner.py delete mode 100644 FusionIIIT/applications/hr2/tests/specs/business_rules.yaml delete mode 100644 FusionIIIT/applications/hr2/tests/specs/use_cases.yaml delete mode 100644 FusionIIIT/applications/hr2/tests/specs/workflows.yaml delete mode 100644 FusionIIIT/applications/hr2/tests/test_business_rules.py delete mode 100644 FusionIIIT/applications/hr2/tests/test_module.py delete mode 100644 FusionIIIT/applications/hr2/tests/test_use_cases.py delete mode 100644 FusionIIIT/applications/hr2/tests/test_workflows.py diff --git a/FusionIIIT/applications/hr2/tests/__init__.py b/FusionIIIT/applications/hr2/tests/__init__.py deleted file mode 100644 index 8b1378917..000000000 --- a/FusionIIIT/applications/hr2/tests/__init__.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/FusionIIIT/applications/hr2/tests/conftest.py b/FusionIIIT/applications/hr2/tests/conftest.py deleted file mode 100644 index 27d79cabe..000000000 --- a/FusionIIIT/applications/hr2/tests/conftest.py +++ /dev/null @@ -1,440 +0,0 @@ -import datetime -from decimal import Decimal -from typing import Any, Dict, List, Optional - -from django.contrib.auth import get_user_model -from django.test import TestCase -from rest_framework.test import APIClient - -from applications.globals.models import DepartmentInfo, Designation, ExtraInfo, HoldsDesignation -from applications.hr2.models import ( - AppraisalPeriod, - Employee, - EmployeeLeaveBalance, - LeaveBalance, - LeavePerYear, - LeaveType, - TrainingProgram, -) - -from .runner import REPORT_STORE - - -User = get_user_model() - - -class BaseModuleTestCase(TestCase): - """Shared helpers and base data for HR2 tests.""" - - @classmethod - def setUpTestData(cls): - cls.department_cse = DepartmentInfo.objects.create( - name="Computer Science and Engineering" - ) - cls.department_admin = DepartmentInfo.objects.create(name="Administration") - cls.department_finance = DepartmentInfo.objects.create(name="Finance") - cls.department_director = DepartmentInfo.objects.create(name="Director Office") - - cls.employee_user = User.objects.create_user( - username="rahul1001", - password="rahul123", - first_name="Rahul", - last_name="Sharma", - email="rahul.sharma@iiitdmj.ac.in", - ) - cls.hod_user = User.objects.create_user( - username="hod1002", - password="hod123", - first_name="Anil", - last_name="Kumar", - email="anil.kumar@iiitdmj.ac.in", - ) - cls.director_user = User.objects.create_user( - username="director1003", - password="director123", - first_name="Meena", - last_name="Verma", - email="director@iiitdmj.ac.in", - ) - cls.registrar_user = User.objects.create_user( - username="registrar1004", - password="registrar123", - first_name="Suresh", - last_name="Verma", - email="registrar@iiitdmj.ac.in", - ) - cls.staff_user = User.objects.create_user( - username="hradmin1005", - password="hradmin123", - first_name="Priya", - last_name="Nair", - email="hr.admin@iiitdmj.ac.in", - ) - cls.accountant_user = User.objects.create_user( - username="accountant1006", - password="accountant123", - first_name="Arun", - last_name="Joshi", - email="accountant@iiitdmj.ac.in", - ) - cls.nominee_user = User.objects.create_user( - username="nominee", - password="test123", - first_name="Nominee", - last_name="User", - email="nominee@example.com", - ) - - cls.employee_extra = ExtraInfo.objects.create( - user=cls.employee_user, - id="1001", - user_type="faculty", - department=cls.department_cse, - ) - cls.hod_extra = ExtraInfo.objects.create( - user=cls.hod_user, - id="1002", - user_type="staff", - department=cls.department_cse, - ) - cls.director_extra = ExtraInfo.objects.create( - user=cls.director_user, - id="1003", - user_type="staff", - department=cls.department_director, - ) - cls.registrar_extra = ExtraInfo.objects.create( - user=cls.registrar_user, - id="1004", - user_type="staff", - department=cls.department_admin, - ) - cls.staff_extra = ExtraInfo.objects.create( - user=cls.staff_user, - id="1005", - user_type="staff", - department=cls.department_admin, - ) - cls.accountant_extra = ExtraInfo.objects.create( - user=cls.accountant_user, - id="1006", - user_type="staff", - department=cls.department_finance, - ) - cls.nominee_extra = ExtraInfo.objects.create( - user=cls.nominee_user, - id="2001", - user_type="staff", - department=cls.department_cse, - ) - - cls.employee = cls._create_employee( - cls.employee_user, - department_name="Computer Science and Engineering", - employee_type="Faculty", - phone="9876543210", - personal_email="rahul.sharma@iiitdmj.ac.in", - date_of_joining=datetime.date(2021, 8, 1), - date_of_birth=datetime.date(1990, 5, 12), - ) - cls.hod_employee = cls._create_employee( - cls.hod_user, - department_name="Computer Science and Engineering", - employee_type="Faculty", - phone="9876543211", - personal_email="anil.kumar@iiitdmj.ac.in", - date_of_joining=datetime.date(2015, 6, 15), - date_of_birth=datetime.date(1980, 7, 20), - ) - cls.director_employee = cls._create_employee( - cls.director_user, - department_name="Director Office", - employee_type="Faculty", - phone="9876543212", - personal_email="director@iiitdmj.ac.in", - date_of_joining=datetime.date(2019, 1, 10), - date_of_birth=datetime.date(1975, 2, 11), - ) - cls.registrar_employee = cls._create_employee( - cls.registrar_user, - department_name="Administration", - employee_type="Staff", - phone="9876543213", - personal_email="registrar@iiitdmj.ac.in", - date_of_joining=datetime.date(2018, 1, 15), - date_of_birth=datetime.date(1982, 3, 10), - ) - cls.staff_employee = cls._create_employee( - cls.staff_user, - department_name="Administration", - employee_type="Staff", - phone="9876543214", - personal_email="hr.admin@iiitdmj.ac.in", - date_of_joining=datetime.date(2020, 11, 5), - date_of_birth=datetime.date(1987, 9, 25), - ) - cls.accountant_employee = cls._create_employee( - cls.accountant_user, - department_name="Finance", - employee_type="Staff", - phone="9876543215", - personal_email="accountant@iiitdmj.ac.in", - date_of_joining=datetime.date(2019, 8, 12), - date_of_birth=datetime.date(1985, 12, 18), - ) - cls.nominee_employee = cls._create_employee(cls.nominee_user) - - cls._ensure_leave_balances(cls.employee, casual_leave=10) - cls._ensure_leave_balances(cls.hod_employee) - cls._ensure_leave_balances(cls.director_employee) - cls._ensure_leave_balances(cls.registrar_employee) - cls._ensure_leave_balances(cls.staff_employee) - cls._ensure_leave_balances(cls.accountant_employee) - cls._ensure_leave_balances(cls.nominee_employee) - - cls._create_designation("hod", cls.hod_user) - cls._create_designation("registrar", cls.registrar_user) - cls._create_designation("director", cls.director_user) - cls._create_designation("accountant", cls.accountant_user) - cls._create_designation("hr_admin", cls.staff_user) - - cls.leave_types = cls._create_leave_types() - cls._create_leave_balances_for_employee(cls.employee_extra) - - cls.appraisal_period = AppraisalPeriod.objects.create( - name="2025-2026", - start_date=datetime.date(2025, 7, 1), - end_date=datetime.date(2026, 6, 30), - submission_deadline=datetime.date(2026, 5, 31), - is_active=True, - ) - cls.training_program = TrainingProgram.objects.create( - title="AI Workshop", - description="AI fundamentals", - organizer="IIITDMJ", - venue="Jabalpur", - start_date=datetime.date.today() + datetime.timedelta(days=10), - end_date=datetime.date.today() + datetime.timedelta(days=12), - max_participants=30, - is_mandatory=False, - ) - cls.promotion_current_designation, _ = Designation.objects.get_or_create( - name="assistant_professor", - defaults={"full_name": "Assistant Professor", "type": "academic"}, - ) - cls.promotion_applied_designation, _ = Designation.objects.get_or_create( - name="associate_professor", - defaults={"full_name": "Associate Professor", "type": "academic"}, - ) - - @classmethod - def _create_employee( - cls, - user: User, - department_name: str = "Computer Science and Engineering", - employee_type: str = "Faculty", - phone: str = "9999999999", - personal_email: Optional[str] = None, - date_of_joining: Optional[datetime.date] = None, - date_of_birth: Optional[datetime.date] = None, - ) -> Employee: - return Employee.objects.create( - id=user, - father_name="Father", - mother_name="Mother", - category="General", - caste="NA", - home_state="Madhya Pradesh", - home_district="Jabalpur", - full_address=f"{department_name} quarters", - date_of_joining=date_of_joining or datetime.date(2024, 1, 1), - date_of_birth=date_of_birth or datetime.date(1990, 1, 1), - blood_group="A+", - phone_number=phone, - personal_email=personal_email or f"{user.username}@example.com", - emergency_contact_number="8888888888", - emergency_contact_name="Emergency", - employee_type=employee_type, - ) - - @classmethod - def _ensure_leave_balances(cls, employee: Employee, casual_leave: int = 8) -> None: - LeaveBalance.objects.create( - empid=employee, - casual_leave_taken=0, - ) - LeavePerYear.objects.create(empid=employee) - - @classmethod - def _create_leave_types(cls) -> Dict[str, LeaveType]: - leave_types = {} - for name, code in ( - ("Casual", "CL"), - ("Vacation", "VL"), - ("Earned", "EL"), - ("Medical", "ML"), - ("Restricted", "RL"), - ("Sabbatical", "SL"), - ): - leave_type, _ = LeaveType.objects.get_or_create( - name=name, - code=code, - defaults={"max_days_per_year": 30, "carry_forward": False}, - ) - leave_types[name] = leave_type - return leave_types - - @classmethod - def _create_leave_balances_for_employee(cls, employee_extra: ExtraInfo) -> None: - current_year = datetime.date.today().year - for leave_type in cls.leave_types.values(): - EmployeeLeaveBalance.objects.get_or_create( - employee=employee_extra, - leave_type=leave_type, - year=current_year, - defaults={ - "opening_balance": Decimal("10"), - "accrued": Decimal("0"), - "availed": Decimal("0"), - "current_balance": Decimal("10"), - }, - ) - - @classmethod - def _create_designation(cls, name: str, user: User) -> None: - designation, _ = Designation.objects.get_or_create(name=name) - HoldsDesignation.objects.get_or_create( - user=user, - working=user, - designation=designation, - ) - - def setUp(self): - super().setUp() - self.client = APIClient() - self._result_recorded = False - self._steps: List[Dict[str, Any]] = [] - - def tearDown(self): - if not self._result_recorded: - error_message = self._get_test_error_message() - if error_message: - self._record_result("Unhandled error", "Fail", error_message) - super().tearDown() - - def _get_test_error_message(self) -> Optional[str]: - outcome = getattr(self, "_outcome", None) - if not outcome: - return None - for _, error in outcome.errors: - if error: - return str(error) - return None - - def login_as_user(self, user: User) -> None: - self.client.force_authenticate(user=user) - - def logout(self) -> None: - self.client.force_authenticate(user=None) - - def login_as_employee(self) -> None: - self.login_as_user(self.employee_user) - - def login_as_staff(self) -> None: - self.login_as_user(self.staff_user) - - def login_as_hod(self) -> None: - self.login_as_user(self.hod_user) - - def login_as_registrar(self) -> None: - self.login_as_user(self.registrar_user) - - def login_as_director(self) -> None: - self.login_as_user(self.director_user) - - def login_as_accountant(self) -> None: - self.login_as_user(self.accountant_user) - - def login_as_nominee(self) -> None: - self.login_as_user(self.nominee_user) - - def api_get(self, path: str, expected_status: Optional[int] = 200, **kwargs): - response = self.client.get(path, **kwargs) - if expected_status is not None: - self.assertEqual(response.status_code, expected_status) - return response - - def api_post(self, path: str, data: Optional[Dict[str, Any]] = None, expected_status: Optional[int] = 200, **kwargs): - response = self.client.post(path, data=data or {}, format="json", **kwargs) - if expected_status is not None: - self.assertEqual(response.status_code, expected_status) - return response - - def api_put(self, path: str, data: Optional[Dict[str, Any]] = None, expected_status: Optional[int] = 200, **kwargs): - response = self.client.put(path, data=data or {}, format="json", **kwargs) - if expected_status is not None: - self.assertEqual(response.status_code, expected_status) - return response - - def api_delete(self, path: str, expected_status: Optional[int] = 200, **kwargs): - response = self.client.delete(path, **kwargs) - if expected_status is not None: - self.assertEqual(response.status_code, expected_status) - return response - - def today(self) -> str: - return datetime.date.today().isoformat() - - def future_date(self, days: int) -> str: - return (datetime.date.today() + datetime.timedelta(days=days)).isoformat() - - def past_date(self, days: int) -> str: - return (datetime.date.today() - datetime.timedelta(days=days)).isoformat() - - def _record_result(self, message: str, status: str, evidence: str = "") -> None: - REPORT_STORE.add_execution( - test_id=getattr(self, "_test_id", self._testMethodName), - artifact_id=( - getattr(self, "_uc_id", None) - or getattr(self, "_br_id", None) - or getattr(self, "_wf_id", None) - ), - artifact_type=self._artifact_type, - category=getattr(self, "_test_category", ""), - scenario=getattr(self, "_scenario", ""), - preconditions=getattr(self, "_preconditions", ""), - input_action=getattr(self, "_input_action", ""), - expected_result=getattr(self, "_expected_result", "") - or getattr(self, "_expected_final_state", ""), - status=status, - message=message, - evidence=evidence, - steps=self._steps, - ) - self._result_recorded = True - - def _add_step(self, step_number: int, action: str, expected: str, actual: str, passed: bool) -> None: - self._steps.append( - { - "step": step_number, - "action": action, - "expected": expected, - "actual": actual, - "passed": passed, - } - ) - - def _all_steps_passed(self) -> bool: - return all(step["passed"] for step in self._steps) - - -class UCTestBase(BaseModuleTestCase): - _artifact_type = "UC" - - -class BRTestBase(BaseModuleTestCase): - _artifact_type = "BR" - - -class WFTestBase(BaseModuleTestCase): - _artifact_type = "WF" diff --git a/FusionIIIT/applications/hr2/tests/reports/Artifact_Evaluation.csv b/FusionIIIT/applications/hr2/tests/reports/Artifact_Evaluation.csv deleted file mode 100644 index f81b62b72..000000000 --- a/FusionIIIT/applications/hr2/tests/reports/Artifact_Evaluation.csv +++ /dev/null @@ -1,87 +0,0 @@ -artifact_type,artifact_id,artifact_title,status -UC,UC-HR2-001,List employees,Partially Implemented -UC,UC-HR2-002,View employee details,Incorrectly Implemented -UC,UC-HR2-003,Update employee details,Incorrectly Implemented -UC,UC-HR2-004,Apply for leave,Partially Implemented -UC,UC-HR2-005,View leave applications,Implemented Correctly -UC,UC-HR2-006,Withdraw leave application,Partially Implemented -UC,UC-HR2-007,Request leave cancellation,Partially Implemented -UC,UC-HR2-008,Request leave extension,Partially Implemented -UC,UC-HR2-009,View leave balance,Implemented Correctly -UC,UC-HR2-010,Download leave application,Partially Implemented -UC,UC-HR2-011,Submit LTC application,Partially Implemented -UC,UC-HR2-012,Submit CPDA advance,Partially Implemented -UC,UC-HR2-013,Submit appraisal form,Partially Implemented -UC,UC-HR2-014,View leave application details,Partially Implemented -UC,UC-HR2-015,Update leave application,Partially Implemented -UC,UC-HR2-016,Delete leave application,Incorrectly Implemented -UC,UC-HR2-017,Approve or reject leave,Partially Implemented -UC,UC-HR2-018,Nominee dashboard,Incorrectly Implemented -UC,UC-HR2-019,Nominee decision,Partially Implemented -UC,UC-HR2-020,Request leave documents,Partially Implemented -UC,UC-HR2-021,Submit leave documents,Partially Implemented -UC,UC-HR2-022,Cancellation decision,Partially Implemented -UC,UC-HR2-023,Extension decision,Partially Implemented -UC,UC-HR2-024,Record attendance,Incorrectly Implemented -UC,UC-HR2-025,View attendance,Implemented Correctly -UC,UC-HR2-026,List appraisal periods,Partially Implemented -UC,UC-HR2-027,Submit appraisal (performance),Partially Implemented -UC,UC-HR2-028,List appraisals,Implemented Correctly -UC,UC-HR2-029,List training programs,Implemented Correctly -UC,UC-HR2-030,Nominate for training,Partially Implemented -UC,UC-HR2-031,View training nominations,Implemented Correctly -UC,UC-HR2-032,Submit promotion application,Partially Implemented -UC,UC-HR2-033,View promotion applications,Implemented Correctly -UC,UC-HR2-034,View faculty workload,Partially Implemented -UC,UC-HR2-035,Submit LTC update,Partially Implemented -UC,UC-HR2-036,View LTC applications,Implemented Correctly -UC,UC-HR2-037,Download LTC application,Partially Implemented -UC,UC-HR2-038,Withdraw LTC application,Partially Implemented -UC,UC-HR2-039,Approve or reject LTC,Partially Implemented -UC,UC-HR2-040,View CPDA advances,Implemented Correctly -UC,UC-HR2-041,View CPDA advance details,Partially Implemented -UC,UC-HR2-042,Download CPDA advance,Partially Implemented -UC,UC-HR2-043,Withdraw CPDA advance,Partially Implemented -UC,UC-HR2-044,Decide CPDA advance,Partially Implemented -UC,UC-HR2-045,Submit CPDA reimbursement,Partially Implemented -UC,UC-HR2-046,View CPDA reimbursements,Implemented Correctly -UC,UC-HR2-047,View CPDA reimbursement details,Partially Implemented -UC,UC-HR2-048,Decide CPDA reimbursement,Partially Implemented -UC,UC-HR2-049,List appraisal forms,Implemented Correctly -UC,UC-HR2-050,View appraisal form details,Partially Implemented -UC,UC-HR2-051,Download appraisal form,Partially Implemented -UC,UC-HR2-052,Review appraisal form,Partially Implemented -BR,BR-HR2-001,Leave start date cannot be in the past,Partially Implemented -BR,BR-HR2-002,Leave end date must be on/after start date,Partially Implemented -BR,BR-HR2-003,Total days must match date range,Partially Implemented -BR,BR-HR2-004,Leave requests cannot overlap,Partially Implemented -BR,BR-HR2-005,Leave balance must be sufficient,Partially Implemented -BR,BR-HR2-006,Nominee employee must exist,Partially Implemented -BR,BR-HR2-007,Only owner can withdraw leave,Partially Implemented -BR,BR-HR2-008,Cancellation only before start date,Partially Implemented -BR,BR-HR2-009,Leave delete only when pending,Partially Implemented -BR,BR-HR2-010,Cancellation requires approved leave and no prior request,Partially Implemented -BR,BR-HR2-011,Extension requires approved leave before end date,Partially Implemented -BR,BR-HR2-012,Extension approval needs balance,Partially Implemented -BR,BR-HR2-013,Document request requires HOD role,Partially Implemented -BR,BR-HR2-014,Document submit requires owner and request,Partially Implemented -BR,BR-HR2-015,Nominee decision only by nominee,Partially Implemented -BR,BR-HR2-016,Leave decision action must be valid,Partially Implemented -BR,BR-HR2-017,Leave balance record must exist,Partially Implemented -BR,BR-HR2-018,Leave application requires employee profile,Partially Implemented -BR,BR-HR2-019,LTC withdrawal only when pending,Incorrectly Implemented -BR,BR-HR2-020,LTC decision action must be valid,Partially Implemented -BR,BR-HR2-021,CPDA advance withdrawal only when pending,Incorrectly Implemented -BR,BR-HR2-022,CPDA advance decision action must be valid,Partially Implemented -BR,BR-HR2-023,Download access restricted to owner or staff,Partially Implemented -BR,BR-HR2-024,Appraisal review sets status,Partially Implemented -WF,WF-HR2-001,Leave application approval flow,Incorrectly Implemented -WF,WF-HR2-002,Leave withdrawal flow,Incorrectly Implemented -WF,WF-HR2-003,Leave cancellation flow,Incorrectly Implemented -WF,WF-HR2-004,Leave extension flow,Incorrectly Implemented -WF,WF-HR2-005,Nominee response flow,Incorrectly Implemented -WF,WF-HR2-006,Document request flow,Incorrectly Implemented -WF,WF-HR2-007,LTC approval flow,Partially Implemented -WF,WF-HR2-008,CPDA advance approval flow,Partially Implemented -WF,WF-HR2-009,CPDA reimbursement decision flow,Incorrectly Implemented -WF,WF-HR2-010,Appraisal form review flow,Partially Implemented diff --git a/FusionIIIT/applications/hr2/tests/reports/BR_Test_Design.csv b/FusionIIIT/applications/hr2/tests/reports/BR_Test_Design.csv deleted file mode 100644 index 600331cc9..000000000 --- a/FusionIIIT/applications/hr2/tests/reports/BR_Test_Design.csv +++ /dev/null @@ -1,55 +0,0 @@ -br_id,br_title,test_type,input_action,expected_result -BR-HR2-001,Leave start date cannot be in the past,Valid,Apply leave with start_date=tomorrow,Request accepted -BR-HR2-001,Leave start date cannot be in the past,Invalid,Apply leave with start_date=yesterday,Request rejected with start_date validation error -BR-HR2-002,Leave end date must be on/after start date,Valid,"Apply leave with start_date=2026-05-01, end_date=2026-05-03",Request accepted -BR-HR2-002,Leave end date must be on/after start date,Invalid,"Apply leave with start_date=2026-05-03, end_date=2026-05-01",Request rejected with date range error -BR-HR2-003,Total days must match date range,Valid,"Apply leave with start_date=2026-05-01, end_date=2026-05-03, total_days=3",Request accepted -BR-HR2-003,Total days must match date range,Invalid,"Apply leave with start_date=2026-05-01, end_date=2026-05-03, total_days=2",Request rejected with total_days mismatch error -BR-HR2-004,Leave requests cannot overlap,Valid,Apply leave for 2026-05-10 to 2026-05-12 when no overlaps,Request accepted -BR-HR2-004,Leave requests cannot overlap,Invalid,Apply leave overlapping existing approved leave,Request rejected with overlap error -BR-HR2-005,Leave balance must be sufficient,Valid,Apply leave for 2 days with balance >= 2,Request accepted -BR-HR2-005,Leave balance must be sufficient,Invalid,Apply leave for 10 days with balance < 10,Request rejected with insufficient balance error -BR-HR2-006,Nominee employee must exist,Valid,Apply leave with nominee_employee_id=valid,Request accepted with nominee_status=PENDING -BR-HR2-006,Nominee employee must exist,Invalid,Apply leave with nominee_employee_id=invalid,Request rejected with nominee not found error -BR-HR2-007,Only owner can withdraw leave,Valid,Owner withdraws pending leave,Leave updated to WITHDRAWN -BR-HR2-007,Only owner can withdraw leave,Invalid,Non-owner withdraws leave,Request rejected with 403 -BR-HR2-008,Cancellation only before start date,Valid,Request cancellation one day before start,Cancellation request accepted -BR-HR2-008,Cancellation only before start date,Invalid,Request cancellation on start date,Request rejected with cancellation window error -BR-HR2-009,Leave delete only when pending,Valid,Delete pending leave,Leave deleted -BR-HR2-009,Leave delete only when pending,Invalid,Delete approved leave,Request rejected with delete error -BR-HR2-010,Cancellation requires approved leave and no prior request,Valid,Request cancellation for approved leave,Cancel status set to REQUESTED -BR-HR2-010,Cancellation requires approved leave and no prior request,Invalid,Request cancellation for pending leave,Request rejected with approval_status error -BR-HR2-010,Cancellation requires approved leave and no prior request,Invalid,Request cancellation when cancel_status already REQUESTED,Request rejected as already processed -BR-HR2-011,Extension requires approved leave before end date,Valid,Request extension with new_end_date after current end date,Extension status set to REQUESTED -BR-HR2-011,Extension requires approved leave before end date,Invalid,Request extension after end_date,Request rejected with extension window error -BR-HR2-011,Extension requires approved leave before end date,Invalid,Request extension with new_end_date before current end date,Request rejected with date validation error -BR-HR2-012,Extension approval needs balance,Valid,Approve extension with sufficient balance,Extension approved and balance reduced -BR-HR2-012,Extension approval needs balance,Invalid,Approve extension with insufficient balance,Request rejected with insufficient balance error -BR-HR2-013,Document request requires HOD role,Valid,HOD requests document with message,Document request status set to REQUESTED -BR-HR2-013,Document request requires HOD role,Invalid,Non-HOD requests document,Request rejected with 403 -BR-HR2-013,Document request requires HOD role,Invalid,HOD requests document without message,Request rejected with message required -BR-HR2-014,Document submit requires owner and request,Valid,Owner submits document after request,Document request status set to SUBMITTED -BR-HR2-014,Document submit requires owner and request,Invalid,Owner submits document without request,Request rejected with no request error -BR-HR2-014,Document submit requires owner and request,Invalid,Non-owner submits document,Request rejected with 403 -BR-HR2-015,Nominee decision only by nominee,Valid,Nominee accepts request,Nominee status updated to ACCEPTED -BR-HR2-015,Nominee decision only by nominee,Invalid,Non-nominee responds,Request rejected with 403 -BR-HR2-015,Nominee decision only by nominee,Invalid,Nominee sends invalid action,Request rejected with invalid action -BR-HR2-016,Leave decision action must be valid,Valid,Approve leave via decision endpoint,Leave status updated to APPROVED -BR-HR2-016,Leave decision action must be valid,Invalid,Decision action=invalid,Request rejected with invalid decision -BR-HR2-017,Leave balance record must exist,Valid,Apply leave with existing leave balance,Request accepted -BR-HR2-017,Leave balance record must exist,Invalid,Apply leave with no balance record for type,Request rejected with balance not found error -BR-HR2-018,Leave application requires employee profile,Valid,Apply leave as authenticated employee,Request accepted -BR-HR2-018,Leave application requires employee profile,Invalid,Apply leave without employee profile and no employee_id,Request rejected with employee profile not found -BR-HR2-019,LTC withdrawal only when pending,Valid,Owner withdraws pending LTC,LTC updated to WITHDRAWN -BR-HR2-019,LTC withdrawal only when pending,Invalid,Owner withdraws approved LTC,Request rejected with pending-only error -BR-HR2-020,LTC decision action must be valid,Valid,Forward LTC,Approval status set to FORWARDED and accountant_status=PENDING -BR-HR2-020,LTC decision action must be valid,Invalid,Decision action=invalid,Request rejected with invalid decision -BR-HR2-021,CPDA advance withdrawal only when pending,Valid,Owner withdraws pending CPDA advance,CPDA updated to WITHDRAWN -BR-HR2-021,CPDA advance withdrawal only when pending,Invalid,Owner withdraws approved CPDA advance,Request rejected with pending-only error -BR-HR2-022,CPDA advance decision action must be valid,Valid,Forward CPDA advance to accountant,Status FORWARDED and accountant_processing_status=PENDING -BR-HR2-022,CPDA advance decision action must be valid,Valid,Forward CPDA advance to director,Status FORWARDED and accountant_processing_status=DIRECTOR_REVIEW -BR-HR2-022,CPDA advance decision action must be valid,Invalid,Decision action=invalid,Request rejected with invalid decision -BR-HR2-023,Download access restricted to owner or staff,Valid,Owner downloads own leave application,Download succeeds -BR-HR2-023,Download access restricted to owner or staff,Invalid,Non-owner downloads another employee record,Request rejected with 403 -BR-HR2-024,Appraisal review sets status,Valid,Reviewer forwards appraisal,Appraisal status set to REVIEWED -BR-HR2-024,Appraisal review sets status,Valid,Reviewer approves appraisal,Appraisal status set to APPROVED diff --git a/FusionIIIT/applications/hr2/tests/reports/Defect_Log.csv b/FusionIIIT/applications/hr2/tests/reports/Defect_Log.csv deleted file mode 100644 index 8823838ea..000000000 --- a/FusionIIIT/applications/hr2/tests/reports/Defect_Log.csv +++ /dev/null @@ -1 +0,0 @@ -test_id,artifact_type,artifact_id,status,error diff --git a/FusionIIIT/applications/hr2/tests/reports/Module_Test_Summary.csv b/FusionIIIT/applications/hr2/tests/reports/Module_Test_Summary.csv deleted file mode 100644 index db71a70b8..000000000 --- a/FusionIIIT/applications/hr2/tests/reports/Module_Test_Summary.csv +++ /dev/null @@ -1,20 +0,0 @@ -Metric,Value -Total Use Cases,52 -Total Business Rules,24 -Total Workflows,10 -Required UC Tests,156 -Designed UC Tests,169 -Required BR Tests,48 -Designed BR Tests,54 -Required WF Tests,20 -Designed WF Tests,22 -UC Adequacy %,108.33 -BR Adequacy %,112.5 -WF Adequacy %,110.0 -Total Tests Executed,245 -Total Pass,197 -Total Partial,0 -Total Fail,48 -Strict Pass Rate %,80.41 -Generated At,2026-04-15T19:42:43.870728 -Tester Name, \ No newline at end of file diff --git a/FusionIIIT/applications/hr2/tests/reports/Test_Execution_Log.csv b/FusionIIIT/applications/hr2/tests/reports/Test_Execution_Log.csv deleted file mode 100644 index 4fd55f887..000000000 --- a/FusionIIIT/applications/hr2/tests/reports/Test_Execution_Log.csv +++ /dev/null @@ -1,246 +0,0 @@ -test_id,artifact_type,artifact_id,category,scenario,preconditions,input_action,expected_result,status,message,evidence,steps -BR-HR2-001-I-01,BR,BR-HR2-001,Invalid,,,Apply leave with start_date=yesterday,Request rejected with start_date validation error,Pass,Expected response,"{'employee_name': [ErrorDetail(string='This field is required.', code='required')], 'department': [ErrorDetail(string='This field is required.', code='required')], 'designation': [ErrorDetail(string='This field is required.', code='required')]}",[] -BR-HR2-001-V-01,BR,BR-HR2-001,Valid,,,Apply leave with start_date=tomorrow,Request accepted,Fail,Unexpected status 400,"{'employee_name': [ErrorDetail(string='This field is required.', code='required')], 'department': [ErrorDetail(string='This field is required.', code='required')], 'designation': [ErrorDetail(string='This field is required.', code='required')]}",[] -BR-HR2-002-I-01,BR,BR-HR2-002,Invalid,,,"Apply leave with start_date=2026-05-03, end_date=2026-05-01",Request rejected with date range error,Pass,Expected response,"{'employee_name': [ErrorDetail(string='This field is required.', code='required')], 'department': [ErrorDetail(string='This field is required.', code='required')], 'designation': [ErrorDetail(string='This field is required.', code='required')]}",[] -BR-HR2-002-V-01,BR,BR-HR2-002,Valid,,,"Apply leave with start_date=2026-05-01, end_date=2026-05-03",Request accepted,Fail,Unexpected status 400,"{'employee_name': [ErrorDetail(string='This field is required.', code='required')], 'department': [ErrorDetail(string='This field is required.', code='required')], 'designation': [ErrorDetail(string='This field is required.', code='required')]}",[] -BR-HR2-003-I-01,BR,BR-HR2-003,Invalid,,,"Apply leave with start_date=2026-05-01, end_date=2026-05-03, total_days=2",Request rejected with total_days mismatch error,Pass,Expected response,"{'employee_name': [ErrorDetail(string='This field is required.', code='required')], 'department': [ErrorDetail(string='This field is required.', code='required')], 'designation': [ErrorDetail(string='This field is required.', code='required')]}",[] -BR-HR2-003-V-01,BR,BR-HR2-003,Valid,,,"Apply leave with start_date=2026-05-01, end_date=2026-05-03, total_days=3",Request accepted,Fail,Unexpected status 400,"{'employee_name': [ErrorDetail(string='This field is required.', code='required')], 'department': [ErrorDetail(string='This field is required.', code='required')], 'designation': [ErrorDetail(string='This field is required.', code='required')]}",[] -BR-HR2-004-I-01,BR,BR-HR2-004,Invalid,,,Apply leave overlapping existing approved leave,Request rejected with overlap error,Pass,Expected response,"{'employee_name': [ErrorDetail(string='This field is required.', code='required')], 'department': [ErrorDetail(string='This field is required.', code='required')], 'designation': [ErrorDetail(string='This field is required.', code='required')]}",[] -BR-HR2-004-V-01,BR,BR-HR2-004,Valid,,,Apply leave for 2026-05-10 to 2026-05-12 when no overlaps,Request accepted,Fail,Unexpected status 400,"{'employee_name': [ErrorDetail(string='This field is required.', code='required')], 'department': [ErrorDetail(string='This field is required.', code='required')], 'designation': [ErrorDetail(string='This field is required.', code='required')]}",[] -BR-HR2-005-I-01,BR,BR-HR2-005,Invalid,,,Apply leave for 10 days with balance < 10,Request rejected with insufficient balance error,Pass,Expected response,"{'employee_name': [ErrorDetail(string='This field is required.', code='required')], 'department': [ErrorDetail(string='This field is required.', code='required')], 'designation': [ErrorDetail(string='This field is required.', code='required')]}",[] -BR-HR2-005-V-01,BR,BR-HR2-005,Valid,,,Apply leave for 2 days with balance >= 2,Request accepted,Fail,Unexpected status 400,"{'employee_name': [ErrorDetail(string='This field is required.', code='required')], 'department': [ErrorDetail(string='This field is required.', code='required')], 'designation': [ErrorDetail(string='This field is required.', code='required')]}",[] -BR-HR2-006-I-01,BR,BR-HR2-006,Invalid,,,Apply leave with nominee_employee_id=invalid,Request rejected with nominee not found error,Pass,Expected response,{'error': 'Invalid action'},[] -BR-HR2-006-V-01,BR,BR-HR2-006,Valid,,,Apply leave with nominee_employee_id=valid,Request accepted with nominee_status=PENDING,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -BR-HR2-007-I-01,BR,BR-HR2-007,Invalid,,,Non-owner withdraws leave,Request rejected with 403,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -BR-HR2-007-V-01,BR,BR-HR2-007,Valid,,,Owner withdraws pending leave,Leave updated to WITHDRAWN,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -BR-HR2-008-I-01,BR,BR-HR2-008,Invalid,,,Request cancellation on start date,Request rejected with cancellation window error,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -BR-HR2-008-V-01,BR,BR-HR2-008,Valid,,,Request cancellation one day before start,Cancellation request accepted,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -BR-HR2-009-I-01,BR,BR-HR2-009,Invalid,,,Delete approved leave,Request rejected with delete error,Pass,Expected response,"{'employee_name': [ErrorDetail(string='This field is required.', code='required')], 'department': [ErrorDetail(string='This field is required.', code='required')], 'designation': [ErrorDetail(string='This field is required.', code='required')]}",[] -BR-HR2-009-V-01,BR,BR-HR2-009,Valid,,,Delete pending leave,Leave deleted,Fail,Unexpected status 400,"{'employee_name': [ErrorDetail(string='This field is required.', code='required')], 'department': [ErrorDetail(string='This field is required.', code='required')], 'designation': [ErrorDetail(string='This field is required.', code='required')]}",[] -BR-HR2-010-I-01,BR,BR-HR2-010,Invalid,,,Request cancellation for pending leave,Request rejected with approval_status error,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -BR-HR2-010-I-02,BR,BR-HR2-010,Invalid,,,Request cancellation when cancel_status already REQUESTED,Request rejected as already processed,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -BR-HR2-010-V-01,BR,BR-HR2-010,Valid,,,Request cancellation for approved leave,Cancel status set to REQUESTED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -BR-HR2-011-I-01,BR,BR-HR2-011,Invalid,,,Request extension after end_date,Request rejected with extension window error,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -BR-HR2-011-I-02,BR,BR-HR2-011,Invalid,,,Request extension with new_end_date before current end date,Request rejected with date validation error,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -BR-HR2-011-V-01,BR,BR-HR2-011,Valid,,,Request extension with new_end_date after current end date,Extension status set to REQUESTED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -BR-HR2-012-I-01,BR,BR-HR2-012,Invalid,,,Approve extension with insufficient balance,Request rejected with insufficient balance error,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -BR-HR2-012-V-01,BR,BR-HR2-012,Valid,,,Approve extension with sufficient balance,Extension approved and balance reduced,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -BR-HR2-013-I-01,BR,BR-HR2-013,Invalid,,,Non-HOD requests document,Request rejected with 403,Pass,Expected response,{'error': 'Document request message is required.'},[] -BR-HR2-013-I-02,BR,BR-HR2-013,Invalid,,,HOD requests document without message,Request rejected with message required,Pass,Expected response,{'error': 'Document request message is required.'},[] -BR-HR2-013-V-01,BR,BR-HR2-013,Valid,,,HOD requests document with message,Document request status set to REQUESTED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -BR-HR2-014-I-01,BR,BR-HR2-014,Invalid,,,Owner submits document without request,Request rejected with no request error,Pass,Expected response,{'error': 'Document request message is required.'},[] -BR-HR2-014-I-02,BR,BR-HR2-014,Invalid,,,Non-owner submits document,Request rejected with 403,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -BR-HR2-014-V-01,BR,BR-HR2-014,Valid,,,Owner submits document after request,Document request status set to SUBMITTED,Fail,Unexpected status 403,{'error': 'Not authorized'},[] -BR-HR2-015-I-01,BR,BR-HR2-015,Invalid,,,Non-nominee responds,Request rejected with 403,Pass,Expected response,{'error': 'Invalid action'},[] -BR-HR2-015-I-02,BR,BR-HR2-015,Invalid,,,Nominee sends invalid action,Request rejected with invalid action,Pass,Expected response,{'error': 'Invalid action'},[] -BR-HR2-015-V-01,BR,BR-HR2-015,Valid,,,Nominee accepts request,Nominee status updated to ACCEPTED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -BR-HR2-016-I-01,BR,BR-HR2-016,Invalid,,,Decision action=invalid,Request rejected with invalid decision,Pass,Expected response,"{'employee_name': [ErrorDetail(string='This field is required.', code='required')], 'department': [ErrorDetail(string='This field is required.', code='required')], 'designation': [ErrorDetail(string='This field is required.', code='required')]}",[] -BR-HR2-016-V-01,BR,BR-HR2-016,Valid,,,Approve leave via decision endpoint,Leave status updated to APPROVED,Fail,Unexpected status 400,"{'employee_name': [ErrorDetail(string='This field is required.', code='required')], 'department': [ErrorDetail(string='This field is required.', code='required')], 'designation': [ErrorDetail(string='This field is required.', code='required')]}",[] -BR-HR2-017-I-01,BR,BR-HR2-017,Invalid,,,Apply leave with no balance record for type,Request rejected with balance not found error,Pass,Expected response,"{'employee_name': [ErrorDetail(string='This field is required.', code='required')], 'department': [ErrorDetail(string='This field is required.', code='required')], 'designation': [ErrorDetail(string='This field is required.', code='required')]}",[] -BR-HR2-017-V-01,BR,BR-HR2-017,Valid,,,Apply leave with existing leave balance,Request accepted,Fail,Unexpected status 400,"{'employee_name': [ErrorDetail(string='This field is required.', code='required')], 'department': [ErrorDetail(string='This field is required.', code='required')], 'designation': [ErrorDetail(string='This field is required.', code='required')]}",[] -BR-HR2-018-I-01,BR,BR-HR2-018,Invalid,,,Apply leave without employee profile and no employee_id,Request rejected with employee profile not found,Pass,Expected response,"{'employee_name': [ErrorDetail(string='This field is required.', code='required')], 'department': [ErrorDetail(string='This field is required.', code='required')], 'designation': [ErrorDetail(string='This field is required.', code='required')]}",[] -BR-HR2-018-V-01,BR,BR-HR2-018,Valid,,,Apply leave as authenticated employee,Request accepted,Fail,Unexpected status 400,"{'employee_name': [ErrorDetail(string='This field is required.', code='required')], 'department': [ErrorDetail(string='This field is required.', code='required')], 'designation': [ErrorDetail(string='This field is required.', code='required')]}",[] -BR-HR2-019-I-01,BR,BR-HR2-019,Invalid,,,Owner withdraws approved LTC,Request rejected with pending-only error,Fail,Unexpected status 200,"{'id': 1, 'employee_name': 'Rahul Sharma', 'department': 'Computer Science and Engineering', 'designation': 'Assistant Professor', 'ltc_block_year': 2025, 'travel_start_date': '2026-04-25', 'travel_end_date': '2026-04-30', 'destination': 'Delhi', 'purpose_of_travel': 'Family travel', 'family_members': '', 'relationship_details': '', 'travel_mode': 'Train', 'ticket_number': '', 'ticket_cost': None, 'accommodation_cost': None, 'other_expenses': None, 'total_amount_claimed': '22000.00', 'tickets_upload': '', 'bills_upload': '', 'previous_ltc_used': False, 'last_ltc_date': None, 'applied_date': '2026-04-15', 'verified_by_hr': False, 'approval_status': 'WITHDRAWN', 'accountant_status': '', 'remarks': '', 'employee': '1001'}",[] -BR-HR2-019-V-01,BR,BR-HR2-019,Valid,,,Owner withdraws pending LTC,LTC updated to WITHDRAWN,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -BR-HR2-020-I-01,BR,BR-HR2-020,Invalid,,,Decision action=invalid,Request rejected with invalid decision,Pass,Expected response,"{'employee_name': [ErrorDetail(string='This field is required.', code='required')], 'department': [ErrorDetail(string='This field is required.', code='required')], 'designation': [ErrorDetail(string='This field is required.', code='required')]}",[] -BR-HR2-020-V-01,BR,BR-HR2-020,Valid,,,Forward LTC,Approval status set to FORWARDED and accountant_status=PENDING,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -BR-HR2-021-I-01,BR,BR-HR2-021,Invalid,,,Owner withdraws approved CPDA advance,Request rejected with pending-only error,Fail,Unexpected status 200,"{'id': 1, 'employee_name': 'Rahul Sharma', 'department': 'Computer Science and Engineering', 'designation': 'Assistant Professor', 'event_name': 'National Conference on AI', 'event_type': 'Conference', 'organized_by': '', 'venue': '', 'start_date': '2026-05-15', 'end_date': '2026-05-17', 'registration_fee': None, 'travel_expense': None, 'accommodation_expense': None, 'other_expenses': None, 'total_amount': '20000.00', 'purpose_of_attending': 'Present paper', 'benefits_to_institution': 'Research exposure', 'invitation_letter': '', 'receipts': '', 'certificates': '', 'applied_date': '2026-04-15', 'verified_by_hr': False, 'approval_status': 'WITHDRAWN', 'accountant_processing_status': '', 'remarks': '', 'employee': '1001'}",[] -BR-HR2-021-V-01,BR,BR-HR2-021,Valid,,,Owner withdraws pending CPDA advance,CPDA updated to WITHDRAWN,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -BR-HR2-022-I-01,BR,BR-HR2-022,Invalid,,,Decision action=invalid,Request rejected with invalid decision,Pass,Expected response,"{'employee_name': [ErrorDetail(string='This field is required.', code='required')], 'department': [ErrorDetail(string='This field is required.', code='required')], 'designation': [ErrorDetail(string='This field is required.', code='required')]}",[] -BR-HR2-022-V-01,BR,BR-HR2-022,Valid,,,Forward CPDA advance to accountant,Status FORWARDED and accountant_processing_status=PENDING,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -BR-HR2-022-V-02,BR,BR-HR2-022,Valid,,,Forward CPDA advance to director,Status FORWARDED and accountant_processing_status=DIRECTOR_REVIEW,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -BR-HR2-023-I-01,BR,BR-HR2-023,Invalid,,,Non-owner downloads another employee record,Request rejected with 403,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -BR-HR2-023-V-01,BR,BR-HR2-023,Valid,,,Owner downloads own leave application,Download succeeds,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -BR-HR2-024-V-01,BR,BR-HR2-024,Valid,,,Reviewer forwards appraisal,Appraisal status set to REVIEWED,Pass,Expected response,"{'id': 1, 'employee_name': 'Rahul Sharma', 'department': 'Computer Science and Engineering', 'designation': 'Assistant Professor', 'appraisal_year': '2025-2026', 'self_summary': 'Completed teaching responsibilities', 'key_responsibilities': 'Teaching and research', 'achievements': 'Published 1 paper', 'challenges_faced': '', 'teaching_performance': '', 'research_work': '', 'publications': '', 'projects_handled': '', 'administrative_contributions': '', 'trainings_attended': '', 'certifications': '', 'workshops': '', 'goals_achieved': 'Completed syllabus', 'future_goals': 'Publish more papers', 'supporting_documents': '', 'reviewer_id': '1001', 'reviewer_comments': '', 'rating': '', 'status': 'APPROVED', 'remarks': '', 'submitted_at': '2026-04-15T19:42:05.361880', 'employee': '1001'}",[] -BR-HR2-024-V-02,BR,BR-HR2-024,Valid,,,Reviewer approves appraisal,Appraisal status set to APPROVED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-001-AL-01,UC,UC-HR2-001,Alternate Path,Request with only department filter,User is authenticated,GET /hr2/api/employees/?department=1,Returns employees in department,Pass,Expected response,[],[] -UC-HR2-001-EX-01,UC,UC-HR2-001,Exception,Unauthorized user tries to list employees,User is not authenticated,GET /hr2/api/employees/,Returns 401 Unauthorized,Pass,Expected response,"{'detail': ErrorDetail(string='Authentication credentials were not provided.', code='not_authenticated')}",[] -UC-HR2-001-HA-01,UC,UC-HR2-001,Happy Path,HR staff lists all employees,User has HR designation or is HR staff,GET /hr2/api/employees/,Returns list of employees,Fail,Unhandled error,"(, ValueError(""Missing staticfiles manifest entry for '404/css/base.css'""), )",[] -UC-HR2-001-HA-02,UC,UC-HR2-001,Happy Path,Filter employees by type and department,User is authenticated,GET /hr2/api/employees/?type=Faculty&department=1,Returns employees matching filters,Pass,Expected response,[],[] -UC-HR2-002-AL-01,UC,UC-HR2-002,Alternate Path,HOD fetches employee details,HOD is authenticated,GET /hr2/api/employees/123/,Returns employee details,Fail,Unhandled error,"(, ValueError(""Missing staticfiles manifest entry for '404/css/base.css'""), )",[] -UC-HR2-002-EX-01,UC,UC-HR2-002,Exception,Employee not found,Employee does not exist,GET /hr2/api/employees/999999/,Returns 404 Not Found,Fail,Unhandled error,"(, ValueError(""Missing staticfiles manifest entry for '404/css/base.css'""), )",[] -UC-HR2-002-HA-01,UC,UC-HR2-002,Happy Path,Fetch employee details by ID,Employee exists,GET /hr2/api/employees/123/,Returns employee details,Fail,Unhandled error,"(, ValueError(""Missing staticfiles manifest entry for '404/css/base.css'""), )",[] -UC-HR2-003-AL-01,UC,UC-HR2-003,Alternate Path,Update employee address,Employee exists,PUT /hr2/api/employees/123/ with address=Updated,Returns updated employee details,Fail,Unhandled error,"(, ValueError(""Missing staticfiles manifest entry for '404/css/base.css'""), )",[] -UC-HR2-003-EX-01,UC,UC-HR2-003,Exception,Invalid data,Employee exists,PUT /hr2/api/employees/123/ with phone_number=invalid,Returns 400 validation error,Fail,Unhandled error,"(, ValueError(""Missing staticfiles manifest entry for '404/css/base.css'""), )",[] -UC-HR2-003-HA-01,UC,UC-HR2-003,Happy Path,Update phone number,Employee exists,PUT /hr2/api/employees/123/ with phone_number=9876543210,Returns updated employee details,Fail,Unhandled error,"(, ValueError(""Missing staticfiles manifest entry for '404/css/base.css'""), )",[] -UC-HR2-004-AL-01,UC,UC-HR2-004,Alternate Path,Nominate substitute during leave,Nominee employee exists,POST /hr2/api/leave-applications/ with nominee_employee_id=456,Leave created with nominee_status=PENDING,Fail,Unexpected status 400,"{'employee_name': [ErrorDetail(string='This field is required.', code='required')], 'department': [ErrorDetail(string='This field is required.', code='required')], 'designation': [ErrorDetail(string='This field is required.', code='required')]}",[] -UC-HR2-004-EX-01,UC,UC-HR2-004,Exception,Start date in the past,Employee is authenticated,POST /hr2/api/leave-applications/ with start_date=yesterday,Returns 400 start_date validation error,Pass,Expected response,"{'employee_name': [ErrorDetail(string='This field is required.', code='required')], 'department': [ErrorDetail(string='This field is required.', code='required')], 'designation': [ErrorDetail(string='This field is required.', code='required')]}",[] -UC-HR2-004-HA-01,UC,UC-HR2-004,Happy Path,Apply for casual leave with valid dates,Leave balance is sufficient,"POST /hr2/api/leave-applications/ with leave_type=CL, start_date=future, end_date=future+2, total_days=3",Leave application created with approval_status=PENDING or FORWARDED,Fail,Unexpected status 400,"{'employee_name': [ErrorDetail(string='This field is required.', code='required')], 'department': [ErrorDetail(string='This field is required.', code='required')], 'designation': [ErrorDetail(string='This field is required.', code='required')]}",[] -UC-HR2-005-AL-01,UC,UC-HR2-005,Alternate Path,HOD views departmental leave applications,User has HOD designation,GET /hr2/api/leave-applications/,Returns department leave applications,Pass,Expected response,[],[] -UC-HR2-005-EX-01,UC,UC-HR2-005,Exception,Unauthorized user tries to list leave applications,User is not authenticated,GET /hr2/api/leave-applications/,Returns 401 Unauthorized,Pass,Expected response,"{'detail': ErrorDetail(string='Authentication credentials were not provided.', code='not_authenticated')}",[] -UC-HR2-005-HA-01,UC,UC-HR2-005,Happy Path,Employee views own leave applications,Employee is authenticated,GET /hr2/api/leave-applications/,Returns only employee's leave applications,Pass,Expected response,[],[] -UC-HR2-005-HA-02,UC,UC-HR2-005,Happy Path,Director views forwarded applications,User has Director designation,GET /hr2/api/leave-applications/,Returns forwarded and requested decisions,Pass,Expected response,[],[] -UC-HR2-006-AL-01,UC,UC-HR2-006,Alternate Path,Registrar withdraws forwarded leave,Leave approval_status is FORWARDED,POST /hr2/api/leave-applications/10/withdraw/,Leave updated to approval_status=REJECTED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-006-EX-01,UC,UC-HR2-006,Exception,Withdraw non-pending leave,Leave approval_status is APPROVED,POST /hr2/api/leave-applications/10/withdraw/,Returns 400 with withdrawal error,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-006-HA-01,UC,UC-HR2-006,Happy Path,Employee withdraws own pending leave,Leave approval_status is PENDING,POST /hr2/api/leave-applications/10/withdraw/,Leave updated to approval_status=WITHDRAWN,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-007-AL-01,UC,UC-HR2-007,Alternate Path,Submit cancellation request with reason,Approved leave exists,POST /hr2/api/leave-applications/10/cancel-request/ with reason=medical,Cancel status set to REQUESTED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-007-EX-01,UC,UC-HR2-007,Exception,Cancellation after start date,Today is on or after start date,POST /hr2/api/leave-applications/10/cancel-request/,Returns 400 cancellation window error,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-007-HA-01,UC,UC-HR2-007,Happy Path,Submit cancellation request before start date,Today is before leave start date,POST /hr2/api/leave-applications/10/cancel-request/ with reason=change of plan,Cancel status set to REQUESTED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-008-AL-01,UC,UC-HR2-008,Alternate Path,Request extension with reason,Extension dates are valid,POST /hr2/api/leave-applications/10/extension-request/ with reason=medical,Extension status set to REQUESTED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-008-EX-01,UC,UC-HR2-008,Exception,Extension with invalid date,New end date before current end date,POST /hr2/api/leave-applications/10/extension-request/ with new_end_date=earlier,Returns 400 validation error,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-008-HA-01,UC,UC-HR2-008,Happy Path,Request extension with new end date,Extension dates are valid,POST /hr2/api/leave-applications/10/extension-request/ with new_end_date=future+2,Extension status set to REQUESTED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-009-AL-01,UC,UC-HR2-009,Alternate Path,Employee views balance with no records,Employee has no balance entries,GET /hr2/api/leave-balance/,Returns empty balance list,Pass,Expected response,[],[] -UC-HR2-009-EX-01,UC,UC-HR2-009,Exception,Unauthorized user tries to view leave balance,User is not authenticated,GET /hr2/api/leave-balance/,Returns 401 Unauthorized,Pass,Expected response,"{'detail': ErrorDetail(string='Authentication credentials were not provided.', code='not_authenticated')}",[] -UC-HR2-009-HA-01,UC,UC-HR2-009,Happy Path,Employee views own leave balance,Employee is authenticated,GET /hr2/api/leave-balance/,Returns leave balance for the employee,Pass,Expected response,[],[] -UC-HR2-009-HA-02,UC,UC-HR2-009,Happy Path,HR views leave balance for another employee,User has HR designation,GET /hr2/api/leave-balance/123/,Returns leave balance for employee 123,Pass,Expected response,"[OrderedDict([('id', 193), ('leave_type_name', 'Casual'), ('year', 2026), ('opening_balance', '10.0'), ('accrued', '0.0'), ('availed', '0.0'), ('current_balance', '10.0'), ('employee', '1001'), ('leave_type', 193)]), OrderedDict([('id', 194), ('leave_type_name', 'Vacation'), ('year', 2026), ('opening_balance', '10.0'), ('accrued', '0.0'), ('availed', '0.0'), ('current_balance', '10.0'), ('employee', '1001'), ('leave_type', 194)]), OrderedDict([('id', 195), ('leave_type_name', 'Earned'), ('year', 2026), ('opening_balance', '10.0'), ('accrued', '0.0'), ('availed', '0.0'), ('current_balance', '10.0'), ('employee', '1001'), ('leave_type', 195)]), OrderedDict([('id', 196), ('leave_type_name', 'Medical'), ('year', 2026), ('opening_balance', '10.0'), ('accrued', '0.0'), ('availed', '0.0'), ('current_balance', '10.0'), ('employee', '1001'), ('leave_type', 196)]), OrderedDict([('id', 197), ('leave_type_name', 'Restricted'), ('year', 2026), ('opening_balance', '10.0'), ('accrued', '0.0'), ('availed', '0.0'), ('current_balance', '10.0'), ('employee', '1001'), ('leave_type', 197)]), OrderedDict([('id', 198), ('leave_type_name', 'Sabbatical'), ('year', 2026), ('opening_balance', '10.0'), ('accrued', '0.0'), ('availed', '0.0'), ('current_balance', '10.0'), ('employee', '1001'), ('leave_type', 198)])]",[] -UC-HR2-010-AL-01,UC,UC-HR2-010,Alternate Path,Download pending leave application,Leave is pending and belongs to employee,GET /hr2/api/leave-applications/10/download/,Returns text file attachment with leave details,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-010-EX-01,UC,UC-HR2-010,Exception,Access another employee's leave,Leave does not belong to requester,GET /hr2/api/leave-applications/999/download/,Returns 403 Not authorized,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-010-HA-01,UC,UC-HR2-010,Happy Path,Download approved leave application,Leave exists and belongs to employee,GET /hr2/api/leave-applications/10/download/,Returns text file attachment with leave details,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-011-AL-01,UC,UC-HR2-011,Alternate Path,Submit LTC claim with optional fields,Required fields provided,POST /hr2/api/ltc/ with optional fields,LTC application created with approval_status=PENDING,Pass,Expected response,"{'id': 3, 'employee_name': 'Rahul Sharma', 'department': 'Computer Science and Engineering', 'designation': 'Assistant Professor', 'ltc_block_year': 2025, 'travel_start_date': '2026-04-25', 'travel_end_date': '2026-04-30', 'destination': 'Delhi', 'purpose_of_travel': 'Family travel', 'family_members': '', 'relationship_details': '', 'travel_mode': 'Train', 'ticket_number': '', 'ticket_cost': None, 'accommodation_cost': None, 'other_expenses': None, 'total_amount_claimed': '22000.00', 'tickets_upload': '', 'bills_upload': '', 'previous_ltc_used': False, 'last_ltc_date': None, 'applied_date': '2026-04-15', 'verified_by_hr': False, 'approval_status': 'PENDING', 'accountant_status': '', 'remarks': '', 'employee': '1001'}",[] -UC-HR2-011-EX-01,UC,UC-HR2-011,Exception,Missing required fields,Employee is authenticated,POST /hr2/api/ltc/ with incomplete payload,Returns 400 validation errors,Fail,Unexpected status 201,"{'id': 4, 'employee_name': 'Rahul Sharma', 'department': 'Computer Science and Engineering', 'designation': 'Assistant Professor', 'ltc_block_year': 2025, 'travel_start_date': '2026-04-25', 'travel_end_date': '2026-04-30', 'destination': 'Delhi', 'purpose_of_travel': 'Family travel', 'family_members': '', 'relationship_details': '', 'travel_mode': 'Train', 'ticket_number': '', 'ticket_cost': None, 'accommodation_cost': None, 'other_expenses': None, 'total_amount_claimed': '22000.00', 'tickets_upload': '', 'bills_upload': '', 'previous_ltc_used': False, 'last_ltc_date': None, 'applied_date': '2026-04-15', 'verified_by_hr': False, 'approval_status': 'PENDING', 'accountant_status': '', 'remarks': '', 'employee': '1001'}",[] -UC-HR2-011-HA-01,UC,UC-HR2-011,Happy Path,Submit LTC claim,Required fields provided,POST /hr2/api/ltc/ with required LTC fields,LTC application created with approval_status=PENDING,Pass,Expected response,"{'id': 5, 'employee_name': 'Rahul Sharma', 'department': 'Computer Science and Engineering', 'designation': 'Assistant Professor', 'ltc_block_year': 2025, 'travel_start_date': '2026-04-25', 'travel_end_date': '2026-04-30', 'destination': 'Delhi', 'purpose_of_travel': 'Family travel', 'family_members': '', 'relationship_details': '', 'travel_mode': 'Train', 'ticket_number': '', 'ticket_cost': None, 'accommodation_cost': None, 'other_expenses': None, 'total_amount_claimed': '22000.00', 'tickets_upload': '', 'bills_upload': '', 'previous_ltc_used': False, 'last_ltc_date': None, 'applied_date': '2026-04-15', 'verified_by_hr': False, 'approval_status': 'PENDING', 'accountant_status': '', 'remarks': '', 'employee': '1001'}",[] -UC-HR2-012-AL-01,UC,UC-HR2-012,Alternate Path,Submit CPDA advance with optional expenses,Required fields provided,POST /hr2/api/cpda-advances/ with optional fields,CPDA advance created with approval_status=PENDING,Pass,Expected response,"{'id': 3, 'employee_name': 'Rahul Sharma', 'department': 'Computer Science and Engineering', 'designation': 'Assistant Professor', 'event_name': 'National Conference on AI', 'event_type': 'Conference', 'organized_by': '', 'venue': '', 'start_date': '2026-05-15', 'end_date': '2026-05-17', 'registration_fee': None, 'travel_expense': None, 'accommodation_expense': None, 'other_expenses': None, 'total_amount': '20000.00', 'purpose_of_attending': 'Present paper', 'benefits_to_institution': 'Research exposure', 'invitation_letter': '', 'receipts': '', 'certificates': '', 'applied_date': '2026-04-15', 'verified_by_hr': False, 'approval_status': 'PENDING', 'accountant_processing_status': '', 'remarks': '', 'employee': '1001'}",[] -UC-HR2-012-EX-01,UC,UC-HR2-012,Exception,Invalid amount,Employee is authenticated,POST /hr2/api/cpda-advances/ with amountRequired=invalid,Returns 400 validation errors,Fail,Unexpected status 201,"{'id': 4, 'employee_name': 'Rahul Sharma', 'department': 'Computer Science and Engineering', 'designation': 'Assistant Professor', 'event_name': 'National Conference on AI', 'event_type': 'Conference', 'organized_by': '', 'venue': '', 'start_date': '2026-05-15', 'end_date': '2026-05-17', 'registration_fee': None, 'travel_expense': None, 'accommodation_expense': None, 'other_expenses': None, 'total_amount': '20000.00', 'purpose_of_attending': 'Present paper', 'benefits_to_institution': 'Research exposure', 'invitation_letter': '', 'receipts': '', 'certificates': '', 'applied_date': '2026-04-15', 'verified_by_hr': False, 'approval_status': 'PENDING', 'accountant_processing_status': '', 'remarks': '', 'employee': '1001'}",[] -UC-HR2-012-HA-01,UC,UC-HR2-012,Happy Path,Submit CPDA advance,Required fields provided,POST /hr2/api/cpda-advances/ with required fields,CPDA advance created with approval_status=PENDING,Pass,Expected response,"{'id': 5, 'employee_name': 'Rahul Sharma', 'department': 'Computer Science and Engineering', 'designation': 'Assistant Professor', 'event_name': 'National Conference on AI', 'event_type': 'Conference', 'organized_by': '', 'venue': '', 'start_date': '2026-05-15', 'end_date': '2026-05-17', 'registration_fee': None, 'travel_expense': None, 'accommodation_expense': None, 'other_expenses': None, 'total_amount': '20000.00', 'purpose_of_attending': 'Present paper', 'benefits_to_institution': 'Research exposure', 'invitation_letter': '', 'receipts': '', 'certificates': '', 'applied_date': '2026-04-15', 'verified_by_hr': False, 'approval_status': 'PENDING', 'accountant_processing_status': '', 'remarks': '', 'employee': '1001'}",[] -UC-HR2-013-AL-01,UC,UC-HR2-013,Alternate Path,Submit appraisal form with optional fields,Required fields provided,POST /hr2/api/appraisal-forms/ with optional fields,Appraisal form created with status=SUBMITTED,Pass,Expected response,"{'id': 3, 'employee_name': 'Rahul Sharma', 'department': 'Computer Science and Engineering', 'designation': 'Assistant Professor', 'appraisal_year': '2025-2026', 'self_summary': 'Completed teaching responsibilities', 'key_responsibilities': 'Teaching and research', 'achievements': 'Published 1 paper', 'challenges_faced': '', 'teaching_performance': '', 'research_work': '', 'publications': '', 'projects_handled': '', 'administrative_contributions': '', 'trainings_attended': '', 'certifications': '', 'workshops': '', 'goals_achieved': 'Completed syllabus', 'future_goals': 'Publish more papers', 'supporting_documents': '', 'reviewer_id': '', 'reviewer_comments': '', 'rating': '', 'status': 'PENDING', 'remarks': '', 'submitted_at': '2026-04-15T19:42:13.348290', 'employee': '1001'}",[] -UC-HR2-013-EX-01,UC,UC-HR2-013,Exception,Missing required fields,Employee is authenticated,POST /hr2/api/appraisal-forms/ with incomplete payload,Returns 400 validation errors,Fail,Unexpected status 201,"{'id': 4, 'employee_name': 'Rahul Sharma', 'department': 'Computer Science and Engineering', 'designation': 'Assistant Professor', 'appraisal_year': '2025-2026', 'self_summary': 'Completed teaching responsibilities', 'key_responsibilities': 'Teaching and research', 'achievements': 'Published 1 paper', 'challenges_faced': '', 'teaching_performance': '', 'research_work': '', 'publications': '', 'projects_handled': '', 'administrative_contributions': '', 'trainings_attended': '', 'certifications': '', 'workshops': '', 'goals_achieved': 'Completed syllabus', 'future_goals': 'Publish more papers', 'supporting_documents': '', 'reviewer_id': '', 'reviewer_comments': '', 'rating': '', 'status': 'PENDING', 'remarks': '', 'submitted_at': '2026-04-15T19:42:13.360130', 'employee': '1001'}",[] -UC-HR2-013-HA-01,UC,UC-HR2-013,Happy Path,Submit appraisal form,Required fields provided,POST /hr2/api/appraisal-forms/ with required fields,Appraisal form created with status=SUBMITTED,Pass,Expected response,"{'id': 5, 'employee_name': 'Rahul Sharma', 'department': 'Computer Science and Engineering', 'designation': 'Assistant Professor', 'appraisal_year': '2025-2026', 'self_summary': 'Completed teaching responsibilities', 'key_responsibilities': 'Teaching and research', 'achievements': 'Published 1 paper', 'challenges_faced': '', 'teaching_performance': '', 'research_work': '', 'publications': '', 'projects_handled': '', 'administrative_contributions': '', 'trainings_attended': '', 'certifications': '', 'workshops': '', 'goals_achieved': 'Completed syllabus', 'future_goals': 'Publish more papers', 'supporting_documents': '', 'reviewer_id': '', 'reviewer_comments': '', 'rating': '', 'status': 'PENDING', 'remarks': '', 'submitted_at': '2026-04-15T19:42:13.368130', 'employee': '1001'}",[] -UC-HR2-014-AL-01,UC,UC-HR2-014,Alternate Path,HR staff fetches leave application by ID,HR staff is authenticated,GET /hr2/api/leave-applications/10/,Returns leave application,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-014-EX-01,UC,UC-HR2-014,Exception,Leave application not found,Leave application does not exist,GET /hr2/api/leave-applications/999/,Returns 404 Not Found,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-014-HA-01,UC,UC-HR2-014,Happy Path,Fetch leave application by ID,Leave application exists,GET /hr2/api/leave-applications/10/,Returns leave application,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-015-AL-01,UC,UC-HR2-015,Alternate Path,Update leave handover notes,Leave belongs to employee,PUT /hr2/api/leave-applications/10/ with handover_notes=updated,Leave application updated,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-015-EX-01,UC,UC-HR2-015,Exception,Update another employee's leave,Leave belongs to another employee,PUT /hr2/api/leave-applications/10/,Returns 403 Not authorized,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-015-HA-01,UC,UC-HR2-015,Happy Path,Update leave reason,Leave belongs to employee,PUT /hr2/api/leave-applications/10/ with reason=updated,Leave application updated,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-016-AL-01,UC,UC-HR2-016,Alternate Path,Delete pending leave without attachments,Leave approval_status is PENDING,DELETE /hr2/api/leave-applications/10/,Leave application deleted,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-016-EX-01,UC,UC-HR2-016,Exception,Delete non-pending leave,Leave approval_status is APPROVED,DELETE /hr2/api/leave-applications/10/,Returns 400 with delete error,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-016-HA-01,UC,UC-HR2-016,Happy Path,Delete pending leave,Leave approval_status is PENDING,DELETE /hr2/api/leave-applications/10/,Leave application deleted,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-017-AL-01,UC,UC-HR2-017,Alternate Path,Reject leave,Leave exists,POST /hr2/api/leave-applications/10/reject/,Leave status updated to REJECTED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-017-EX-01,UC,UC-HR2-017,Exception,Invalid decision,Leave exists,POST /hr2/api/leave-applications/10/invalid/,Returns 400 invalid decision,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-017-HA-01,UC,UC-HR2-017,Happy Path,Approve leave,Leave exists,POST /hr2/api/leave-applications/10/approve/,Leave status updated to APPROVED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-017-HA-02,UC,UC-HR2-017,Happy Path,Forward leave,Leave exists,POST /hr2/api/leave-applications/10/forward/,Leave status updated to FORWARDED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-018-AL-01,UC,UC-HR2-018,Alternate Path,Nominee has no pending requests,Nominee has no pending requests,GET /hr2/api/leave-nominee/,Returns empty list,Fail,Unexpected status 405,"{'detail': ErrorDetail(string='Method ""GET"" not allowed.', code='method_not_allowed')}",[] -UC-HR2-018-EX-01,UC,UC-HR2-018,Exception,Unauthorized user tries to view nominee dashboard,User is not authenticated,GET /hr2/api/leave-nominee/,Returns 401 Unauthorized,Fail,Unexpected status 405,"{'detail': ErrorDetail(string='Method ""GET"" not allowed.', code='method_not_allowed')}",[] -UC-HR2-018-HA-01,UC,UC-HR2-018,Happy Path,Nominee views pending requests,Nominee has pending requests,GET /hr2/api/leave-nominee/,Returns pending nominee requests,Fail,Unexpected status 405,"{'detail': ErrorDetail(string='Method ""GET"" not allowed.', code='method_not_allowed')}",[] -UC-HR2-019-AL-01,UC,UC-HR2-019,Alternate Path,Nominee declines,Nominee is assigned on leave,POST /hr2/api/leave-nominee/10/ with action=decline,Nominee status updated to DECLINED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-019-EX-01,UC,UC-HR2-019,Exception,Invalid action,Nominee is assigned,POST /hr2/api/leave-nominee/10/ with action=invalid,Returns 400 invalid action,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-019-HA-01,UC,UC-HR2-019,Happy Path,Nominee accepts,Nominee is assigned on leave,POST /hr2/api/leave-nominee/10/ with action=accept,Nominee status updated to ACCEPTED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-020-AL-01,UC,UC-HR2-020,Alternate Path,Request documents with updated message,Leave exists,POST /hr2/api/leave-applications/10/request-document/ with message=submit updated proof,Document request status set to REQUESTED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-020-EX-01,UC,UC-HR2-020,Exception,Missing message,Leave exists,POST /hr2/api/leave-applications/10/request-document/,Returns 400 message required,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-020-HA-01,UC,UC-HR2-020,Happy Path,Request documents with message,Leave exists,POST /hr2/api/leave-applications/10/request-document/ with message=submit proof,Document request status set to REQUESTED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-021-AL-01,UC,UC-HR2-021,Alternate Path,Submit updated document,Document request exists,POST /hr2/api/leave-applications/10/submit-document/ with submission=updated ref,Document request status set to SUBMITTED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-021-EX-01,UC,UC-HR2-021,Exception,Submit without request,No document request exists,POST /hr2/api/leave-applications/10/submit-document/ with submission=doc ref,Returns 400 no request,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-021-HA-01,UC,UC-HR2-021,Happy Path,Submit document after request,Document request exists,POST /hr2/api/leave-applications/10/submit-document/ with submission=doc ref,Document request status set to SUBMITTED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-022-AL-01,UC,UC-HR2-022,Alternate Path,Approve cancellation with remarks,Approver role matches,POST /hr2/api/leave-applications/10/cancel-decision/approve/ with remarks=ok,Cancellation approved and leave cancelled,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-022-EX-01,UC,UC-HR2-022,Exception,Invalid decision,Cancellation request exists,POST /hr2/api/leave-applications/10/cancel-decision/invalid/,Returns 400 invalid decision,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-022-HA-01,UC,UC-HR2-022,Happy Path,Approve cancellation,Approver role matches,POST /hr2/api/leave-applications/10/cancel-decision/approve/,Cancellation approved and leave cancelled,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-022-HA-02,UC,UC-HR2-022,Happy Path,Reject cancellation,Approver role matches,POST /hr2/api/leave-applications/10/cancel-decision/reject/,Cancellation rejected,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-023-AL-01,UC,UC-HR2-023,Alternate Path,Approve extension with remarks,Approver role matches,POST /hr2/api/leave-applications/10/extension-decision/approve/ with remarks=ok,Extension approved and leave updated,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-023-EX-01,UC,UC-HR2-023,Exception,Invalid decision,Extension request exists,POST /hr2/api/leave-applications/10/extension-decision/invalid/,Returns 400 invalid decision,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-023-HA-01,UC,UC-HR2-023,Happy Path,Approve extension,Approver role matches,POST /hr2/api/leave-applications/10/extension-decision/approve/,Extension approved and leave updated,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-023-HA-02,UC,UC-HR2-023,Happy Path,Reject extension,Approver role matches,POST /hr2/api/leave-applications/10/extension-decision/reject/,Extension rejected,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-024-AL-01,UC,UC-HR2-024,Alternate Path,Mark half-day attendance,Valid status and date,"POST /hr2/api/attendance/ with date=today, status=HALF_DAY",Attendance record created,Fail,Unhandled error,"(, ValueError(""Missing staticfiles manifest entry for '404/css/base.css'""), )",[] -UC-HR2-024-EX-01,UC,UC-HR2-024,Exception,Missing attendance status,Employee is authenticated,POST /hr2/api/attendance/ with date=today,Returns 400 validation errors,Fail,Unhandled error,"(, ValueError(""Missing staticfiles manifest entry for '404/css/base.css'""), )",[] -UC-HR2-024-HA-01,UC,UC-HR2-024,Happy Path,Mark attendance,Valid status and date,"POST /hr2/api/attendance/ with date=today, status=PRESENT",Attendance record created,Fail,Unhandled error,"(, ValueError(""Missing staticfiles manifest entry for '404/css/base.css'""), )",[] -UC-HR2-025-AL-01,UC,UC-HR2-025,Alternate Path,View attendance without filters,Attendance exists,GET /hr2/api/attendance/,Returns attendance records,Pass,Expected response,[],[] -UC-HR2-025-EX-01,UC,UC-HR2-025,Exception,Unauthorized user tries to view attendance,User is not authenticated,GET /hr2/api/attendance/,Returns 401 Unauthorized,Pass,Expected response,"{'detail': ErrorDetail(string='Authentication credentials were not provided.', code='not_authenticated')}",[] -UC-HR2-025-HA-01,UC,UC-HR2-025,Happy Path,View attendance for date range,Attendance exists,GET /hr2/api/attendance/?from_date=2026-05-01&to_date=2026-05-10,Returns attendance records,Pass,Expected response,[],[] -UC-HR2-026-AL-01,UC,UC-HR2-026,Alternate Path,List all periods without filter,Periods exist,GET /hr2/api/appraisal-periods/,Returns appraisal periods,Pass,Expected response,"[OrderedDict([('id', 50), ('name', '2025-2026'), ('start_date', '2025-07-01'), ('end_date', '2026-06-30'), ('submission_deadline', '2026-05-31'), ('is_active', True)])]",[] -UC-HR2-026-EX-01,UC,UC-HR2-026,Exception,Unauthorized user tries to view appraisal periods,User is not authenticated,GET /hr2/api/appraisal-periods/,Returns 401 Unauthorized,Pass,Expected response,"{'detail': ErrorDetail(string='Authentication credentials were not provided.', code='not_authenticated')}",[] -UC-HR2-026-HA-01,UC,UC-HR2-026,Happy Path,List active periods,Periods exist,GET /hr2/api/appraisal-periods/?is_active=true,Returns appraisal periods,Fail,Unhandled error,"(, ValueError(""Missing staticfiles manifest entry for '404/css/base.css'""), )",[] -UC-HR2-027-AL-01,UC,UC-HR2-027,Alternate Path,Submit appraisal with remarks,Required fields provided,"POST /hr2/api/appraisals/ with period, scores, remarks",Performance appraisal created,Fail,Unexpected status 400,"{'employee': [ErrorDetail(string='This field is required.', code='required')]}",[] -UC-HR2-027-EX-01,UC,UC-HR2-027,Exception,Missing required fields,Employee is authenticated,POST /hr2/api/appraisals/ with incomplete payload,Returns 400 validation errors,Pass,Expected response,"{'employee': [ErrorDetail(string='This field is required.', code='required')]}",[] -UC-HR2-027-HA-01,UC,UC-HR2-027,Happy Path,Submit appraisal,Required fields provided,POST /hr2/api/appraisals/ with period and scores,Performance appraisal created,Fail,Unexpected status 400,"{'employee': [ErrorDetail(string='This field is required.', code='required')]}",[] -UC-HR2-028-AL-01,UC,UC-HR2-028,Alternate Path,List all appraisals,Appraisals exist,GET /hr2/api/appraisals/,Returns appraisals,Pass,Expected response,[],[] -UC-HR2-028-EX-01,UC,UC-HR2-028,Exception,Unauthorized user tries to list appraisals,User is not authenticated,GET /hr2/api/appraisals/,Returns 401 Unauthorized,Pass,Expected response,"{'detail': ErrorDetail(string='Authentication credentials were not provided.', code='not_authenticated')}",[] -UC-HR2-028-HA-01,UC,UC-HR2-028,Happy Path,List appraisals for period,Appraisals exist,GET /hr2/api/appraisals/?period=1,Returns appraisals,Pass,Expected response,[],[] -UC-HR2-029-AL-01,UC,UC-HR2-029,Alternate Path,List programs when none are available,No programs available,GET /hr2/api/training-programs/,Returns empty list,Pass,Expected response,"[OrderedDict([('id', 53), ('title', 'AI Workshop'), ('description', 'AI fundamentals'), ('organizer', 'IIITDMJ'), ('venue', 'Jabalpur'), ('start_date', '2026-04-25'), ('end_date', '2026-04-27'), ('max_participants', 30), ('is_mandatory', False)])]",[] -UC-HR2-029-EX-01,UC,UC-HR2-029,Exception,Unauthorized user tries to list programs,User is not authenticated,GET /hr2/api/training-programs/,Returns 401 Unauthorized,Pass,Expected response,"{'detail': ErrorDetail(string='Authentication credentials were not provided.', code='not_authenticated')}",[] -UC-HR2-029-HA-01,UC,UC-HR2-029,Happy Path,List available programs,Programs exist,GET /hr2/api/training-programs/,Returns training programs,Pass,Expected response,"[OrderedDict([('id', 53), ('title', 'AI Workshop'), ('description', 'AI fundamentals'), ('organizer', 'IIITDMJ'), ('venue', 'Jabalpur'), ('start_date', '2026-04-25'), ('end_date', '2026-04-27'), ('max_participants', 30), ('is_mandatory', False)])]",[] -UC-HR2-030-AL-01,UC,UC-HR2-030,Alternate Path,Nominate for mandatory program,Program exists and is mandatory,POST /hr2/api/training-nominations/ with program data,Nomination created,Fail,Unexpected status 400,"{'employee': [ErrorDetail(string='This field is required.', code='required')]}",[] -UC-HR2-030-EX-01,UC,UC-HR2-030,Exception,Invalid program,Employee is authenticated,POST /hr2/api/training-nominations/ with invalid program,Returns 400 validation errors,Pass,Expected response,"{'employee': [ErrorDetail(string='This field is required.', code='required')]}",[] -UC-HR2-030-HA-01,UC,UC-HR2-030,Happy Path,Submit training nomination,Program exists,POST /hr2/api/training-nominations/ with program data,Nomination created,Fail,Unexpected status 400,"{'employee': [ErrorDetail(string='This field is required.', code='required')]}",[] -UC-HR2-031-AL-01,UC,UC-HR2-031,Alternate Path,List nominations after submission,Nomination exists,GET /hr2/api/training-nominations/,Returns training nominations,Pass,Expected response,[],[] -UC-HR2-031-EX-01,UC,UC-HR2-031,Exception,Unauthorized user tries to list nominations,User is not authenticated,GET /hr2/api/training-nominations/,Returns 401 Unauthorized,Pass,Expected response,"{'detail': ErrorDetail(string='Authentication credentials were not provided.', code='not_authenticated')}",[] -UC-HR2-031-HA-01,UC,UC-HR2-031,Happy Path,List nominations,Nominations exist,GET /hr2/api/training-nominations/,Returns training nominations,Pass,Expected response,[],[] -UC-HR2-032-AL-01,UC,UC-HR2-032,Alternate Path,Submit promotion with API score,Required fields provided,POST /hr2/api/promotions/ with api_score,Promotion application created,Fail,Unexpected status 400,"{'employee': [ErrorDetail(string='This field is required.', code='required')]}",[] -UC-HR2-032-EX-01,UC,UC-HR2-032,Exception,Missing required fields,Employee is authenticated,POST /hr2/api/promotions/ with incomplete payload,Returns 400 validation errors,Pass,Expected response,"{'employee': [ErrorDetail(string='This field is required.', code='required')]}",[] -UC-HR2-032-HA-01,UC,UC-HR2-032,Happy Path,Submit promotion,Required fields provided,POST /hr2/api/promotions/ with required fields,Promotion application created,Fail,Unexpected status 400,"{'employee': [ErrorDetail(string='This field is required.', code='required')]}",[] -UC-HR2-033-AL-01,UC,UC-HR2-033,Alternate Path,List promotions when none exist,No promotions exist,GET /hr2/api/promotions/,Returns empty list,Pass,Expected response,[],[] -UC-HR2-033-EX-01,UC,UC-HR2-033,Exception,Unauthorized user tries to list promotions,User is not authenticated,GET /hr2/api/promotions/,Returns 401 Unauthorized,Pass,Expected response,"{'detail': ErrorDetail(string='Authentication credentials were not provided.', code='not_authenticated')}",[] -UC-HR2-033-HA-01,UC,UC-HR2-033,Happy Path,List promotions,Applications exist,GET /hr2/api/promotions/,Returns promotion applications,Pass,Expected response,[],[] -UC-HR2-034-AL-01,UC,UC-HR2-034,Alternate Path,Get workload without semester filter,Workload exists,GET /hr2/api/workload/?year=2026,Returns workload records,Fail,Unhandled error,"(, ValueError(""Missing staticfiles manifest entry for '404/css/base.css'""), )",[] -UC-HR2-034-EX-01,UC,UC-HR2-034,Exception,Unauthorized user tries to view workload,User is not authenticated,GET /hr2/api/workload/,Returns 401 Unauthorized,Pass,Expected response,"{'detail': ErrorDetail(string='Authentication credentials were not provided.', code='not_authenticated')}",[] -UC-HR2-034-HA-01,UC,UC-HR2-034,Happy Path,Get workload by semester,Workload exists,GET /hr2/api/workload/?semester=Spring&year=2026,Returns workload records,Fail,Unhandled error,"(, ValueError(""Missing staticfiles manifest entry for '404/css/base.css'""), )",[] -UC-HR2-035-AL-01,UC,UC-HR2-035,Alternate Path,Update LTC purpose,LTC belongs to employee,PUT /hr2/api/ltc/10/ with purpose_of_travel=updated,LTC updated,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-035-EX-01,UC,UC-HR2-035,Exception,Update another employee's LTC,LTC belongs to another employee,PUT /hr2/api/ltc/10/,Returns 403 Not authorized,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-035-HA-01,UC,UC-HR2-035,Happy Path,Update LTC details,LTC belongs to employee,PUT /hr2/api/ltc/10/ with destination=updated,LTC updated,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-036-AL-01,UC,UC-HR2-036,Alternate Path,Accountant views forwarded LTC,User is Accountant,GET /hr2/api/ltc/,Returns forwarded LTC,Pass,Expected response,[],[] -UC-HR2-036-EX-01,UC,UC-HR2-036,Exception,Unauthorized user tries to list LTC,User is not authenticated,GET /hr2/api/ltc/,Returns 401 Unauthorized,Pass,Expected response,"{'detail': ErrorDetail(string='Authentication credentials were not provided.', code='not_authenticated')}",[] -UC-HR2-036-HA-01,UC,UC-HR2-036,Happy Path,Employee views own LTC,Employee is authenticated,GET /hr2/api/ltc/,Returns employee LTC applications,Pass,Expected response,[],[] -UC-HR2-036-HA-02,UC,UC-HR2-036,Happy Path,HR staff views pending LTC,User is HR staff,GET /hr2/api/ltc/,Returns pending/forwarded LTC,Pass,Expected response,[],[] -UC-HR2-037-AL-01,UC,UC-HR2-037,Alternate Path,HR staff downloads LTC,HR staff is authenticated,GET /hr2/api/ltc/10/download/,Returns text file attachment,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-037-EX-01,UC,UC-HR2-037,Exception,Unauthorized user tries to download LTC,User is not authenticated,GET /hr2/api/ltc/10/download/,Returns 401 Unauthorized,Pass,Expected response,"{'detail': ErrorDetail(string='Authentication credentials were not provided.', code='not_authenticated')}",[] -UC-HR2-037-HA-01,UC,UC-HR2-037,Happy Path,Download LTC,LTC exists,GET /hr2/api/ltc/10/download/,Returns text file attachment,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-038-AL-01,UC,UC-HR2-038,Alternate Path,Withdraw pending LTC with remarks,LTC approval_status is PENDING,POST /hr2/api/ltc/10/withdraw/ with remarks=updated,LTC updated to WITHDRAWN,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-038-EX-01,UC,UC-HR2-038,Exception,Withdraw non-pending LTC,LTC approval_status is APPROVED,POST /hr2/api/ltc/10/withdraw/,Returns 400 with withdrawal error,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-038-HA-01,UC,UC-HR2-038,Happy Path,Withdraw pending LTC,LTC approval_status is PENDING,POST /hr2/api/ltc/10/withdraw/,LTC updated to WITHDRAWN,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-039-AL-01,UC,UC-HR2-039,Alternate Path,Reject LTC,LTC exists,POST /hr2/api/ltc/10/reject/,LTC status REJECTED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-039-EX-01,UC,UC-HR2-039,Exception,Invalid decision,LTC exists,POST /hr2/api/ltc/10/invalid/,Returns 400 invalid decision,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-039-HA-01,UC,UC-HR2-039,Happy Path,Forward LTC,LTC exists,POST /hr2/api/ltc/10/forward/,LTC status FORWARDED and accountant_status=PENDING,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-039-HA-02,UC,UC-HR2-039,Happy Path,Approve LTC,LTC exists,POST /hr2/api/ltc/10/approve/,LTC status APPROVED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-040-AL-01,UC,UC-HR2-040,Alternate Path,HR staff views pending advances,User is HR staff,GET /hr2/api/cpda-advances/,Returns pending advances,Pass,Expected response,[],[] -UC-HR2-040-EX-01,UC,UC-HR2-040,Exception,Unauthorized user tries to list advances,User is not authenticated,GET /hr2/api/cpda-advances/,Returns 401 Unauthorized,Pass,Expected response,"{'detail': ErrorDetail(string='Authentication credentials were not provided.', code='not_authenticated')}",[] -UC-HR2-040-HA-01,UC,UC-HR2-040,Happy Path,Employee views own advances,Employee is authenticated,GET /hr2/api/cpda-advances/,Returns employee CPDA advances,Pass,Expected response,[],[] -UC-HR2-041-AL-01,UC,UC-HR2-041,Alternate Path,HR staff fetches CPDA advance,HR staff is authenticated,GET /hr2/api/cpda-advances/10/,Returns CPDA advance,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-041-EX-01,UC,UC-HR2-041,Exception,CPDA advance not found,CPDA advance does not exist,GET /hr2/api/cpda-advances/999/,Returns 404 Not Found,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-041-HA-01,UC,UC-HR2-041,Happy Path,Fetch CPDA advance,CPDA advance exists,GET /hr2/api/cpda-advances/10/,Returns CPDA advance,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-042-AL-01,UC,UC-HR2-042,Alternate Path,HR staff downloads CPDA advance,HR staff is authenticated,GET /hr2/api/cpda-advances/10/download/,Returns text file attachment,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-042-EX-01,UC,UC-HR2-042,Exception,Unauthorized user tries to download CPDA,User is not authenticated,GET /hr2/api/cpda-advances/10/download/,Returns 401 Unauthorized,Pass,Expected response,"{'detail': ErrorDetail(string='Authentication credentials were not provided.', code='not_authenticated')}",[] -UC-HR2-042-HA-01,UC,UC-HR2-042,Happy Path,Download CPDA advance,CPDA exists,GET /hr2/api/cpda-advances/10/download/,Returns text file attachment,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-043-AL-01,UC,UC-HR2-043,Alternate Path,Withdraw pending CPDA advance with remarks,CPDA approval_status is PENDING,POST /hr2/api/cpda-advances/10/withdraw/ with remarks=updated,CPDA updated to WITHDRAWN,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-043-EX-01,UC,UC-HR2-043,Exception,Withdraw non-pending CPDA advance,CPDA approval_status is APPROVED,POST /hr2/api/cpda-advances/10/withdraw/,Returns 400 with withdrawal error,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-043-HA-01,UC,UC-HR2-043,Happy Path,Withdraw pending CPDA advance,CPDA approval_status is PENDING,POST /hr2/api/cpda-advances/10/withdraw/,CPDA updated to WITHDRAWN,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-044-AL-01,UC,UC-HR2-044,Alternate Path,Reject CPDA,CPDA exists,POST /hr2/api/cpda-advances/10/reject/,Status REJECTED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-044-EX-01,UC,UC-HR2-044,Exception,Invalid decision,CPDA exists,POST /hr2/api/cpda-advances/10/invalid/,Returns 400 invalid decision,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-044-HA-01,UC,UC-HR2-044,Happy Path,Forward CPDA to accountant,CPDA exists,POST /hr2/api/cpda-advances/10/forward-accountant/,Status FORWARDED and accountant_processing_status=PENDING,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-044-HA-02,UC,UC-HR2-044,Happy Path,Forward CPDA to director,CPDA exists,POST /hr2/api/cpda-advances/10/forward-director/,Status FORWARDED and accountant_processing_status=DIRECTOR_REVIEW,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-044-HA-03,UC,UC-HR2-044,Happy Path,Approve CPDA,CPDA exists,POST /hr2/api/cpda-advances/10/approve/,Status APPROVED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-045-AL-01,UC,UC-HR2-045,Alternate Path,Submit CPDA reimbursement with optional expenses,Required fields provided,POST /hr2/api/cpda-reimbursements/ with optional fields,CPDA reimbursement created,Fail,Unexpected status 400,"{'employee': [ErrorDetail(string='This field is required.', code='required')]}",[] -UC-HR2-045-EX-01,UC,UC-HR2-045,Exception,Missing required fields,Employee is authenticated,POST /hr2/api/cpda-reimbursements/ with incomplete payload,Returns 400 validation errors,Pass,Expected response,"{'employee': [ErrorDetail(string='This field is required.', code='required')]}",[] -UC-HR2-045-HA-01,UC,UC-HR2-045,Happy Path,Submit CPDA reimbursement,Required fields provided,POST /hr2/api/cpda-reimbursements/ with required fields,CPDA reimbursement created,Fail,Unexpected status 400,"{'employee': [ErrorDetail(string='This field is required.', code='required')]}",[] -UC-HR2-046-AL-01,UC,UC-HR2-046,Alternate Path,List CPDA reimbursements when none exist,No requests exist,GET /hr2/api/cpda-reimbursements/,Returns empty list,Pass,Expected response,[],[] -UC-HR2-046-EX-01,UC,UC-HR2-046,Exception,Unauthorized user tries to list reimbursements,User is not authenticated,GET /hr2/api/cpda-reimbursements/,Returns 401 Unauthorized,Pass,Expected response,"{'detail': ErrorDetail(string='Authentication credentials were not provided.', code='not_authenticated')}",[] -UC-HR2-046-HA-01,UC,UC-HR2-046,Happy Path,List CPDA reimbursements,Requests exist,GET /hr2/api/cpda-reimbursements/,Returns CPDA reimbursements,Pass,Expected response,[],[] -UC-HR2-047-AL-01,UC,UC-HR2-047,Alternate Path,HR staff fetches reimbursement,HR staff is authenticated,GET /hr2/api/cpda-reimbursements/10/,Returns CPDA reimbursement,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-047-EX-01,UC,UC-HR2-047,Exception,CPDA reimbursement not found,CPDA reimbursement does not exist,GET /hr2/api/cpda-reimbursements/999/,Returns 404 Not Found,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-047-HA-01,UC,UC-HR2-047,Happy Path,Fetch CPDA reimbursement,CPDA reimbursement exists,GET /hr2/api/cpda-reimbursements/10/,Returns CPDA reimbursement,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-048-AL-01,UC,UC-HR2-048,Alternate Path,Reject CPDA reimbursement with remarks,Request exists,POST /hr2/api/cpda-reimbursements/10/reject/ with remarks=invalid,Reimbursement rejected,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-048-EX-01,UC,UC-HR2-048,Exception,Invalid decision,Request exists,POST /hr2/api/cpda-reimbursements/10/invalid/,Returns 400 invalid decision,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-048-HA-01,UC,UC-HR2-048,Happy Path,Approve CPDA reimbursement,Request exists,POST /hr2/api/cpda-reimbursements/10/approve/,Reimbursement approved,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-048-HA-02,UC,UC-HR2-048,Happy Path,Reject CPDA reimbursement,Request exists,POST /hr2/api/cpda-reimbursements/10/reject/,Reimbursement rejected,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-049-AL-01,UC,UC-HR2-049,Alternate Path,HR staff views all appraisal forms,User is HR staff,GET /hr2/api/appraisal-forms/,Returns appraisal forms,Pass,Expected response,[],[] -UC-HR2-049-EX-01,UC,UC-HR2-049,Exception,Unauthorized user tries to list appraisal forms,User is not authenticated,GET /hr2/api/appraisal-forms/,Returns 401 Unauthorized,Pass,Expected response,"{'detail': ErrorDetail(string='Authentication credentials were not provided.', code='not_authenticated')}",[] -UC-HR2-049-HA-01,UC,UC-HR2-049,Happy Path,Employee views own appraisal forms,Employee is authenticated,GET /hr2/api/appraisal-forms/,Returns employee appraisal forms,Pass,Expected response,[],[] -UC-HR2-049-HA-02,UC,UC-HR2-049,Happy Path,Director views reviewed forms,User is Director,GET /hr2/api/appraisal-forms/,Returns reviewed appraisal forms,Pass,Expected response,[],[] -UC-HR2-050-AL-01,UC,UC-HR2-050,Alternate Path,HR staff fetches appraisal form,HR staff is authenticated,GET /hr2/api/appraisal-forms/10/,Returns appraisal form,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-050-EX-01,UC,UC-HR2-050,Exception,Appraisal form not found,Form does not exist,GET /hr2/api/appraisal-forms/999/,Returns 404 Not Found,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-050-HA-01,UC,UC-HR2-050,Happy Path,Fetch appraisal form,Form exists,GET /hr2/api/appraisal-forms/10/,Returns appraisal form,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-051-AL-01,UC,UC-HR2-051,Alternate Path,HR staff downloads appraisal form,HR staff is authenticated,GET /hr2/api/appraisal-forms/10/download/,Returns text file attachment,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-051-EX-01,UC,UC-HR2-051,Exception,Unauthorized user tries to download appraisal form,User is not authenticated,GET /hr2/api/appraisal-forms/10/download/,Returns 401 Unauthorized,Pass,Expected response,"{'detail': ErrorDetail(string='Authentication credentials were not provided.', code='not_authenticated')}",[] -UC-HR2-051-HA-01,UC,UC-HR2-051,Happy Path,Download appraisal form,Form exists,GET /hr2/api/appraisal-forms/10/download/,Returns text file attachment,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-052-AL-01,UC,UC-HR2-052,Alternate Path,Review appraisal with rating,Reviewer assigned,"POST /hr2/api/appraisal-forms/10/review/ with action=forward, rating=4",Appraisal status set to REVIEWED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-052-EX-01,UC,UC-HR2-052,Exception,Invalid review action,Reviewer assigned,POST /hr2/api/appraisal-forms/10/review/ with action=invalid,Returns 400 invalid action,Pass,Expected response,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-052-HA-01,UC,UC-HR2-052,Happy Path,Forward appraisal for director,Reviewer assigned,POST /hr2/api/appraisal-forms/10/review/ with action=forward,Appraisal status set to REVIEWED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -UC-HR2-052-HA-02,UC,UC-HR2-052,Happy Path,Approve appraisal,Reviewer assigned,POST /hr2/api/appraisal-forms/10/review/ with action=approve,Appraisal status set to APPROVED,Fail,Unexpected status 404,"{'detail': ErrorDetail(string='Not found.', code='not_found')}",[] -WF-HR2-001-E2E-01,WF,WF-HR2-001,End-to-End,Employee applies and leave is approved,,,"approval_status=APPROVED, leave balance reduced",Fail,Workflow incomplete,,"[{'step': 1, 'action': 'Employee applies', 'expected': 'Leave created', 'actual': '1', 'passed': True}, {'step': 2, 'action': 'Director approves', 'expected': 'Status approved', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}]" -WF-HR2-001-E2E-02,WF,WF-HR2-001,End-to-End,Employee applies and leave is forwarded then approved,,,"approval_status=APPROVED, current_approver_role=Director",Fail,Workflow incomplete,,"[{'step': 1, 'action': 'Employee applies', 'expected': 'Leave created', 'actual': '1', 'passed': True}, {'step': 2, 'action': 'HOD forwards', 'expected': 'Status forwarded', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}, {'step': 3, 'action': 'Director approves', 'expected': 'Status approved', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}]" -WF-HR2-001-NEG-01,WF,WF-HR2-001,Negative,Employee applies and leave is rejected,,,"approval_status=REJECTED, leave balance unchanged",Fail,Workflow incomplete,,"[{'step': 1, 'action': 'Employee applies', 'expected': 'Leave created', 'actual': '1', 'passed': True}, {'step': 2, 'action': 'Director rejects', 'expected': 'Status rejected', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}]" -WF-HR2-002-E2E-01,WF,WF-HR2-002,End-to-End,Employee withdraws pending leave,,,approval_status=WITHDRAWN,Fail,Workflow incomplete,,"[{'step': 1, 'action': 'Withdraw pending leave', 'expected': 'Withdrawn', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}]" -WF-HR2-002-NEG-01,WF,WF-HR2-002,Negative,Employee tries to withdraw approved leave,,,Request rejected with withdrawal error,Fail,Workflow incomplete,,"[{'step': 1, 'action': 'Withdraw approved leave', 'expected': 'Rejected', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}]" -WF-HR2-003-E2E-01,WF,WF-HR2-003,End-to-End,Cancellation approved,,,"cancel_status=APPROVED, approval_status=CANCELLED, leave balance restored",Fail,Workflow incomplete,,"[{'step': 1, 'action': 'Request cancellation', 'expected': 'Requested', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}, {'step': 2, 'action': 'Approve cancellation', 'expected': 'Cancelled', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}]" -WF-HR2-003-NEG-01,WF,WF-HR2-003,Negative,Cancellation requested after start date,,,Request rejected with cancellation window error,Fail,Workflow incomplete,,"[{'step': 1, 'action': 'Cancel request late', 'expected': 'Rejected', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}]" -WF-HR2-004-E2E-01,WF,WF-HR2-004,End-to-End,Extension approved with sufficient balance,,,"extension_status=APPROVED, end_date updated, balance reduced",Fail,Workflow incomplete,,"[{'step': 1, 'action': 'Request extension', 'expected': 'Requested', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}, {'step': 2, 'action': 'Approve extension', 'expected': 'Approved or rejected', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}]" -WF-HR2-004-NEG-01,WF,WF-HR2-004,Negative,Extension approved with insufficient balance,,,Request rejected with insufficient balance error,Fail,Workflow incomplete,,"[{'step': 1, 'action': 'Request extension', 'expected': 'Requested', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}, {'step': 2, 'action': 'Approve extension', 'expected': 'Approved or rejected', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}]" -WF-HR2-005-E2E-01,WF,WF-HR2-005,End-to-End,Nominee accepts,,,nominee_status=ACCEPTED,Fail,Workflow incomplete,,"[{'step': 1, 'action': 'Nominee accepts', 'expected': 'Accepted', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}]" -WF-HR2-005-NEG-01,WF,WF-HR2-005,Negative,Non-nominee responds,,,Request rejected with 403,Fail,Workflow incomplete,,"[{'step': 1, 'action': 'Non-nominee responds', 'expected': 'Forbidden', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}]" -WF-HR2-006-E2E-01,WF,WF-HR2-006,End-to-End,"HOD requests, employee submits",,,document_request_status=SUBMITTED,Fail,Workflow incomplete,,"[{'step': 1, 'action': 'HOD requests document', 'expected': 'Requested', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}, {'step': 2, 'action': 'Employee submits', 'expected': 'Submitted', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}]" -WF-HR2-006-NEG-01,WF,WF-HR2-006,Negative,Employee submits without request,,,Request rejected with no request error,Fail,Workflow incomplete,,"[{'step': 1, 'action': 'Submit without request', 'expected': 'Rejected', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}]" -WF-HR2-007-E2E-01,WF,WF-HR2-007,End-to-End,LTC forwarded and approved,,,"approval_status=APPROVED, accountant_status=APPROVED",Pass,Workflow completed,,"[{'step': 1, 'action': 'Forward LTC', 'expected': 'Forwarded', 'actual': ""{'id': 6, 'employee_name': 'Rahul Sharma', 'department': 'Computer Science and Engineering', 'designation': 'Assistant Professor', 'ltc_block_year': 2025, 'travel_start_date': '2026-04-25', 'travel_end_date': '2026-04-30', 'destination': 'Delhi', 'purpose_of_travel': 'Family travel', 'family_members': '', 'relationship_details': '', 'travel_mode': 'Train', 'ticket_number': '', 'ticket_cost': None, 'accommodation_cost': None, 'other_expenses': None, 'total_amount_claimed': '22000.00', 'tickets_upload': '', 'bills_upload': '', 'previous_ltc_used': False, 'last_ltc_date': None, 'applied_date': '2026-04-15', 'verified_by_hr': True, 'approval_status': 'FORWARDED', 'accountant_status': 'PENDING', 'remarks': 'Forward', 'employee': '1001'}"", 'passed': True}, {'step': 2, 'action': 'Approve LTC', 'expected': 'Approved', 'actual': ""{'id': 6, 'employee_name': 'Rahul Sharma', 'department': 'Computer Science and Engineering', 'designation': 'Assistant Professor', 'ltc_block_year': 2025, 'travel_start_date': '2026-04-25', 'travel_end_date': '2026-04-30', 'destination': 'Delhi', 'purpose_of_travel': 'Family travel', 'family_members': '', 'relationship_details': '', 'travel_mode': 'Train', 'ticket_number': '', 'ticket_cost': None, 'accommodation_cost': None, 'other_expenses': None, 'total_amount_claimed': '22000.00', 'tickets_upload': '', 'bills_upload': '', 'previous_ltc_used': False, 'last_ltc_date': None, 'applied_date': '2026-04-15', 'verified_by_hr': True, 'approval_status': 'APPROVED', 'accountant_status': 'APPROVED', 'remarks': 'Approved', 'employee': '1001'}"", 'passed': True}]" -WF-HR2-007-NEG-01,WF,WF-HR2-007,Negative,LTC rejected,,,approval_status=REJECTED,Fail,Workflow incomplete,,"[{'step': 1, 'action': 'Reject LTC', 'expected': 'Rejected', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}]" -WF-HR2-008-E2E-01,WF,WF-HR2-008,End-to-End,HR forwards to accountant and approves,,,"approval_status=APPROVED, accountant_processing_status=APPROVED",Pass,Workflow completed,,"[{'step': 1, 'action': 'Forward to accountant', 'expected': 'Forwarded', 'actual': ""{'id': 6, 'employee_name': 'Rahul Sharma', 'department': 'Computer Science and Engineering', 'designation': 'Assistant Professor', 'event_name': 'National Conference on AI', 'event_type': 'Conference', 'organized_by': '', 'venue': '', 'start_date': '2026-05-15', 'end_date': '2026-05-17', 'registration_fee': None, 'travel_expense': None, 'accommodation_expense': None, 'other_expenses': None, 'total_amount': '20000.00', 'purpose_of_attending': 'Present paper', 'benefits_to_institution': 'Research exposure', 'invitation_letter': '', 'receipts': '', 'certificates': '', 'applied_date': '2026-04-15', 'verified_by_hr': True, 'approval_status': 'FORWARDED', 'accountant_processing_status': 'PENDING', 'remarks': 'Forward', 'employee': '1001'}"", 'passed': True}, {'step': 2, 'action': 'Accountant approves', 'expected': 'Approved', 'actual': ""{'id': 6, 'employee_name': 'Rahul Sharma', 'department': 'Computer Science and Engineering', 'designation': 'Assistant Professor', 'event_name': 'National Conference on AI', 'event_type': 'Conference', 'organized_by': '', 'venue': '', 'start_date': '2026-05-15', 'end_date': '2026-05-17', 'registration_fee': None, 'travel_expense': None, 'accommodation_expense': None, 'other_expenses': None, 'total_amount': '20000.00', 'purpose_of_attending': 'Present paper', 'benefits_to_institution': 'Research exposure', 'invitation_letter': '', 'receipts': '', 'certificates': '', 'applied_date': '2026-04-15', 'verified_by_hr': True, 'approval_status': 'APPROVED', 'accountant_processing_status': 'APPROVED', 'remarks': 'Approved', 'employee': '1001'}"", 'passed': True}]" -WF-HR2-008-E2E-02,WF,WF-HR2-008,End-to-End,Forwarded to director then approved,,,"accountant_processing_status=DIRECTOR_APPROVED, approval_status=FORWARDED",Fail,Workflow incomplete,,"[{'step': 1, 'action': 'Forward to director', 'expected': 'Forwarded', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}, {'step': 2, 'action': 'Director approves', 'expected': 'Approved', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}]" -WF-HR2-008-NEG-01,WF,WF-HR2-008,Negative,CPDA advance rejected,,,approval_status=REJECTED,Fail,Workflow incomplete,,"[{'step': 1, 'action': 'Reject CPDA', 'expected': 'Rejected', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}]" -WF-HR2-009-E2E-01,WF,WF-HR2-009,End-to-End,CPDA reimbursement approved,,,approval_status=APPROVED,Fail,Workflow incomplete,,"[{'step': 1, 'action': 'Approve reimbursement', 'expected': 'Approved', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}]" -WF-HR2-009-NEG-01,WF,WF-HR2-009,Negative,CPDA reimbursement rejected,,,approval_status=REJECTED,Fail,Workflow incomplete,,"[{'step': 1, 'action': 'Reject reimbursement', 'expected': 'Rejected', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}]" -WF-HR2-010-E2E-01,WF,WF-HR2-010,End-to-End,HOD forwards to director,,,status=REVIEWED,Pass,Workflow completed,,"[{'step': 1, 'action': 'Director approves', 'expected': 'Approved', 'actual': ""{'id': 6, 'employee_name': 'Rahul Sharma', 'department': 'Computer Science and Engineering', 'designation': 'Assistant Professor', 'appraisal_year': '2025-2026', 'self_summary': 'Completed teaching responsibilities', 'key_responsibilities': 'Teaching and research', 'achievements': 'Published 1 paper', 'challenges_faced': '', 'teaching_performance': '', 'research_work': '', 'publications': '', 'projects_handled': '', 'administrative_contributions': '', 'trainings_attended': '', 'certifications': '', 'workshops': '', 'goals_achieved': 'Completed syllabus', 'future_goals': 'Publish more papers', 'supporting_documents': '', 'reviewer_id': '1003', 'reviewer_comments': '', 'rating': '', 'status': 'APPROVED', 'remarks': '', 'submitted_at': '2026-04-15T19:42:43.668756', 'employee': '1001'}"", 'passed': True}]" -WF-HR2-010-E2E-02,WF,WF-HR2-010,End-to-End,Director approves,,,status=APPROVED,Fail,Workflow incomplete,,"[{'step': 1, 'action': 'Director approves', 'expected': 'Approved', 'actual': ""{'detail': ErrorDetail(string='Not found.', code='not_found')}"", 'passed': False}]" diff --git a/FusionIIIT/applications/hr2/tests/reports/UC_Test_Design.csv b/FusionIIIT/applications/hr2/tests/reports/UC_Test_Design.csv deleted file mode 100644 index 56ad4cf69..000000000 --- a/FusionIIIT/applications/hr2/tests/reports/UC_Test_Design.csv +++ /dev/null @@ -1,170 +0,0 @@ -uc_id,uc_title,test_type,scenario,preconditions,input_action,expected_result -UC-HR2-001,List employees,Happy Path,HR staff lists all employees,User has HR designation or is HR staff,GET /hr2/api/employees/,Returns list of employees -UC-HR2-001,List employees,Happy Path,Filter employees by type and department,User is authenticated,GET /hr2/api/employees/?type=Faculty&department=1,Returns employees matching filters -UC-HR2-001,List employees,Alternate Path,Request with only department filter,User is authenticated,GET /hr2/api/employees/?department=1,Returns employees in department -UC-HR2-001,List employees,Exception,Unauthorized user tries to list employees,User is not authenticated,GET /hr2/api/employees/,Returns 401 Unauthorized -UC-HR2-002,View employee details,Happy Path,Fetch employee details by ID,Employee exists,GET /hr2/api/employees/123/,Returns employee details -UC-HR2-002,View employee details,Alternate Path,HOD fetches employee details,HOD is authenticated,GET /hr2/api/employees/123/,Returns employee details -UC-HR2-002,View employee details,Exception,Employee not found,Employee does not exist,GET /hr2/api/employees/999999/,Returns 404 Not Found -UC-HR2-003,Update employee details,Happy Path,Update phone number,Employee exists,PUT /hr2/api/employees/123/ with phone_number=9876543210,Returns updated employee details -UC-HR2-003,Update employee details,Alternate Path,Update employee address,Employee exists,PUT /hr2/api/employees/123/ with address=Updated,Returns updated employee details -UC-HR2-003,Update employee details,Exception,Invalid data,Employee exists,PUT /hr2/api/employees/123/ with phone_number=invalid,Returns 400 validation error -UC-HR2-004,Apply for leave,Happy Path,Apply for casual leave with valid dates,Leave balance is sufficient,"POST /hr2/api/leave-applications/ with leave_type=CL, start_date=future, end_date=future+2, total_days=3",Leave application created with approval_status=PENDING or FORWARDED -UC-HR2-004,Apply for leave,Alternate Path,Nominate substitute during leave,Nominee employee exists,POST /hr2/api/leave-applications/ with nominee_employee_id=456,Leave created with nominee_status=PENDING -UC-HR2-004,Apply for leave,Exception,Start date in the past,Employee is authenticated,POST /hr2/api/leave-applications/ with start_date=yesterday,Returns 400 start_date validation error -UC-HR2-005,View leave applications,Happy Path,Employee views own leave applications,Employee is authenticated,GET /hr2/api/leave-applications/,Returns only employee's leave applications -UC-HR2-005,View leave applications,Happy Path,Director views forwarded applications,User has Director designation,GET /hr2/api/leave-applications/,Returns forwarded and requested decisions -UC-HR2-005,View leave applications,Alternate Path,HOD views departmental leave applications,User has HOD designation,GET /hr2/api/leave-applications/,Returns department leave applications -UC-HR2-005,View leave applications,Exception,Unauthorized user tries to list leave applications,User is not authenticated,GET /hr2/api/leave-applications/,Returns 401 Unauthorized -UC-HR2-006,Withdraw leave application,Happy Path,Employee withdraws own pending leave,Leave approval_status is PENDING,POST /hr2/api/leave-applications/10/withdraw/,Leave updated to approval_status=WITHDRAWN -UC-HR2-006,Withdraw leave application,Alternate Path,Registrar withdraws forwarded leave,Leave approval_status is FORWARDED,POST /hr2/api/leave-applications/10/withdraw/,Leave updated to approval_status=REJECTED -UC-HR2-006,Withdraw leave application,Exception,Withdraw non-pending leave,Leave approval_status is APPROVED,POST /hr2/api/leave-applications/10/withdraw/,Returns 400 with withdrawal error -UC-HR2-007,Request leave cancellation,Happy Path,Submit cancellation request before start date,Today is before leave start date,POST /hr2/api/leave-applications/10/cancel-request/ with reason=change of plan,Cancel status set to REQUESTED -UC-HR2-007,Request leave cancellation,Alternate Path,Submit cancellation request with reason,Approved leave exists,POST /hr2/api/leave-applications/10/cancel-request/ with reason=medical,Cancel status set to REQUESTED -UC-HR2-007,Request leave cancellation,Exception,Cancellation after start date,Today is on or after start date,POST /hr2/api/leave-applications/10/cancel-request/,Returns 400 cancellation window error -UC-HR2-008,Request leave extension,Happy Path,Request extension with new end date,Extension dates are valid,POST /hr2/api/leave-applications/10/extension-request/ with new_end_date=future+2,Extension status set to REQUESTED -UC-HR2-008,Request leave extension,Alternate Path,Request extension with reason,Extension dates are valid,POST /hr2/api/leave-applications/10/extension-request/ with reason=medical,Extension status set to REQUESTED -UC-HR2-008,Request leave extension,Exception,Extension with invalid date,New end date before current end date,POST /hr2/api/leave-applications/10/extension-request/ with new_end_date=earlier,Returns 400 validation error -UC-HR2-009,View leave balance,Happy Path,Employee views own leave balance,Employee is authenticated,GET /hr2/api/leave-balance/,Returns leave balance for the employee -UC-HR2-009,View leave balance,Happy Path,HR views leave balance for another employee,User has HR designation,GET /hr2/api/leave-balance/123/,Returns leave balance for employee 123 -UC-HR2-009,View leave balance,Alternate Path,Employee views balance with no records,Employee has no balance entries,GET /hr2/api/leave-balance/,Returns empty balance list -UC-HR2-009,View leave balance,Exception,Unauthorized user tries to view leave balance,User is not authenticated,GET /hr2/api/leave-balance/,Returns 401 Unauthorized -UC-HR2-010,Download leave application,Happy Path,Download approved leave application,Leave exists and belongs to employee,GET /hr2/api/leave-applications/10/download/,Returns text file attachment with leave details -UC-HR2-010,Download leave application,Alternate Path,Download pending leave application,Leave is pending and belongs to employee,GET /hr2/api/leave-applications/10/download/,Returns text file attachment with leave details -UC-HR2-010,Download leave application,Exception,Access another employee's leave,Leave does not belong to requester,GET /hr2/api/leave-applications/999/download/,Returns 403 Not authorized -UC-HR2-011,Submit LTC application,Happy Path,Submit LTC claim,Required fields provided,POST /hr2/api/ltc/ with required LTC fields,LTC application created with approval_status=PENDING -UC-HR2-011,Submit LTC application,Alternate Path,Submit LTC claim with optional fields,Required fields provided,POST /hr2/api/ltc/ with optional fields,LTC application created with approval_status=PENDING -UC-HR2-011,Submit LTC application,Exception,Missing required fields,Employee is authenticated,POST /hr2/api/ltc/ with incomplete payload,Returns 400 validation errors -UC-HR2-012,Submit CPDA advance,Happy Path,Submit CPDA advance,Required fields provided,POST /hr2/api/cpda-advances/ with required fields,CPDA advance created with approval_status=PENDING -UC-HR2-012,Submit CPDA advance,Alternate Path,Submit CPDA advance with optional expenses,Required fields provided,POST /hr2/api/cpda-advances/ with optional fields,CPDA advance created with approval_status=PENDING -UC-HR2-012,Submit CPDA advance,Exception,Invalid amount,Employee is authenticated,POST /hr2/api/cpda-advances/ with amountRequired=invalid,Returns 400 validation errors -UC-HR2-013,Submit appraisal form,Happy Path,Submit appraisal form,Required fields provided,POST /hr2/api/appraisal-forms/ with required fields,Appraisal form created with status=SUBMITTED -UC-HR2-013,Submit appraisal form,Alternate Path,Submit appraisal form with optional fields,Required fields provided,POST /hr2/api/appraisal-forms/ with optional fields,Appraisal form created with status=SUBMITTED -UC-HR2-013,Submit appraisal form,Exception,Missing required fields,Employee is authenticated,POST /hr2/api/appraisal-forms/ with incomplete payload,Returns 400 validation errors -UC-HR2-014,View leave application details,Happy Path,Fetch leave application by ID,Leave application exists,GET /hr2/api/leave-applications/10/,Returns leave application -UC-HR2-014,View leave application details,Alternate Path,HR staff fetches leave application by ID,HR staff is authenticated,GET /hr2/api/leave-applications/10/,Returns leave application -UC-HR2-014,View leave application details,Exception,Leave application not found,Leave application does not exist,GET /hr2/api/leave-applications/999/,Returns 404 Not Found -UC-HR2-015,Update leave application,Happy Path,Update leave reason,Leave belongs to employee,PUT /hr2/api/leave-applications/10/ with reason=updated,Leave application updated -UC-HR2-015,Update leave application,Alternate Path,Update leave handover notes,Leave belongs to employee,PUT /hr2/api/leave-applications/10/ with handover_notes=updated,Leave application updated -UC-HR2-015,Update leave application,Exception,Update another employee's leave,Leave belongs to another employee,PUT /hr2/api/leave-applications/10/,Returns 403 Not authorized -UC-HR2-016,Delete leave application,Happy Path,Delete pending leave,Leave approval_status is PENDING,DELETE /hr2/api/leave-applications/10/,Leave application deleted -UC-HR2-016,Delete leave application,Alternate Path,Delete pending leave without attachments,Leave approval_status is PENDING,DELETE /hr2/api/leave-applications/10/,Leave application deleted -UC-HR2-016,Delete leave application,Exception,Delete non-pending leave,Leave approval_status is APPROVED,DELETE /hr2/api/leave-applications/10/,Returns 400 with delete error -UC-HR2-017,Approve or reject leave,Happy Path,Approve leave,Leave exists,POST /hr2/api/leave-applications/10/approve/,Leave status updated to APPROVED -UC-HR2-017,Approve or reject leave,Happy Path,Forward leave,Leave exists,POST /hr2/api/leave-applications/10/forward/,Leave status updated to FORWARDED -UC-HR2-017,Approve or reject leave,Alternate Path,Reject leave,Leave exists,POST /hr2/api/leave-applications/10/reject/,Leave status updated to REJECTED -UC-HR2-017,Approve or reject leave,Exception,Invalid decision,Leave exists,POST /hr2/api/leave-applications/10/invalid/,Returns 400 invalid decision -UC-HR2-018,Nominee dashboard,Happy Path,Nominee views pending requests,Nominee has pending requests,GET /hr2/api/leave-nominee/,Returns pending nominee requests -UC-HR2-018,Nominee dashboard,Alternate Path,Nominee has no pending requests,Nominee has no pending requests,GET /hr2/api/leave-nominee/,Returns empty list -UC-HR2-018,Nominee dashboard,Exception,Unauthorized user tries to view nominee dashboard,User is not authenticated,GET /hr2/api/leave-nominee/,Returns 401 Unauthorized -UC-HR2-019,Nominee decision,Happy Path,Nominee accepts,Nominee is assigned on leave,POST /hr2/api/leave-nominee/10/ with action=accept,Nominee status updated to ACCEPTED -UC-HR2-019,Nominee decision,Alternate Path,Nominee declines,Nominee is assigned on leave,POST /hr2/api/leave-nominee/10/ with action=decline,Nominee status updated to DECLINED -UC-HR2-019,Nominee decision,Exception,Invalid action,Nominee is assigned,POST /hr2/api/leave-nominee/10/ with action=invalid,Returns 400 invalid action -UC-HR2-020,Request leave documents,Happy Path,Request documents with message,Leave exists,POST /hr2/api/leave-applications/10/request-document/ with message=submit proof,Document request status set to REQUESTED -UC-HR2-020,Request leave documents,Alternate Path,Request documents with updated message,Leave exists,POST /hr2/api/leave-applications/10/request-document/ with message=submit updated proof,Document request status set to REQUESTED -UC-HR2-020,Request leave documents,Exception,Missing message,Leave exists,POST /hr2/api/leave-applications/10/request-document/,Returns 400 message required -UC-HR2-021,Submit leave documents,Happy Path,Submit document after request,Document request exists,POST /hr2/api/leave-applications/10/submit-document/ with submission=doc ref,Document request status set to SUBMITTED -UC-HR2-021,Submit leave documents,Alternate Path,Submit updated document,Document request exists,POST /hr2/api/leave-applications/10/submit-document/ with submission=updated ref,Document request status set to SUBMITTED -UC-HR2-021,Submit leave documents,Exception,Submit without request,No document request exists,POST /hr2/api/leave-applications/10/submit-document/ with submission=doc ref,Returns 400 no request -UC-HR2-022,Cancellation decision,Happy Path,Approve cancellation,Approver role matches,POST /hr2/api/leave-applications/10/cancel-decision/approve/,Cancellation approved and leave cancelled -UC-HR2-022,Cancellation decision,Happy Path,Reject cancellation,Approver role matches,POST /hr2/api/leave-applications/10/cancel-decision/reject/,Cancellation rejected -UC-HR2-022,Cancellation decision,Alternate Path,Approve cancellation with remarks,Approver role matches,POST /hr2/api/leave-applications/10/cancel-decision/approve/ with remarks=ok,Cancellation approved and leave cancelled -UC-HR2-022,Cancellation decision,Exception,Invalid decision,Cancellation request exists,POST /hr2/api/leave-applications/10/cancel-decision/invalid/,Returns 400 invalid decision -UC-HR2-023,Extension decision,Happy Path,Approve extension,Approver role matches,POST /hr2/api/leave-applications/10/extension-decision/approve/,Extension approved and leave updated -UC-HR2-023,Extension decision,Happy Path,Reject extension,Approver role matches,POST /hr2/api/leave-applications/10/extension-decision/reject/,Extension rejected -UC-HR2-023,Extension decision,Alternate Path,Approve extension with remarks,Approver role matches,POST /hr2/api/leave-applications/10/extension-decision/approve/ with remarks=ok,Extension approved and leave updated -UC-HR2-023,Extension decision,Exception,Invalid decision,Extension request exists,POST /hr2/api/leave-applications/10/extension-decision/invalid/,Returns 400 invalid decision -UC-HR2-024,Record attendance,Happy Path,Mark attendance,Valid status and date,"POST /hr2/api/attendance/ with date=today, status=PRESENT",Attendance record created -UC-HR2-024,Record attendance,Alternate Path,Mark half-day attendance,Valid status and date,"POST /hr2/api/attendance/ with date=today, status=HALF_DAY",Attendance record created -UC-HR2-024,Record attendance,Exception,Missing attendance status,Employee is authenticated,POST /hr2/api/attendance/ with date=today,Returns 400 validation errors -UC-HR2-025,View attendance,Happy Path,View attendance for date range,Attendance exists,GET /hr2/api/attendance/?from_date=2026-05-01&to_date=2026-05-10,Returns attendance records -UC-HR2-025,View attendance,Alternate Path,View attendance without filters,Attendance exists,GET /hr2/api/attendance/,Returns attendance records -UC-HR2-025,View attendance,Exception,Unauthorized user tries to view attendance,User is not authenticated,GET /hr2/api/attendance/,Returns 401 Unauthorized -UC-HR2-026,List appraisal periods,Happy Path,List active periods,Periods exist,GET /hr2/api/appraisal-periods/?is_active=true,Returns appraisal periods -UC-HR2-026,List appraisal periods,Alternate Path,List all periods without filter,Periods exist,GET /hr2/api/appraisal-periods/,Returns appraisal periods -UC-HR2-026,List appraisal periods,Exception,Unauthorized user tries to view appraisal periods,User is not authenticated,GET /hr2/api/appraisal-periods/,Returns 401 Unauthorized -UC-HR2-027,Submit appraisal (performance),Happy Path,Submit appraisal,Required fields provided,POST /hr2/api/appraisals/ with period and scores,Performance appraisal created -UC-HR2-027,Submit appraisal (performance),Alternate Path,Submit appraisal with remarks,Required fields provided,"POST /hr2/api/appraisals/ with period, scores, remarks",Performance appraisal created -UC-HR2-027,Submit appraisal (performance),Exception,Missing required fields,Employee is authenticated,POST /hr2/api/appraisals/ with incomplete payload,Returns 400 validation errors -UC-HR2-028,List appraisals,Happy Path,List appraisals for period,Appraisals exist,GET /hr2/api/appraisals/?period=1,Returns appraisals -UC-HR2-028,List appraisals,Alternate Path,List all appraisals,Appraisals exist,GET /hr2/api/appraisals/,Returns appraisals -UC-HR2-028,List appraisals,Exception,Unauthorized user tries to list appraisals,User is not authenticated,GET /hr2/api/appraisals/,Returns 401 Unauthorized -UC-HR2-029,List training programs,Happy Path,List available programs,Programs exist,GET /hr2/api/training-programs/,Returns training programs -UC-HR2-029,List training programs,Alternate Path,List programs when none are available,No programs available,GET /hr2/api/training-programs/,Returns empty list -UC-HR2-029,List training programs,Exception,Unauthorized user tries to list programs,User is not authenticated,GET /hr2/api/training-programs/,Returns 401 Unauthorized -UC-HR2-030,Nominate for training,Happy Path,Submit training nomination,Program exists,POST /hr2/api/training-nominations/ with program data,Nomination created -UC-HR2-030,Nominate for training,Alternate Path,Nominate for mandatory program,Program exists and is mandatory,POST /hr2/api/training-nominations/ with program data,Nomination created -UC-HR2-030,Nominate for training,Exception,Invalid program,Employee is authenticated,POST /hr2/api/training-nominations/ with invalid program,Returns 400 validation errors -UC-HR2-031,View training nominations,Happy Path,List nominations,Nominations exist,GET /hr2/api/training-nominations/,Returns training nominations -UC-HR2-031,View training nominations,Alternate Path,List nominations after submission,Nomination exists,GET /hr2/api/training-nominations/,Returns training nominations -UC-HR2-031,View training nominations,Exception,Unauthorized user tries to list nominations,User is not authenticated,GET /hr2/api/training-nominations/,Returns 401 Unauthorized -UC-HR2-032,Submit promotion application,Happy Path,Submit promotion,Required fields provided,POST /hr2/api/promotions/ with required fields,Promotion application created -UC-HR2-032,Submit promotion application,Alternate Path,Submit promotion with API score,Required fields provided,POST /hr2/api/promotions/ with api_score,Promotion application created -UC-HR2-032,Submit promotion application,Exception,Missing required fields,Employee is authenticated,POST /hr2/api/promotions/ with incomplete payload,Returns 400 validation errors -UC-HR2-033,View promotion applications,Happy Path,List promotions,Applications exist,GET /hr2/api/promotions/,Returns promotion applications -UC-HR2-033,View promotion applications,Alternate Path,List promotions when none exist,No promotions exist,GET /hr2/api/promotions/,Returns empty list -UC-HR2-033,View promotion applications,Exception,Unauthorized user tries to list promotions,User is not authenticated,GET /hr2/api/promotions/,Returns 401 Unauthorized -UC-HR2-034,View faculty workload,Happy Path,Get workload by semester,Workload exists,GET /hr2/api/workload/?semester=Spring&year=2026,Returns workload records -UC-HR2-034,View faculty workload,Alternate Path,Get workload without semester filter,Workload exists,GET /hr2/api/workload/?year=2026,Returns workload records -UC-HR2-034,View faculty workload,Exception,Unauthorized user tries to view workload,User is not authenticated,GET /hr2/api/workload/,Returns 401 Unauthorized -UC-HR2-035,Submit LTC update,Happy Path,Update LTC details,LTC belongs to employee,PUT /hr2/api/ltc/10/ with destination=updated,LTC updated -UC-HR2-035,Submit LTC update,Alternate Path,Update LTC purpose,LTC belongs to employee,PUT /hr2/api/ltc/10/ with purpose_of_travel=updated,LTC updated -UC-HR2-035,Submit LTC update,Exception,Update another employee's LTC,LTC belongs to another employee,PUT /hr2/api/ltc/10/,Returns 403 Not authorized -UC-HR2-036,View LTC applications,Happy Path,Employee views own LTC,Employee is authenticated,GET /hr2/api/ltc/,Returns employee LTC applications -UC-HR2-036,View LTC applications,Happy Path,HR staff views pending LTC,User is HR staff,GET /hr2/api/ltc/,Returns pending/forwarded LTC -UC-HR2-036,View LTC applications,Alternate Path,Accountant views forwarded LTC,User is Accountant,GET /hr2/api/ltc/,Returns forwarded LTC -UC-HR2-036,View LTC applications,Exception,Unauthorized user tries to list LTC,User is not authenticated,GET /hr2/api/ltc/,Returns 401 Unauthorized -UC-HR2-037,Download LTC application,Happy Path,Download LTC,LTC exists,GET /hr2/api/ltc/10/download/,Returns text file attachment -UC-HR2-037,Download LTC application,Alternate Path,HR staff downloads LTC,HR staff is authenticated,GET /hr2/api/ltc/10/download/,Returns text file attachment -UC-HR2-037,Download LTC application,Exception,Unauthorized user tries to download LTC,User is not authenticated,GET /hr2/api/ltc/10/download/,Returns 401 Unauthorized -UC-HR2-038,Withdraw LTC application,Happy Path,Withdraw pending LTC,LTC approval_status is PENDING,POST /hr2/api/ltc/10/withdraw/,LTC updated to WITHDRAWN -UC-HR2-038,Withdraw LTC application,Alternate Path,Withdraw pending LTC with remarks,LTC approval_status is PENDING,POST /hr2/api/ltc/10/withdraw/ with remarks=updated,LTC updated to WITHDRAWN -UC-HR2-038,Withdraw LTC application,Exception,Withdraw non-pending LTC,LTC approval_status is APPROVED,POST /hr2/api/ltc/10/withdraw/,Returns 400 with withdrawal error -UC-HR2-039,Approve or reject LTC,Happy Path,Forward LTC,LTC exists,POST /hr2/api/ltc/10/forward/,LTC status FORWARDED and accountant_status=PENDING -UC-HR2-039,Approve or reject LTC,Happy Path,Approve LTC,LTC exists,POST /hr2/api/ltc/10/approve/,LTC status APPROVED -UC-HR2-039,Approve or reject LTC,Alternate Path,Reject LTC,LTC exists,POST /hr2/api/ltc/10/reject/,LTC status REJECTED -UC-HR2-039,Approve or reject LTC,Exception,Invalid decision,LTC exists,POST /hr2/api/ltc/10/invalid/,Returns 400 invalid decision -UC-HR2-040,View CPDA advances,Happy Path,Employee views own advances,Employee is authenticated,GET /hr2/api/cpda-advances/,Returns employee CPDA advances -UC-HR2-040,View CPDA advances,Alternate Path,HR staff views pending advances,User is HR staff,GET /hr2/api/cpda-advances/,Returns pending advances -UC-HR2-040,View CPDA advances,Exception,Unauthorized user tries to list advances,User is not authenticated,GET /hr2/api/cpda-advances/,Returns 401 Unauthorized -UC-HR2-041,View CPDA advance details,Happy Path,Fetch CPDA advance,CPDA advance exists,GET /hr2/api/cpda-advances/10/,Returns CPDA advance -UC-HR2-041,View CPDA advance details,Alternate Path,HR staff fetches CPDA advance,HR staff is authenticated,GET /hr2/api/cpda-advances/10/,Returns CPDA advance -UC-HR2-041,View CPDA advance details,Exception,CPDA advance not found,CPDA advance does not exist,GET /hr2/api/cpda-advances/999/,Returns 404 Not Found -UC-HR2-042,Download CPDA advance,Happy Path,Download CPDA advance,CPDA exists,GET /hr2/api/cpda-advances/10/download/,Returns text file attachment -UC-HR2-042,Download CPDA advance,Alternate Path,HR staff downloads CPDA advance,HR staff is authenticated,GET /hr2/api/cpda-advances/10/download/,Returns text file attachment -UC-HR2-042,Download CPDA advance,Exception,Unauthorized user tries to download CPDA,User is not authenticated,GET /hr2/api/cpda-advances/10/download/,Returns 401 Unauthorized -UC-HR2-043,Withdraw CPDA advance,Happy Path,Withdraw pending CPDA advance,CPDA approval_status is PENDING,POST /hr2/api/cpda-advances/10/withdraw/,CPDA updated to WITHDRAWN -UC-HR2-043,Withdraw CPDA advance,Alternate Path,Withdraw pending CPDA advance with remarks,CPDA approval_status is PENDING,POST /hr2/api/cpda-advances/10/withdraw/ with remarks=updated,CPDA updated to WITHDRAWN -UC-HR2-043,Withdraw CPDA advance,Exception,Withdraw non-pending CPDA advance,CPDA approval_status is APPROVED,POST /hr2/api/cpda-advances/10/withdraw/,Returns 400 with withdrawal error -UC-HR2-044,Decide CPDA advance,Happy Path,Forward CPDA to accountant,CPDA exists,POST /hr2/api/cpda-advances/10/forward-accountant/,Status FORWARDED and accountant_processing_status=PENDING -UC-HR2-044,Decide CPDA advance,Happy Path,Forward CPDA to director,CPDA exists,POST /hr2/api/cpda-advances/10/forward-director/,Status FORWARDED and accountant_processing_status=DIRECTOR_REVIEW -UC-HR2-044,Decide CPDA advance,Happy Path,Approve CPDA,CPDA exists,POST /hr2/api/cpda-advances/10/approve/,Status APPROVED -UC-HR2-044,Decide CPDA advance,Alternate Path,Reject CPDA,CPDA exists,POST /hr2/api/cpda-advances/10/reject/,Status REJECTED -UC-HR2-044,Decide CPDA advance,Exception,Invalid decision,CPDA exists,POST /hr2/api/cpda-advances/10/invalid/,Returns 400 invalid decision -UC-HR2-045,Submit CPDA reimbursement,Happy Path,Submit CPDA reimbursement,Required fields provided,POST /hr2/api/cpda-reimbursements/ with required fields,CPDA reimbursement created -UC-HR2-045,Submit CPDA reimbursement,Alternate Path,Submit CPDA reimbursement with optional expenses,Required fields provided,POST /hr2/api/cpda-reimbursements/ with optional fields,CPDA reimbursement created -UC-HR2-045,Submit CPDA reimbursement,Exception,Missing required fields,Employee is authenticated,POST /hr2/api/cpda-reimbursements/ with incomplete payload,Returns 400 validation errors -UC-HR2-046,View CPDA reimbursements,Happy Path,List CPDA reimbursements,Requests exist,GET /hr2/api/cpda-reimbursements/,Returns CPDA reimbursements -UC-HR2-046,View CPDA reimbursements,Alternate Path,List CPDA reimbursements when none exist,No requests exist,GET /hr2/api/cpda-reimbursements/,Returns empty list -UC-HR2-046,View CPDA reimbursements,Exception,Unauthorized user tries to list reimbursements,User is not authenticated,GET /hr2/api/cpda-reimbursements/,Returns 401 Unauthorized -UC-HR2-047,View CPDA reimbursement details,Happy Path,Fetch CPDA reimbursement,CPDA reimbursement exists,GET /hr2/api/cpda-reimbursements/10/,Returns CPDA reimbursement -UC-HR2-047,View CPDA reimbursement details,Alternate Path,HR staff fetches reimbursement,HR staff is authenticated,GET /hr2/api/cpda-reimbursements/10/,Returns CPDA reimbursement -UC-HR2-047,View CPDA reimbursement details,Exception,CPDA reimbursement not found,CPDA reimbursement does not exist,GET /hr2/api/cpda-reimbursements/999/,Returns 404 Not Found -UC-HR2-048,Decide CPDA reimbursement,Happy Path,Approve CPDA reimbursement,Request exists,POST /hr2/api/cpda-reimbursements/10/approve/,Reimbursement approved -UC-HR2-048,Decide CPDA reimbursement,Happy Path,Reject CPDA reimbursement,Request exists,POST /hr2/api/cpda-reimbursements/10/reject/,Reimbursement rejected -UC-HR2-048,Decide CPDA reimbursement,Alternate Path,Reject CPDA reimbursement with remarks,Request exists,POST /hr2/api/cpda-reimbursements/10/reject/ with remarks=invalid,Reimbursement rejected -UC-HR2-048,Decide CPDA reimbursement,Exception,Invalid decision,Request exists,POST /hr2/api/cpda-reimbursements/10/invalid/,Returns 400 invalid decision -UC-HR2-049,List appraisal forms,Happy Path,Employee views own appraisal forms,Employee is authenticated,GET /hr2/api/appraisal-forms/,Returns employee appraisal forms -UC-HR2-049,List appraisal forms,Happy Path,Director views reviewed forms,User is Director,GET /hr2/api/appraisal-forms/,Returns reviewed appraisal forms -UC-HR2-049,List appraisal forms,Alternate Path,HR staff views all appraisal forms,User is HR staff,GET /hr2/api/appraisal-forms/,Returns appraisal forms -UC-HR2-049,List appraisal forms,Exception,Unauthorized user tries to list appraisal forms,User is not authenticated,GET /hr2/api/appraisal-forms/,Returns 401 Unauthorized -UC-HR2-050,View appraisal form details,Happy Path,Fetch appraisal form,Form exists,GET /hr2/api/appraisal-forms/10/,Returns appraisal form -UC-HR2-050,View appraisal form details,Alternate Path,HR staff fetches appraisal form,HR staff is authenticated,GET /hr2/api/appraisal-forms/10/,Returns appraisal form -UC-HR2-050,View appraisal form details,Exception,Appraisal form not found,Form does not exist,GET /hr2/api/appraisal-forms/999/,Returns 404 Not Found -UC-HR2-051,Download appraisal form,Happy Path,Download appraisal form,Form exists,GET /hr2/api/appraisal-forms/10/download/,Returns text file attachment -UC-HR2-051,Download appraisal form,Alternate Path,HR staff downloads appraisal form,HR staff is authenticated,GET /hr2/api/appraisal-forms/10/download/,Returns text file attachment -UC-HR2-051,Download appraisal form,Exception,Unauthorized user tries to download appraisal form,User is not authenticated,GET /hr2/api/appraisal-forms/10/download/,Returns 401 Unauthorized -UC-HR2-052,Review appraisal form,Happy Path,Forward appraisal for director,Reviewer assigned,POST /hr2/api/appraisal-forms/10/review/ with action=forward,Appraisal status set to REVIEWED -UC-HR2-052,Review appraisal form,Happy Path,Approve appraisal,Reviewer assigned,POST /hr2/api/appraisal-forms/10/review/ with action=approve,Appraisal status set to APPROVED -UC-HR2-052,Review appraisal form,Alternate Path,Review appraisal with rating,Reviewer assigned,"POST /hr2/api/appraisal-forms/10/review/ with action=forward, rating=4",Appraisal status set to REVIEWED -UC-HR2-052,Review appraisal form,Exception,Invalid review action,Reviewer assigned,POST /hr2/api/appraisal-forms/10/review/ with action=invalid,Returns 400 invalid action diff --git a/FusionIIIT/applications/hr2/tests/reports/WF_Test_Design.csv b/FusionIIIT/applications/hr2/tests/reports/WF_Test_Design.csv deleted file mode 100644 index a143bd7ef..000000000 --- a/FusionIIIT/applications/hr2/tests/reports/WF_Test_Design.csv +++ /dev/null @@ -1,23 +0,0 @@ -wf_id,wf_title,test_type,scenario,expected_final_state -WF-HR2-001,Leave application approval flow,End-to-End,Employee applies and leave is approved,"approval_status=APPROVED, leave balance reduced" -WF-HR2-001,Leave application approval flow,End-to-End,Employee applies and leave is forwarded then approved,"approval_status=APPROVED, current_approver_role=Director" -WF-HR2-001,Leave application approval flow,Negative,Employee applies and leave is rejected,"approval_status=REJECTED, leave balance unchanged" -WF-HR2-002,Leave withdrawal flow,End-to-End,Employee withdraws pending leave,approval_status=WITHDRAWN -WF-HR2-002,Leave withdrawal flow,Negative,Employee tries to withdraw approved leave,Request rejected with withdrawal error -WF-HR2-003,Leave cancellation flow,End-to-End,Cancellation approved,"cancel_status=APPROVED, approval_status=CANCELLED, leave balance restored" -WF-HR2-003,Leave cancellation flow,Negative,Cancellation requested after start date,Request rejected with cancellation window error -WF-HR2-004,Leave extension flow,End-to-End,Extension approved with sufficient balance,"extension_status=APPROVED, end_date updated, balance reduced" -WF-HR2-004,Leave extension flow,Negative,Extension approved with insufficient balance,Request rejected with insufficient balance error -WF-HR2-005,Nominee response flow,End-to-End,Nominee accepts,nominee_status=ACCEPTED -WF-HR2-005,Nominee response flow,Negative,Non-nominee responds,Request rejected with 403 -WF-HR2-006,Document request flow,End-to-End,"HOD requests, employee submits",document_request_status=SUBMITTED -WF-HR2-006,Document request flow,Negative,Employee submits without request,Request rejected with no request error -WF-HR2-007,LTC approval flow,End-to-End,LTC forwarded and approved,"approval_status=APPROVED, accountant_status=APPROVED" -WF-HR2-007,LTC approval flow,Negative,LTC rejected,approval_status=REJECTED -WF-HR2-008,CPDA advance approval flow,End-to-End,HR forwards to accountant and approves,"approval_status=APPROVED, accountant_processing_status=APPROVED" -WF-HR2-008,CPDA advance approval flow,End-to-End,Forwarded to director then approved,"accountant_processing_status=DIRECTOR_APPROVED, approval_status=FORWARDED" -WF-HR2-008,CPDA advance approval flow,Negative,CPDA advance rejected,approval_status=REJECTED -WF-HR2-009,CPDA reimbursement decision flow,End-to-End,CPDA reimbursement approved,approval_status=APPROVED -WF-HR2-009,CPDA reimbursement decision flow,Negative,CPDA reimbursement rejected,approval_status=REJECTED -WF-HR2-010,Appraisal form review flow,End-to-End,HOD forwards to director,status=REVIEWED -WF-HR2-010,Appraisal form review flow,End-to-End,Director approves,status=APPROVED diff --git a/FusionIIIT/applications/hr2/tests/runner.py b/FusionIIIT/applications/hr2/tests/runner.py deleted file mode 100644 index 05caeac7d..000000000 --- a/FusionIIIT/applications/hr2/tests/runner.py +++ /dev/null @@ -1,379 +0,0 @@ -import csv -import os -from dataclasses import dataclass, field -from datetime import datetime -from typing import Any, Dict, List, Optional - -import yaml -from django.test.runner import DiscoverRunner - - -REPORTS_DIRNAME = "reports" - - -@dataclass -class ExecutionRecord: - test_id: str - artifact_id: Optional[str] - artifact_type: str - category: str - scenario: str - preconditions: str - input_action: str - expected_result: str - status: str - message: str - evidence: str - steps: List[Dict[str, Any]] = field(default_factory=list) - - -@dataclass -class DefectRecord: - test_id: str - artifact_id: Optional[str] - artifact_type: str - status: str - error: str - - -class ReportStore: - def __init__(self) -> None: - self.execution_log: List[ExecutionRecord] = [] - self.defect_log: List[DefectRecord] = [] - - def add_execution( - self, - test_id: str, - artifact_id: Optional[str], - artifact_type: str, - category: str, - scenario: str, - preconditions: str, - input_action: str, - expected_result: str, - status: str, - message: str, - evidence: str, - steps: List[Dict[str, Any]], - ) -> None: - self.execution_log.append( - ExecutionRecord( - test_id=test_id, - artifact_id=artifact_id, - artifact_type=artifact_type, - category=category, - scenario=scenario, - preconditions=preconditions, - input_action=input_action, - expected_result=expected_result, - status=status, - message=message, - evidence=evidence, - steps=steps, - ) - ) - - def add_defect( - self, test_id: str, artifact_id: Optional[str], artifact_type: str, status: str, error: str - ) -> None: - self.defect_log.append( - DefectRecord( - test_id=test_id, - artifact_id=artifact_id, - artifact_type=artifact_type, - status=status, - error=error, - ) - ) - - -REPORT_STORE = ReportStore() - - -def _specs_dir() -> str: - return os.path.join(os.path.dirname(__file__), "specs") - - -def _reports_dir() -> str: - return os.path.join(os.path.dirname(__file__), REPORTS_DIRNAME) - - -def _load_yaml(filename: str) -> Dict[str, Any]: - path = os.path.join(_specs_dir(), filename) - if not os.path.exists(path): - return {} - with open(path, "r", encoding="utf-8") as handle: - return yaml.safe_load(handle) or {} - - -def load_specs() -> Dict[str, Any]: - return { - "use_cases": _load_yaml("use_cases.yaml").get("use_cases", []), - "business_rules": _load_yaml("business_rules.yaml").get("business_rules", []), - "workflows": _load_yaml("workflows.yaml").get("workflows", []), - } - - -def _write_csv(path: str, rows: List[Dict[str, Any]], fieldnames: List[str]) -> None: - os.makedirs(os.path.dirname(path), exist_ok=True) - with open(path, "w", newline="", encoding="utf-8") as handle: - writer = csv.DictWriter(handle, fieldnames=fieldnames) - writer.writeheader() - writer.writerows(rows) - - -def _uc_design_rows(use_cases: List[Dict[str, Any]]) -> List[Dict[str, Any]]: - rows: List[Dict[str, Any]] = [] - for uc in use_cases: - for test_type, key in ( - ("Happy Path", "happy_paths"), - ("Alternate Path", "alternate_paths"), - ("Exception", "exception_paths"), - ): - for item in uc.get(key, []): - rows.append( - { - "uc_id": uc.get("id"), - "uc_title": uc.get("title"), - "test_type": test_type, - "scenario": item.get("scenario"), - "preconditions": item.get("preconditions"), - "input_action": item.get("input_action"), - "expected_result": item.get("expected_result"), - } - ) - return rows - - -def _br_design_rows(business_rules: List[Dict[str, Any]]) -> List[Dict[str, Any]]: - rows: List[Dict[str, Any]] = [] - for br in business_rules: - for test_type, key in (("Valid", "valid_tests"), ("Invalid", "invalid_tests")): - for item in br.get(key, []): - rows.append( - { - "br_id": br.get("id"), - "br_title": br.get("title"), - "test_type": test_type, - "input_action": item.get("input_action"), - "expected_result": item.get("expected_result"), - } - ) - return rows - - -def _wf_design_rows(workflows: List[Dict[str, Any]]) -> List[Dict[str, Any]]: - rows: List[Dict[str, Any]] = [] - for wf in workflows: - for test_type, key in (("End-to-End", "e2e_tests"), ("Negative", "negative_tests")): - for item in wf.get(key, []): - rows.append( - { - "wf_id": wf.get("id"), - "wf_title": wf.get("title"), - "test_type": test_type, - "scenario": item.get("scenario"), - "expected_final_state": item.get("expected_final_state"), - } - ) - return rows - - -def _artifact_status(records: List[ExecutionRecord]) -> str: - if not records: - return "Not Implemented" - statuses = {record.status for record in records} - if statuses == {"Pass"}: - return "Implemented Correctly" - if "Pass" in statuses and ("Fail" in statuses or "Partial" in statuses): - return "Partially Implemented" - if "Fail" in statuses and "Pass" not in statuses: - return "Incorrectly Implemented" - return "Partially Implemented" - - -def _evaluation_rows( - artifact_type: str, artifacts: List[Dict[str, Any]], executions: List[ExecutionRecord] -) -> List[Dict[str, Any]]: - rows: List[Dict[str, Any]] = [] - for artifact in artifacts: - artifact_id = artifact.get("id") - relevant = [record for record in executions if record.artifact_id == artifact_id] - status = _artifact_status(relevant) - rows.append( - { - "artifact_type": artifact_type, - "artifact_id": artifact_id, - "artifact_title": artifact.get("title"), - "status": status, - } - ) - return rows - - -def _summary_rows(specs: Dict[str, Any], executions: List[ExecutionRecord]) -> List[Dict[str, Any]]: - uc_count = len(specs["use_cases"]) - br_count = len(specs["business_rules"]) - wf_count = len(specs["workflows"]) - - required_uc = uc_count * 3 - required_br = br_count * 2 - required_wf = wf_count * 2 - - designed_uc = len([record for record in executions if record.artifact_type == "UC"]) - designed_br = len([record for record in executions if record.artifact_type == "BR"]) - designed_wf = len([record for record in executions if record.artifact_type == "WF"]) - - total_executed = len(executions) - total_pass = len([record for record in executions if record.status == "Pass"]) - total_partial = len([record for record in executions if record.status == "Partial"]) - total_fail = len([record for record in executions if record.status == "Fail"]) - - def adequacy(designed: int, required: int) -> float: - if required == 0: - return 0.0 - return round((designed / required) * 100, 2) - - strict_pass_rate = 0.0 - if total_executed: - strict_pass_rate = round((total_pass / total_executed) * 100, 2) - - return [ - {"Metric": "Total Use Cases", "Value": uc_count}, - {"Metric": "Total Business Rules", "Value": br_count}, - {"Metric": "Total Workflows", "Value": wf_count}, - {"Metric": "Required UC Tests", "Value": required_uc}, - {"Metric": "Designed UC Tests", "Value": designed_uc}, - {"Metric": "Required BR Tests", "Value": required_br}, - {"Metric": "Designed BR Tests", "Value": designed_br}, - {"Metric": "Required WF Tests", "Value": required_wf}, - {"Metric": "Designed WF Tests", "Value": designed_wf}, - {"Metric": "UC Adequacy %", "Value": adequacy(designed_uc, required_uc)}, - {"Metric": "BR Adequacy %", "Value": adequacy(designed_br, required_br)}, - {"Metric": "WF Adequacy %", "Value": adequacy(designed_wf, required_wf)}, - {"Metric": "Total Tests Executed", "Value": total_executed}, - {"Metric": "Total Pass", "Value": total_pass}, - {"Metric": "Total Partial", "Value": total_partial}, - {"Metric": "Total Fail", "Value": total_fail}, - {"Metric": "Strict Pass Rate %", "Value": strict_pass_rate}, - {"Metric": "Generated At", "Value": datetime.now().isoformat()}, - {"Metric": "Tester Name", "Value": os.getenv("TESTER_NAME", "")}, - ] - - -def _execution_rows(executions: List[ExecutionRecord]) -> List[Dict[str, Any]]: - rows: List[Dict[str, Any]] = [] - for record in executions: - rows.append( - { - "test_id": record.test_id, - "artifact_type": record.artifact_type, - "artifact_id": record.artifact_id, - "category": record.category, - "scenario": record.scenario, - "preconditions": record.preconditions, - "input_action": record.input_action, - "expected_result": record.expected_result, - "status": record.status, - "message": record.message, - "evidence": record.evidence, - "steps": record.steps, - } - ) - return rows - - -def _defect_rows(defects: List[DefectRecord]) -> List[Dict[str, Any]]: - rows: List[Dict[str, Any]] = [] - for record in defects: - rows.append( - { - "test_id": record.test_id, - "artifact_type": record.artifact_type, - "artifact_id": record.artifact_id, - "status": record.status, - "error": record.error, - } - ) - return rows - - -class ReportingTestRunner(DiscoverRunner): - def run_suite(self, suite, **kwargs): - result = super().run_suite(suite, **kwargs) - self._write_reports() - return result - - def _write_reports(self) -> None: - specs = load_specs() - reports_dir = _reports_dir() - - uc_design = _uc_design_rows(specs["use_cases"]) - br_design = _br_design_rows(specs["business_rules"]) - wf_design = _wf_design_rows(specs["workflows"]) - - summary_rows = _summary_rows(specs, REPORT_STORE.execution_log) - execution_rows = _execution_rows(REPORT_STORE.execution_log) - defect_rows = _defect_rows(REPORT_STORE.defect_log) - evaluation_rows = ( - _evaluation_rows("UC", specs["use_cases"], REPORT_STORE.execution_log) - + _evaluation_rows("BR", specs["business_rules"], REPORT_STORE.execution_log) - + _evaluation_rows("WF", specs["workflows"], REPORT_STORE.execution_log) - ) - - _write_csv( - os.path.join(reports_dir, "Module_Test_Summary.csv"), - summary_rows, - ["Metric", "Value"], - ) - _write_csv( - os.path.join(reports_dir, "UC_Test_Design.csv"), - uc_design, - [ - "uc_id", - "uc_title", - "test_type", - "scenario", - "preconditions", - "input_action", - "expected_result", - ], - ) - _write_csv( - os.path.join(reports_dir, "BR_Test_Design.csv"), - br_design, - ["br_id", "br_title", "test_type", "input_action", "expected_result"], - ) - _write_csv( - os.path.join(reports_dir, "WF_Test_Design.csv"), - wf_design, - ["wf_id", "wf_title", "test_type", "scenario", "expected_final_state"], - ) - _write_csv( - os.path.join(reports_dir, "Test_Execution_Log.csv"), - execution_rows, - [ - "test_id", - "artifact_type", - "artifact_id", - "category", - "scenario", - "preconditions", - "input_action", - "expected_result", - "status", - "message", - "evidence", - "steps", - ], - ) - _write_csv( - os.path.join(reports_dir, "Defect_Log.csv"), - defect_rows, - ["test_id", "artifact_type", "artifact_id", "status", "error"], - ) - _write_csv( - os.path.join(reports_dir, "Artifact_Evaluation.csv"), - evaluation_rows, - ["artifact_type", "artifact_id", "artifact_title", "status"], - ) diff --git a/FusionIIIT/applications/hr2/tests/specs/business_rules.yaml b/FusionIIIT/applications/hr2/tests/specs/business_rules.yaml deleted file mode 100644 index edbebcc66..000000000 --- a/FusionIIIT/applications/hr2/tests/specs/business_rules.yaml +++ /dev/null @@ -1,251 +0,0 @@ -business_rules: - - id: "BR-HR2-001" - title: "Leave start date cannot be in the past" - description: "Leave application start date must be today or later" - valid_tests: - - input_action: "Apply leave with start_date=tomorrow" - expected_result: "Request accepted" - invalid_tests: - - input_action: "Apply leave with start_date=yesterday" - expected_result: "Request rejected with start_date validation error" - - - id: "BR-HR2-002" - title: "Leave end date must be on/after start date" - description: "End date must be greater than or equal to start date" - valid_tests: - - input_action: "Apply leave with start_date=2026-05-01, end_date=2026-05-03" - expected_result: "Request accepted" - invalid_tests: - - input_action: "Apply leave with start_date=2026-05-03, end_date=2026-05-01" - expected_result: "Request rejected with date range error" - - - id: "BR-HR2-003" - title: "Total days must match date range" - description: "total_days must equal (end_date - start_date + 1)" - valid_tests: - - input_action: "Apply leave with start_date=2026-05-01, end_date=2026-05-03, total_days=3" - expected_result: "Request accepted" - invalid_tests: - - input_action: "Apply leave with start_date=2026-05-01, end_date=2026-05-03, total_days=2" - expected_result: "Request rejected with total_days mismatch error" - - - id: "BR-HR2-004" - title: "Leave requests cannot overlap" - description: "Employee cannot have overlapping pending/forwarded/approved leave" - valid_tests: - - input_action: "Apply leave for 2026-05-10 to 2026-05-12 when no overlaps" - expected_result: "Request accepted" - invalid_tests: - - input_action: "Apply leave overlapping existing approved leave" - expected_result: "Request rejected with overlap error" - - - id: "BR-HR2-005" - title: "Leave balance must be sufficient" - description: "Requested days must be <= current leave balance" - valid_tests: - - input_action: "Apply leave for 2 days with balance >= 2" - expected_result: "Request accepted" - invalid_tests: - - input_action: "Apply leave for 10 days with balance < 10" - expected_result: "Request rejected with insufficient balance error" - - - id: "BR-HR2-006" - title: "Nominee employee must exist" - description: "Nominee ID must match an existing employee" - valid_tests: - - input_action: "Apply leave with nominee_employee_id=valid" - expected_result: "Request accepted with nominee_status=PENDING" - invalid_tests: - - input_action: "Apply leave with nominee_employee_id=invalid" - expected_result: "Request rejected with nominee not found error" - - - id: "BR-HR2-007" - title: "Only owner can withdraw leave" - description: "Employee can withdraw own pending/forwarded leave" - valid_tests: - - input_action: "Owner withdraws pending leave" - expected_result: "Leave updated to WITHDRAWN" - invalid_tests: - - input_action: "Non-owner withdraws leave" - expected_result: "Request rejected with 403" - - - id: "BR-HR2-008" - title: "Cancellation only before start date" - description: "Cancellation request allowed only up to 1 day before start" - valid_tests: - - input_action: "Request cancellation one day before start" - expected_result: "Cancellation request accepted" - invalid_tests: - - input_action: "Request cancellation on start date" - expected_result: "Request rejected with cancellation window error" - - - id: "BR-HR2-009" - title: "Leave delete only when pending" - description: "Only pending leave applications can be deleted" - valid_tests: - - input_action: "Delete pending leave" - expected_result: "Leave deleted" - invalid_tests: - - input_action: "Delete approved leave" - expected_result: "Request rejected with delete error" - - - id: "BR-HR2-010" - title: "Cancellation requires approved leave and no prior request" - description: "Cancel request allowed only for approved leave with cancel_status=NOT_REQUESTED" - valid_tests: - - input_action: "Request cancellation for approved leave" - expected_result: "Cancel status set to REQUESTED" - invalid_tests: - - input_action: "Request cancellation for pending leave" - expected_result: "Request rejected with approval_status error" - - input_action: "Request cancellation when cancel_status already REQUESTED" - expected_result: "Request rejected as already processed" - - - id: "BR-HR2-011" - title: "Extension requires approved leave before end date" - description: "Extension request allowed only before end_date and with new_end_date after current end" - valid_tests: - - input_action: "Request extension with new_end_date after current end date" - expected_result: "Extension status set to REQUESTED" - invalid_tests: - - input_action: "Request extension after end_date" - expected_result: "Request rejected with extension window error" - - input_action: "Request extension with new_end_date before current end date" - expected_result: "Request rejected with date validation error" - - - id: "BR-HR2-012" - title: "Extension approval needs balance" - description: "Extension approval must have enough remaining leave balance" - valid_tests: - - input_action: "Approve extension with sufficient balance" - expected_result: "Extension approved and balance reduced" - invalid_tests: - - input_action: "Approve extension with insufficient balance" - expected_result: "Request rejected with insufficient balance error" - - - id: "BR-HR2-013" - title: "Document request requires HOD role" - description: "Only HOD can request documents with non-empty message" - valid_tests: - - input_action: "HOD requests document with message" - expected_result: "Document request status set to REQUESTED" - invalid_tests: - - input_action: "Non-HOD requests document" - expected_result: "Request rejected with 403" - - input_action: "HOD requests document without message" - expected_result: "Request rejected with message required" - - - id: "BR-HR2-014" - title: "Document submit requires owner and request" - description: "Only leave owner can submit document after it was requested" - valid_tests: - - input_action: "Owner submits document after request" - expected_result: "Document request status set to SUBMITTED" - invalid_tests: - - input_action: "Owner submits document without request" - expected_result: "Request rejected with no request error" - - input_action: "Non-owner submits document" - expected_result: "Request rejected with 403" - - - id: "BR-HR2-015" - title: "Nominee decision only by nominee" - description: "Only the nominated employee can accept or decline" - valid_tests: - - input_action: "Nominee accepts request" - expected_result: "Nominee status updated to ACCEPTED" - invalid_tests: - - input_action: "Non-nominee responds" - expected_result: "Request rejected with 403" - - input_action: "Nominee sends invalid action" - expected_result: "Request rejected with invalid action" - - - id: "BR-HR2-016" - title: "Leave decision action must be valid" - description: "Decision must be approve, reject, or forward" - valid_tests: - - input_action: "Approve leave via decision endpoint" - expected_result: "Leave status updated to APPROVED" - invalid_tests: - - input_action: "Decision action=invalid" - expected_result: "Request rejected with invalid decision" - - - id: "BR-HR2-017" - title: "Leave balance record must exist" - description: "Leave type balance must exist for employee" - valid_tests: - - input_action: "Apply leave with existing leave balance" - expected_result: "Request accepted" - invalid_tests: - - input_action: "Apply leave with no balance record for type" - expected_result: "Request rejected with balance not found error" - - - id: "BR-HR2-018" - title: "Leave application requires employee profile" - description: "Employee profile must be resolved from user or employee_id" - valid_tests: - - input_action: "Apply leave as authenticated employee" - expected_result: "Request accepted" - invalid_tests: - - input_action: "Apply leave without employee profile and no employee_id" - expected_result: "Request rejected with employee profile not found" - - - id: "BR-HR2-019" - title: "LTC withdrawal only when pending" - description: "Only pending LTC requests can be withdrawn by owner" - valid_tests: - - input_action: "Owner withdraws pending LTC" - expected_result: "LTC updated to WITHDRAWN" - invalid_tests: - - input_action: "Owner withdraws approved LTC" - expected_result: "Request rejected with pending-only error" - - - id: "BR-HR2-020" - title: "LTC decision action must be valid" - description: "Decision must be approve, reject, or forward" - valid_tests: - - input_action: "Forward LTC" - expected_result: "Approval status set to FORWARDED and accountant_status=PENDING" - invalid_tests: - - input_action: "Decision action=invalid" - expected_result: "Request rejected with invalid decision" - - - id: "BR-HR2-021" - title: "CPDA advance withdrawal only when pending" - description: "Only pending CPDA advance requests can be withdrawn by owner" - valid_tests: - - input_action: "Owner withdraws pending CPDA advance" - expected_result: "CPDA updated to WITHDRAWN" - invalid_tests: - - input_action: "Owner withdraws approved CPDA advance" - expected_result: "Request rejected with pending-only error" - - - id: "BR-HR2-022" - title: "CPDA advance decision action must be valid" - description: "Decision must be approve, reject, forward-accountant, or forward-director" - valid_tests: - - input_action: "Forward CPDA advance to accountant" - expected_result: "Status FORWARDED and accountant_processing_status=PENDING" - - input_action: "Forward CPDA advance to director" - expected_result: "Status FORWARDED and accountant_processing_status=DIRECTOR_REVIEW" - invalid_tests: - - input_action: "Decision action=invalid" - expected_result: "Request rejected with invalid decision" - - - id: "BR-HR2-023" - title: "Download access restricted to owner or staff" - description: "Leave/LTC/CPDA/Appraisal downloads require ownership or staff access" - valid_tests: - - input_action: "Owner downloads own leave application" - expected_result: "Download succeeds" - invalid_tests: - - input_action: "Non-owner downloads another employee record" - expected_result: "Request rejected with 403" - - - id: "BR-HR2-024" - title: "Appraisal review sets status" - description: "Review action sets status to REVIEWED or APPROVED" - valid_tests: - - input_action: "Reviewer forwards appraisal" - expected_result: "Appraisal status set to REVIEWED" - - input_action: "Reviewer approves appraisal" - expected_result: "Appraisal status set to APPROVED" diff --git a/FusionIIIT/applications/hr2/tests/specs/use_cases.yaml b/FusionIIIT/applications/hr2/tests/specs/use_cases.yaml deleted file mode 100644 index c3bd2c2b7..000000000 --- a/FusionIIIT/applications/hr2/tests/specs/use_cases.yaml +++ /dev/null @@ -1,1144 +0,0 @@ -use_cases: - - id: "UC-HR2-001" - title: "List employees" - description: "HR staff or authorized users retrieve employees filtered by type or department" - actors: "HR staff, HOD, Employee" - preconditions: "Authenticated user with access to HR2 module" - happy_paths: - - scenario: "HR staff lists all employees" - preconditions: "User has HR designation or is HR staff" - input_action: "GET /hr2/api/employees/" - expected_result: "Returns list of employees" - - scenario: "Filter employees by type and department" - preconditions: "User is authenticated" - input_action: "GET /hr2/api/employees/?type=Faculty&department=1" - expected_result: "Returns employees matching filters" - alternate_paths: - - scenario: "Request with only department filter" - preconditions: "User is authenticated" - input_action: "GET /hr2/api/employees/?department=1" - expected_result: "Returns employees in department" - exception_paths: - - scenario: "Unauthorized user tries to list employees" - preconditions: "User is not authenticated" - input_action: "GET /hr2/api/employees/" - expected_result: "Returns 401 Unauthorized" - - - id: "UC-HR2-002" - title: "View employee details" - description: "Fetch a specific employee profile" - actors: "HR staff, HOD, Employee" - preconditions: "Authenticated user with access to HR2 module" - happy_paths: - - scenario: "Fetch employee details by ID" - preconditions: "Employee exists" - input_action: "GET /hr2/api/employees/123/" - expected_result: "Returns employee details" - alternate_paths: - - scenario: "HOD fetches employee details" - preconditions: "HOD is authenticated" - input_action: "GET /hr2/api/employees/123/" - expected_result: "Returns employee details" - exception_paths: - - scenario: "Employee not found" - preconditions: "Employee does not exist" - input_action: "GET /hr2/api/employees/999999/" - expected_result: "Returns 404 Not Found" - - - id: "UC-HR2-003" - title: "Update employee details" - description: "Update employee profile information" - actors: "HR staff" - preconditions: "Authenticated HR staff" - happy_paths: - - scenario: "Update phone number" - preconditions: "Employee exists" - input_action: "PUT /hr2/api/employees/123/ with phone_number=9876543210" - expected_result: "Returns updated employee details" - alternate_paths: - - scenario: "Update employee address" - preconditions: "Employee exists" - input_action: "PUT /hr2/api/employees/123/ with address=Updated" - expected_result: "Returns updated employee details" - exception_paths: - - scenario: "Invalid data" - preconditions: "Employee exists" - input_action: "PUT /hr2/api/employees/123/ with phone_number=invalid" - expected_result: "Returns 400 validation error" - - - id: "UC-HR2-004" - title: "Apply for leave" - description: "Employee submits a leave application" - actors: "Employee" - preconditions: "Authenticated employee with available leave balance" - happy_paths: - - scenario: "Apply for casual leave with valid dates" - preconditions: "Leave balance is sufficient" - input_action: "POST /hr2/api/leave-applications/ with leave_type=CL, start_date=future, end_date=future+2, total_days=3" - expected_result: "Leave application created with approval_status=PENDING or FORWARDED" - alternate_paths: - - scenario: "Nominate substitute during leave" - preconditions: "Nominee employee exists" - input_action: "POST /hr2/api/leave-applications/ with nominee_employee_id=456" - expected_result: "Leave created with nominee_status=PENDING" - exception_paths: - - scenario: "Start date in the past" - preconditions: "Employee is authenticated" - input_action: "POST /hr2/api/leave-applications/ with start_date=yesterday" - expected_result: "Returns 400 start_date validation error" - - - id: "UC-HR2-005" - title: "View leave applications" - description: "Fetch leave applications based on user role" - actors: "Employee, HOD, Registrar, Director, HR staff" - preconditions: "Authenticated user" - happy_paths: - - scenario: "Employee views own leave applications" - preconditions: "Employee is authenticated" - input_action: "GET /hr2/api/leave-applications/" - expected_result: "Returns only employee's leave applications" - - scenario: "Director views forwarded applications" - preconditions: "User has Director designation" - input_action: "GET /hr2/api/leave-applications/" - expected_result: "Returns forwarded and requested decisions" - alternate_paths: - - scenario: "HOD views departmental leave applications" - preconditions: "User has HOD designation" - input_action: "GET /hr2/api/leave-applications/" - expected_result: "Returns department leave applications" - exception_paths: - - scenario: "Unauthorized user tries to list leave applications" - preconditions: "User is not authenticated" - input_action: "GET /hr2/api/leave-applications/" - expected_result: "Returns 401 Unauthorized" - - - id: "UC-HR2-006" - title: "Withdraw leave application" - description: "Employee or approver withdraws a pending or forwarded leave" - actors: "Employee, Registrar, HR admin, Accountant" - preconditions: "Authenticated user with appropriate role" - happy_paths: - - scenario: "Employee withdraws own pending leave" - preconditions: "Leave approval_status is PENDING" - input_action: "POST /hr2/api/leave-applications/10/withdraw/" - expected_result: "Leave updated to approval_status=WITHDRAWN" - alternate_paths: - - scenario: "Registrar withdraws forwarded leave" - preconditions: "Leave approval_status is FORWARDED" - input_action: "POST /hr2/api/leave-applications/10/withdraw/" - expected_result: "Leave updated to approval_status=REJECTED" - exception_paths: - - scenario: "Withdraw non-pending leave" - preconditions: "Leave approval_status is APPROVED" - input_action: "POST /hr2/api/leave-applications/10/withdraw/" - expected_result: "Returns 400 with withdrawal error" - - - id: "UC-HR2-007" - title: "Request leave cancellation" - description: "Employee requests cancellation of an approved leave" - actors: "Employee" - preconditions: "Authenticated employee with approved leave" - happy_paths: - - scenario: "Submit cancellation request before start date" - preconditions: "Today is before leave start date" - input_action: "POST /hr2/api/leave-applications/10/cancel-request/ with reason=change of plan" - expected_result: "Cancel status set to REQUESTED" - alternate_paths: - - scenario: "Submit cancellation request with reason" - preconditions: "Approved leave exists" - input_action: "POST /hr2/api/leave-applications/10/cancel-request/ with reason=medical" - expected_result: "Cancel status set to REQUESTED" - exception_paths: - - scenario: "Cancellation after start date" - preconditions: "Today is on or after start date" - input_action: "POST /hr2/api/leave-applications/10/cancel-request/" - expected_result: "Returns 400 cancellation window error" - - - id: "UC-HR2-008" - title: "Request leave extension" - description: "Employee requests extension of an approved leave" - actors: "Employee" - preconditions: "Authenticated employee with approved leave" - happy_paths: - - scenario: "Request extension with new end date" - preconditions: "Extension dates are valid" - input_action: "POST /hr2/api/leave-applications/10/extension-request/ with new_end_date=future+2" - expected_result: "Extension status set to REQUESTED" - alternate_paths: - - scenario: "Request extension with reason" - preconditions: "Extension dates are valid" - input_action: "POST /hr2/api/leave-applications/10/extension-request/ with reason=medical" - expected_result: "Extension status set to REQUESTED" - exception_paths: - - scenario: "Extension with invalid date" - preconditions: "New end date before current end date" - input_action: "POST /hr2/api/leave-applications/10/extension-request/ with new_end_date=earlier" - expected_result: "Returns 400 validation error" - - - id: "UC-HR2-009" - title: "View leave balance" - description: "Employee or HR staff checks leave balance" - actors: "Employee, HR staff" - preconditions: "Authenticated user" - happy_paths: - - scenario: "Employee views own leave balance" - preconditions: "Employee is authenticated" - input_action: "GET /hr2/api/leave-balance/" - expected_result: "Returns leave balance for the employee" - - scenario: "HR views leave balance for another employee" - preconditions: "User has HR designation" - input_action: "GET /hr2/api/leave-balance/123/" - expected_result: "Returns leave balance for employee 123" - alternate_paths: - - scenario: "Employee views balance with no records" - preconditions: "Employee has no balance entries" - input_action: "GET /hr2/api/leave-balance/" - expected_result: "Returns empty balance list" - exception_paths: - - scenario: "Unauthorized user tries to view leave balance" - preconditions: "User is not authenticated" - input_action: "GET /hr2/api/leave-balance/" - expected_result: "Returns 401 Unauthorized" - - - id: "UC-HR2-010" - title: "Download leave application" - description: "Employee downloads a leave application summary" - actors: "Employee" - preconditions: "Authenticated employee who owns the leave application" - happy_paths: - - scenario: "Download approved leave application" - preconditions: "Leave exists and belongs to employee" - input_action: "GET /hr2/api/leave-applications/10/download/" - expected_result: "Returns text file attachment with leave details" - alternate_paths: - - scenario: "Download pending leave application" - preconditions: "Leave is pending and belongs to employee" - input_action: "GET /hr2/api/leave-applications/10/download/" - expected_result: "Returns text file attachment with leave details" - exception_paths: - - scenario: "Access another employee's leave" - preconditions: "Leave does not belong to requester" - input_action: "GET /hr2/api/leave-applications/999/download/" - expected_result: "Returns 403 Not authorized" - - - id: "UC-HR2-011" - title: "Submit LTC application" - description: "Employee submits LTC claim" - actors: "Employee" - preconditions: "Authenticated employee" - happy_paths: - - scenario: "Submit LTC claim" - preconditions: "Required fields provided" - input_action: "POST /hr2/api/ltc/ with required LTC fields" - expected_result: "LTC application created with approval_status=PENDING" - alternate_paths: - - scenario: "Submit LTC claim with optional fields" - preconditions: "Required fields provided" - input_action: "POST /hr2/api/ltc/ with optional fields" - expected_result: "LTC application created with approval_status=PENDING" - exception_paths: - - scenario: "Missing required fields" - preconditions: "Employee is authenticated" - input_action: "POST /hr2/api/ltc/ with incomplete payload" - expected_result: "Returns 400 validation errors" - - - id: "UC-HR2-012" - title: "Submit CPDA advance" - description: "Employee submits CPDA advance request" - actors: "Employee" - preconditions: "Authenticated employee" - happy_paths: - - scenario: "Submit CPDA advance" - preconditions: "Required fields provided" - input_action: "POST /hr2/api/cpda-advances/ with required fields" - expected_result: "CPDA advance created with approval_status=PENDING" - alternate_paths: - - scenario: "Submit CPDA advance with optional expenses" - preconditions: "Required fields provided" - input_action: "POST /hr2/api/cpda-advances/ with optional fields" - expected_result: "CPDA advance created with approval_status=PENDING" - exception_paths: - - scenario: "Invalid amount" - preconditions: "Employee is authenticated" - input_action: "POST /hr2/api/cpda-advances/ with amountRequired=invalid" - expected_result: "Returns 400 validation errors" - - - id: "UC-HR2-013" - title: "Submit appraisal form" - description: "Employee submits appraisal form for review" - actors: "Employee" - preconditions: "Authenticated employee" - happy_paths: - - scenario: "Submit appraisal form" - preconditions: "Required fields provided" - input_action: "POST /hr2/api/appraisal-forms/ with required fields" - expected_result: "Appraisal form created with status=SUBMITTED" - alternate_paths: - - scenario: "Submit appraisal form with optional fields" - preconditions: "Required fields provided" - input_action: "POST /hr2/api/appraisal-forms/ with optional fields" - expected_result: "Appraisal form created with status=SUBMITTED" - exception_paths: - - scenario: "Missing required fields" - preconditions: "Employee is authenticated" - input_action: "POST /hr2/api/appraisal-forms/ with incomplete payload" - expected_result: "Returns 400 validation errors" - - - id: "UC-HR2-014" - title: "View leave application details" - description: "Fetch a specific leave application" - actors: "Employee, HR staff" - preconditions: "Authenticated user" - happy_paths: - - scenario: "Fetch leave application by ID" - preconditions: "Leave application exists" - input_action: "GET /hr2/api/leave-applications/10/" - expected_result: "Returns leave application" - alternate_paths: - - scenario: "HR staff fetches leave application by ID" - preconditions: "HR staff is authenticated" - input_action: "GET /hr2/api/leave-applications/10/" - expected_result: "Returns leave application" - exception_paths: - - scenario: "Leave application not found" - preconditions: "Leave application does not exist" - input_action: "GET /hr2/api/leave-applications/999/" - expected_result: "Returns 404 Not Found" - - - id: "UC-HR2-015" - title: "Update leave application" - description: "Employee updates a leave application" - actors: "Employee" - preconditions: "Authenticated employee who owns the leave" - happy_paths: - - scenario: "Update leave reason" - preconditions: "Leave belongs to employee" - input_action: "PUT /hr2/api/leave-applications/10/ with reason=updated" - expected_result: "Leave application updated" - alternate_paths: - - scenario: "Update leave handover notes" - preconditions: "Leave belongs to employee" - input_action: "PUT /hr2/api/leave-applications/10/ with handover_notes=updated" - expected_result: "Leave application updated" - exception_paths: - - scenario: "Update another employee's leave" - preconditions: "Leave belongs to another employee" - input_action: "PUT /hr2/api/leave-applications/10/" - expected_result: "Returns 403 Not authorized" - - - id: "UC-HR2-016" - title: "Delete leave application" - description: "Employee deletes a pending leave application" - actors: "Employee" - preconditions: "Authenticated employee who owns the leave" - happy_paths: - - scenario: "Delete pending leave" - preconditions: "Leave approval_status is PENDING" - input_action: "DELETE /hr2/api/leave-applications/10/" - expected_result: "Leave application deleted" - alternate_paths: - - scenario: "Delete pending leave without attachments" - preconditions: "Leave approval_status is PENDING" - input_action: "DELETE /hr2/api/leave-applications/10/" - expected_result: "Leave application deleted" - exception_paths: - - scenario: "Delete non-pending leave" - preconditions: "Leave approval_status is APPROVED" - input_action: "DELETE /hr2/api/leave-applications/10/" - expected_result: "Returns 400 with delete error" - - - id: "UC-HR2-017" - title: "Approve or reject leave" - description: "Approver updates leave status" - actors: "HOD, Registrar, Director" - preconditions: "Authenticated approver" - happy_paths: - - scenario: "Approve leave" - preconditions: "Leave exists" - input_action: "POST /hr2/api/leave-applications/10/approve/" - expected_result: "Leave status updated to APPROVED" - - scenario: "Forward leave" - preconditions: "Leave exists" - input_action: "POST /hr2/api/leave-applications/10/forward/" - expected_result: "Leave status updated to FORWARDED" - alternate_paths: - - scenario: "Reject leave" - preconditions: "Leave exists" - input_action: "POST /hr2/api/leave-applications/10/reject/" - expected_result: "Leave status updated to REJECTED" - exception_paths: - - scenario: "Invalid decision" - preconditions: "Leave exists" - input_action: "POST /hr2/api/leave-applications/10/invalid/" - expected_result: "Returns 400 invalid decision" - - - id: "UC-HR2-018" - title: "Nominee dashboard" - description: "Nominee views pending substitution requests" - actors: "Employee" - preconditions: "Authenticated nominee" - happy_paths: - - scenario: "Nominee views pending requests" - preconditions: "Nominee has pending requests" - input_action: "GET /hr2/api/leave-nominee/" - expected_result: "Returns pending nominee requests" - alternate_paths: - - scenario: "Nominee has no pending requests" - preconditions: "Nominee has no pending requests" - input_action: "GET /hr2/api/leave-nominee/" - expected_result: "Returns empty list" - exception_paths: - - scenario: "Unauthorized user tries to view nominee dashboard" - preconditions: "User is not authenticated" - input_action: "GET /hr2/api/leave-nominee/" - expected_result: "Returns 401 Unauthorized" - - - id: "UC-HR2-019" - title: "Nominee decision" - description: "Nominee accepts or declines substitution request" - actors: "Employee" - preconditions: "Authenticated nominee" - happy_paths: - - scenario: "Nominee accepts" - preconditions: "Nominee is assigned on leave" - input_action: "POST /hr2/api/leave-nominee/10/ with action=accept" - expected_result: "Nominee status updated to ACCEPTED" - alternate_paths: - - scenario: "Nominee declines" - preconditions: "Nominee is assigned on leave" - input_action: "POST /hr2/api/leave-nominee/10/ with action=decline" - expected_result: "Nominee status updated to DECLINED" - exception_paths: - - scenario: "Invalid action" - preconditions: "Nominee is assigned" - input_action: "POST /hr2/api/leave-nominee/10/ with action=invalid" - expected_result: "Returns 400 invalid action" - - - id: "UC-HR2-020" - title: "Request leave documents" - description: "HOD requests supporting documents" - actors: "HOD" - preconditions: "Authenticated HOD" - happy_paths: - - scenario: "Request documents with message" - preconditions: "Leave exists" - input_action: "POST /hr2/api/leave-applications/10/request-document/ with message=submit proof" - expected_result: "Document request status set to REQUESTED" - alternate_paths: - - scenario: "Request documents with updated message" - preconditions: "Leave exists" - input_action: "POST /hr2/api/leave-applications/10/request-document/ with message=submit updated proof" - expected_result: "Document request status set to REQUESTED" - exception_paths: - - scenario: "Missing message" - preconditions: "Leave exists" - input_action: "POST /hr2/api/leave-applications/10/request-document/" - expected_result: "Returns 400 message required" - - - id: "UC-HR2-021" - title: "Submit leave documents" - description: "Employee submits requested documents" - actors: "Employee" - preconditions: "Authenticated employee" - happy_paths: - - scenario: "Submit document after request" - preconditions: "Document request exists" - input_action: "POST /hr2/api/leave-applications/10/submit-document/ with submission=doc ref" - expected_result: "Document request status set to SUBMITTED" - alternate_paths: - - scenario: "Submit updated document" - preconditions: "Document request exists" - input_action: "POST /hr2/api/leave-applications/10/submit-document/ with submission=updated ref" - expected_result: "Document request status set to SUBMITTED" - exception_paths: - - scenario: "Submit without request" - preconditions: "No document request exists" - input_action: "POST /hr2/api/leave-applications/10/submit-document/ with submission=doc ref" - expected_result: "Returns 400 no request" - - - id: "UC-HR2-022" - title: "Cancellation decision" - description: "Approver approves or rejects cancellation" - actors: "HOD, Registrar, Director" - preconditions: "Cancellation request exists" - happy_paths: - - scenario: "Approve cancellation" - preconditions: "Approver role matches" - input_action: "POST /hr2/api/leave-applications/10/cancel-decision/approve/" - expected_result: "Cancellation approved and leave cancelled" - - scenario: "Reject cancellation" - preconditions: "Approver role matches" - input_action: "POST /hr2/api/leave-applications/10/cancel-decision/reject/" - expected_result: "Cancellation rejected" - alternate_paths: - - scenario: "Approve cancellation with remarks" - preconditions: "Approver role matches" - input_action: "POST /hr2/api/leave-applications/10/cancel-decision/approve/ with remarks=ok" - expected_result: "Cancellation approved and leave cancelled" - exception_paths: - - scenario: "Invalid decision" - preconditions: "Cancellation request exists" - input_action: "POST /hr2/api/leave-applications/10/cancel-decision/invalid/" - expected_result: "Returns 400 invalid decision" - - - id: "UC-HR2-023" - title: "Extension decision" - description: "Approver approves or rejects extension" - actors: "HOD, Registrar, Director" - preconditions: "Extension request exists" - happy_paths: - - scenario: "Approve extension" - preconditions: "Approver role matches" - input_action: "POST /hr2/api/leave-applications/10/extension-decision/approve/" - expected_result: "Extension approved and leave updated" - - scenario: "Reject extension" - preconditions: "Approver role matches" - input_action: "POST /hr2/api/leave-applications/10/extension-decision/reject/" - expected_result: "Extension rejected" - alternate_paths: - - scenario: "Approve extension with remarks" - preconditions: "Approver role matches" - input_action: "POST /hr2/api/leave-applications/10/extension-decision/approve/ with remarks=ok" - expected_result: "Extension approved and leave updated" - exception_paths: - - scenario: "Invalid decision" - preconditions: "Extension request exists" - input_action: "POST /hr2/api/leave-applications/10/extension-decision/invalid/" - expected_result: "Returns 400 invalid decision" - - - id: "UC-HR2-024" - title: "Record attendance" - description: "Employee marks attendance" - actors: "Employee" - preconditions: "Authenticated employee" - happy_paths: - - scenario: "Mark attendance" - preconditions: "Valid status and date" - input_action: "POST /hr2/api/attendance/ with date=today, status=PRESENT" - expected_result: "Attendance record created" - alternate_paths: - - scenario: "Mark half-day attendance" - preconditions: "Valid status and date" - input_action: "POST /hr2/api/attendance/ with date=today, status=HALF_DAY" - expected_result: "Attendance record created" - exception_paths: - - scenario: "Missing attendance status" - preconditions: "Employee is authenticated" - input_action: "POST /hr2/api/attendance/ with date=today" - expected_result: "Returns 400 validation errors" - - - id: "UC-HR2-025" - title: "View attendance" - description: "Employee views attendance history" - actors: "Employee" - preconditions: "Authenticated employee" - happy_paths: - - scenario: "View attendance for date range" - preconditions: "Attendance exists" - input_action: "GET /hr2/api/attendance/?from_date=2026-05-01&to_date=2026-05-10" - expected_result: "Returns attendance records" - alternate_paths: - - scenario: "View attendance without filters" - preconditions: "Attendance exists" - input_action: "GET /hr2/api/attendance/" - expected_result: "Returns attendance records" - exception_paths: - - scenario: "Unauthorized user tries to view attendance" - preconditions: "User is not authenticated" - input_action: "GET /hr2/api/attendance/" - expected_result: "Returns 401 Unauthorized" - - - id: "UC-HR2-026" - title: "List appraisal periods" - description: "Fetch appraisal periods" - actors: "Employee, HR staff" - preconditions: "Authenticated user" - happy_paths: - - scenario: "List active periods" - preconditions: "Periods exist" - input_action: "GET /hr2/api/appraisal-periods/?is_active=true" - expected_result: "Returns appraisal periods" - alternate_paths: - - scenario: "List all periods without filter" - preconditions: "Periods exist" - input_action: "GET /hr2/api/appraisal-periods/" - expected_result: "Returns appraisal periods" - exception_paths: - - scenario: "Unauthorized user tries to view appraisal periods" - preconditions: "User is not authenticated" - input_action: "GET /hr2/api/appraisal-periods/" - expected_result: "Returns 401 Unauthorized" - - - id: "UC-HR2-027" - title: "Submit appraisal (performance)" - description: "Employee submits performance appraisal" - actors: "Employee" - preconditions: "Authenticated employee" - happy_paths: - - scenario: "Submit appraisal" - preconditions: "Required fields provided" - input_action: "POST /hr2/api/appraisals/ with period and scores" - expected_result: "Performance appraisal created" - alternate_paths: - - scenario: "Submit appraisal with remarks" - preconditions: "Required fields provided" - input_action: "POST /hr2/api/appraisals/ with period, scores, remarks" - expected_result: "Performance appraisal created" - exception_paths: - - scenario: "Missing required fields" - preconditions: "Employee is authenticated" - input_action: "POST /hr2/api/appraisals/ with incomplete payload" - expected_result: "Returns 400 validation errors" - - - id: "UC-HR2-028" - title: "List appraisals" - description: "Employee views appraisals" - actors: "Employee" - preconditions: "Authenticated employee" - happy_paths: - - scenario: "List appraisals for period" - preconditions: "Appraisals exist" - input_action: "GET /hr2/api/appraisals/?period=1" - expected_result: "Returns appraisals" - alternate_paths: - - scenario: "List all appraisals" - preconditions: "Appraisals exist" - input_action: "GET /hr2/api/appraisals/" - expected_result: "Returns appraisals" - exception_paths: - - scenario: "Unauthorized user tries to list appraisals" - preconditions: "User is not authenticated" - input_action: "GET /hr2/api/appraisals/" - expected_result: "Returns 401 Unauthorized" - - - id: "UC-HR2-029" - title: "List training programs" - description: "View upcoming training programs" - actors: "Employee" - preconditions: "Authenticated employee" - happy_paths: - - scenario: "List available programs" - preconditions: "Programs exist" - input_action: "GET /hr2/api/training-programs/" - expected_result: "Returns training programs" - alternate_paths: - - scenario: "List programs when none are available" - preconditions: "No programs available" - input_action: "GET /hr2/api/training-programs/" - expected_result: "Returns empty list" - exception_paths: - - scenario: "Unauthorized user tries to list programs" - preconditions: "User is not authenticated" - input_action: "GET /hr2/api/training-programs/" - expected_result: "Returns 401 Unauthorized" - - - id: "UC-HR2-030" - title: "Nominate for training" - description: "Employee submits training nomination" - actors: "Employee" - preconditions: "Authenticated employee" - happy_paths: - - scenario: "Submit training nomination" - preconditions: "Program exists" - input_action: "POST /hr2/api/training-nominations/ with program data" - expected_result: "Nomination created" - alternate_paths: - - scenario: "Nominate for mandatory program" - preconditions: "Program exists and is mandatory" - input_action: "POST /hr2/api/training-nominations/ with program data" - expected_result: "Nomination created" - exception_paths: - - scenario: "Invalid program" - preconditions: "Employee is authenticated" - input_action: "POST /hr2/api/training-nominations/ with invalid program" - expected_result: "Returns 400 validation errors" - - - id: "UC-HR2-031" - title: "View training nominations" - description: "Employee views training nominations" - actors: "Employee" - preconditions: "Authenticated employee" - happy_paths: - - scenario: "List nominations" - preconditions: "Nominations exist" - input_action: "GET /hr2/api/training-nominations/" - expected_result: "Returns training nominations" - alternate_paths: - - scenario: "List nominations after submission" - preconditions: "Nomination exists" - input_action: "GET /hr2/api/training-nominations/" - expected_result: "Returns training nominations" - exception_paths: - - scenario: "Unauthorized user tries to list nominations" - preconditions: "User is not authenticated" - input_action: "GET /hr2/api/training-nominations/" - expected_result: "Returns 401 Unauthorized" - - - id: "UC-HR2-032" - title: "Submit promotion application" - description: "Employee submits promotion application" - actors: "Employee" - preconditions: "Authenticated employee" - happy_paths: - - scenario: "Submit promotion" - preconditions: "Required fields provided" - input_action: "POST /hr2/api/promotions/ with required fields" - expected_result: "Promotion application created" - alternate_paths: - - scenario: "Submit promotion with API score" - preconditions: "Required fields provided" - input_action: "POST /hr2/api/promotions/ with api_score" - expected_result: "Promotion application created" - exception_paths: - - scenario: "Missing required fields" - preconditions: "Employee is authenticated" - input_action: "POST /hr2/api/promotions/ with incomplete payload" - expected_result: "Returns 400 validation errors" - - - id: "UC-HR2-033" - title: "View promotion applications" - description: "Employee views promotion applications" - actors: "Employee" - preconditions: "Authenticated employee" - happy_paths: - - scenario: "List promotions" - preconditions: "Applications exist" - input_action: "GET /hr2/api/promotions/" - expected_result: "Returns promotion applications" - alternate_paths: - - scenario: "List promotions when none exist" - preconditions: "No promotions exist" - input_action: "GET /hr2/api/promotions/" - expected_result: "Returns empty list" - exception_paths: - - scenario: "Unauthorized user tries to list promotions" - preconditions: "User is not authenticated" - input_action: "GET /hr2/api/promotions/" - expected_result: "Returns 401 Unauthorized" - - - id: "UC-HR2-034" - title: "View faculty workload" - description: "Faculty views workload" - actors: "Faculty" - preconditions: "Authenticated faculty" - happy_paths: - - scenario: "Get workload by semester" - preconditions: "Workload exists" - input_action: "GET /hr2/api/workload/?semester=Spring&year=2026" - expected_result: "Returns workload records" - alternate_paths: - - scenario: "Get workload without semester filter" - preconditions: "Workload exists" - input_action: "GET /hr2/api/workload/?year=2026" - expected_result: "Returns workload records" - exception_paths: - - scenario: "Unauthorized user tries to view workload" - preconditions: "User is not authenticated" - input_action: "GET /hr2/api/workload/" - expected_result: "Returns 401 Unauthorized" - - - id: "UC-HR2-035" - title: "Submit LTC update" - description: "Employee updates LTC application" - actors: "Employee" - preconditions: "Authenticated employee who owns the LTC" - happy_paths: - - scenario: "Update LTC details" - preconditions: "LTC belongs to employee" - input_action: "PUT /hr2/api/ltc/10/ with destination=updated" - expected_result: "LTC updated" - alternate_paths: - - scenario: "Update LTC purpose" - preconditions: "LTC belongs to employee" - input_action: "PUT /hr2/api/ltc/10/ with purpose_of_travel=updated" - expected_result: "LTC updated" - exception_paths: - - scenario: "Update another employee's LTC" - preconditions: "LTC belongs to another employee" - input_action: "PUT /hr2/api/ltc/10/" - expected_result: "Returns 403 Not authorized" - - - id: "UC-HR2-036" - title: "View LTC applications" - description: "View LTC list based on role" - actors: "Employee, HR staff, Accountant" - preconditions: "Authenticated user" - happy_paths: - - scenario: "Employee views own LTC" - preconditions: "Employee is authenticated" - input_action: "GET /hr2/api/ltc/" - expected_result: "Returns employee LTC applications" - - scenario: "HR staff views pending LTC" - preconditions: "User is HR staff" - input_action: "GET /hr2/api/ltc/" - expected_result: "Returns pending/forwarded LTC" - alternate_paths: - - scenario: "Accountant views forwarded LTC" - preconditions: "User is Accountant" - input_action: "GET /hr2/api/ltc/" - expected_result: "Returns forwarded LTC" - exception_paths: - - scenario: "Unauthorized user tries to list LTC" - preconditions: "User is not authenticated" - input_action: "GET /hr2/api/ltc/" - expected_result: "Returns 401 Unauthorized" - - - id: "UC-HR2-037" - title: "Download LTC application" - description: "Employee downloads LTC summary" - actors: "Employee" - preconditions: "Authenticated employee who owns the LTC" - happy_paths: - - scenario: "Download LTC" - preconditions: "LTC exists" - input_action: "GET /hr2/api/ltc/10/download/" - expected_result: "Returns text file attachment" - alternate_paths: - - scenario: "HR staff downloads LTC" - preconditions: "HR staff is authenticated" - input_action: "GET /hr2/api/ltc/10/download/" - expected_result: "Returns text file attachment" - exception_paths: - - scenario: "Unauthorized user tries to download LTC" - preconditions: "User is not authenticated" - input_action: "GET /hr2/api/ltc/10/download/" - expected_result: "Returns 401 Unauthorized" - - - id: "UC-HR2-038" - title: "Withdraw LTC application" - description: "Employee withdraws pending LTC" - actors: "Employee" - preconditions: "Authenticated employee" - happy_paths: - - scenario: "Withdraw pending LTC" - preconditions: "LTC approval_status is PENDING" - input_action: "POST /hr2/api/ltc/10/withdraw/" - expected_result: "LTC updated to WITHDRAWN" - alternate_paths: - - scenario: "Withdraw pending LTC with remarks" - preconditions: "LTC approval_status is PENDING" - input_action: "POST /hr2/api/ltc/10/withdraw/ with remarks=updated" - expected_result: "LTC updated to WITHDRAWN" - exception_paths: - - scenario: "Withdraw non-pending LTC" - preconditions: "LTC approval_status is APPROVED" - input_action: "POST /hr2/api/ltc/10/withdraw/" - expected_result: "Returns 400 with withdrawal error" - - - id: "UC-HR2-039" - title: "Approve or reject LTC" - description: "Approver updates LTC status" - actors: "HR staff, Accountant" - preconditions: "Authenticated approver" - happy_paths: - - scenario: "Forward LTC" - preconditions: "LTC exists" - input_action: "POST /hr2/api/ltc/10/forward/" - expected_result: "LTC status FORWARDED and accountant_status=PENDING" - - scenario: "Approve LTC" - preconditions: "LTC exists" - input_action: "POST /hr2/api/ltc/10/approve/" - expected_result: "LTC status APPROVED" - alternate_paths: - - scenario: "Reject LTC" - preconditions: "LTC exists" - input_action: "POST /hr2/api/ltc/10/reject/" - expected_result: "LTC status REJECTED" - exception_paths: - - scenario: "Invalid decision" - preconditions: "LTC exists" - input_action: "POST /hr2/api/ltc/10/invalid/" - expected_result: "Returns 400 invalid decision" - - - id: "UC-HR2-040" - title: "View CPDA advances" - description: "View CPDA advance list based on role" - actors: "Employee, HR staff, Accountant, Director" - preconditions: "Authenticated user" - happy_paths: - - scenario: "Employee views own advances" - preconditions: "Employee is authenticated" - input_action: "GET /hr2/api/cpda-advances/" - expected_result: "Returns employee CPDA advances" - alternate_paths: - - scenario: "HR staff views pending advances" - preconditions: "User is HR staff" - input_action: "GET /hr2/api/cpda-advances/" - expected_result: "Returns pending advances" - exception_paths: - - scenario: "Unauthorized user tries to list advances" - preconditions: "User is not authenticated" - input_action: "GET /hr2/api/cpda-advances/" - expected_result: "Returns 401 Unauthorized" - - - id: "UC-HR2-041" - title: "View CPDA advance details" - description: "Fetch a CPDA advance by ID" - actors: "Employee, HR staff" - preconditions: "Authenticated user" - happy_paths: - - scenario: "Fetch CPDA advance" - preconditions: "CPDA advance exists" - input_action: "GET /hr2/api/cpda-advances/10/" - expected_result: "Returns CPDA advance" - alternate_paths: - - scenario: "HR staff fetches CPDA advance" - preconditions: "HR staff is authenticated" - input_action: "GET /hr2/api/cpda-advances/10/" - expected_result: "Returns CPDA advance" - exception_paths: - - scenario: "CPDA advance not found" - preconditions: "CPDA advance does not exist" - input_action: "GET /hr2/api/cpda-advances/999/" - expected_result: "Returns 404 Not Found" - - - id: "UC-HR2-042" - title: "Download CPDA advance" - description: "Employee downloads CPDA advance summary" - actors: "Employee" - preconditions: "Authenticated employee who owns the CPDA" - happy_paths: - - scenario: "Download CPDA advance" - preconditions: "CPDA exists" - input_action: "GET /hr2/api/cpda-advances/10/download/" - expected_result: "Returns text file attachment" - alternate_paths: - - scenario: "HR staff downloads CPDA advance" - preconditions: "HR staff is authenticated" - input_action: "GET /hr2/api/cpda-advances/10/download/" - expected_result: "Returns text file attachment" - exception_paths: - - scenario: "Unauthorized user tries to download CPDA" - preconditions: "User is not authenticated" - input_action: "GET /hr2/api/cpda-advances/10/download/" - expected_result: "Returns 401 Unauthorized" - - - id: "UC-HR2-043" - title: "Withdraw CPDA advance" - description: "Employee withdraws pending CPDA advance" - actors: "Employee" - preconditions: "Authenticated employee" - happy_paths: - - scenario: "Withdraw pending CPDA advance" - preconditions: "CPDA approval_status is PENDING" - input_action: "POST /hr2/api/cpda-advances/10/withdraw/" - expected_result: "CPDA updated to WITHDRAWN" - alternate_paths: - - scenario: "Withdraw pending CPDA advance with remarks" - preconditions: "CPDA approval_status is PENDING" - input_action: "POST /hr2/api/cpda-advances/10/withdraw/ with remarks=updated" - expected_result: "CPDA updated to WITHDRAWN" - exception_paths: - - scenario: "Withdraw non-pending CPDA advance" - preconditions: "CPDA approval_status is APPROVED" - input_action: "POST /hr2/api/cpda-advances/10/withdraw/" - expected_result: "Returns 400 with withdrawal error" - - - id: "UC-HR2-044" - title: "Decide CPDA advance" - description: "HR/Accountant/Director updates CPDA advance status" - actors: "HR staff, Accountant, Director" - preconditions: "Authenticated approver" - happy_paths: - - scenario: "Forward CPDA to accountant" - preconditions: "CPDA exists" - input_action: "POST /hr2/api/cpda-advances/10/forward-accountant/" - expected_result: "Status FORWARDED and accountant_processing_status=PENDING" - - scenario: "Forward CPDA to director" - preconditions: "CPDA exists" - input_action: "POST /hr2/api/cpda-advances/10/forward-director/" - expected_result: "Status FORWARDED and accountant_processing_status=DIRECTOR_REVIEW" - - scenario: "Approve CPDA" - preconditions: "CPDA exists" - input_action: "POST /hr2/api/cpda-advances/10/approve/" - expected_result: "Status APPROVED" - alternate_paths: - - scenario: "Reject CPDA" - preconditions: "CPDA exists" - input_action: "POST /hr2/api/cpda-advances/10/reject/" - expected_result: "Status REJECTED" - exception_paths: - - scenario: "Invalid decision" - preconditions: "CPDA exists" - input_action: "POST /hr2/api/cpda-advances/10/invalid/" - expected_result: "Returns 400 invalid decision" - - - id: "UC-HR2-045" - title: "Submit CPDA reimbursement" - description: "Employee submits CPDA reimbursement request" - actors: "Employee" - preconditions: "Authenticated employee" - happy_paths: - - scenario: "Submit CPDA reimbursement" - preconditions: "Required fields provided" - input_action: "POST /hr2/api/cpda-reimbursements/ with required fields" - expected_result: "CPDA reimbursement created" - alternate_paths: - - scenario: "Submit CPDA reimbursement with optional expenses" - preconditions: "Required fields provided" - input_action: "POST /hr2/api/cpda-reimbursements/ with optional fields" - expected_result: "CPDA reimbursement created" - exception_paths: - - scenario: "Missing required fields" - preconditions: "Employee is authenticated" - input_action: "POST /hr2/api/cpda-reimbursements/ with incomplete payload" - expected_result: "Returns 400 validation errors" - - - id: "UC-HR2-046" - title: "View CPDA reimbursements" - description: "Employee views CPDA reimbursement list" - actors: "Employee" - preconditions: "Authenticated employee" - happy_paths: - - scenario: "List CPDA reimbursements" - preconditions: "Requests exist" - input_action: "GET /hr2/api/cpda-reimbursements/" - expected_result: "Returns CPDA reimbursements" - alternate_paths: - - scenario: "List CPDA reimbursements when none exist" - preconditions: "No requests exist" - input_action: "GET /hr2/api/cpda-reimbursements/" - expected_result: "Returns empty list" - exception_paths: - - scenario: "Unauthorized user tries to list reimbursements" - preconditions: "User is not authenticated" - input_action: "GET /hr2/api/cpda-reimbursements/" - expected_result: "Returns 401 Unauthorized" - - - id: "UC-HR2-047" - title: "View CPDA reimbursement details" - description: "Fetch a CPDA reimbursement by ID" - actors: "Employee" - preconditions: "Authenticated employee" - happy_paths: - - scenario: "Fetch CPDA reimbursement" - preconditions: "CPDA reimbursement exists" - input_action: "GET /hr2/api/cpda-reimbursements/10/" - expected_result: "Returns CPDA reimbursement" - alternate_paths: - - scenario: "HR staff fetches reimbursement" - preconditions: "HR staff is authenticated" - input_action: "GET /hr2/api/cpda-reimbursements/10/" - expected_result: "Returns CPDA reimbursement" - exception_paths: - - scenario: "CPDA reimbursement not found" - preconditions: "CPDA reimbursement does not exist" - input_action: "GET /hr2/api/cpda-reimbursements/999/" - expected_result: "Returns 404 Not Found" - - - id: "UC-HR2-048" - title: "Decide CPDA reimbursement" - description: "Approver approves or rejects CPDA reimbursement" - actors: "Accountant, HR staff" - preconditions: "Authenticated approver" - happy_paths: - - scenario: "Approve CPDA reimbursement" - preconditions: "Request exists" - input_action: "POST /hr2/api/cpda-reimbursements/10/approve/" - expected_result: "Reimbursement approved" - - scenario: "Reject CPDA reimbursement" - preconditions: "Request exists" - input_action: "POST /hr2/api/cpda-reimbursements/10/reject/" - expected_result: "Reimbursement rejected" - alternate_paths: - - scenario: "Reject CPDA reimbursement with remarks" - preconditions: "Request exists" - input_action: "POST /hr2/api/cpda-reimbursements/10/reject/ with remarks=invalid" - expected_result: "Reimbursement rejected" - exception_paths: - - scenario: "Invalid decision" - preconditions: "Request exists" - input_action: "POST /hr2/api/cpda-reimbursements/10/invalid/" - expected_result: "Returns 400 invalid decision" - - - id: "UC-HR2-049" - title: "List appraisal forms" - description: "View appraisal forms based on role" - actors: "Employee, HR staff, HOD, Director" - preconditions: "Authenticated user" - happy_paths: - - scenario: "Employee views own appraisal forms" - preconditions: "Employee is authenticated" - input_action: "GET /hr2/api/appraisal-forms/" - expected_result: "Returns employee appraisal forms" - - scenario: "Director views reviewed forms" - preconditions: "User is Director" - input_action: "GET /hr2/api/appraisal-forms/" - expected_result: "Returns reviewed appraisal forms" - alternate_paths: - - scenario: "HR staff views all appraisal forms" - preconditions: "User is HR staff" - input_action: "GET /hr2/api/appraisal-forms/" - expected_result: "Returns appraisal forms" - exception_paths: - - scenario: "Unauthorized user tries to list appraisal forms" - preconditions: "User is not authenticated" - input_action: "GET /hr2/api/appraisal-forms/" - expected_result: "Returns 401 Unauthorized" - - - id: "UC-HR2-050" - title: "View appraisal form details" - description: "Fetch appraisal form by ID" - actors: "Employee, HR staff" - preconditions: "Authenticated user" - happy_paths: - - scenario: "Fetch appraisal form" - preconditions: "Form exists" - input_action: "GET /hr2/api/appraisal-forms/10/" - expected_result: "Returns appraisal form" - alternate_paths: - - scenario: "HR staff fetches appraisal form" - preconditions: "HR staff is authenticated" - input_action: "GET /hr2/api/appraisal-forms/10/" - expected_result: "Returns appraisal form" - exception_paths: - - scenario: "Appraisal form not found" - preconditions: "Form does not exist" - input_action: "GET /hr2/api/appraisal-forms/999/" - expected_result: "Returns 404 Not Found" - - - id: "UC-HR2-051" - title: "Download appraisal form" - description: "Employee downloads appraisal form" - actors: "Employee" - preconditions: "Authenticated employee who owns the form" - happy_paths: - - scenario: "Download appraisal form" - preconditions: "Form exists" - input_action: "GET /hr2/api/appraisal-forms/10/download/" - expected_result: "Returns text file attachment" - alternate_paths: - - scenario: "HR staff downloads appraisal form" - preconditions: "HR staff is authenticated" - input_action: "GET /hr2/api/appraisal-forms/10/download/" - expected_result: "Returns text file attachment" - exception_paths: - - scenario: "Unauthorized user tries to download appraisal form" - preconditions: "User is not authenticated" - input_action: "GET /hr2/api/appraisal-forms/10/download/" - expected_result: "Returns 401 Unauthorized" - - - id: "UC-HR2-052" - title: "Review appraisal form" - description: "Reviewer forwards or approves appraisal form" - actors: "HOD, Director" - preconditions: "Authenticated reviewer" - happy_paths: - - scenario: "Forward appraisal for director" - preconditions: "Reviewer assigned" - input_action: "POST /hr2/api/appraisal-forms/10/review/ with action=forward" - expected_result: "Appraisal status set to REVIEWED" - - scenario: "Approve appraisal" - preconditions: "Reviewer assigned" - input_action: "POST /hr2/api/appraisal-forms/10/review/ with action=approve" - expected_result: "Appraisal status set to APPROVED" - alternate_paths: - - scenario: "Review appraisal with rating" - preconditions: "Reviewer assigned" - input_action: "POST /hr2/api/appraisal-forms/10/review/ with action=forward, rating=4" - expected_result: "Appraisal status set to REVIEWED" - exception_paths: - - scenario: "Invalid review action" - preconditions: "Reviewer assigned" - input_action: "POST /hr2/api/appraisal-forms/10/review/ with action=invalid" - expected_result: "Returns 400 invalid action" diff --git a/FusionIIIT/applications/hr2/tests/specs/workflows.yaml b/FusionIIIT/applications/hr2/tests/specs/workflows.yaml deleted file mode 100644 index fbd0c0924..000000000 --- a/FusionIIIT/applications/hr2/tests/specs/workflows.yaml +++ /dev/null @@ -1,103 +0,0 @@ -workflows: - - id: "WF-HR2-001" - title: "Leave application approval flow" - description: "Employee applies → HOD/Registrar/Director decides → balance updates on approval" - e2e_tests: - - scenario: "Employee applies and leave is approved" - expected_final_state: "approval_status=APPROVED, leave balance reduced" - - scenario: "Employee applies and leave is forwarded then approved" - expected_final_state: "approval_status=APPROVED, current_approver_role=Director" - negative_tests: - - scenario: "Employee applies and leave is rejected" - expected_final_state: "approval_status=REJECTED, leave balance unchanged" - - - id: "WF-HR2-002" - title: "Leave withdrawal flow" - description: "Employee withdraws pending/forwarded leave" - e2e_tests: - - scenario: "Employee withdraws pending leave" - expected_final_state: "approval_status=WITHDRAWN" - negative_tests: - - scenario: "Employee tries to withdraw approved leave" - expected_final_state: "Request rejected with withdrawal error" - - - id: "WF-HR2-003" - title: "Leave cancellation flow" - description: "Employee requests cancellation → approver decides → balance restored on approval" - e2e_tests: - - scenario: "Cancellation approved" - expected_final_state: "cancel_status=APPROVED, approval_status=CANCELLED, leave balance restored" - negative_tests: - - scenario: "Cancellation requested after start date" - expected_final_state: "Request rejected with cancellation window error" - - - id: "WF-HR2-004" - title: "Leave extension flow" - description: "Employee requests extension → approver decides → balance adjusted on approval" - e2e_tests: - - scenario: "Extension approved with sufficient balance" - expected_final_state: "extension_status=APPROVED, end_date updated, balance reduced" - negative_tests: - - scenario: "Extension approved with insufficient balance" - expected_final_state: "Request rejected with insufficient balance error" - - - id: "WF-HR2-005" - title: "Nominee response flow" - description: "Nominee sees request → accepts or declines" - e2e_tests: - - scenario: "Nominee accepts" - expected_final_state: "nominee_status=ACCEPTED" - negative_tests: - - scenario: "Non-nominee responds" - expected_final_state: "Request rejected with 403" - - - id: "WF-HR2-006" - title: "Document request flow" - description: "HOD requests documents → employee submits" - e2e_tests: - - scenario: "HOD requests, employee submits" - expected_final_state: "document_request_status=SUBMITTED" - negative_tests: - - scenario: "Employee submits without request" - expected_final_state: "Request rejected with no request error" - - - id: "WF-HR2-007" - title: "LTC approval flow" - description: "Employee applies → HR forwards → accountant approves/rejects" - e2e_tests: - - scenario: "LTC forwarded and approved" - expected_final_state: "approval_status=APPROVED, accountant_status=APPROVED" - negative_tests: - - scenario: "LTC rejected" - expected_final_state: "approval_status=REJECTED" - - - id: "WF-HR2-008" - title: "CPDA advance approval flow" - description: "Employee applies → HR forwards → accountant/director decides" - e2e_tests: - - scenario: "HR forwards to accountant and approves" - expected_final_state: "approval_status=APPROVED, accountant_processing_status=APPROVED" - - scenario: "Forwarded to director then approved" - expected_final_state: "accountant_processing_status=DIRECTOR_APPROVED, approval_status=FORWARDED" - negative_tests: - - scenario: "CPDA advance rejected" - expected_final_state: "approval_status=REJECTED" - - - id: "WF-HR2-009" - title: "CPDA reimbursement decision flow" - description: "Employee submits reimbursement → approver approves/rejects" - e2e_tests: - - scenario: "CPDA reimbursement approved" - expected_final_state: "approval_status=APPROVED" - negative_tests: - - scenario: "CPDA reimbursement rejected" - expected_final_state: "approval_status=REJECTED" - - - id: "WF-HR2-010" - title: "Appraisal form review flow" - description: "Employee submits → HOD reviews → Director approves" - e2e_tests: - - scenario: "HOD forwards to director" - expected_final_state: "status=REVIEWED" - - scenario: "Director approves" - expected_final_state: "status=APPROVED" diff --git a/FusionIIIT/applications/hr2/tests/test_business_rules.py b/FusionIIIT/applications/hr2/tests/test_business_rules.py deleted file mode 100644 index eab68c038..000000000 --- a/FusionIIIT/applications/hr2/tests/test_business_rules.py +++ /dev/null @@ -1,336 +0,0 @@ -import os -import re -from typing import Any, Dict, Optional, Tuple - -import yaml - -from .conftest import BRTestBase - - -class HR2BRTestBase(BRTestBase): - """Dynamic BR tests generated from specs/business_rules.yaml.""" - - _created_ids: Dict[str, int] = {} - - def _login_for_action(self, text: str) -> None: - normalized = text.lower() - if "not authenticated" in normalized or "unauthorized" in normalized: - self.logout() - return - - if "director" in normalized: - self.login_as_director() - elif "registrar" in normalized: - self.login_as_registrar() - elif "hod" in normalized: - self.login_as_hod() - elif "accountant" in normalized: - self.login_as_accountant() - elif "hr" in normalized or "staff" in normalized: - self.login_as_staff() - else: - self.login_as_employee() - - def _extract_id(self, data: Any) -> Optional[int]: - if isinstance(data, dict): - for key in ("id", "pk", "leave_id", "ltc_id", "cpda_id", "appraisal_id"): - value = data.get(key) - if isinstance(value, int): - return value - return None - - def _create_resource(self, endpoint: str, payload: Dict[str, Any]) -> int: - response = self.api_post(endpoint, payload, expected_status=None) - if response.status_code in {200, 201}: - extracted = self._extract_id(getattr(response, "data", {})) - if extracted is not None: - return extracted - return 1 - - def _ensure_leave_id(self) -> int: - if "leave" not in self._created_ids: - self.login_as_employee() - payload = { - "leave_type": "Casual", - "start_date": self.future_date(3), - "end_date": self.future_date(4), - "total_days": 2, - "reason": "Personal work", - "contact_during_leave": "9876543210", - "address_during_leave": "Jabalpur, MP", - } - self._created_ids["leave"] = self._create_resource( - "/hr2/api/leave-applications/", payload - ) - return self._created_ids["leave"] - - def _ensure_ltc_id(self) -> int: - if "ltc" not in self._created_ids: - self.login_as_employee() - payload = { - "employee_name": "Rahul Sharma", - "department": "Computer Science and Engineering", - "designation": "Assistant Professor", - "ltc_block_year": 2025, - "travel_start_date": self.future_date(10), - "travel_end_date": self.future_date(15), - "destination": "Delhi", - "purpose_of_travel": "Family travel", - "travel_mode": "Train", - "total_amount_claimed": 22000, - } - self._created_ids["ltc"] = self._create_resource("/hr2/api/ltc/", payload) - return self._created_ids["ltc"] - - def _ensure_cpda_advance_id(self) -> int: - if "cpda_advance" not in self._created_ids: - self.login_as_employee() - payload = { - "employee_name": "Rahul Sharma", - "department": "Computer Science and Engineering", - "designation": "Assistant Professor", - "event_name": "National Conference on AI", - "event_type": "Conference", - "start_date": self.future_date(30), - "end_date": self.future_date(32), - "total_amount": 20000, - "purpose_of_attending": "Present paper", - "benefits_to_institution": "Research exposure", - } - self._created_ids["cpda_advance"] = self._create_resource( - "/hr2/api/cpda-advances/", payload - ) - return self._created_ids["cpda_advance"] - - def _ensure_appraisal_form_id(self) -> int: - if "appraisal_form" not in self._created_ids: - self.login_as_employee() - payload = { - "employee_name": "Rahul Sharma", - "department": "Computer Science and Engineering", - "designation": "Assistant Professor", - "appraisal_year": "2025-2026", - "self_summary": "Completed teaching responsibilities", - "key_responsibilities": "Teaching and research", - "achievements": "Published 1 paper", - "goals_achieved": "Completed syllabus", - "future_goals": "Publish more papers", - } - self._created_ids["appraisal_form"] = self._create_resource( - "/hr2/api/appraisal-forms/", payload - ) - return self._created_ids["appraisal_form"] - - def _resolve_path(self, path: str) -> str: - if "/leave-applications/" in path: - leave_id = self._ensure_leave_id() - return re.sub(r"/leave-applications/\d+", f"/leave-applications/{leave_id}", path) - if "/ltc/" in path: - ltc_id = self._ensure_ltc_id() - return re.sub(r"/ltc/\d+", f"/ltc/{ltc_id}", path) - if "/cpda-advances/" in path: - cpda_id = self._ensure_cpda_advance_id() - return re.sub(r"/cpda-advances/\d+", f"/cpda-advances/{cpda_id}", path) - if "/appraisal-forms/" in path: - appraisal_id = self._ensure_appraisal_form_id() - return re.sub(r"/appraisal-forms/\d+", f"/appraisal-forms/{appraisal_id}", path) - return path - - def _payload_for(self, path: str, action_text: str, valid_case: bool) -> Dict[str, Any]: - action_lower = action_text.lower() - payload: Dict[str, Any] = {} - - day_match = re.search(r"(\d+)\s*day", action_lower) - requested_days = int(day_match.group(1)) if day_match else None - - start_date = self.future_date(1) if valid_case else self.past_date(1) - total_days = requested_days or (3 if valid_case else 2) - end_date = self.future_date((total_days or 1) - 1) if valid_case else self.future_date(1) - - if "/leave-applications/" in path and path.endswith("/leave-applications/"): - if "total_days" in action_lower and "mismatch" in action_lower: - total_days = 2 if valid_case else 1 - payload = { - "leave_type": "Casual", - "start_date": start_date, - "end_date": end_date, - "total_days": total_days, - "reason": "Personal work", - "contact_during_leave": "9876543210", - "address_during_leave": "Jabalpur, MP", - } - if "nominee" in action_lower: - payload["nominee_employee_id"] = self.nominee_extra.id if valid_case else 999999 - elif path.endswith("/withdraw/"): - payload = {} - elif path.endswith("/cancel-request/"): - payload = {"reason": "Change of plan"} - elif path.endswith("/extension-request/"): - payload = {"new_end_date": self.future_date(6)} - elif path.endswith("/request-document/"): - payload = {"message": "Submit proof"} if valid_case else {} - elif path.endswith("/submit-document/"): - payload = {"submission": "doc-ref"} - elif path.endswith("/leave-nominee/") or "/leave-nominee/" in path: - payload = {"action": "accept" if valid_case else "invalid"} - elif path.endswith("/ltc/"): - payload = { - "employee_name": "Rahul Sharma", - "department": "Computer Science and Engineering", - "designation": "Assistant Professor", - "ltc_block_year": 2025, - "travel_start_date": self.future_date(10), - "travel_end_date": self.future_date(15), - "destination": "Delhi", - "purpose_of_travel": "Family travel", - "travel_mode": "Train", - "total_amount_claimed": 22000, - } - elif path.endswith("/cpda-advances/"): - payload = { - "employee_name": "Rahul Sharma", - "department": "Computer Science and Engineering", - "designation": "Assistant Professor", - "event_name": "National Conference on AI", - "event_type": "Conference", - "start_date": self.future_date(30), - "end_date": self.future_date(32), - "total_amount": 20000, - "purpose_of_attending": "Present paper", - "benefits_to_institution": "Research exposure", - } - elif path.endswith("/appraisal-forms/"): - payload = { - "employee_name": "Rahul Sharma", - "department": "Computer Science and Engineering", - "designation": "Assistant Professor", - "appraisal_year": "2025-2026", - "self_summary": "Completed teaching responsibilities", - "key_responsibilities": "Teaching and research", - "achievements": "Published 1 paper", - "goals_achieved": "Completed syllabus", - "future_goals": "Publish more papers", - } - elif path.endswith("/review/"): - payload = {"action": "approve" if valid_case else "invalid"} - - return payload - - def _endpoint_for_action(self, action_text: str) -> str: - action_lower = action_text.lower() - if "ltc" in action_lower: - if "withdraw" in action_lower: - return "/hr2/api/ltc/1/withdraw/" - if "decision" in action_lower or "approve" in action_lower or "forward" in action_lower: - return "/hr2/api/ltc/1/forward/" - return "/hr2/api/ltc/" - if "cpda" in action_lower: - if "advance" in action_lower: - if "withdraw" in action_lower: - return "/hr2/api/cpda-advances/1/withdraw/" - if "decision" in action_lower or "approve" in action_lower or "forward" in action_lower: - return "/hr2/api/cpda-advances/1/forward-accountant/" - return "/hr2/api/cpda-advances/" - if "appraisal" in action_lower and "review" in action_lower: - return "/hr2/api/appraisal-forms/1/review/" - if "document" in action_lower and "request" in action_lower: - return "/hr2/api/leave-applications/1/request-document/" - if "document" in action_lower and "submit" in action_lower: - return "/hr2/api/leave-applications/1/submit-document/" - if "nominee" in action_lower: - return "/hr2/api/leave-nominee/1/" - if "extension" in action_lower: - return "/hr2/api/leave-applications/1/extension-request/" - if "cancellation" in action_lower or "cancel" in action_lower: - return "/hr2/api/leave-applications/1/cancel-request/" - if "withdraw" in action_lower: - return "/hr2/api/leave-applications/1/withdraw/" - if "download" in action_lower: - return "/hr2/api/leave-applications/1/download/" - if "leave" in action_lower: - return "/hr2/api/leave-applications/" - return "/hr2/api/leave-applications/" - - def _method_for_action(self, action_text: str) -> str: - action_lower = action_text.lower() - if "download" in action_lower: - return "GET" - if "apply" in action_lower or "request" in action_lower or "submit" in action_lower: - return "POST" - if "withdraw" in action_lower or "approve" in action_lower or "reject" in action_lower or "forward" in action_lower: - return "POST" - return "POST" - - def _dispatch(self, method: str, path: str, payload: Dict[str, Any]): - if method == "POST": - return self.api_post(path, payload, expected_status=None) - if method == "PUT": - return self.api_put(path, payload, expected_status=None) - if method == "DELETE": - return self.api_delete(path, expected_status=None) - return self.api_get(path, expected_status=None) - - -def _load_business_rules() -> Dict[str, Any]: - specs_path = os.path.join(os.path.dirname(__file__), "specs", "business_rules.yaml") - with open(specs_path, "r", encoding="utf-8") as handle: - return yaml.safe_load(handle) or {} - - -def _slugify(text: str) -> str: - slug = re.sub(r"[^a-zA-Z0-9]+", "_", text).strip("_") - return slug.lower() or "rule" - - -def _expected_statuses(valid_case: bool) -> Tuple[int, ...]: - return (200, 201) if valid_case else (400, 401, 403, 404, 302) - - -def _build_test(rule: Dict[str, Any], case: Dict[str, Any], valid_case: bool, index: int): - def _test(self: HR2BRTestBase): - suffix = "V" if valid_case else "I" - self._test_id = f"{rule.get('id')}-{suffix}-{index:02d}" - self._br_id = rule.get("id") - self._test_category = "Valid" if valid_case else "Invalid" - self._input_action = case.get("input_action", "") - self._expected_result = case.get("expected_result", "") - - self._login_for_action(self._input_action) - method = self._method_for_action(self._input_action) - path = self._endpoint_for_action(self._input_action) - path = self._resolve_path(path) - payload = self._payload_for(path, self._input_action, valid_case) - response = self._dispatch(method, path, payload) - expected = _expected_statuses(valid_case) - - if response.status_code in expected: - self._record_result("Expected response", "Pass", str(getattr(response, "data", ""))) - else: - self._record_result( - f"Unexpected status {response.status_code}", - "Fail", - str(getattr(response, "data", "")), - ) - self.fail(f"Expected status in {expected}, got {response.status_code}") - - return _test - - -def _generate_br_tests(): - specs = _load_business_rules() - rules = specs.get("business_rules", []) - for rule in rules: - class_name = f"Test_{rule.get('id', 'BR')}_{_slugify(rule.get('title', 'rule'))}" - attrs: Dict[str, Any] = {"__doc__": f"{rule.get('id')}: {rule.get('title')}"} - - for valid_case, key in ((True, "valid_tests"), (False, "invalid_tests")): - cases = rule.get(key, []) or [] - for index, case in enumerate(cases, start=1): - prefix = "valid" if valid_case else "invalid" - name = f"test_{prefix}_{index:02d}_{_slugify(case.get('input_action', 'case'))}" - attrs[name] = _build_test(rule, case, valid_case, index) - - globals()[class_name] = type(class_name, (HR2BRTestBase,), attrs) - - -_generate_br_tests() diff --git a/FusionIIIT/applications/hr2/tests/test_module.py b/FusionIIIT/applications/hr2/tests/test_module.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/FusionIIIT/applications/hr2/tests/test_use_cases.py b/FusionIIIT/applications/hr2/tests/test_use_cases.py deleted file mode 100644 index ba8d6e2dc..000000000 --- a/FusionIIIT/applications/hr2/tests/test_use_cases.py +++ /dev/null @@ -1,563 +0,0 @@ -import os -import re -from typing import Any, Dict, Optional, Tuple - -import yaml - -from .conftest import UCTestBase - - -class HR2UCTestBase(UCTestBase): - """Dynamic UC tests generated from specs/use_cases.yaml.""" - - _created_ids: Dict[str, int] = {} - - def _login_for_context(self, text: str) -> None: - normalized = text.lower() - if "not authenticated" in normalized or "unauthorized" in normalized: - self.logout() - return - - if "director" in normalized: - self.login_as_director() - elif "registrar" in normalized: - self.login_as_registrar() - elif "hod" in normalized: - self.login_as_hod() - elif "accountant" in normalized: - self.login_as_accountant() - elif "nominee" in normalized: - self.login_as_nominee() - elif "hr" in normalized or "staff" in normalized: - self.login_as_staff() - else: - self.login_as_employee() - - def _parse_action(self, input_action: str) -> Tuple[str, str]: - match = re.search(r"\b(GET|POST|PUT|DELETE)\b\s+([^\s]+)", input_action) - if not match: - return "GET", "/" - method = match.group(1).upper() - path = match.group(2) - if not path.startswith("/"): - path = f"/{path}" - return method, path - - def _extract_id(self, data: Any) -> Optional[int]: - if isinstance(data, dict): - for key in ("id", "pk", "leave_id", "ltc_id", "cpda_id", "appraisal_id"): - value = data.get(key) - if isinstance(value, int): - return value - return None - - def _create_resource(self, endpoint: str, payload: Dict[str, Any]) -> int: - response = self.api_post(endpoint, payload, expected_status=None) - if response.status_code in {200, 201}: - extracted = self._extract_id(getattr(response, "data", {})) - if extracted is not None: - return extracted - return 1 - - def _ensure_leave_id(self) -> int: - if "leave" not in self._created_ids: - self.login_as_employee() - payload = { - "leave_type": "Casual", - "start_date": self.future_date(3), - "end_date": self.future_date(4), - "total_days": 2, - "reason": "Personal work", - "contact_during_leave": "9876543210", - "address_during_leave": "Jabalpur, MP", - } - self._created_ids["leave"] = self._create_resource( - "/hr2/api/leave-applications/", payload - ) - return self._created_ids["leave"] - - def _ensure_approved_leave_id(self) -> int: - leave_id = self._ensure_leave_id() - self.login_as_director() - self.api_post( - f"/hr2/api/leave-applications/{leave_id}/approve/", - {"remarks": "Approved"}, - expected_status=None, - ) - return leave_id - - def _ensure_leave_with_nominee_id(self) -> int: - if "leave_nominee" not in self._created_ids: - self.login_as_employee() - payload = { - "leave_type": "Casual", - "start_date": self.future_date(3), - "end_date": self.future_date(4), - "total_days": 2, - "reason": "Personal work", - "contact_during_leave": "9876543210", - "address_during_leave": "Jabalpur, MP", - "nominee_employee_id": self.nominee_extra.id, - } - self._created_ids["leave_nominee"] = self._create_resource( - "/hr2/api/leave-applications/", payload - ) - return self._created_ids["leave_nominee"] - - def _ensure_ltc_id(self) -> int: - if "ltc" not in self._created_ids: - self.login_as_employee() - payload = { - "employee_name": "Rahul Sharma", - "department": "Computer Science and Engineering", - "designation": "Assistant Professor", - "ltc_block_year": 2025, - "travel_start_date": self.future_date(10), - "travel_end_date": self.future_date(15), - "destination": "Delhi", - "purpose_of_travel": "Family travel", - "travel_mode": "Train", - "total_amount_claimed": 22000, - } - self._created_ids["ltc"] = self._create_resource("/hr2/api/ltc/", payload) - return self._created_ids["ltc"] - - def _ensure_cpda_advance_id(self) -> int: - if "cpda_advance" not in self._created_ids: - self.login_as_employee() - payload = { - "employee_name": "Rahul Sharma", - "department": "Computer Science and Engineering", - "designation": "Assistant Professor", - "event_name": "National Conference on AI", - "event_type": "Conference", - "start_date": self.future_date(30), - "end_date": self.future_date(32), - "total_amount": 20000, - "purpose_of_attending": "Present paper", - "benefits_to_institution": "Research exposure", - } - self._created_ids["cpda_advance"] = self._create_resource( - "/hr2/api/cpda-advances/", payload - ) - return self._created_ids["cpda_advance"] - - def _ensure_cpda_reimbursement_id(self) -> int: - if "cpda_reimbursement" not in self._created_ids: - self.login_as_employee() - payload = { - "employee_name": "Rahul Sharma", - "department": "Computer Science and Engineering", - "designation": "Assistant Professor", - "event_name": "National Conference on AI", - "event_type": "Conference", - "start_date": self.future_date(30), - "end_date": self.future_date(32), - "total_amount": 20000, - "purpose_of_attending": "Present paper", - "benefits_to_institution": "Research exposure", - } - self._created_ids["cpda_reimbursement"] = self._create_resource( - "/hr2/api/cpda-reimbursements/", payload - ) - return self._created_ids["cpda_reimbursement"] - - def _ensure_appraisal_form_id(self) -> int: - if "appraisal_form" not in self._created_ids: - self.login_as_employee() - payload = { - "employee_name": "Rahul Sharma", - "department": "Computer Science and Engineering", - "designation": "Assistant Professor", - "appraisal_year": "2025-2026", - "self_summary": "Completed teaching responsibilities", - "key_responsibilities": "Teaching and research", - "achievements": "Published 1 paper", - "goals_achieved": "Completed syllabus", - "future_goals": "Publish more papers", - } - self._created_ids["appraisal_form"] = self._create_resource( - "/hr2/api/appraisal-forms/", payload - ) - return self._created_ids["appraisal_form"] - - def _resolve_path(self, path: str) -> str: - if "/leave-applications/" in path: - leave_id = self._ensure_leave_id() - return re.sub(r"/leave-applications/\d+", f"/leave-applications/{leave_id}", path) - if "/leave-balance/" in path: - return re.sub( - r"/leave-balance/\d+", - f"/leave-balance/{self.employee_extra.id}", - path, - ) - if "/employees/" in path: - return re.sub(r"/employees/\d+", f"/employees/{self.employee_extra.id}", path) - if "/ltc/" in path: - ltc_id = self._ensure_ltc_id() - return re.sub(r"/ltc/\d+", f"/ltc/{ltc_id}", path) - if "/cpda-advances/" in path: - cpda_id = self._ensure_cpda_advance_id() - return re.sub(r"/cpda-advances/\d+", f"/cpda-advances/{cpda_id}", path) - if "/cpda-reimbursements/" in path: - cpda_id = self._ensure_cpda_reimbursement_id() - return re.sub(r"/cpda-reimbursements/\d+", f"/cpda-reimbursements/{cpda_id}", path) - if "/appraisal-forms/" in path: - appraisal_id = self._ensure_appraisal_form_id() - return re.sub(r"/appraisal-forms/\d+", f"/appraisal-forms/{appraisal_id}", path) - return path - - def _payload_for(self, path: str, scenario: str) -> Dict[str, Any]: - payload: Dict[str, Any] = {} - scenario_lower = scenario.lower() - - if "/leave-applications/" in path and path.endswith("/leave-applications/"): - leave_type = "Casual" - if "vacation" in scenario_lower: - leave_type = "Vacation" - payload = { - "leave_type": leave_type, - "start_date": self.future_date(2), - "end_date": self.future_date(4), - "total_days": 3, - "reason": "Personal work", - "contact_during_leave": "9876543210", - "address_during_leave": "Jabalpur, MP", - } - if "nominee" in scenario_lower: - payload["nominee_employee_id"] = self.nominee_extra.id - elif "/leave-applications/" in path and path.endswith("/cancel-request/"): - payload = {"reason": "Change of plan"} - elif "/leave-applications/" in path and path.endswith("/extension-request/"): - payload = {"new_end_date": self.future_date(6), "reason": "Medical"} - elif "/leave-applications/" in path and path.endswith("/request-document/"): - payload = {"message": "Submit proof"} - elif "/leave-applications/" in path and path.endswith("/submit-document/"): - payload = {"submission": "doc-ref"} - elif "/leave-applications/" in path and path.endswith("/cancel-decision/approve/"): - payload = {"remarks": "Approved"} - elif "/leave-applications/" in path and path.endswith("/extension-decision/approve/"): - payload = {"remarks": "Approved"} - elif "/leave-nominee/" in path: - if "decline" in scenario_lower: - payload = {"action": "decline"} - else: - payload = {"action": "accept"} - elif "/attendance/" in path and path.endswith("/attendance/"): - status = "PRESENT" if "half" not in scenario_lower else "HALF_DAY" - payload = {"date": self.today(), "status": status} - elif "/ltc/" in path and path.endswith("/ltc/"): - payload = { - "employee_name": "Rahul Sharma", - "department": "Computer Science and Engineering", - "designation": "Assistant Professor", - "ltc_block_year": 2025, - "travel_start_date": self.future_date(10), - "travel_end_date": self.future_date(15), - "destination": "Delhi", - "purpose_of_travel": "Family travel", - "travel_mode": "Train", - "total_amount_claimed": 22000, - } - elif "/cpda-advances/" in path and path.endswith("/cpda-advances/"): - payload = { - "employee_name": "Rahul Sharma", - "department": "Computer Science and Engineering", - "designation": "Assistant Professor", - "event_name": "National Conference on AI", - "event_type": "Conference", - "start_date": self.future_date(30), - "end_date": self.future_date(32), - "total_amount": 20000, - "purpose_of_attending": "Present paper", - "benefits_to_institution": "Research exposure", - } - elif "/cpda-reimbursements/" in path and path.endswith("/cpda-reimbursements/"): - payload = { - "employee_name": "Rahul Sharma", - "department": "Computer Science and Engineering", - "designation": "Assistant Professor", - "event_name": "National Conference on AI", - "event_type": "Conference", - "start_date": self.future_date(30), - "end_date": self.future_date(32), - "total_amount": 20000, - "purpose_of_attending": "Present paper", - "benefits_to_institution": "Research exposure", - } - elif "/appraisal-forms/" in path and path.endswith("/appraisal-forms/"): - payload = { - "employee_name": "Rahul Sharma", - "department": "Computer Science and Engineering", - "designation": "Assistant Professor", - "appraisal_year": "2025-2026", - "self_summary": "Completed teaching responsibilities", - "key_responsibilities": "Teaching and research", - "achievements": "Published 1 paper", - "goals_achieved": "Completed syllabus", - "future_goals": "Publish more papers", - } - elif "/appraisals/" in path and path.endswith("/appraisals/"): - payload = { - "period": self.appraisal_period.id, - "teaching_score": 4, - "research_score": 4, - "admin_score": 3, - } - elif "/training-nominations/" in path: - payload = {"program": self.training_program.id} - elif "/promotions/" in path: - payload = { - "current_designation": self.promotion_current_designation.id, - "applied_designation": self.promotion_applied_designation.id, - "application_date": self.today(), - "eligibility_date": self.today(), - "api_score": 8, - } - elif "/employees/" in path: - payload = {"phone_number": "9876543210", "full_address": "Updated"} - - return payload - - def _dispatch(self, method: str, path: str, payload: Dict[str, Any]): - if method == "POST": - return self.api_post(path, payload, expected_status=None) - if method == "PUT": - return self.api_put(path, payload, expected_status=None) - if method == "DELETE": - return self.api_delete(path, expected_status=None) - return self.api_get(path, expected_status=None) - - def _prepare_state_for(self, path: str, method: str) -> str: - if "/leave-applications/" in path and path.endswith("/cancel-request/"): - leave_id = self._ensure_approved_leave_id() - self.login_as_employee() - return f"/hr2/api/leave-applications/{leave_id}/cancel-request/" - - if "/leave-applications/" in path and path.endswith("/extension-request/"): - leave_id = self._ensure_approved_leave_id() - self.login_as_employee() - return f"/hr2/api/leave-applications/{leave_id}/extension-request/" - - if "/leave-applications/" in path and "/cancel-decision/" in path: - leave_id = self._ensure_approved_leave_id() - self.login_as_employee() - self.api_post( - f"/hr2/api/leave-applications/{leave_id}/cancel-request/", - {"reason": "Change of plan"}, - expected_status=None, - ) - self.login_as_director() - return re.sub( - r"/leave-applications/\d+/cancel-decision/", - f"/leave-applications/{leave_id}/cancel-decision/", - path, - ) - - if "/leave-applications/" in path and "/extension-decision/" in path: - leave_id = self._ensure_approved_leave_id() - self.login_as_employee() - self.api_post( - f"/hr2/api/leave-applications/{leave_id}/extension-request/", - {"new_end_date": self.future_date(6)}, - expected_status=None, - ) - self.login_as_director() - return re.sub( - r"/leave-applications/\d+/extension-decision/", - f"/leave-applications/{leave_id}/extension-decision/", - path, - ) - - if "/leave-applications/" in path and "/request-document/" in path: - leave_id = self._ensure_leave_id() - self.login_as_hod() - return f"/hr2/api/leave-applications/{leave_id}/request-document/" - - if "/leave-applications/" in path and "/submit-document/" in path: - leave_id = self._ensure_leave_id() - self.login_as_hod() - self.api_post( - f"/hr2/api/leave-applications/{leave_id}/request-document/", - {"message": "Submit proof"}, - expected_status=None, - ) - self.login_as_employee() - return f"/hr2/api/leave-applications/{leave_id}/submit-document/" - - if "/leave-applications/" in path and "/approve/" in path: - leave_id = self._ensure_leave_id() - self.login_as_director() - return f"/hr2/api/leave-applications/{leave_id}/approve/" - - if "/leave-applications/" in path and "/forward/" in path: - leave_id = self._ensure_leave_id() - self.login_as_hod() - return f"/hr2/api/leave-applications/{leave_id}/forward/" - - if "/leave-applications/" in path and "/reject/" in path: - leave_id = self._ensure_leave_id() - self.login_as_director() - return f"/hr2/api/leave-applications/{leave_id}/reject/" - - if "/leave-applications/" in path and "/withdraw/" in path: - leave_id = self._ensure_leave_id() - self.login_as_employee() - return f"/hr2/api/leave-applications/{leave_id}/withdraw/" - - if "/leave-nominee/" in path and "/leave-nominee/" in path: - leave_id = self._ensure_leave_with_nominee_id() - self.login_as_nominee() - return f"/hr2/api/leave-nominee/{leave_id}/" - - if "/ltc/" in path and "/download/" in path: - ltc_id = self._ensure_ltc_id() - return f"/hr2/api/ltc/{ltc_id}/download/" - - if "/ltc/" in path and "/withdraw/" in path: - ltc_id = self._ensure_ltc_id() - self.login_as_employee() - return f"/hr2/api/ltc/{ltc_id}/withdraw/" - - if "/ltc/" in path and "/forward/" in path: - ltc_id = self._ensure_ltc_id() - self.login_as_staff() - return f"/hr2/api/ltc/{ltc_id}/forward/" - - if "/ltc/" in path and "/approve/" in path: - ltc_id = self._ensure_ltc_id() - self.login_as_accountant() - return f"/hr2/api/ltc/{ltc_id}/approve/" - - if "/ltc/" in path and "/reject/" in path: - ltc_id = self._ensure_ltc_id() - self.login_as_accountant() - return f"/hr2/api/ltc/{ltc_id}/reject/" - - if "/cpda-advances/" in path and "/download/" in path: - cpda_id = self._ensure_cpda_advance_id() - return f"/hr2/api/cpda-advances/{cpda_id}/download/" - - if "/cpda-advances/" in path and "/withdraw/" in path: - cpda_id = self._ensure_cpda_advance_id() - self.login_as_employee() - return f"/hr2/api/cpda-advances/{cpda_id}/withdraw/" - - if "/cpda-advances/" in path and "/forward-accountant/" in path: - cpda_id = self._ensure_cpda_advance_id() - self.login_as_staff() - return f"/hr2/api/cpda-advances/{cpda_id}/forward-accountant/" - - if "/cpda-advances/" in path and "/forward-director/" in path: - cpda_id = self._ensure_cpda_advance_id() - self.login_as_staff() - return f"/hr2/api/cpda-advances/{cpda_id}/forward-director/" - - if "/cpda-advances/" in path and "/approve/" in path: - cpda_id = self._ensure_cpda_advance_id() - self.login_as_accountant() - return f"/hr2/api/cpda-advances/{cpda_id}/approve/" - - if "/cpda-advances/" in path and "/reject/" in path: - cpda_id = self._ensure_cpda_advance_id() - self.login_as_accountant() - return f"/hr2/api/cpda-advances/{cpda_id}/reject/" - - if "/cpda-reimbursements/" in path and "/approve/" in path: - cpda_id = self._ensure_cpda_reimbursement_id() - self.login_as_accountant() - return f"/hr2/api/cpda-reimbursements/{cpda_id}/approve/" - - if "/cpda-reimbursements/" in path and "/reject/" in path: - cpda_id = self._ensure_cpda_reimbursement_id() - self.login_as_accountant() - return f"/hr2/api/cpda-reimbursements/{cpda_id}/reject/" - - if "/appraisal-forms/" in path and "/download/" in path: - appraisal_id = self._ensure_appraisal_form_id() - return f"/hr2/api/appraisal-forms/{appraisal_id}/download/" - - if "/appraisal-forms/" in path and "/review/" in path: - appraisal_id = self._ensure_appraisal_form_id() - self.login_as_hod() - return f"/hr2/api/appraisal-forms/{appraisal_id}/review/" - - return path - - -def _load_use_cases() -> Dict[str, Any]: - specs_path = os.path.join(os.path.dirname(__file__), "specs", "use_cases.yaml") - with open(specs_path, "r", encoding="utf-8") as handle: - return yaml.safe_load(handle) or {} - - -def _slugify(text: str) -> str: - slug = re.sub(r"[^a-zA-Z0-9]+", "_", text).strip("_") - return slug.lower() or "scenario" - - -def _expected_statuses(category: str, method: str) -> Tuple[int, ...]: - if method == "DELETE": - return (204,) - if category == "Exception": - return (400, 401, 403, 404, 302) - return (200, 201) - - -def _build_test( - uc: Dict[str, Any], category: str, scenario: Dict[str, Any], index: int -): - def _test(self: HR2UCTestBase): - self._test_id = f"{uc.get('id')}-{category[:2].upper()}-{index:02d}" - self._uc_id = uc.get("id") - self._test_category = category - self._scenario = scenario.get("scenario") - self._preconditions = scenario.get("preconditions", uc.get("preconditions", "")) - self._input_action = scenario.get("input_action", "") - self._expected_result = scenario.get("expected_result", "") - - login_text = f"{uc.get('actors', '')} {self._preconditions}" - self._login_for_context(login_text) - - method, path = self._parse_action(self._input_action) - path = self._resolve_path(path) - path = self._prepare_state_for(path, method) - payload = self._payload_for(path, self._scenario or "") if method in {"POST", "PUT"} else {} - - response = self._dispatch(method, path, payload) - expected_statuses = _expected_statuses(category, method) - - if response.status_code in expected_statuses: - self._record_result("Expected response", "Pass", str(getattr(response, "data", ""))) - else: - self._record_result( - f"Unexpected status {response.status_code}", - "Fail", - str(getattr(response, "data", "")), - ) - self.fail(f"Expected status in {expected_statuses}, got {response.status_code}") - - return _test - - -def _generate_uc_tests(): - specs = _load_use_cases() - use_cases = specs.get("use_cases", []) - for uc in use_cases: - class_name = f"Test_{uc.get('id', 'UC')}_{_slugify(uc.get('title', 'uc'))}" - attrs: Dict[str, Any] = {"__doc__": f"{uc.get('id')}: {uc.get('title')}"} - - for category, key in ( - ("Happy Path", "happy_paths"), - ("Alternate Path", "alternate_paths"), - ("Exception", "exception_paths"), - ): - scenarios = uc.get(key, []) or [] - for index, scenario in enumerate(scenarios, start=1): - test_name = f"test_{category.split()[0].lower()}_{index:02d}_{_slugify(scenario.get('scenario', 'case'))}" - attrs[test_name] = _build_test(uc, category, scenario, index) - - globals()[class_name] = type(class_name, (HR2UCTestBase,), attrs) - - -_generate_uc_tests() diff --git a/FusionIIIT/applications/hr2/tests/test_workflows.py b/FusionIIIT/applications/hr2/tests/test_workflows.py deleted file mode 100644 index 784d30a60..000000000 --- a/FusionIIIT/applications/hr2/tests/test_workflows.py +++ /dev/null @@ -1,593 +0,0 @@ -import os -import re -from typing import Any, Dict, Optional - -import yaml - -from .conftest import WFTestBase - - -class HR2WFTestBase(WFTestBase): - """Dynamic WF tests generated from specs/workflows.yaml.""" - - _created_ids: Dict[str, int] = {} - - def _extract_id(self, data: Any) -> Optional[int]: - if isinstance(data, dict): - for key in ("id", "pk", "leave_id", "ltc_id", "cpda_id", "appraisal_id"): - value = data.get(key) - if isinstance(value, int): - return value - return None - - def _create_resource(self, endpoint: str, payload: Dict[str, Any]) -> int: - response = self.api_post(endpoint, payload, expected_status=None) - if response.status_code in {200, 201}: - extracted = self._extract_id(getattr(response, "data", {})) - if extracted is not None: - return extracted - return 1 - - def _ensure_leave_id(self) -> int: - if "leave" not in self._created_ids: - self.login_as_employee() - payload = { - "leave_type": "Casual", - "start_date": self.future_date(3), - "end_date": self.future_date(4), - "total_days": 2, - "reason": "Personal work", - "contact_during_leave": "9876543210", - "address_during_leave": "Jabalpur, MP", - } - self._created_ids["leave"] = self._create_resource( - "/hr2/api/leave-applications/", payload - ) - return self._created_ids["leave"] - - def _ensure_approved_leave_id(self) -> int: - leave_id = self._ensure_leave_id() - self.login_as_director() - self.api_post( - f"/hr2/api/leave-applications/{leave_id}/approve/", - {"remarks": "Approved"}, - expected_status=None, - ) - return leave_id - - def _ensure_leave_with_nominee_id(self) -> int: - if "leave_nominee" not in self._created_ids: - self.login_as_employee() - payload = { - "leave_type": "Casual", - "start_date": self.future_date(3), - "end_date": self.future_date(4), - "total_days": 2, - "reason": "Personal work", - "contact_during_leave": "9876543210", - "address_during_leave": "Jabalpur, MP", - "nominee_employee_id": self.nominee_extra.id, - } - self._created_ids["leave_nominee"] = self._create_resource( - "/hr2/api/leave-applications/", payload - ) - return self._created_ids["leave_nominee"] - - def _ensure_ltc_id(self) -> int: - if "ltc" not in self._created_ids: - self.login_as_employee() - payload = { - "employee_name": "Rahul Sharma", - "department": "Computer Science and Engineering", - "designation": "Assistant Professor", - "ltc_block_year": 2025, - "travel_start_date": self.future_date(10), - "travel_end_date": self.future_date(15), - "destination": "Delhi", - "purpose_of_travel": "Family travel", - "travel_mode": "Train", - "total_amount_claimed": 22000, - } - self._created_ids["ltc"] = self._create_resource("/hr2/api/ltc/", payload) - return self._created_ids["ltc"] - - def _ensure_cpda_advance_id(self) -> int: - if "cpda_advance" not in self._created_ids: - self.login_as_employee() - payload = { - "employee_name": "Rahul Sharma", - "department": "Computer Science and Engineering", - "designation": "Assistant Professor", - "event_name": "National Conference on AI", - "event_type": "Conference", - "start_date": self.future_date(30), - "end_date": self.future_date(32), - "total_amount": 20000, - "purpose_of_attending": "Present paper", - "benefits_to_institution": "Research exposure", - } - self._created_ids["cpda_advance"] = self._create_resource( - "/hr2/api/cpda-advances/", payload - ) - return self._created_ids["cpda_advance"] - - def _ensure_cpda_reimbursement_id(self) -> int: - if "cpda_reimbursement" not in self._created_ids: - self.login_as_employee() - payload = { - "employee_name": "Rahul Sharma", - "department": "Computer Science and Engineering", - "designation": "Assistant Professor", - "event_name": "National Conference on AI", - "event_type": "Conference", - "start_date": self.future_date(30), - "end_date": self.future_date(32), - "total_amount": 20000, - "purpose_of_attending": "Present paper", - "benefits_to_institution": "Research exposure", - } - self._created_ids["cpda_reimbursement"] = self._create_resource( - "/hr2/api/cpda-reimbursements/", payload - ) - return self._created_ids["cpda_reimbursement"] - - def _ensure_appraisal_form_id(self) -> int: - if "appraisal_form" not in self._created_ids: - self.login_as_employee() - payload = { - "employee_name": "Rahul Sharma", - "department": "Computer Science and Engineering", - "designation": "Assistant Professor", - "appraisal_year": "2025-2026", - "self_summary": "Completed teaching responsibilities", - "key_responsibilities": "Teaching and research", - "achievements": "Published 1 paper", - "goals_achieved": "Completed syllabus", - "future_goals": "Publish more papers", - } - self._created_ids["appraisal_form"] = self._create_resource( - "/hr2/api/appraisal-forms/", payload - ) - return self._created_ids["appraisal_form"] - - def _resolve_path(self, path: str) -> str: - if "/leave-applications/" in path: - leave_id = self._ensure_leave_id() - return re.sub(r"/leave-applications/\d+", f"/leave-applications/{leave_id}", path) - if "/ltc/" in path: - ltc_id = self._ensure_ltc_id() - return re.sub(r"/ltc/\d+", f"/ltc/{ltc_id}", path) - if "/cpda-advances/" in path: - cpda_id = self._ensure_cpda_advance_id() - return re.sub(r"/cpda-advances/\d+", f"/cpda-advances/{cpda_id}", path) - if "/cpda-reimbursements/" in path: - cpda_id = self._ensure_cpda_reimbursement_id() - return re.sub(r"/cpda-reimbursements/\d+", f"/cpda-reimbursements/{cpda_id}", path) - if "/appraisal-forms/" in path: - appraisal_id = self._ensure_appraisal_form_id() - return re.sub(r"/appraisal-forms/\d+", f"/appraisal-forms/{appraisal_id}", path) - return path - - def _payload_for(self, path: str, scenario: str) -> Dict[str, Any]: - scenario_lower = scenario.lower() - payload: Dict[str, Any] = {} - - if "/leave-applications/" in path and path.endswith("/leave-applications/"): - payload = { - "leave_type": "Casual", - "start_date": self.future_date(3), - "end_date": self.future_date(4), - "total_days": 2, - "reason": "Personal work", - "contact_during_leave": "9876543210", - "address_during_leave": "Jabalpur, MP", - } - elif path.endswith("/withdraw/"): - payload = {} - elif path.endswith("/cancel-request/"): - payload = {"reason": "Change of plan"} - elif path.endswith("/extension-request/"): - payload = {"new_end_date": self.future_date(6), "reason": "Medical"} - elif path.endswith("/request-document/"): - payload = {"message": "Submit proof"} - elif path.endswith("/submit-document/"): - payload = {"submission": "doc-ref"} - elif "/leave-nominee/" in path: - payload = {"action": "accept" if "accept" in scenario_lower else "decline"} - elif path.endswith("/ltc/"): - payload = { - "employee_name": "Rahul Sharma", - "department": "Computer Science and Engineering", - "designation": "Assistant Professor", - "ltc_block_year": 2025, - "travel_start_date": self.future_date(10), - "travel_end_date": self.future_date(15), - "destination": "Delhi", - "purpose_of_travel": "Family travel", - "travel_mode": "Train", - "total_amount_claimed": 22000, - } - elif path.endswith("/cpda-advances/"): - payload = { - "employee_name": "Rahul Sharma", - "department": "Computer Science and Engineering", - "designation": "Assistant Professor", - "event_name": "National Conference on AI", - "event_type": "Conference", - "start_date": self.future_date(30), - "end_date": self.future_date(32), - "total_amount": 20000, - "purpose_of_attending": "Present paper", - "benefits_to_institution": "Research exposure", - } - elif path.endswith("/cpda-reimbursements/"): - payload = { - "employee_name": "Rahul Sharma", - "department": "Computer Science and Engineering", - "designation": "Assistant Professor", - "event_name": "National Conference on AI", - "event_type": "Conference", - "start_date": self.future_date(30), - "end_date": self.future_date(32), - "total_amount": 20000, - "purpose_of_attending": "Present paper", - "benefits_to_institution": "Research exposure", - } - elif path.endswith("/review/"): - payload = {"action": "forward" if "forward" in scenario_lower else "approve"} - - return payload - - def _dispatch(self, method: str, path: str, payload: Dict[str, Any]): - if method == "POST": - return self.api_post(path, payload, expected_status=None) - if method == "PUT": - return self.api_put(path, payload, expected_status=None) - if method == "DELETE": - return self.api_delete(path, expected_status=None) - return self.api_get(path, expected_status=None) - - -def _load_workflows() -> Dict[str, Any]: - specs_path = os.path.join(os.path.dirname(__file__), "specs", "workflows.yaml") - with open(specs_path, "r", encoding="utf-8") as handle: - return yaml.safe_load(handle) or {} - - -def _slugify(text: str) -> str: - slug = re.sub(r"[^a-zA-Z0-9]+", "_", text).strip("_") - return slug.lower() or "workflow" - - -def _build_test(workflow: Dict[str, Any], scenario: Dict[str, Any], category: str, index: int): - def _test(self: HR2WFTestBase): - suffix = "E2E" if category == "End-to-End" else "NEG" - self._test_id = f"{workflow.get('id')}-{suffix}-{index:02d}" - self._wf_id = workflow.get("id") - self._test_category = category - self._scenario = scenario.get("scenario") - self._expected_final_state = scenario.get("expected_final_state", "") - - workflow_id = workflow.get("id") or "" - scenario_lower = (self._scenario or "").lower() - - if workflow_id == "WF-HR2-001": - self.login_as_employee() - leave_id = self._ensure_leave_id() - self._add_step(1, "Employee applies", "Leave created", str(leave_id), True) - - if "rejected" in scenario_lower: - self.login_as_director() - resp = self.api_post( - f"/hr2/api/leave-applications/{leave_id}/reject/", - {"remarks": "Rejected"}, - expected_status=None, - ) - step_ok = resp.status_code in {200, 201} - self._add_step(2, "Director rejects", "Status rejected", str(resp.data), step_ok) - elif "forwarded" in scenario_lower: - self.login_as_hod() - resp = self.api_post( - f"/hr2/api/leave-applications/{leave_id}/forward/", - {"remarks": "Forward"}, - expected_status=None, - ) - step_ok = resp.status_code in {200, 201} - self._add_step(2, "HOD forwards", "Status forwarded", str(resp.data), step_ok) - self.login_as_director() - resp2 = self.api_post( - f"/hr2/api/leave-applications/{leave_id}/approve/", - {"remarks": "Approved"}, - expected_status=None, - ) - step2_ok = resp2.status_code in {200, 201} - self._add_step(3, "Director approves", "Status approved", str(resp2.data), step2_ok) - else: - self.login_as_director() - resp = self.api_post( - f"/hr2/api/leave-applications/{leave_id}/approve/", - {"remarks": "Approved"}, - expected_status=None, - ) - step_ok = resp.status_code in {200, 201} - self._add_step(2, "Director approves", "Status approved", str(resp.data), step_ok) - - elif workflow_id == "WF-HR2-002": - leave_id = self._ensure_leave_id() - if "approved" in scenario_lower: - self.login_as_director() - self.api_post( - f"/hr2/api/leave-applications/{leave_id}/approve/", - {"remarks": "Approved"}, - expected_status=None, - ) - self.login_as_employee() - resp = self.api_post( - f"/hr2/api/leave-applications/{leave_id}/withdraw/", - {}, - expected_status=None, - ) - step_ok = resp.status_code in {400, 403} - self._add_step(1, "Withdraw approved leave", "Rejected", str(resp.data), step_ok) - else: - self.login_as_employee() - resp = self.api_post( - f"/hr2/api/leave-applications/{leave_id}/withdraw/", - {}, - expected_status=None, - ) - step_ok = resp.status_code in {200, 201} - self._add_step(1, "Withdraw pending leave", "Withdrawn", str(resp.data), step_ok) - - elif workflow_id == "WF-HR2-003": - leave_id = self._ensure_approved_leave_id() - if "after start date" in scenario_lower: - self.login_as_employee() - resp = self.api_post( - f"/hr2/api/leave-applications/{leave_id}/cancel-request/", - {"reason": "Late"}, - expected_status=None, - ) - step_ok = resp.status_code in {400, 403} - self._add_step(1, "Cancel request late", "Rejected", str(resp.data), step_ok) - else: - self.login_as_employee() - resp = self.api_post( - f"/hr2/api/leave-applications/{leave_id}/cancel-request/", - {"reason": "Change of plan"}, - expected_status=None, - ) - step1_ok = resp.status_code in {200, 201} - self._add_step(1, "Request cancellation", "Requested", str(resp.data), step1_ok) - self.login_as_director() - resp2 = self.api_post( - f"/hr2/api/leave-applications/{leave_id}/cancel-decision/approve/", - {"remarks": "Approved"}, - expected_status=None, - ) - step2_ok = resp2.status_code in {200, 201} - self._add_step(2, "Approve cancellation", "Cancelled", str(resp2.data), step2_ok) - - elif workflow_id == "WF-HR2-004": - leave_id = self._ensure_approved_leave_id() - self.login_as_employee() - new_end = self.future_date(6) - if "insufficient" in scenario_lower: - new_end = self.future_date(30) - resp = self.api_post( - f"/hr2/api/leave-applications/{leave_id}/extension-request/", - {"new_end_date": new_end}, - expected_status=None, - ) - step1_ok = resp.status_code in {200, 201} - self._add_step(1, "Request extension", "Requested", str(resp.data), step1_ok) - self.login_as_director() - resp2 = self.api_post( - f"/hr2/api/leave-applications/{leave_id}/extension-decision/approve/", - {"remarks": "Approved"}, - expected_status=None, - ) - step2_ok = resp2.status_code in ({400} if "insufficient" in scenario_lower else {200, 201}) - self._add_step(2, "Approve extension", "Approved or rejected", str(resp2.data), step2_ok) - - elif workflow_id == "WF-HR2-005": - leave_id = self._ensure_leave_with_nominee_id() - if "non-nominee" in scenario_lower: - self.login_as_employee() - resp = self.api_post( - f"/hr2/api/leave-nominee/{leave_id}/", - {"action": "accept"}, - expected_status=None, - ) - step_ok = resp.status_code in {403} - self._add_step(1, "Non-nominee responds", "Forbidden", str(resp.data), step_ok) - else: - self.login_as_nominee() - resp = self.api_post( - f"/hr2/api/leave-nominee/{leave_id}/", - {"action": "accept"}, - expected_status=None, - ) - step_ok = resp.status_code in {200, 201} - self._add_step(1, "Nominee accepts", "Accepted", str(resp.data), step_ok) - - elif workflow_id == "WF-HR2-006": - leave_id = self._ensure_leave_id() - if "without request" in scenario_lower: - self.login_as_employee() - resp = self.api_post( - f"/hr2/api/leave-applications/{leave_id}/submit-document/", - {"submission": "doc-ref"}, - expected_status=None, - ) - step_ok = resp.status_code in {400, 403} - self._add_step(1, "Submit without request", "Rejected", str(resp.data), step_ok) - else: - self.login_as_hod() - resp = self.api_post( - f"/hr2/api/leave-applications/{leave_id}/request-document/", - {"message": "Submit proof"}, - expected_status=None, - ) - step1_ok = resp.status_code in {200, 201} - self._add_step(1, "HOD requests document", "Requested", str(resp.data), step1_ok) - self.login_as_employee() - resp2 = self.api_post( - f"/hr2/api/leave-applications/{leave_id}/submit-document/", - {"submission": "doc-ref"}, - expected_status=None, - ) - step2_ok = resp2.status_code in {200, 201} - self._add_step(2, "Employee submits", "Submitted", str(resp2.data), step2_ok) - - elif workflow_id == "WF-HR2-007": - ltc_id = self._ensure_ltc_id() - if "rejected" in scenario_lower: - self.login_as_accountant() - resp = self.api_post( - f"/hr2/api/ltc/{ltc_id}/reject/", - {"remarks": "Rejected"}, - expected_status=None, - ) - step_ok = resp.status_code in {200, 201} - self._add_step(1, "Reject LTC", "Rejected", str(resp.data), step_ok) - else: - self.login_as_staff() - resp = self.api_post( - f"/hr2/api/ltc/{ltc_id}/forward/", - {"remarks": "Forward"}, - expected_status=None, - ) - step1_ok = resp.status_code in {200, 201} - self._add_step(1, "Forward LTC", "Forwarded", str(resp.data), step1_ok) - self.login_as_accountant() - resp2 = self.api_post( - f"/hr2/api/ltc/{ltc_id}/approve/", - {"remarks": "Approved"}, - expected_status=None, - ) - step2_ok = resp2.status_code in {200, 201} - self._add_step(2, "Approve LTC", "Approved", str(resp2.data), step2_ok) - - elif workflow_id == "WF-HR2-008": - cpda_id = self._ensure_cpda_advance_id() - if "director" in scenario_lower: - self.login_as_staff() - resp = self.api_post( - f"/hr2/api/cpda-advances/{cpda_id}/forward-director/", - {"remarks": "Forward"}, - expected_status=None, - ) - step1_ok = resp.status_code in {200, 201} - self._add_step(1, "Forward to director", "Forwarded", str(resp.data), step1_ok) - self.login_as_director() - resp2 = self.api_post( - f"/hr2/api/cpda-advances/{cpda_id}/approve/", - {"remarks": "Approved"}, - expected_status=None, - ) - step2_ok = resp2.status_code in {200, 201} - self._add_step(2, "Director approves", "Approved", str(resp2.data), step2_ok) - elif "rejected" in scenario_lower: - self.login_as_accountant() - resp = self.api_post( - f"/hr2/api/cpda-advances/{cpda_id}/reject/", - {"remarks": "Rejected"}, - expected_status=None, - ) - step_ok = resp.status_code in {200, 201} - self._add_step(1, "Reject CPDA", "Rejected", str(resp.data), step_ok) - else: - self.login_as_staff() - resp = self.api_post( - f"/hr2/api/cpda-advances/{cpda_id}/forward-accountant/", - {"remarks": "Forward"}, - expected_status=None, - ) - step1_ok = resp.status_code in {200, 201} - self._add_step(1, "Forward to accountant", "Forwarded", str(resp.data), step1_ok) - self.login_as_accountant() - resp2 = self.api_post( - f"/hr2/api/cpda-advances/{cpda_id}/approve/", - {"remarks": "Approved"}, - expected_status=None, - ) - step2_ok = resp2.status_code in {200, 201} - self._add_step(2, "Accountant approves", "Approved", str(resp2.data), step2_ok) - - elif workflow_id == "WF-HR2-009": - cpda_id = self._ensure_cpda_reimbursement_id() - if "rejected" in scenario_lower: - self.login_as_accountant() - resp = self.api_post( - f"/hr2/api/cpda-reimbursements/{cpda_id}/reject/", - {"remarks": "Rejected"}, - expected_status=None, - ) - step_ok = resp.status_code in {200, 201} - self._add_step(1, "Reject reimbursement", "Rejected", str(resp.data), step_ok) - else: - self.login_as_accountant() - resp = self.api_post( - f"/hr2/api/cpda-reimbursements/{cpda_id}/approve/", - {"remarks": "Approved"}, - expected_status=None, - ) - step_ok = resp.status_code in {200, 201} - self._add_step(1, "Approve reimbursement", "Approved", str(resp.data), step_ok) - - elif workflow_id == "WF-HR2-010": - appraisal_id = self._ensure_appraisal_form_id() - if "director" in scenario_lower: - self.login_as_hod() - self.api_post( - f"/hr2/api/appraisal-forms/{appraisal_id}/review/", - {"action": "forward"}, - expected_status=None, - ) - self.login_as_director() - resp = self.api_post( - f"/hr2/api/appraisal-forms/{appraisal_id}/review/", - {"action": "approve"}, - expected_status=None, - ) - step_ok = resp.status_code in {200, 201} - self._add_step(1, "Director approves", "Approved", str(resp.data), step_ok) - else: - self.login_as_hod() - resp = self.api_post( - f"/hr2/api/appraisal-forms/{appraisal_id}/review/", - {"action": "forward"}, - expected_status=None, - ) - step_ok = resp.status_code in {200, 201} - self._add_step(1, "HOD forwards", "Reviewed", str(resp.data), step_ok) - - if self._all_steps_passed(): - self._record_result("Workflow completed", "Pass") - else: - self._record_result("Workflow incomplete", "Fail") - self.fail("Workflow did not complete successfully") - - return _test - - -def _generate_wf_tests(): - specs = _load_workflows() - workflows = specs.get("workflows", []) - for workflow in workflows: - class_name = f"Test_{workflow.get('id', 'WF')}_{_slugify(workflow.get('title', 'workflow'))}" - attrs: Dict[str, Any] = { - "__doc__": f"{workflow.get('id')}: {workflow.get('title')}" - } - - for category, key in (("End-to-End", "e2e_tests"), ("Negative", "negative_tests")): - scenarios = workflow.get(key, []) or [] - for index, scenario in enumerate(scenarios, start=1): - name = f"test_{category.split('-')[0].lower()}_{index:02d}_{_slugify(scenario.get('scenario', 'case'))}" - attrs[name] = _build_test(workflow, scenario, category, index) - - globals()[class_name] = type(class_name, (HR2WFTestBase,), attrs) - - -_generate_wf_tests()