Skip to content

feat: Implement attendance and payroll management features#1

Open
denmark0128 wants to merge 1 commit intomainfrom
feat/payroll
Open

feat: Implement attendance and payroll management features#1
denmark0128 wants to merge 1 commit intomainfrom
feat/payroll

Conversation

@denmark0128
Copy link
Owner

  • 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.
Copilot AI review requested due to automatic review settings February 23, 2026 05:41
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +115 to +119
def save(self, *args, **kwargs):
if self.is_day_off:
self.scheduled_start = None
self.scheduled_end = None
super().save(*args, **kwargs)
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +166 to +215
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())
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +88 to +144
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
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Comment on lines +104 to +105


Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
def get_queryset(self):
"""Optimize schedule list by selecting related employee records."""
return super().get_queryset().select_related('employee')

Copilot uses AI. Check for mistakes.
Comment on lines +131 to +132


Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
def get_queryset(self):
"""
Optimize attendance listing by fetching related employees in a single query.
"""
return super().get_queryset().select_related('employee')

Copilot uses AI. Check for mistakes.
Comment on lines 202 to 210
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)
Copy link

Copilot AI Feb 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants