2025-10-03 20:11:25 +03:00

1600 lines
60 KiB
Python

"""
HR app forms with healthcare-focused validation and user experience.
"""
from django import forms
from django.core.exceptions import ValidationError
from django.utils import timezone
from datetime import date, timedelta
from decimal import Decimal
from .models import (
Employee, Department, Schedule, ScheduleAssignment,
TimeEntry, PerformanceReview, TrainingRecord, TrainingPrograms,
TrainingSession, ProgramModule, ProgramPrerequisite,
TrainingAttendance, TrainingAssessment, TrainingCertificates
)
class EmployeeForm(forms.ModelForm):
"""
Form for creating and updating employees.
"""
class Meta:
model = Employee
fields = [
'first_name', 'last_name', 'father_name', 'grandfather_name',
'email', 'phone', 'mobile_phone', 'address_line_1', 'address_line_2',
'city', 'postal_code', 'country', 'date_of_birth',
'gender', 'marital_status', 'department', 'job_title',
'employment_type', 'employment_status', 'hire_date', 'termination_date',
'supervisor', 'standard_hours_per_week', 'fte_percentage',
'hourly_rate', 'annual_salary', 'license_number', 'license_expiry_date',
'emergency_contact_name', 'emergency_contact_relationship',
'emergency_contact_phone', 'notes'
]
widgets = {
'first_name': forms.TextInput(attrs={
'class': 'form-control form-control-sm',
'placeholder': 'First name'
}),
'last_name': forms.TextInput(attrs={
'class': 'form-control form-control-sm',
'placeholder': 'Last name'
}),
'father_name': forms.TextInput(attrs={
'class': 'form-control form-control-sm',
'placeholder': 'Father name'
}),
'grandfather_name': forms.TextInput(attrs={
'class': 'form-control form-control-sm',
'placeholder': 'Grandfather name'
}),
'email': forms.EmailInput(attrs={
'class': 'form-control form-control-sm',
'placeholder': 'employee@hospital.com'
}),
'phone': forms.TextInput(attrs={
'class': 'form-control form-control-sm',
'placeholder': '+1-555-123-4567'
}),
'mobile_phone': forms.TextInput(attrs={
'class': 'form-control form-control-sm',
'placeholder': '+1-555-123-4567'
}),
'address_line_1': forms.TextInput(attrs={
'class': 'form-control form-control-sm',
'placeholder': 'Street address'
}),
'address_line_2': forms.TextInput(attrs={
'class': 'form-control form-control-sm',
'placeholder': 'Apt, Suite, etc.'
}),
'city': forms.TextInput(attrs={
'class': 'form-control form-control-sm',
'placeholder': 'City'
}),
'state': forms.TextInput(attrs={
'class': 'form-control form-control-sm',
'placeholder': 'State/Province'
}),
'postal_code': forms.TextInput(attrs={
'class': 'form-control form-control-sm',
'placeholder': 'Postal/ZIP code'
}),
'country': forms.TextInput(attrs={
'class': 'form-control form-control-sm',
'placeholder': 'Country'
}),
'department': forms.Select(attrs={'class': 'form-select form-select-sm'}),
'job_title': forms.TextInput(attrs={
'class': 'form-control form-control-sm',
'placeholder': 'Job title/position'
}),
'employment_status': forms.Select(attrs={'class': 'form-select form-select-sm'}),
'employment_type': forms.Select(attrs={'class': 'form-select form-select-sm'}),
'hire_date': forms.DateInput(attrs={
'class': 'form-control form-control-sm',
'type': 'date'
}),
'termination_date': forms.DateInput(attrs={
'class': 'form-control form-control-sm',
'type': 'date'
}),
'annual_salary': forms.NumberInput(attrs={
'class': 'form-control form-control-sm',
'step': '0.01',
'min': '0'
}),
'hourly_rate': forms.NumberInput(attrs={
'class': 'form-control form-control-sm',
'step': '0.01',
'min': '0'
}),
'supervisor': forms.Select(attrs={'class': 'form-select form-select-sm'}),
'emergency_contact_name': forms.TextInput(attrs={
'class': 'form-control form-control-sm',
'placeholder': 'Emergency contact name'
}),
'emergency_contact_relationship': forms.TextInput(attrs={
'class': 'form-control form-control-sm',
'placeholder': 'Relationship to employee'
}),
'emergency_contact_phone': forms.TextInput(attrs={
'class': 'form-control form-control-sm',
'placeholder': 'Emergency contact phone'
}),
'date_of_birth': forms.DateInput(attrs={
'class': 'form-control form-control-sm',
'type': 'date'
}),
'gender': forms.Select(attrs={'class': 'form-select form-select-sm'}),
'marital_status': forms.Select(attrs={'class': 'form-control form-control-sm'}),
'standard_hours_per_week': forms.NumberInput(attrs={
'class': 'form-control form-control-sm',
'step': '0.5',
'min': '0',
'max': '168'
}),
'fte_percentage': forms.NumberInput(attrs={
'class': 'form-control form-control-sm',
'step': '1',
'min': '0',
'max': '100'
}),
'license_number': forms.TextInput(attrs={
'class': 'form-control form-control-sm',
'placeholder': 'Professional license number'
}),
'license_expiry_date': forms.DateInput(attrs={
'class': 'form-control form-control-sm',
'type': 'date'
}),
'certifications': forms.Textarea(attrs={
'class': 'form-control form-control-sm',
'rows': 3,
'placeholder': 'List of certifications'
}),
'notes': forms.Textarea(attrs={
'class': 'form-control form-control-sm',
'rows': 3,
'placeholder': 'Additional notes'
}),
}
help_texts = {
'employment_status': 'Current employment status',
'employment_type': 'Full-time, part-time, contract, etc.',
'supervisor': 'Direct supervisor (if any)',
'annual_salary': 'Annual salary (for salaried employees)',
'hourly_rate': 'Hourly rate (for hourly employees)',
'fte_percentage': 'Full-time equivalent percentage (0-100)',
'license_number': 'Professional license number (if applicable)',
'license_expiry_date': 'License expiration date (if applicable)',
}
def __init__(self, *args, **kwargs):
user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
if user and hasattr(user, 'tenant'):
# Filter department and supervisor by tenant
self.fields['department'].queryset = Department.objects.filter(
tenant=user.tenant
).order_by('name')
self.fields['supervisor'].queryset = Employee.objects.filter(
tenant=user.tenant,
employment_status='ACTIVE'
).order_by('last_name', 'first_name')
# Exclude current instance from supervisor choices
if self.instance.pk:
self.fields['supervisor'].queryset = self.fields['supervisor'].queryset.exclude(
pk=self.instance.pk
)
# def clean_employee_number(self):
# employee_number = self.cleaned_data.get('employee_number')
# if employee_number:
# # Check for uniqueness within tenant (excluding current instance)
# queryset = Employee.objects.filter(employee_number=employee_number)
# if self.instance.pk:
# queryset = queryset.exclude(pk=self.instance.pk)
#
# if queryset.exists():
# raise ValidationError('Employee ID must be unique.')
#
# return employee_number
def clean_hire_date(self):
hire_date = self.cleaned_data.get('hire_date')
if hire_date and hire_date > timezone.now().date():
raise ValidationError('Hire date cannot be in the future.')
return hire_date
def clean_termination_date(self):
termination_date = self.cleaned_data.get('termination_date')
hire_date = self.cleaned_data.get('hire_date')
if termination_date:
if hire_date and termination_date < hire_date:
raise ValidationError('Termination date cannot be before hire date.')
return termination_date
def clean_date_of_birth(self):
date_of_birth = self.cleaned_data.get('date_of_birth')
if date_of_birth:
# Check minimum age (typically 16 for employment)
min_age_date = timezone.now().date() - timedelta(days=16*365)
if date_of_birth > min_age_date:
raise ValidationError('Employee must be at least 16 years old.')
# Check maximum age (reasonable limit)
max_age_date = timezone.now().date() - timedelta(days=100*365)
if date_of_birth < max_age_date:
raise ValidationError('Please verify the date of birth.')
return date_of_birth
def clean_license_expiry_date(self):
license_expiry_date = self.cleaned_data.get('license_expiry_date')
license_number = self.cleaned_data.get('license_number')
if license_number and not license_expiry_date:
raise ValidationError('License expiry date is required when license number is provided.')
return license_expiry_date
def clean(self):
cleaned_data = super().clean()
employment_type = cleaned_data.get('employment_type')
annual_salary = cleaned_data.get('annual_salary')
hourly_rate = cleaned_data.get('hourly_rate')
# Validate salary/hourly rate based on employment type
if employment_type == 'FULL_TIME' and not annual_salary:
self.add_error('annual_salary', 'Annual salary is required for full-time employees.')
if employment_type == 'PART_TIME' and not hourly_rate:
self.add_error('hourly_rate', 'Hourly rate is required for part-time employees.')
return cleaned_data
class TrainingProgramForm(forms.ModelForm):
"""
Form for creating and updating training programs.
"""
class Meta:
model = TrainingPrograms
fields = [
'name', 'description', 'program_type', 'program_provider',
'instructor', 'start_date', 'end_date', 'duration_hours',
'cost', 'is_certified', 'validity_days', 'notify_before_days'
]
widgets = {
'name': forms.TextInput(attrs={
'class': 'form-control form-control-sm',
'placeholder': 'Training program name'
}),
'description': forms.Textarea(attrs={
'class': 'form-control form-control-sm',
'rows': 3,
'placeholder': 'Program description'
}),
'program_type': forms.Select(attrs={'class': 'form-select form-select-sm'}),
'program_provider': forms.TextInput(attrs={
'class': 'form-control form-control-sm',
'placeholder': 'Training provider/organization'
}),
'instructor': forms.Select(attrs={'class': 'form-select form-select-sm'}),
'start_date': forms.DateInput(attrs={
'class': 'form-control form-control-sm',
'type': 'date'
}),
'end_date': forms.DateInput(attrs={
'class': 'form-control form-control-sm',
'type': 'date'
}),
'duration_hours': forms.NumberInput(attrs={
'class': 'form-control form-control-sm',
'step': '0.5',
'min': '0'
}),
'cost': forms.NumberInput(attrs={
'class': 'form-control form-control-sm',
'step': '0.01',
'min': '0'
}),
'is_certified': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'validity_days': forms.NumberInput(attrs={
'class': 'form-control form-control-sm',
'min': '1'
}),
'notify_before_days': forms.NumberInput(attrs={
'class': 'form-control form-control-sm',
'min': '1'
}),
}
help_texts = {
'program_type': 'Type of training program',
'duration_hours': 'Program duration in hours',
'cost': 'Program cost per participant',
'is_certified': 'Check if this program provides certification',
'validity_days': 'Certificate validity period in days',
'notify_before_days': 'Days before expiry to send notification',
}
def __init__(self, *args, **kwargs):
user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
if user and hasattr(user, 'tenant'):
# Filter instructor by tenant
self.fields['instructor'].queryset = Employee.objects.filter(
tenant=user.tenant,
employment_status='ACTIVE'
).order_by('last_name', 'first_name')
def clean(self):
cleaned_data = super().clean()
start_date = cleaned_data.get('start_date')
end_date = cleaned_data.get('end_date')
is_certified = cleaned_data.get('is_certified')
validity_days = cleaned_data.get('validity_days')
if start_date and end_date:
if end_date < start_date:
self.add_error('end_date', 'End date cannot be before start date.')
if is_certified and not validity_days:
self.add_error('validity_days', 'Validity period is required for certified programs.')
return cleaned_data
class TrainingSessionForm(forms.ModelForm):
"""
Form for creating and updating training sessions.
"""
class Meta:
model = TrainingSession
fields = [
'program', 'title', 'instructor', 'delivery_method',
'start_at', 'end_at', 'location', 'capacity',
'cost_override', 'hours_override'
]
widgets = {
'program': forms.Select(attrs={'class': 'form-select form-select-sm'}),
'title': forms.TextInput(attrs={
'class': 'form-control form-control-sm',
'placeholder': 'Session title (optional)'
}),
'instructor': forms.Select(attrs={'class': 'form-select form-select-sm'}),
'delivery_method': forms.Select(attrs={'class': 'form-select form-select-sm'}),
'start_at': forms.DateTimeInput(attrs={
'class': 'form-control form-control-sm',
'type': 'datetime-local'
}),
'end_at': forms.DateTimeInput(attrs={
'class': 'form-control form-control-sm',
'type': 'datetime-local'
}),
'location': forms.TextInput(attrs={
'class': 'form-control form-control-sm',
'placeholder': 'Training location'
}),
'capacity': forms.NumberInput(attrs={
'class': 'form-control form-control-sm',
'min': '1'
}),
'cost_override': forms.NumberInput(attrs={
'class': 'form-control form-control-sm',
'step': '0.01',
'min': '0'
}),
'hours_override': forms.NumberInput(attrs={
'class': 'form-control form-control-sm',
'step': '0.5',
'min': '0'
}),
}
help_texts = {
'title': 'Optional session title (defaults to program name)',
'delivery_method': 'How the training will be delivered',
'capacity': 'Maximum number of participants',
'cost_override': 'Override program cost for this session',
'hours_override': 'Override program hours for this session',
}
def __init__(self, *args, **kwargs):
user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
if user and hasattr(user, 'tenant'):
# Filter program and instructor by tenant
self.fields['program'].queryset = TrainingPrograms.objects.filter(
tenant=user.tenant
).order_by('name')
self.fields['instructor'].queryset = Employee.objects.filter(
tenant=user.tenant,
employment_status='ACTIVE'
).order_by('last_name', 'first_name')
def clean(self):
cleaned_data = super().clean()
start_at = cleaned_data.get('start_at')
end_at = cleaned_data.get('end_at')
if start_at and end_at:
if end_at <= start_at:
self.add_error('end_at', 'End time must be after start time.')
return cleaned_data
class ProgramModuleForm(forms.ModelForm):
"""
Form for creating and updating program modules.
"""
class Meta:
model = ProgramModule
fields = ['program', 'title', 'order', 'hours']
widgets = {
'program': forms.Select(attrs={'class': 'form-select form-select-sm'}),
'title': forms.TextInput(attrs={
'class': 'form-control form-control-sm',
'placeholder': 'Module title'
}),
'order': forms.NumberInput(attrs={
'class': 'form-control form-control-sm',
'min': '1'
}),
'hours': forms.NumberInput(attrs={
'class': 'form-control form-control-sm',
'step': '0.5',
'min': '0'
}),
}
help_texts = {
'order': 'Module order within the program',
'hours': 'Module duration in hours',
}
def __init__(self, *args, **kwargs):
user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
if user and hasattr(user, 'tenant'):
# Filter program by tenant
self.fields['program'].queryset = TrainingPrograms.objects.filter(
tenant=user.tenant
).order_by('name')
class TrainingAttendanceForm(forms.ModelForm):
"""
Form for marking training attendance.
"""
class Meta:
model = TrainingAttendance
fields = ['enrollment', 'checked_in_at', 'checked_out_at', 'status', 'notes']
widgets = {
'enrollment': forms.Select(attrs={'class': 'form-select form-select-sm'}),
'checked_in_at': forms.DateTimeInput(attrs={
'class': 'form-control form-control-sm',
'type': 'datetime-local'
}),
'checked_out_at': forms.DateTimeInput(attrs={
'class': 'form-control form-control-sm',
'type': 'datetime-local'
}),
'status': forms.Select(attrs={'class': 'form-select form-select-sm'}),
'notes': forms.TextInput(attrs={
'class': 'form-control form-control-sm',
'placeholder': 'Attendance notes'
}),
}
help_texts = {
'status': 'Attendance status',
'notes': 'Additional notes about attendance',
}
def clean(self):
cleaned_data = super().clean()
checked_in_at = cleaned_data.get('checked_in_at')
checked_out_at = cleaned_data.get('checked_out_at')
status = cleaned_data.get('status')
if status in ['PRESENT', 'LATE'] and not checked_in_at:
self.add_error('checked_in_at', 'Check-in time is required for present/late status.')
if checked_in_at and checked_out_at:
if checked_out_at <= checked_in_at:
self.add_error('checked_out_at', 'Check-out time must be after check-in time.')
return cleaned_data
class TrainingAssessmentForm(forms.ModelForm):
"""
Form for creating and updating training assessments.
"""
class Meta:
model = TrainingAssessment
fields = ['enrollment', 'name', 'max_score', 'score', 'passed', 'taken_at', 'notes']
widgets = {
'enrollment': forms.Select(attrs={'class': 'form-select form-select-sm'}),
'name': forms.TextInput(attrs={
'class': 'form-control form-control-sm',
'placeholder': 'Assessment name'
}),
'max_score': forms.NumberInput(attrs={
'class': 'form-control form-control-sm',
'step': '0.01',
'min': '0'
}),
'score': forms.NumberInput(attrs={
'class': 'form-control form-control-sm',
'step': '0.01',
'min': '0'
}),
'passed': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'taken_at': forms.DateTimeInput(attrs={
'class': 'form-control form-control-sm',
'type': 'datetime-local'
}),
'notes': forms.Textarea(attrs={
'class': 'form-control form-control-sm',
'rows': 3,
'placeholder': 'Assessment notes'
}),
}
help_texts = {
'max_score': 'Maximum possible score',
'score': 'Actual score achieved',
'passed': 'Check if assessment was passed',
}
def clean(self):
cleaned_data = super().clean()
max_score = cleaned_data.get('max_score')
score = cleaned_data.get('score')
if max_score and score and score > max_score:
self.add_error('score', 'Score cannot exceed maximum score.')
return cleaned_data
class TrainingCertificateForm(forms.ModelForm):
"""
Form for creating and updating training certificates.
"""
class Meta:
model = TrainingCertificates
fields = [
'program', 'employee', 'enrollment', 'certificate_name',
'certificate_number', 'certification_body', 'expiry_date',
'signed_by'
]
widgets = {
'program': forms.Select(attrs={'class': 'form-select form-select-sm'}),
'employee': forms.Select(attrs={'class': 'form-select form-select-sm'}),
'enrollment': forms.Select(attrs={'class': 'form-select form-select-sm'}),
'certificate_name': forms.TextInput(attrs={
'class': 'form-control form-control-sm',
'placeholder': 'Certificate name'
}),
'certificate_number': forms.TextInput(attrs={
'class': 'form-control form-control-sm',
'placeholder': 'Certificate number'
}),
'certification_body': forms.TextInput(attrs={
'class': 'form-control form-control-sm',
'placeholder': 'Issuing organization'
}),
'expiry_date': forms.DateInput(attrs={
'class': 'form-control form-control-sm',
'type': 'date'
}),
'signed_by': forms.Select(attrs={'class': 'form-select form-select-sm'}),
}
help_texts = {
'certificate_number': 'Unique certificate identifier',
'certification_body': 'Organization that issued the certificate',
'expiry_date': 'Certificate expiration date',
'signed_by': 'Authority who signed the certificate',
}
def __init__(self, *args, **kwargs):
user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
if user and hasattr(user, 'tenant'):
# Filter by tenant
self.fields['program'].queryset = TrainingPrograms.objects.filter(
tenant=user.tenant,
is_certified=True
).order_by('name')
self.fields['employee'].queryset = Employee.objects.filter(
tenant=user.tenant,
employment_status='ACTIVE'
).order_by('last_name', 'first_name')
self.fields['enrollment'].queryset = TrainingRecord.objects.filter(
employee__tenant=user.tenant,
status='COMPLETED',
passed=True
).order_by('-completion_date')
self.fields['signed_by'].queryset = Employee.objects.filter(
tenant=user.tenant,
employment_status='ACTIVE'
).order_by('last_name', 'first_name')
def clean(self):
cleaned_data = super().clean()
enrollment = cleaned_data.get('enrollment')
program = cleaned_data.get('program')
employee = cleaned_data.get('employee')
if enrollment and program and enrollment.program != program:
self.add_error('enrollment', 'Selected enrollment does not match the program.')
if enrollment and employee and enrollment.employee != employee:
self.add_error('enrollment', 'Selected enrollment does not match the employee.')
return cleaned_data
class TrainingSearchForm(forms.Form):
"""
Form for searching and filtering training records.
"""
search = forms.CharField(
required=False,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Search training records...'
})
)
employee = forms.ModelChoiceField(
queryset=Employee.objects.none(),
required=False,
widget=forms.Select(attrs={'class': 'form-control'})
)
program = forms.ModelChoiceField(
queryset=TrainingPrograms.objects.none(),
required=False,
widget=forms.Select(attrs={'class': 'form-control'})
)
status = forms.ChoiceField(
choices=[('', 'All Statuses')] + list(TrainingRecord.TrainingStatus.choices),
required=False,
widget=forms.Select(attrs={'class': 'form-control'})
)
program_type = forms.ChoiceField(
choices=[('', 'All Types')] + list(TrainingPrograms.TrainingType.choices),
required=False,
widget=forms.Select(attrs={'class': 'form-control'})
)
start_date = forms.DateField(
required=False,
widget=forms.DateInput(attrs={
'class': 'form-control',
'type': 'date'
})
)
end_date = forms.DateField(
required=False,
widget=forms.DateInput(attrs={
'class': 'form-control',
'type': 'date'
})
)
def __init__(self, *args, **kwargs):
tenant = kwargs.pop('tenant', None)
super().__init__(*args, **kwargs)
if tenant:
self.fields['employee'].queryset = Employee.objects.filter(
tenant=tenant,
employment_status='ACTIVE'
).order_by('last_name', 'first_name')
self.fields['program'].queryset = TrainingPrograms.objects.filter(
tenant=tenant
).order_by('name')
class DepartmentForm(forms.ModelForm):
"""
Form for creating and updating departments.
"""
class Meta:
model = Department
fields = [
'name', 'code', 'description', 'department_type',
'parent_department', 'department_head', 'annual_budget',
'cost_center', 'location', 'is_active', 'notes'
]
widgets = {
'name': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Department name'
}),
'code': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Unique department code'
}),
'description': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Department description'
}),
'department_type': forms.Select(attrs={'class': 'form-control'}),
'parent_department': forms.Select(attrs={'class': 'form-control'}),
'department_head': forms.Select(attrs={'class': 'form-control'}),
'annual_budget': forms.NumberInput(attrs={
'class': 'form-control',
'step': '0.01',
'min': '0'
}),
'cost_center': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Cost center code'
}),
'location': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Department location'
}),
'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'notes': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Additional notes'
}),
}
help_texts = {
'code': 'Unique code to identify this department',
'department_head': 'Department manager (optional)',
'annual_budget': 'Annual budget for this department',
'parent_department': 'Parent department (if any)',
}
def __init__(self, *args, **kwargs):
user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
if user and hasattr(user, 'tenant'):
# Filter parent department and department head by tenant
self.fields['parent_department'].queryset = Department.objects.filter(
tenant=user.tenant
).order_by('name')
self.fields['department_head'].queryset = Employee.objects.filter(
tenant=user.tenant,
employment_status='ACTIVE'
).order_by('last_name', 'first_name')
# Exclude current instance from parent department choices
if self.instance.pk:
self.fields['parent_department'].queryset = self.fields['parent_department'].queryset.exclude(
pk=self.instance.pk
)
def clean_code(self):
code = self.cleaned_data.get('code')
if code:
# Check for uniqueness within tenant (excluding current instance)
queryset = Department.objects.filter(code=code)
if self.instance.pk:
queryset = queryset.exclude(pk=self.instance.pk)
if queryset.exists():
raise ValidationError('Department code must be unique.')
return code
class ScheduleForm(forms.ModelForm):
"""
Form for creating and updating schedules.
"""
# Additional fields to match template expectations
start_date = forms.DateField(
required=True,
widget=forms.DateInput(attrs={
'class': 'form-control',
'type': 'date'
}),
help_text='Schedule start date'
)
department = forms.ModelChoiceField(
queryset=Department.objects.none(),
required=False,
widget=forms.Select(attrs={'class': 'form-control'}),
help_text='Department for this schedule'
)
status = forms.ChoiceField(
choices=[
('DRAFT', 'Draft'),
('PUBLISHED', 'Published'),
('ARCHIVED', 'Archived'),
],
initial='DRAFT',
widget=forms.Select(attrs={'class': 'form-control'}),
help_text='Schedule status'
)
class Meta:
model = Schedule
fields = [
'employee', 'name', 'description', 'schedule_type',
'effective_date', 'end_date', 'schedule_pattern',
'is_active', 'notes'
]
widgets = {
'employee': forms.Select(attrs={'class': 'form-control'}),
'name': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Schedule name'
}),
'description': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Schedule description'
}),
'schedule_type': forms.Select(attrs={'class': 'form-control'}),
'effective_date': forms.HiddenInput(),
'end_date': forms.DateInput(attrs={
'class': 'form-control',
'type': 'date'
}),
'schedule_pattern': forms.Textarea(attrs={
'class': 'form-control',
'rows': 5,
'placeholder': '{"monday": {"start": "09:00", "end": "17:00"}, ...}'
}),
'is_active': forms.HiddenInput(),
'notes': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Schedule notes or instructions'
}),
}
help_texts = {
'schedule_type': 'Type of schedule (regular, rotating, etc.)',
'effective_date': 'Date when this schedule becomes effective',
'end_date': 'Date when this schedule ends (leave blank for indefinite)',
'schedule_pattern': 'JSON configuration for schedule pattern',
}
def __init__(self, *args, **kwargs):
user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
if user and hasattr(user, 'tenant'):
# Filter employee and department by tenant
self.fields['employee'].queryset = Employee.objects.filter(
tenant=user.tenant,
employment_status='ACTIVE'
).order_by('last_name', 'first_name')
self.fields['department'].queryset = Department.objects.filter(
tenant=user.tenant,
is_active=True
).order_by('name')
# Initialize template-compatible fields from model fields
if self.instance and self.instance.pk:
self.fields['start_date'].initial = self.instance.effective_date
# Set status based on is_active field
if self.instance.is_active:
self.fields['status'].initial = 'PUBLISHED'
else:
self.fields['status'].initial = 'DRAFT'
def clean(self):
cleaned_data = super().clean()
# Map template fields to model fields
start_date = cleaned_data.get('start_date')
status = cleaned_data.get('status')
end_date = cleaned_data.get('end_date')
# Set model field values from template fields
if start_date:
cleaned_data['effective_date'] = start_date
if status:
cleaned_data['is_active'] = (status == 'PUBLISHED')
# Validation
effective_date = cleaned_data.get('effective_date')
if end_date and effective_date:
if end_date < effective_date:
self.add_error('end_date', 'End date cannot be before start date.')
return cleaned_data
def save(self, commit=True):
instance = super().save(commit=False)
# Ensure model fields are set from template fields
if hasattr(self, 'cleaned_data'):
if self.cleaned_data.get('start_date'):
instance.effective_date = self.cleaned_data['start_date']
if self.cleaned_data.get('status'):
instance.is_active = (self.cleaned_data['status'] == 'PUBLISHED')
if commit:
instance.save()
return instance
class ScheduleAssignmentForm(forms.ModelForm):
"""
Form for creating and updating schedule assignments.
"""
class Meta:
model = ScheduleAssignment
fields = [
'schedule', 'assignment_date', 'start_time', 'end_time',
'shift_type', 'department', 'location', 'status',
'break_minutes', 'lunch_minutes', 'notes'
]
widgets = {
'schedule': forms.Select(attrs={'class': 'form-control'}),
'assignment_date': forms.DateInput(attrs={
'class': 'form-control',
'type': 'date'
}),
'start_time': forms.TimeInput(attrs={
'class': 'form-control',
'type': 'time'
}),
'end_time': forms.TimeInput(attrs={
'class': 'form-control',
'type': 'time'
}),
'shift_type': forms.Select(attrs={'class': 'form-control'}),
'department': forms.Select(attrs={'class': 'form-control'}),
'location': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Specific location'
}),
'status': forms.Select(attrs={'class': 'form-control'}),
'break_minutes': forms.NumberInput(attrs={
'class': 'form-control',
'min': '0',
'max': '480' # 8 hours max
}),
'lunch_minutes': forms.NumberInput(attrs={
'class': 'form-control',
'min': '0',
'max': '480' # 8 hours max
}),
'notes': forms.Textarea(attrs={
'class': 'form-control',
'rows': 2,
'placeholder': 'Assignment notes'
}),
}
help_texts = {
'break_minutes': 'Break duration in minutes',
'lunch_minutes': 'Lunch duration in minutes',
'shift_type': 'Type of shift',
'status': 'Current status of the assignment',
}
def __init__(self, *args, **kwargs):
user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
if user and hasattr(user, 'tenant'):
# Filter schedule and department by tenant
self.fields['schedule'].queryset = Schedule.objects.filter(
employee__tenant=user.tenant,
is_active=True
).order_by('-effective_date')
self.fields['department'].queryset = Department.objects.filter(
tenant=user.tenant,
is_active=True
).order_by('name')
def clean(self):
cleaned_data = super().clean()
start_time = cleaned_data.get('start_time')
end_time = cleaned_data.get('end_time')
if start_time and end_time:
# Handle overnight shifts
if end_time < start_time:
# This is valid for overnight shifts
pass
elif end_time == start_time:
self.add_error('end_time', 'End time cannot be the same as start time.')
return cleaned_data
class TimeEntryForm(forms.ModelForm):
"""
Form for creating and updating time entries with template-compatible field names.
"""
# Template-compatible field names
date = forms.DateField(
required=True,
widget=forms.DateInput(attrs={
'class': 'form-control',
'type': 'date'
}),
help_text='Work date'
)
start_time = forms.TimeField(
required=True,
widget=forms.TimeInput(attrs={
'class': 'form-control',
'type': 'time'
}),
help_text='Clock in time'
)
end_time = forms.TimeField(
required=True,
widget=forms.TimeInput(attrs={
'class': 'form-control',
'type': 'time'
}),
help_text='Clock out time'
)
hours = forms.DecimalField(
max_digits=5,
decimal_places=2,
required=True,
widget=forms.NumberInput(attrs={
'class': 'form-control',
'step': '0.01',
'readonly': True
}),
help_text='Total hours (calculated automatically)'
)
break_duration = forms.IntegerField(
required=False,
min_value=0,
widget=forms.NumberInput(attrs={
'class': 'form-control',
'min': '0'
}),
help_text='Break duration in minutes'
)
description = forms.CharField(
required=False,
widget=forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Provide details about the work performed during this time period'
}),
help_text='Work description'
)
schedule_assignment = forms.ModelChoiceField(
queryset=ScheduleAssignment.objects.none(),
required=False,
widget=forms.Select(attrs={
'class': 'form-control'
}),
help_text='Link this time entry to a schedule assignment'
)
is_overtime = forms.BooleanField(
required=False,
widget=forms.HiddenInput(),
help_text='Is this overtime work'
)
auto_approve = forms.BooleanField(
required=False,
widget=forms.CheckboxInput(attrs={
'class': 'form-check-input'
}),
help_text='Auto-approve this time entry'
)
class Meta:
model = TimeEntry
fields = [
'employee', 'department', 'location', 'entry_type', 'status'
]
widgets = {
'employee': forms.Select(attrs={
'class': 'form-control'
}),
'department': forms.Select(attrs={
'class': 'form-control'
}),
'location': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Work location'
}),
'entry_type': forms.HiddenInput(),
'status': forms.HiddenInput(),
}
def __init__(self, *args, **kwargs):
user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
if user and hasattr(user, 'tenant'):
# Filter employee and department by tenant
self.fields['employee'].queryset = Employee.objects.filter(
tenant=user.tenant,
employment_status='ACTIVE'
).order_by('last_name', 'first_name')
self.fields['department'].queryset = Department.objects.filter(
tenant=user.tenant,
is_active=True
).order_by('name')
# Filter schedule assignments for today
today = timezone.now().date()
self.fields['schedule_assignment'].queryset = ScheduleAssignment.objects.filter(
schedule__employee__tenant=user.tenant,
assignment_date=today
).select_related('schedule__employee')
# Set initial values for template compatibility
if self.instance and self.instance.pk:
self.fields['date'].initial = self.instance.work_date
self.fields['start_time'].initial = self.instance.clock_in_time.time() if self.instance.clock_in_time else None
self.fields['end_time'].initial = self.instance.clock_out_time.time() if self.instance.clock_out_time else None
self.fields['hours'].initial = self.instance.total_hours
self.fields['description'].initial = self.instance.notes
# Calculate break duration
if self.instance.break_start_time and self.instance.break_end_time:
break_duration = (self.instance.break_end_time - self.instance.break_start_time).total_seconds() / 60
self.fields['break_duration'].initial = int(break_duration)
# Set overtime based on entry type
self.fields['is_overtime'].initial = (self.instance.entry_type == 'OVERTIME')
def clean(self):
cleaned_data = super().clean()
# Map template fields to model fields
date = cleaned_data.get('date')
start_time = cleaned_data.get('start_time')
end_time = cleaned_data.get('end_time')
hours = cleaned_data.get('hours')
break_duration = cleaned_data.get('break_duration', 0)
description = cleaned_data.get('description')
schedule_assignment = cleaned_data.get('schedule_assignment')
is_overtime = cleaned_data.get('is_overtime')
# Set model field values
if date:
cleaned_data['work_date'] = date
# Combine date and time for datetime fields
if date and start_time:
cleaned_data['clock_in_time'] = timezone.datetime.combine(date, start_time)
if date and end_time:
cleaned_data['clock_out_time'] = timezone.datetime.combine(date, end_time)
if hours:
cleaned_data['total_hours'] = hours
if description:
cleaned_data['notes'] = description
if is_overtime:
cleaned_data['entry_type'] = 'OVERTIME'
else:
cleaned_data['entry_type'] = 'REGULAR'
# Set status based on auto_approve
auto_approve = cleaned_data.get('auto_approve', False)
if auto_approve:
cleaned_data['status'] = 'APPROVED'
else:
cleaned_data['status'] = 'SUBMITTED'
# Validation
clock_in_time = cleaned_data.get('clock_in_time')
clock_out_time = cleaned_data.get('clock_out_time')
if clock_in_time and clock_out_time:
if clock_out_time <= clock_in_time:
self.add_error('end_time', 'End time must be after start time.')
# Check for reasonable shift length (max 24 hours)
duration = clock_out_time - clock_in_time
if duration.total_seconds() > 24 * 3600:
self.add_error('end_time', 'Shift duration cannot exceed 24 hours.')
return cleaned_data
def save(self, commit=True):
instance = super().save(commit=False)
# Ensure model fields are set from template fields
if hasattr(self, 'cleaned_data'):
if self.cleaned_data.get('work_date'):
instance.work_date = self.cleaned_data['work_date']
if self.cleaned_data.get('clock_in_time'):
instance.clock_in_time = self.cleaned_data['clock_in_time']
if self.cleaned_data.get('clock_out_time'):
instance.clock_out_time = self.cleaned_data['clock_out_time']
if self.cleaned_data.get('total_hours'):
instance.total_hours = self.cleaned_data['total_hours']
if self.cleaned_data.get('notes'):
instance.notes = self.cleaned_data['notes']
if self.cleaned_data.get('entry_type'):
instance.entry_type = self.cleaned_data['entry_type']
if self.cleaned_data.get('status'):
instance.status = self.cleaned_data['status']
if commit:
instance.save()
return instance
class PerformanceReviewForm(forms.ModelForm):
"""
Form for creating and updating performance reviews with enhanced template compatibility.
"""
# Additional fields to match template expectations
period_start = forms.DateField(
required=False,
widget=forms.HiddenInput(),
help_text='Review period start date'
)
period_end = forms.DateField(
required=False,
widget=forms.HiddenInput(),
help_text='Review period end date'
)
due_date = forms.DateField(
required=True,
widget=forms.DateInput(attrs={
'class': 'form-control form-control-sm',
'type': 'date'
}),
help_text='Review due date'
)
overall_score = forms.DecimalField(
required=False,
max_digits=3,
decimal_places=1,
widget=forms.NumberInput(attrs={
'class': 'form-control form-control-sm',
'step': '0.1',
'min': '1',
'max': '5'
}),
help_text='Overall score (1-5)'
)
completion_percentage = forms.IntegerField(
required=False,
widget=forms.NumberInput(attrs={
'class': 'form-control form-control-sm',
'min': '0',
'max': '100'
}),
help_text='Completion percentage for in-progress reviews'
)
reviewer_comments = forms.CharField(
required=False,
widget=forms.Textarea(attrs={
'class': 'form-control form-control-sm',
'rows': 4,
'placeholder': 'Reviewer comments and feedback'
}),
help_text='Comments from the reviewer'
)
attachments = forms.FileField(
required=False,
widget=forms.FileInput(attrs={
'class': 'form-control form-control-sm'
}),
help_text='Upload supporting documents'
)
class Meta:
model = PerformanceReview
fields = [
'employee', 'reviewer', 'review_type', 'status',
'review_period_start', 'review_period_end', 'review_date',
'overall_rating', 'competency_ratings', 'goals_achieved',
'goals_not_achieved', 'future_goals', 'strengths',
'areas_for_improvement', 'development_plan',
'training_recommendations', 'employee_comments',
'employee_signature_date', 'notes'
]
widgets = {
'employee': forms.Select(attrs={
'class': 'form-select form-select-sm',
'data-placeholder': 'Select employee...'
}),
'reviewer': forms.Select(attrs={
'class': 'form-select form-select-sm',
'data-placeholder': 'Select reviewer...'
}),
'review_type': forms.Select(attrs={
'class': 'form-select form-select-sm'
}),
'status': forms.Select(attrs={
'class': 'form-select form-select-sm'
}),
'review_period_start': forms.HiddenInput(),
'review_period_end': forms.HiddenInput(),
'review_date': forms.HiddenInput(),
'overall_rating': forms.HiddenInput(),
'competency_ratings': forms.HiddenInput(),
'goals_achieved': forms.HiddenInput(),
'goals_not_achieved': forms.HiddenInput(),
'future_goals': forms.HiddenInput(),
'strengths': forms.Textarea(attrs={
'class': 'form-control form-control-sm',
'rows': 4,
'placeholder': 'List employee strengths (one per line)'
}),
'areas_for_improvement': forms.Textarea(attrs={
'class': 'form-control form-control-sm',
'rows': 4,
'placeholder': 'List areas for improvement (one per line)'
}),
'development_plan': forms.Textarea(attrs={
'class': 'form-control form-control-sm',
'rows': 4,
'placeholder': 'Outline professional development plan'
}),
'training_recommendations': forms.Textarea(attrs={
'class': 'form-control form-control-sm',
'rows': 3,
'placeholder': 'Recommended training programs'
}),
'employee_comments': forms.Textarea(attrs={
'class': 'form-control form-control-sm',
'rows': 4,
'placeholder': 'Employee self-assessment and comments'
}),
'employee_signature_date': forms.DateInput(attrs={
'class': 'form-control form-control-sm',
'type': 'date'
}),
'notes': forms.Textarea(attrs={
'class': 'form-control form-control-sm',
'rows': 3,
'placeholder': 'Additional notes and observations'
}),
}
help_texts = {
'employee': 'Employee being reviewed',
'reviewer': 'Person conducting the review',
'review_type': 'Type of performance review',
'status': 'Current status of the review',
'overall_rating': 'Overall performance rating (1-5)',
'competency_ratings': 'Individual competency ratings as JSON',
'employee_signature_date': 'Date employee acknowledged review',
}
def __init__(self, *args, **kwargs):
user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
if user and hasattr(user, 'tenant'):
# Filter employee and reviewer by tenant
self.fields['employee'].queryset = Employee.objects.filter(
tenant=user.tenant,
employment_status='ACTIVE'
).order_by('last_name', 'first_name')
self.fields['reviewer'].queryset = Employee.objects.filter(
tenant=user.tenant,
employment_status='ACTIVE'
).order_by('last_name', 'first_name')
# Initialize template-compatible fields from model fields
if self.instance and self.instance.pk:
self.fields['period_start'].initial = self.instance.review_period_start
self.fields['period_end'].initial = self.instance.review_period_end
self.fields['due_date'].initial = self.instance.review_date
self.fields['overall_score'].initial = self.instance.overall_rating
# Set reviewer_comments from notes field for backward compatibility
self.fields['reviewer_comments'].initial = self.instance.notes
def clean(self):
cleaned_data = super().clean()
# Map template fields to model fields
period_start = cleaned_data.get('period_start')
period_end = cleaned_data.get('period_end')
due_date = cleaned_data.get('due_date')
overall_score = cleaned_data.get('overall_score')
reviewer_comments = cleaned_data.get('reviewer_comments')
# Set model field values from template fields
if period_start:
cleaned_data['review_period_start'] = period_start
if period_end:
cleaned_data['review_period_end'] = period_end
if due_date:
cleaned_data['review_date'] = due_date
if overall_score:
cleaned_data['overall_rating'] = overall_score
if reviewer_comments:
cleaned_data['notes'] = reviewer_comments
# Validation
review_period_start = cleaned_data.get('review_period_start')
review_period_end = cleaned_data.get('review_period_end')
review_date = cleaned_data.get('review_date')
status = cleaned_data.get('status')
employee_signature_date = cleaned_data.get('employee_signature_date')
if review_period_start and review_period_end:
if review_period_end < review_period_start:
self.add_error('period_end', 'Review period end date must be after start date.')
if review_date and review_period_end:
if review_date < review_period_end:
self.add_error('due_date', 'Review date should be on or after the review period end date.')
if status == 'ACKNOWLEDGED' and not employee_signature_date:
self.add_error('employee_signature_date', 'Employee signature date is required for acknowledged reviews.')
return cleaned_data
def save(self, commit=True):
instance = super().save(commit=False)
# Ensure model fields are set from template fields
if hasattr(self, 'cleaned_data'):
if self.cleaned_data.get('period_start'):
instance.review_period_start = self.cleaned_data['period_start']
if self.cleaned_data.get('period_end'):
instance.review_period_end = self.cleaned_data['period_end']
if self.cleaned_data.get('due_date'):
instance.review_date = self.cleaned_data['due_date']
if self.cleaned_data.get('overall_score'):
instance.overall_rating = self.cleaned_data['overall_score']
if self.cleaned_data.get('reviewer_comments'):
instance.notes = self.cleaned_data['reviewer_comments']
if commit:
instance.save()
return instance
class TrainingRecordForm(forms.ModelForm):
"""
Form for creating and updating training records.
"""
class Meta:
model = TrainingRecord
fields = [
'employee', 'program', 'session', 'started_at', 'completion_date',
'status', 'credits_earned', 'score', 'passed', 'notes', 'cost_paid'
]
widgets = {
'employee': forms.Select(attrs={'class': 'form-select form-select-sm'}),
'program': forms.Select(attrs={'class': 'form-select form-select-sm'}),
'session': forms.Select(attrs={'class': 'form-select form-select-sm'}),
'started_at': forms.DateTimeInput(attrs={
'class': 'form-control form-control-sm',
'type': 'datetime-local'
}),
'completion_date': forms.DateInput(attrs={
'class': 'form-control form-control-sm',
'type': 'date'
}),
'status': forms.Select(attrs={'class': 'form-select form-select-sm'}),
'credits_earned': forms.NumberInput(attrs={
'class': 'form-control form-control-sm',
'step': '0.1',
'min': '0'
}),
'score': forms.NumberInput(attrs={
'class': 'form-control form-control-sm',
'step': '0.01',
'min': '0',
'max': '100'
}),
'passed': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'cost_paid': forms.NumberInput(attrs={
'class': 'form-control form-control-sm',
'step': '0.01',
'min': '0'
}),
'notes': forms.Textarea(attrs={
'class': 'form-control form-control-sm',
'rows': 3,
'placeholder': 'Training notes and comments'
}),
}
help_texts = {
'program': 'Training program',
'session': 'Specific training session (optional)',
'credits_earned': 'Continuing education credits earned',
'cost_paid': 'Amount paid for training',
'score': 'Score achieved (0-100)',
}
def __init__(self, *args, **kwargs):
user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
if user and hasattr(user, 'tenant'):
# Filter by tenant
self.fields['employee'].queryset = Employee.objects.filter(
tenant=user.tenant,
employment_status='ACTIVE'
).order_by('last_name', 'first_name')
self.fields['program'].queryset = TrainingPrograms.objects.filter(
tenant=user.tenant
).order_by('name')
self.fields['session'].queryset = TrainingSession.objects.filter(
program__tenant=user.tenant
).order_by('-start_at')
# Make session optional
self.fields['session'].required = False
def clean(self):
cleaned_data = super().clean()
started_at = cleaned_data.get('started_at')
completion_date = cleaned_data.get('completion_date')
status = cleaned_data.get('status')
passed = cleaned_data.get('passed')
session = cleaned_data.get('session')
program = cleaned_data.get('program')
if completion_date and started_at:
if completion_date < started_at.date():
self.add_error('completion_date', 'Completion date cannot be before start date.')
if status == 'COMPLETED' and not completion_date:
self.add_error('completion_date', 'Completion date is required for completed training.')
if passed and status not in ['COMPLETED']:
self.add_error('passed', 'Training must be completed to mark as passed.')
if session and program and session.program != program:
self.add_error('session', 'Selected session does not belong to the selected program.')
return cleaned_data