""" 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 ) class EmployeeForm(forms.ModelForm): """ Form for creating and updating employees. """ class Meta: model = Employee fields = [ 'employee_number', 'first_name', 'last_name', 'middle_name', 'preferred_name', 'email', 'phone', 'mobile_phone', 'address_line_1', 'address_line_2', 'city', 'state', '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', 'certifications', 'emergency_contact_name', 'emergency_contact_relationship', 'emergency_contact_phone', 'notes' ] widgets = { 'employee_number': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Unique employee ID' }), 'first_name': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'First name' }), 'last_name': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Last name' }), 'middle_name': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Middle name' }), 'preferred_name': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Preferred name' }), 'email': forms.EmailInput(attrs={ 'class': 'form-control', 'placeholder': 'employee@hospital.com' }), 'phone': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': '+1-555-123-4567' }), 'mobile_phone': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': '+1-555-123-4567' }), 'address_line_1': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Street address' }), 'address_line_2': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Apt, Suite, etc.' }), 'city': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'City' }), 'state': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'State/Province' }), 'postal_code': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Postal/ZIP code' }), 'country': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Country' }), 'department': forms.Select(attrs={'class': 'form-control'}), 'job_title': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Job title/position' }), 'employment_status': forms.Select(attrs={'class': 'form-control'}), 'employment_type': forms.Select(attrs={'class': 'form-control'}), 'hire_date': forms.DateInput(attrs={ 'class': 'form-control', 'type': 'date' }), 'termination_date': forms.DateInput(attrs={ 'class': 'form-control', 'type': 'date' }), 'annual_salary': forms.NumberInput(attrs={ 'class': 'form-control', 'step': '0.01', 'min': '0' }), 'hourly_rate': forms.NumberInput(attrs={ 'class': 'form-control', 'step': '0.01', 'min': '0' }), 'supervisor': forms.Select(attrs={'class': 'form-control'}), 'emergency_contact_name': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Emergency contact name' }), 'emergency_contact_relationship': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Relationship to employee' }), 'emergency_contact_phone': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Emergency contact phone' }), 'date_of_birth': forms.DateInput(attrs={ 'class': 'form-control', 'type': 'date' }), 'gender': forms.Select(attrs={'class': 'form-control'}), 'marital_status': forms.Select(attrs={'class': 'form-control'}), 'standard_hours_per_week': forms.NumberInput(attrs={ 'class': 'form-control', 'step': '0.5', 'min': '0', 'max': '168' }), 'fte_percentage': forms.NumberInput(attrs={ 'class': 'form-control', 'step': '1', 'min': '0', 'max': '100' }), 'license_number': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Professional license number' }), 'license_expiry_date': forms.DateInput(attrs={ 'class': 'form-control', 'type': 'date' }), 'certifications': forms.Textarea(attrs={ 'class': 'form-control', 'rows': 3, 'placeholder': 'List of certifications' }), 'notes': forms.Textarea(attrs={ 'class': 'form-control', 'rows': 3, 'placeholder': 'Additional notes' }), } help_texts = { 'employee_number': 'Unique identifier for this employee', '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 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' }), 'department_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 = { 'department_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_department_code(self): department_code = self.cleaned_data.get('department_code') if department_code: # Check for uniqueness within tenant (excluding current instance) queryset = Department.objects.filter(department_code=department_code) if self.instance.pk: queryset = queryset.exclude(pk=self.instance.pk) if queryset.exists(): raise ValidationError('Department code must be unique.') return department_code class ScheduleForm(forms.ModelForm): """ Form for creating and updating schedules. """ 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.DateInput(attrs={ 'class': 'form-control', 'type': 'date' }), '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.CheckboxInput(attrs={'class': 'form-check-input'}), '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 by tenant self.fields['employee'].queryset = Employee.objects.filter( tenant=user.tenant, employment_status='ACTIVE' ).order_by('last_name', 'first_name') def clean_end_date(self): end_date = self.cleaned_data.get('end_date') effective_date = self.cleaned_data.get('effective_date') if end_date and effective_date: if end_date < effective_date: raise ValidationError('End date cannot be before effective date.') return end_date 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. """ class Meta: model = TimeEntry fields = [ 'employee', 'work_date', 'clock_in_time', 'clock_out_time', 'break_start_time', 'break_end_time', 'lunch_start_time', 'lunch_end_time', 'entry_type', 'department', 'location', 'status', 'notes' ] widgets = { 'employee': forms.Select(attrs={'class': 'form-control'}), 'work_date': forms.DateInput(attrs={ 'class': 'form-control', 'type': 'date' }), 'clock_in_time': forms.DateTimeInput(attrs={ 'class': 'form-control', 'type': 'datetime-local' }), 'clock_out_time': forms.DateTimeInput(attrs={ 'class': 'form-control', 'type': 'datetime-local' }), 'break_start_time': forms.DateTimeInput(attrs={ 'class': 'form-control', 'type': 'datetime-local' }), 'break_end_time': forms.DateTimeInput(attrs={ 'class': 'form-control', 'type': 'datetime-local' }), 'lunch_start_time': forms.DateTimeInput(attrs={ 'class': 'form-control', 'type': 'datetime-local' }), 'lunch_end_time': forms.DateTimeInput(attrs={ 'class': 'form-control', 'type': 'datetime-local' }), 'entry_type': forms.Select(attrs={'class': 'form-control'}), 'department': forms.Select(attrs={'class': 'form-control'}), 'location': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Work location' }), 'status': forms.Select(attrs={'class': 'form-control'}), 'notes': forms.Textarea(attrs={ 'class': 'form-control', 'rows': 2, 'placeholder': 'Time entry notes' }), } help_texts = { 'work_date': 'Date of work', 'clock_in_time': 'Clock in date and time', 'clock_out_time': 'Clock out date and time', 'entry_type': 'Type of time entry', 'status': 'Current status of the time entry', } 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') def clean(self): cleaned_data = super().clean() clock_in_time = cleaned_data.get('clock_in_time') clock_out_time = cleaned_data.get('clock_out_time') break_start_time = cleaned_data.get('break_start_time') break_end_time = cleaned_data.get('break_end_time') lunch_start_time = cleaned_data.get('lunch_start_time') lunch_end_time = cleaned_data.get('lunch_end_time') if clock_in_time and clock_out_time: if clock_out_time <= clock_in_time: self.add_error('clock_out_time', 'Clock out time must be after clock in 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('clock_out_time', 'Shift duration cannot exceed 24 hours.') # Validate break times if break_start_time and not break_end_time: self.add_error('break_end_time', 'Break end time is required if break start time is provided.') if break_end_time and not break_start_time: self.add_error('break_start_time', 'Break start time is required if break end time is provided.') if break_start_time and break_end_time: if break_end_time <= break_start_time: self.add_error('break_end_time', 'Break end time must be after break start time.') if clock_in_time and break_start_time < clock_in_time: self.add_error('break_start_time', 'Break start time must be after clock in time.') if clock_out_time and break_end_time > clock_out_time: self.add_error('break_end_time', 'Break end time must be before clock out time.') # Validate lunch times if lunch_start_time and not lunch_end_time: self.add_error('lunch_end_time', 'Lunch end time is required if lunch start time is provided.') if lunch_end_time and not lunch_start_time: self.add_error('lunch_start_time', 'Lunch start time is required if lunch end time is provided.') if lunch_start_time and lunch_end_time: if lunch_end_time <= lunch_start_time: self.add_error('lunch_end_time', 'Lunch end time must be after lunch start time.') if clock_in_time and lunch_start_time < clock_in_time: self.add_error('lunch_start_time', 'Lunch start time must be after clock in time.') if clock_out_time and lunch_end_time > clock_out_time: self.add_error('lunch_end_time', 'Lunch end time must be before clock out time.') return cleaned_data class PerformanceReviewForm(forms.ModelForm): """ Form for creating and updating performance reviews. """ class Meta: model = PerformanceReview fields = [ 'employee', 'review_period_start', 'review_period_end', 'review_date', 'review_type', 'overall_rating', 'competency_ratings', 'goals_achieved', 'goals_not_achieved', 'future_goals', 'strengths', 'areas_for_improvement', 'development_plan', 'training_recommendations', 'employee_comments', 'employee_signature_date', 'status', 'notes' ] widgets = { 'employee': forms.Select(attrs={'class': 'form-control'}), 'review_period_start': forms.DateInput(attrs={ 'class': 'form-control', 'type': 'date' }), 'review_period_end': forms.DateInput(attrs={ 'class': 'form-control', 'type': 'date' }), 'review_date': forms.DateInput(attrs={ 'class': 'form-control', 'type': 'date' }), 'review_type': forms.Select(attrs={'class': 'form-control'}), 'overall_rating': forms.NumberInput(attrs={ 'class': 'form-control', 'step': '0.1', 'min': '1', 'max': '5' }), 'competency_ratings': forms.Textarea(attrs={ 'class': 'form-control', 'rows': 5, 'placeholder': '{"communication": 4, "teamwork": 5, ...}' }), 'goals_achieved': forms.Textarea(attrs={ 'class': 'form-control', 'rows': 3, 'placeholder': 'Goals achieved during review period' }), 'goals_not_achieved': forms.Textarea(attrs={ 'class': 'form-control', 'rows': 3, 'placeholder': 'Goals not achieved during review period' }), 'future_goals': forms.Textarea(attrs={ 'class': 'form-control', 'rows': 3, 'placeholder': 'Goals for next review period' }), 'strengths': forms.Textarea(attrs={ 'class': 'form-control', 'rows': 3, 'placeholder': 'Employee strengths' }), 'areas_for_improvement': forms.Textarea(attrs={ 'class': 'form-control', 'rows': 3, 'placeholder': 'Areas for improvement' }), 'development_plan': forms.Textarea(attrs={ 'class': 'form-control', 'rows': 3, 'placeholder': 'Professional development plan' }), 'training_recommendations': forms.Textarea(attrs={ 'class': 'form-control', 'rows': 3, 'placeholder': 'Training recommendations' }), 'employee_comments': forms.Textarea(attrs={ 'class': 'form-control', 'rows': 3, 'placeholder': 'Employee comments' }), 'employee_signature_date': forms.DateInput(attrs={ 'class': 'form-control', 'type': 'date' }), 'status': forms.Select(attrs={'class': 'form-control'}), 'notes': forms.Textarea(attrs={ 'class': 'form-control', 'rows': 3, 'placeholder': 'Additional notes' }), } help_texts = { 'review_type': 'Type of performance review', 'overall_rating': 'Overall rating (1-5)', 'competency_ratings': 'Individual competency ratings as JSON', 'status': 'Current status of the review', } def __init__(self, *args, **kwargs): user = kwargs.pop('user', None) super().__init__(*args, **kwargs) if user and hasattr(user, 'tenant'): # Filter employee by tenant self.fields['employee'].queryset = Employee.objects.filter( tenant=user.tenant, employment_status='ACTIVE' ).order_by('last_name', 'first_name') def clean(self): cleaned_data = super().clean() 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('review_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('review_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 class TrainingRecordForm(forms.ModelForm): """ Form for creating and updating training records. """ class Meta: model = TrainingRecord fields = [ 'employee', 'training_name', 'training_description', 'training_type', 'training_provider', 'instructor', 'training_date', 'completion_date', 'expiry_date', 'duration_hours', 'credits_earned', 'status', 'score', 'passed', 'certificate_number', 'certification_body', 'training_cost', 'notes' ] widgets = { 'employee': forms.Select(attrs={'class': 'form-control'}), 'training_name': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Training program name' }), 'training_description': forms.Textarea(attrs={ 'class': 'form-control', 'rows': 3, 'placeholder': 'Training description' }), 'training_type': forms.Select(attrs={'class': 'form-control'}), 'training_provider': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Training provider/organization' }), 'instructor': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Instructor name' }), 'training_date': forms.DateInput(attrs={ 'class': 'form-control', 'type': 'date' }), 'completion_date': forms.DateInput(attrs={ 'class': 'form-control', 'type': 'date' }), 'expiry_date': forms.DateInput(attrs={ 'class': 'form-control', 'type': 'date' }), 'duration_hours': forms.NumberInput(attrs={ 'class': 'form-control', 'step': '0.5', 'min': '0' }), 'credits_earned': forms.NumberInput(attrs={ 'class': 'form-control', 'step': '0.1', 'min': '0' }), 'status': forms.Select(attrs={'class': 'form-control'}), 'score': forms.NumberInput(attrs={ 'class': 'form-control', 'step': '0.01', 'min': '0', 'max': '100' }), 'passed': forms.CheckboxInput(attrs={'class': 'form-check-input'}), 'certificate_number': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Certificate or credential number' }), 'certification_body': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Certification organization' }), 'training_cost': forms.NumberInput(attrs={ 'class': 'form-control', 'step': '0.01', 'min': '0' }), 'notes': forms.Textarea(attrs={ 'class': 'form-control', 'rows': 3, 'placeholder': 'Training notes and comments' }), } help_texts = { 'training_type': 'Type of training (mandatory, optional, certification, etc.)', 'duration_hours': 'Training duration in hours', 'credits_earned': 'Continuing education credits earned', 'training_cost': 'Training cost', 'expiry_date': 'Certification expiry date (if applicable)', } def __init__(self, *args, **kwargs): user = kwargs.pop('user', None) super().__init__(*args, **kwargs) if user and hasattr(user, 'tenant'): # Filter employee by tenant self.fields['employee'].queryset = Employee.objects.filter( tenant=user.tenant, employment_status='ACTIVE' ).order_by('last_name', 'first_name') def clean(self): cleaned_data = super().clean() training_date = cleaned_data.get('training_date') completion_date = cleaned_data.get('completion_date') expiry_date = cleaned_data.get('expiry_date') status = cleaned_data.get('status') passed = cleaned_data.get('passed') certificate_number = cleaned_data.get('certificate_number') if completion_date and training_date: if completion_date < training_date: self.add_error('completion_date', 'Completion date cannot be before training date.') if expiry_date and completion_date: if expiry_date <= completion_date: self.add_error('expiry_date', 'Expiry date must be after completion 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', 'PASSED']: self.add_error('passed', 'Training must be completed or passed to mark as passed.') if certificate_number and not passed: self.add_error('certificate_number', 'Certificate number can only be provided for passed training.') return cleaned_data