feat: Implement attendance and payroll management features#1
feat: Implement attendance and payroll management features#1denmark0128 wants to merge 1 commit intomainfrom
Conversation
denmark0128
commented
Feb 23, 2026
- Add PayrollComputationService for calculating payroll deductions based on attendance.
- Create views for managing attendance records, including list, create, update, and delete functionalities.
- Introduce EmployeeSchedule management with views for listing, creating, updating, and deleting schedules.
- Update payroll processing to automatically compute deductions from attendance records.
- Enhance templates for attendance and schedule management, including forms and confirmation dialogs.
- Update employee list to display additional information such as job title, status, and emergency contact.
- Improve the dashboard to include recent schedules and attendance logs.
- Add PayrollComputationService for calculating payroll deductions based on attendance. - Create views for managing attendance records, including list, create, update, and delete functionalities. - Introduce EmployeeSchedule management with views for listing, creating, updating, and deleting schedules. - Update payroll processing to automatically compute deductions from attendance records. - Enhance templates for attendance and schedule management, including forms and confirmation dialogs. - Update employee list to display additional information such as job title, status, and emergency contact. - Improve the dashboard to include recent schedules and attendance logs.
There was a problem hiding this comment.
Pull request overview
This pull request implements comprehensive attendance and payroll management features for the workforce module, significantly expanding HR capabilities. The changes introduce attendance tracking with automated late/undertime calculations, employee schedule management, and integration with payroll deductions based on attendance records.
Changes:
- Added Attendance and EmployeeSchedule models with validation logic and automatic field calculations
- Implemented PayrollComputationService for calculating payroll deductions from attendance records
- Created complete CRUD views and templates for schedules and attendance management
- Enhanced Employee model with HR fields (job title, employment status, manager hierarchy, emergency contacts, tenure tracking)
- Updated payroll processing to automatically compute late, absent, and undertime deductions
- Added UI enhancements including collapsible sidebar and horizontal scrolling for wide tables
- Expanded test coverage across all modules with 17 passing tests
Reviewed changes
Copilot reviewed 28 out of 28 changed files in this pull request and generated 6 comments.
Show a summary per file
| File | Description |
|---|---|
| apps/workforce/models.py | Added Attendance and EmployeeSchedule models with validation; enhanced Employee with HR fields and tenure calculation; expanded Payroll with deduction breakdown fields |
| apps/workforce/services.py | New PayrollComputationService for calculating attendance-based deductions using configurable rates |
| apps/workforce/forms.py | Added AttendanceForm and EmployeeScheduleForm with auto-computation logic; expanded EmployeeForm with new HR fields |
| apps/workforce/views.py | Added CRUD views for schedules and attendance; integrated PayrollComputationService into payroll create/update flows |
| apps/workforce/urls.py | Added URL patterns for schedule and attendance CRUD operations |
| apps/workforce/admin.py | Enhanced admin configurations for new models and expanded field displays |
| apps/workforce/tests.py | New comprehensive test suite for attendance form validation, payroll computation service, schedule validation, and tenure tracking |
| apps/workforce/migrations/*.py | Three migrations adding new models and fields |
| templates/workforce/*.html | New templates for schedule and attendance management; updated payroll and employee lists with expanded columns |
| templates/base.html | Added collapsible sidebar with localStorage persistence and horizontal scroll support for wide tables |
| apps/*/tests.py | Added test coverage for authentication, clients, sales, inventory, finance, and audit modules |
| PROGRESS.md | Documentation of development steps 40-45 |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| def save(self, *args, **kwargs): | ||
| if self.is_day_off: | ||
| self.scheduled_start = None | ||
| self.scheduled_end = None | ||
| super().save(*args, **kwargs) |
There was a problem hiding this comment.
The EmployeeSchedule model's save() method bypasses the clean() validation. When is_day_off is False, the clean() method requires scheduled_start and scheduled_end to be set, but the save() method can be called directly without calling full_clean() first (which happens when using Django ModelForm save or direct model save operations in many cases). This creates inconsistent data validation depending on how the model is saved. Consider calling self.full_clean() at the beginning of the save() method to ensure validation always runs.
| def form_valid(self, form): | ||
| payroll = form.save(commit=False) | ||
| computation = PayrollComputationService.calculate( | ||
| employee=payroll.employee, | ||
| period_start=payroll.period_start, | ||
| period_end=payroll.period_end, | ||
| additions=payroll.additions, | ||
| other_deductions=payroll.other_deductions, | ||
| ) | ||
| payroll.gross_pay = computation['gross_pay'] | ||
| payroll.absent_days = computation['absent_days'] | ||
| payroll.late_minutes = computation['late_minutes'] | ||
| payroll.undertime_minutes = computation['undertime_minutes'] | ||
| payroll.late_deduction = computation['late_deduction'] | ||
| payroll.undertime_deduction = computation['undertime_deduction'] | ||
| payroll.absent_deduction = computation['absent_deduction'] | ||
| payroll.deductions = computation['deductions'] | ||
| payroll.net_pay = computation['net_pay'] | ||
| payroll.save() | ||
| self.object = payroll | ||
| return HttpResponseRedirect(self.get_success_url()) | ||
|
|
||
|
|
||
| class PayrollUpdateView(UpdateView): | ||
| template_name = 'workforce/payroll_form.html' | ||
| model = Payroll | ||
| form_class = PayrollForm | ||
| success_url = reverse_lazy('payrolls-list') | ||
|
|
||
| def form_valid(self, form): | ||
| payroll = form.save(commit=False) | ||
| computation = PayrollComputationService.calculate( | ||
| employee=payroll.employee, | ||
| period_start=payroll.period_start, | ||
| period_end=payroll.period_end, | ||
| additions=payroll.additions, | ||
| other_deductions=payroll.other_deductions, | ||
| ) | ||
| payroll.gross_pay = computation['gross_pay'] | ||
| payroll.absent_days = computation['absent_days'] | ||
| payroll.late_minutes = computation['late_minutes'] | ||
| payroll.undertime_minutes = computation['undertime_minutes'] | ||
| payroll.late_deduction = computation['late_deduction'] | ||
| payroll.undertime_deduction = computation['undertime_deduction'] | ||
| payroll.absent_deduction = computation['absent_deduction'] | ||
| payroll.deductions = computation['deductions'] | ||
| payroll.net_pay = computation['net_pay'] | ||
| payroll.save() | ||
| self.object = payroll | ||
| return HttpResponseRedirect(self.get_success_url()) |
There was a problem hiding this comment.
The form_valid methods in PayrollCreateView and PayrollUpdateView contain identical code for computing payroll deductions (lines 166-186 and 195-215). This duplicated logic makes the code harder to maintain - any bug fixes or enhancements need to be applied in two places. Consider extracting this shared logic into a private helper method (e.g., _apply_payroll_computations) that both views can call.
| def clean(self): | ||
| cleaned_data = super().clean() | ||
| employee = cleaned_data.get('employee') | ||
| attendance_date = cleaned_data.get('attendance_date') | ||
| status = cleaned_data.get('status') | ||
| scheduled_start = cleaned_data.get('scheduled_start') | ||
| scheduled_end = cleaned_data.get('scheduled_end') | ||
| clock_in = cleaned_data.get('clock_in') | ||
| clock_out = cleaned_data.get('clock_out') | ||
|
|
||
| if employee and attendance_date: | ||
| schedule = employee.get_schedule_for_date(attendance_date) | ||
| if schedule: | ||
| if schedule.is_day_off: | ||
| cleaned_data['status'] = Attendance.STATUS_LEAVE | ||
| cleaned_data['scheduled_start'] = None | ||
| cleaned_data['scheduled_end'] = None | ||
| cleaned_data['late_minutes'] = 0 | ||
| cleaned_data['undertime_minutes'] = 0 | ||
| return cleaned_data | ||
|
|
||
| cleaned_data['scheduled_start'] = schedule.scheduled_start | ||
| cleaned_data['scheduled_end'] = schedule.scheduled_end | ||
| scheduled_start = schedule.scheduled_start | ||
| scheduled_end = schedule.scheduled_end | ||
|
|
||
| if not scheduled_start or not scheduled_end: | ||
| cleaned_data['late_minutes'] = 0 | ||
| cleaned_data['undertime_minutes'] = 0 | ||
| return cleaned_data | ||
|
|
||
| start_dt = datetime.combine(datetime.today(), scheduled_start) | ||
| end_dt = datetime.combine(datetime.today(), scheduled_end) | ||
| if end_dt <= start_dt: | ||
| self.add_error('scheduled_end', 'Scheduled end must be later than scheduled start.') | ||
| return cleaned_data | ||
|
|
||
| if status == Attendance.STATUS_PRESENT: | ||
| late_minutes = 0 | ||
| undertime_minutes = 0 | ||
| if clock_in: | ||
| clock_in_dt = datetime.combine(datetime.today(), clock_in) | ||
| if clock_in_dt > start_dt: | ||
| late_minutes = int((clock_in_dt - start_dt).total_seconds() // 60) | ||
|
|
||
| if clock_out: | ||
| clock_out_dt = datetime.combine(datetime.today(), clock_out) | ||
| if clock_out_dt < end_dt: | ||
| undertime_minutes = int((end_dt - clock_out_dt).total_seconds() // 60) | ||
|
|
||
| cleaned_data['late_minutes'] = max(late_minutes, 0) | ||
| cleaned_data['undertime_minutes'] = max(undertime_minutes, 0) | ||
| else: | ||
| cleaned_data['late_minutes'] = 0 | ||
| cleaned_data['undertime_minutes'] = 0 | ||
|
|
||
| return cleaned_data |
There was a problem hiding this comment.
The AttendanceForm does not validate that clock_out occurs after clock_in. If a user enters clock_in as 17:00 and clock_out as 08:00, the form would accept this invalid data. This could lead to negative or incorrect calculations for work hours if such edge cases are encountered. Consider adding validation to ensure clock_out is after clock_in when both are provided and status is PRESENT.
|
|
||
|
|
There was a problem hiding this comment.
The EmployeeScheduleListView does not use select_related() to optimize foreign key queries. Since the schedule_list.html template accesses schedule.employee.employee_code and schedule.employee.full_name for each schedule, this will result in additional database queries (N+1 problem). Consider overriding get_queryset() to return EmployeeSchedule.objects.select_related('employee') to optimize performance.
| def get_queryset(self): | |
| """Optimize schedule list by selecting related employee records.""" | |
| return super().get_queryset().select_related('employee') |
|
|
||
|
|
There was a problem hiding this comment.
The AttendanceListView does not use select_related() to optimize foreign key queries. Since the attendance_list.html template accesses attendance.employee.employee_code and attendance.employee.full_name for each attendance record, this will result in additional database queries (N+1 problem). Consider overriding get_queryset() to return Attendance.objects.select_related('employee') to optimize performance.
| def get_queryset(self): | |
| """ | |
| Optimize attendance listing by fetching related employees in a single query. | |
| """ | |
| return super().get_queryset().select_related('employee') |
| def save(self, *args, **kwargs): | ||
| self.net_pay = self.gross_pay - self.deductions | ||
| self.deductions = ( | ||
| self.late_deduction | ||
| + self.undertime_deduction | ||
| + self.absent_deduction | ||
| + self.other_deductions | ||
| ) | ||
| self.net_pay = self.gross_pay + self.additions - self.deductions | ||
| super().save(*args, **kwargs) |
There was a problem hiding this comment.
The Payroll model's save() method calculates net_pay which could be negative if deductions exceed gross_pay + additions, but this is only caught by a database constraint. Consider adding validation in the model's clean() method or in the PayrollComputationService to check this condition and raise a ValidationError with a user-friendly message before attempting to save, providing better error handling.