""" 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 django.db import models as django_models from datetime import date, timedelta from decimal import Decimal from .models import * 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 class LeaveTypeForm(forms.ModelForm): """ Form for creating and updating leave types (Admin only). """ class Meta: model = LeaveType fields = [ 'name', 'code', 'description', 'is_paid', 'requires_approval', 'requires_documentation', 'accrual_method', 'annual_entitlement', 'max_carry_over', 'max_consecutive_days', 'min_notice_days', 'is_active', 'available_for_all', 'gender_specific' ] widgets = { 'name': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Leave type name (e.g., Annual Leave)' }), 'code': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Short code (e.g., AL, SL)' }), 'description': forms.Textarea(attrs={ 'class': 'form-control', 'rows': 3, 'placeholder': 'Description of this leave type' }), 'is_paid': forms.CheckboxInput(attrs={'class': 'form-check-input'}), 'requires_approval': forms.CheckboxInput(attrs={'class': 'form-check-input'}), 'requires_documentation': forms.CheckboxInput(attrs={'class': 'form-check-input'}), 'accrual_method': forms.Select(attrs={'class': 'form-select'}), 'annual_entitlement': forms.NumberInput(attrs={ 'class': 'form-control', 'step': '0.5', 'min': '0' }), 'max_carry_over': forms.NumberInput(attrs={ 'class': 'form-control', 'step': '0.5', 'min': '0' }), 'max_consecutive_days': forms.NumberInput(attrs={ 'class': 'form-control', 'min': '1' }), 'min_notice_days': forms.NumberInput(attrs={ 'class': 'form-control', 'min': '0' }), 'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}), 'available_for_all': forms.CheckboxInput(attrs={'class': 'form-check-input'}), 'gender_specific': forms.Select(attrs={'class': 'form-select'}), } help_texts = { 'code': 'Unique code to identify this leave type', 'is_paid': 'Is this a paid leave type?', 'requires_approval': 'Does this leave require manager approval?', 'requires_documentation': 'Requires supporting documents (e.g., medical certificate)?', 'accrual_method': 'How leave days are accrued', 'annual_entitlement': 'Number of days entitled per year', 'max_carry_over': 'Maximum days that can be carried to next year', 'max_consecutive_days': 'Maximum consecutive days allowed in one request', 'min_notice_days': 'Minimum advance notice required', 'gender_specific': 'Restrict to specific gender (for maternity/paternity)', } def clean_code(self): code = self.cleaned_data.get('code') if code: code = code.upper() return code class LeaveRequestForm(forms.ModelForm): """ Form for creating and updating leave requests. """ class Meta: model = LeaveRequest fields = [ 'leave_type', 'start_date', 'end_date', 'start_day_type', 'end_day_type', 'reason', 'contact_number', 'emergency_contact', 'attachment' ] widgets = { 'leave_type': forms.Select(attrs={ 'class': 'form-select', 'required': True }), 'start_date': forms.DateInput(attrs={ 'class': 'form-control', 'type': 'date', 'required': True }), 'end_date': forms.DateInput(attrs={ 'class': 'form-control', 'type': 'date', 'required': True }), 'start_day_type': forms.Select(attrs={'class': 'form-select'}), 'end_day_type': forms.Select(attrs={'class': 'form-select'}), 'reason': forms.Textarea(attrs={ 'class': 'form-control', 'rows': 4, 'placeholder': 'Please provide a reason for your leave request', 'required': True }), 'contact_number': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Contact number during leave' }), 'emergency_contact': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Emergency contact person' }), 'attachment': forms.FileInput(attrs={ 'class': 'form-control', 'accept': '.pdf,.jpg,.jpeg,.png,.doc,.docx' }), } help_texts = { 'leave_type': 'Select the type of leave you are requesting', 'start_date': 'First day of leave', 'end_date': 'Last day of leave', 'start_day_type': 'Full day or half day for start date', 'end_day_type': 'Full day or half day for end date', 'attachment': 'Upload supporting documents (medical certificate, etc.)', } def __init__(self, *args, **kwargs): self.employee = kwargs.pop('employee', None) self.user = kwargs.pop('user', None) super().__init__(*args, **kwargs) # Filter leave types by tenant and availability if self.user and hasattr(self.user, 'tenant'): leave_types = LeaveType.objects.filter( tenant=self.user.tenant, is_active=True ) # Filter by gender if applicable if self.employee and self.employee.gender: leave_types = leave_types.filter( django_models.Q(gender_specific__isnull=True) | django_models.Q(gender_specific=self.employee.gender) ) self.fields['leave_type'].queryset = leave_types.order_by('name') def clean(self): cleaned_data = super().clean() start_date = cleaned_data.get('start_date') end_date = cleaned_data.get('end_date') leave_type = cleaned_data.get('leave_type') if start_date and end_date: # Validate date range if end_date < start_date: raise ValidationError({ 'end_date': 'End date cannot be before start date.' }) # Check if dates are in the past if start_date < date.today(): raise ValidationError({ 'start_date': 'Cannot request leave for past dates.' }) # Check minimum notice period if leave_type and leave_type.min_notice_days: notice_date = date.today() + timedelta(days=leave_type.min_notice_days) if start_date < notice_date: raise ValidationError({ 'start_date': f'Minimum {leave_type.min_notice_days} days advance notice required.' }) # Calculate total days total_days = (end_date - start_date).days + 1 days = Decimal(str(total_days)) # Adjust for half days start_day_type = cleaned_data.get('start_day_type') end_day_type = cleaned_data.get('end_day_type') if start_day_type in ['HALF_DAY_AM', 'HALF_DAY_PM']: days -= Decimal('0.5') if end_day_type in ['HALF_DAY_AM', 'HALF_DAY_PM'] and start_date != end_date: days -= Decimal('0.5') # Check maximum consecutive days if leave_type and leave_type.max_consecutive_days: if days > leave_type.max_consecutive_days: raise ValidationError({ 'end_date': f'Maximum {leave_type.max_consecutive_days} consecutive days allowed for this leave type.' }) # Check available balance if self.employee and leave_type: from .models import LeaveBalance current_year = date.today().year try: balance = LeaveBalance.objects.get( employee=self.employee, leave_type=leave_type, year=current_year ) if not balance.can_request(days): raise ValidationError({ 'leave_type': f'Insufficient leave balance. Available: {balance.available} days, Requested: {days} days.' }) except LeaveBalance.DoesNotExist: # No balance record exists if leave_type.requires_approval: raise ValidationError({ 'leave_type': 'No leave balance found for this leave type. Please contact HR.' }) # Check if documentation is required if leave_type and leave_type.requires_documentation: attachment = cleaned_data.get('attachment') if not attachment and not self.instance.pk: raise ValidationError({ 'attachment': 'Supporting documentation is required for this leave type.' }) return cleaned_data class LeaveApprovalForm(forms.ModelForm): """ Form for approving/rejecting leave requests. """ class Meta: model = LeaveApproval fields = ['action', 'comments'] widgets = { 'action': forms.Select(attrs={ 'class': 'form-select', 'required': True }), 'comments': forms.Textarea(attrs={ 'class': 'form-control', 'rows': 4, 'placeholder': 'Add comments about your decision (optional)' }), } help_texts = { 'action': 'Approve or reject this leave request', 'comments': 'Provide feedback or conditions for approval', } def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) # Limit action choices to APPROVED and REJECTED self.fields['action'].choices = [ ('APPROVED', 'Approve'), ('REJECTED', 'Reject'), ] class LeaveDelegateForm(forms.ModelForm): """ Form for creating leave approval delegations. """ class Meta: model = LeaveDelegate fields = ['delegate', 'start_date', 'end_date', 'reason'] widgets = { 'delegate': forms.Select(attrs={ 'class': 'form-select', 'required': True }), 'start_date': forms.DateInput(attrs={ 'class': 'form-control', 'type': 'date', 'required': True }), 'end_date': forms.DateInput(attrs={ 'class': 'form-control', 'type': 'date', 'required': True }), 'reason': forms.Textarea(attrs={ 'class': 'form-control', 'rows': 3, 'placeholder': 'Reason for delegation (e.g., vacation, business trip)', 'required': True }), } help_texts = { 'delegate': 'Person who will approve leave requests on your behalf', 'start_date': 'Delegation start date', 'end_date': 'Delegation end date', 'reason': 'Reason for delegating approval authority', } def __init__(self, *args, **kwargs): self.delegator = kwargs.pop('delegator', None) self.user = kwargs.pop('user', None) super().__init__(*args, **kwargs) # Filter delegates by tenant, exclude self if self.user and hasattr(self.user, 'tenant'): delegates = Employee.objects.filter( tenant=self.user.tenant, employment_status='ACTIVE' ) if self.delegator: delegates = delegates.exclude(pk=self.delegator.pk) self.fields['delegate'].queryset = delegates.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') delegate = cleaned_data.get('delegate') if start_date and end_date: # Validate date range if end_date < start_date: raise ValidationError({ 'end_date': 'End date cannot be before start date.' }) # Check if start date is in the past if start_date < date.today(): raise ValidationError({ 'start_date': 'Delegation cannot start in the past.' }) # Check if delegating to self if self.delegator and delegate and self.delegator == delegate: raise ValidationError({ 'delegate': 'Cannot delegate to yourself.' }) # Check for overlapping delegations if self.delegator and start_date and end_date: overlapping = LeaveDelegate.objects.filter( delegator=self.delegator, is_active=True, start_date__lte=end_date, end_date__gte=start_date ) if self.instance.pk: overlapping = overlapping.exclude(pk=self.instance.pk) if overlapping.exists(): raise ValidationError( 'You already have an active delegation for this period. ' 'Please revoke the existing delegation first.' ) return cleaned_data class LeaveBalanceAdjustmentForm(forms.Form): """ Form for manual leave balance adjustments (HR only). """ employee = forms.ModelChoiceField( queryset=Employee.objects.none(), widget=forms.Select(attrs={'class': 'form-select'}), help_text='Select employee' ) leave_type = forms.ModelChoiceField( queryset=LeaveType.objects.none(), widget=forms.Select(attrs={'class': 'form-select'}), help_text='Select leave type' ) year = forms.IntegerField( initial=date.today().year, widget=forms.NumberInput(attrs={ 'class': 'form-control', 'min': '2020', 'max': '2050' }), help_text='Year for adjustment' ) adjustment_type = forms.ChoiceField( choices=[ ('ADD', 'Add Days'), ('DEDUCT', 'Deduct Days'), ('SET', 'Set Balance'), ], widget=forms.Select(attrs={'class': 'form-select'}), help_text='Type of adjustment' ) days = forms.DecimalField( max_digits=6, decimal_places=2, widget=forms.NumberInput(attrs={ 'class': 'form-control', 'step': '0.5', 'min': '0' }), help_text='Number of days' ) reason = forms.CharField( widget=forms.Textarea(attrs={ 'class': 'form-control', 'rows': 3, 'placeholder': 'Reason for adjustment' }), help_text='Reason for this adjustment' ) def __init__(self, *args, **kwargs): user = kwargs.pop('user', None) super().__init__(*args, **kwargs) if user and hasattr(user, 'tenant'): self.fields['employee'].queryset = Employee.objects.filter( tenant=user.tenant, employment_status='ACTIVE' ).order_by('last_name', 'first_name') self.fields['leave_type'].queryset = LeaveType.objects.filter( tenant=user.tenant, is_active=True ).order_by('name') def clean_days(self): days = self.cleaned_data.get('days') if days and days <= 0: raise ValidationError('Days must be greater than zero.') return days class LeaveCancellationForm(forms.Form): """ Form for cancelling leave requests. """ cancellation_reason = forms.CharField( widget=forms.Textarea(attrs={ 'class': 'form-control', 'rows': 4, 'placeholder': 'Please provide a reason for cancelling this leave request', 'required': True }), help_text='Reason for cancellation' ) def __init__(self, *args, **kwargs): self.leave_request = kwargs.pop('leave_request', None) super().__init__(*args, **kwargs) def clean(self): cleaned_data = super().clean() # Check if leave request can be cancelled if self.leave_request: if not self.leave_request.can_cancel: raise ValidationError( 'This leave request cannot be cancelled. ' 'It may have already started or been completed.' ) return cleaned_data class LeaveSearchForm(forms.Form): """ Form for searching and filtering leave requests. """ search = forms.CharField( required=False, widget=forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Search by employee name or reason...' }) ) employee = forms.ModelChoiceField( queryset=Employee.objects.none(), required=False, widget=forms.Select(attrs={'class': 'form-select'}) ) leave_type = forms.ModelChoiceField( queryset=LeaveType.objects.none(), required=False, widget=forms.Select(attrs={'class': 'form-select'}) ) status = forms.ChoiceField( choices=[('', 'All Statuses')] + list(LeaveRequest.RequestStatus.choices), required=False, widget=forms.Select(attrs={'class': 'form-select'}) ) 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['leave_type'].queryset = LeaveType.objects.filter( tenant=tenant, is_active=True ).order_by('name') class SalaryInformationForm(forms.ModelForm): """ Form for creating and updating employee salary information. """ class Meta: model = SalaryInformation fields = [ 'employee', 'effective_date', 'end_date', 'basic_salary', 'housing_allowance', 'transportation_allowance', 'food_allowance', 'other_allowances', 'currency', 'payment_frequency', 'bank_name', 'account_number', 'iban', 'swift_code', 'is_active', 'notes' ] widgets = { 'employee': forms.Select(attrs={ 'class': 'form-select', 'required': True }), 'effective_date': forms.DateInput(attrs={ 'class': 'form-control', 'type': 'date', 'required': True }), 'end_date': forms.DateInput(attrs={ 'class': 'form-control', 'type': 'date' }), 'basic_salary': forms.NumberInput(attrs={ 'class': 'form-control', 'step': '0.01', 'min': '0', 'required': True }), 'housing_allowance': forms.NumberInput(attrs={ 'class': 'form-control', 'step': '0.01', 'min': '0' }), 'transportation_allowance': forms.NumberInput(attrs={ 'class': 'form-control', 'step': '0.01', 'min': '0' }), 'food_allowance': forms.NumberInput(attrs={ 'class': 'form-control', 'step': '0.01', 'min': '0' }), 'other_allowances': forms.Textarea(attrs={ 'class': 'form-control', 'rows': 3, 'placeholder': '{"bonus": 1000, "car_allowance": 500}' }), 'currency': forms.Select(attrs={'class': 'form-select'}), 'payment_frequency': forms.Select(attrs={'class': 'form-select'}), 'bank_name': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Bank name' }), 'account_number': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Account number' }), 'iban': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'SA00 0000 0000 0000 0000 0000' }), 'swift_code': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'SWIFT/BIC code' }), 'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}), 'notes': forms.Textarea(attrs={ 'class': 'form-control', 'rows': 3, 'placeholder': 'Additional notes' }), } help_texts = { 'effective_date': 'Date when this salary becomes effective', 'end_date': 'Date when this salary ends (leave blank if current)', 'basic_salary': 'Basic salary amount', 'other_allowances': 'Additional allowances as JSON (optional)', 'iban': 'International Bank Account Number', 'is_active': 'Mark as active salary record', } def __init__(self, *args, **kwargs): user = kwargs.pop('user', None) super().__init__(*args, **kwargs) if user and hasattr(user, '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() effective_date = cleaned_data.get('effective_date') end_date = cleaned_data.get('end_date') iban = cleaned_data.get('iban') # Validate dates if effective_date and effective_date > date.today(): raise ValidationError({ 'effective_date': 'Effective date cannot be in the future.' }) if end_date and effective_date: if end_date < effective_date: raise ValidationError({ 'end_date': 'End date cannot be before effective date.' }) # Validate IBAN format (basic check) if iban: iban_clean = iban.replace(' ', '').upper() if not iban_clean.startswith('SA') or len(iban_clean) != 24: raise ValidationError({ 'iban': 'Invalid Saudi IBAN format. Should be 24 characters starting with SA.' }) return cleaned_data class SalaryAdjustmentForm(forms.ModelForm): """ Form for creating salary adjustments. """ class Meta: model = SalaryAdjustment fields = [ 'employee', 'previous_salary', 'new_salary', 'adjustment_type', 'adjustment_reason', 'effective_date', 'notes' ] widgets = { 'employee': forms.Select(attrs={ 'class': 'form-select', 'required': True }), 'previous_salary': forms.Select(attrs={ 'class': 'form-select', 'required': True }), 'new_salary': forms.Select(attrs={ 'class': 'form-select', 'required': True }), 'adjustment_type': forms.Select(attrs={ 'class': 'form-select', 'required': True }), 'adjustment_reason': forms.Textarea(attrs={ 'class': 'form-control', 'rows': 4, 'placeholder': 'Provide detailed reason for salary adjustment', 'required': True }), 'effective_date': forms.DateInput(attrs={ 'class': 'form-control', 'type': 'date', 'required': True }), 'notes': forms.Textarea(attrs={ 'class': 'form-control', 'rows': 3, 'placeholder': 'Additional notes' }), } help_texts = { 'previous_salary': 'Current/previous salary record', 'new_salary': 'New salary record', 'adjustment_type': 'Type of salary adjustment', 'effective_date': 'Date when adjustment becomes effective', } 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') # Filter salary records by tenant self.fields['previous_salary'].queryset = SalaryInformation.objects.filter( employee__tenant=user.tenant ).order_by('-effective_date') self.fields['new_salary'].queryset = SalaryInformation.objects.filter( employee__tenant=user.tenant ).order_by('-effective_date') def clean(self): cleaned_data = super().clean() employee = cleaned_data.get('employee') previous_salary = cleaned_data.get('previous_salary') new_salary = cleaned_data.get('new_salary') effective_date = cleaned_data.get('effective_date') # Validate employee matches if previous_salary and employee and previous_salary.employee != employee: raise ValidationError({ 'previous_salary': 'Previous salary must belong to the selected employee.' }) if new_salary and employee and new_salary.employee != employee: raise ValidationError({ 'new_salary': 'New salary must belong to the selected employee.' }) # Validate salary records are different if previous_salary and new_salary and previous_salary == new_salary: raise ValidationError({ 'new_salary': 'New salary must be different from previous salary.' }) # Validate effective date if effective_date and effective_date > date.today(): raise ValidationError({ 'effective_date': 'Effective date cannot be in the future.' }) return cleaned_data class DocumentRequestForm(forms.ModelForm): """ Form for creating document requests. """ class Meta: model = DocumentRequest fields = [ 'document_type', 'custom_document_name', 'purpose', 'addressee', 'language', 'delivery_method', 'delivery_address', 'delivery_email', 'required_by_date', 'include_salary', 'additional_notes' ] widgets = { 'document_type': forms.Select(attrs={ 'class': 'form-select', 'required': True }), 'custom_document_name': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Custom document name (if type is Custom)' }), 'purpose': forms.Textarea(attrs={ 'class': 'form-control', 'rows': 4, 'placeholder': 'Purpose/reason for requesting this document', 'required': True }), 'addressee': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'To whom the document should be addressed (optional)' }), 'language': forms.Select(attrs={'class': 'form-select'}), 'delivery_method': forms.Select(attrs={'class': 'form-select'}), 'delivery_address': forms.Textarea(attrs={ 'class': 'form-control', 'rows': 3, 'placeholder': 'Delivery address (required for mail delivery)' }), 'delivery_email': forms.EmailInput(attrs={ 'class': 'form-control', 'placeholder': 'Email address for delivery (if different from your email)' }), 'required_by_date': forms.DateInput(attrs={ 'class': 'form-control', 'type': 'date' }), 'include_salary': forms.CheckboxInput(attrs={'class': 'form-check-input'}), 'additional_notes': forms.Textarea(attrs={ 'class': 'form-control', 'rows': 3, 'placeholder': 'Any special requirements or additional information' }), } help_texts = { 'document_type': 'Type of document you need', 'custom_document_name': 'Required if document type is Custom', 'purpose': 'Why do you need this document?', 'addressee': 'Who should the document be addressed to?', 'language': 'Preferred language for the document', 'delivery_method': 'How would you like to receive the document?', 'required_by_date': 'When do you need this document by?', 'include_salary': 'Include salary information in the document?', } def __init__(self, *args, **kwargs): self.employee = kwargs.pop('employee', None) user = kwargs.pop('user', None) super().__init__(*args, **kwargs) # Pre-fill delivery email with employee email if self.employee and self.employee.email: self.fields['delivery_email'].initial = self.employee.email def clean(self): cleaned_data = super().clean() document_type = cleaned_data.get('document_type') custom_document_name = cleaned_data.get('custom_document_name') delivery_method = cleaned_data.get('delivery_method') delivery_address = cleaned_data.get('delivery_address') required_by_date = cleaned_data.get('required_by_date') # Validate custom document name if document_type == 'CUSTOM' and not custom_document_name: raise ValidationError({ 'custom_document_name': 'Custom document name is required for custom documents.' }) # Validate delivery address for mail delivery if delivery_method == 'MAIL' and not delivery_address: raise ValidationError({ 'delivery_address': 'Delivery address is required for mail delivery.' }) # Validate required by date if required_by_date and required_by_date < date.today(): raise ValidationError({ 'required_by_date': 'Required by date cannot be in the past.' }) return cleaned_data class DocumentTemplateForm(forms.ModelForm): """ Form for creating and updating document templates (Admin only). """ class Meta: model = DocumentTemplate fields = [ 'name', 'description', 'document_type', 'language', 'template_content', 'header_content', 'footer_content', 'available_placeholders', 'is_active', 'is_default', 'requires_approval', 'css_styles' ] widgets = { 'name': forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Template name', 'required': True }), 'description': forms.Textarea(attrs={ 'class': 'form-control', 'rows': 3, 'placeholder': 'Template description' }), 'document_type': forms.Select(attrs={ 'class': 'form-select', 'required': True }), 'language': forms.Select(attrs={ 'class': 'form-select', 'required': True }), 'template_content': forms.Textarea(attrs={ 'class': 'form-control', 'rows': 15, 'placeholder': 'Template content with placeholders (HTML supported)', 'required': True, 'style': 'font-family: monospace;' }), 'header_content': forms.Textarea(attrs={ 'class': 'form-control', 'rows': 5, 'placeholder': 'Header content (letterhead, logo, etc.)', 'style': 'font-family: monospace;' }), 'footer_content': forms.Textarea(attrs={ 'class': 'form-control', 'rows': 5, 'placeholder': 'Footer content (signatures, contact info, etc.)', 'style': 'font-family: monospace;' }), 'available_placeholders': forms.Textarea(attrs={ 'class': 'form-control', 'rows': 8, 'placeholder': '{"employee_name": "Employee full name", "employee_id": "Employee ID"}', 'style': 'font-family: monospace;' }), 'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}), 'is_default': forms.CheckboxInput(attrs={'class': 'form-check-input'}), 'requires_approval': forms.CheckboxInput(attrs={'class': 'form-check-input'}), 'css_styles': forms.Textarea(attrs={ 'class': 'form-control', 'rows': 10, 'placeholder': 'Custom CSS styles for the template', 'style': 'font-family: monospace;' }), } help_texts = { 'name': 'Descriptive name for this template', 'document_type': 'Type of document this template is for', 'language': 'Language of the template', 'template_content': 'Main template content with placeholders like {{employee_name}}', 'available_placeholders': 'JSON object of available placeholders and their descriptions', 'is_default': 'Set as default template for this document type and language', 'requires_approval': 'Documents generated from this template require approval', } def __init__(self, *args, **kwargs): user = kwargs.pop('user', None) super().__init__(*args, **kwargs) # Set initial placeholders if creating new template if not self.instance.pk: self.fields['available_placeholders'].initial = json.dumps({ 'employee_name': 'Employee full name', 'employee_id': 'Employee ID', 'job_title': 'Job title', 'department': 'Department name', 'hire_date': 'Hire date', 'current_date': 'Current date', 'company_name': 'Company/Hospital name', 'company_address': 'Company address', }, indent=2) def clean_available_placeholders(self): """Validate JSON format for placeholders""" placeholders = self.cleaned_data.get('available_placeholders') if placeholders: try: # Try to parse as JSON if isinstance(placeholders, str): json.loads(placeholders) except json.JSONDecodeError: raise ValidationError('Invalid JSON format for placeholders.') return placeholders class DocumentRequestFilterForm(forms.Form): """ Form for filtering document requests. """ search = forms.CharField( required=False, widget=forms.TextInput(attrs={ 'class': 'form-control', 'placeholder': 'Search by employee name, document type, or purpose...' }) ) employee = forms.ModelChoiceField( queryset=Employee.objects.none(), required=False, widget=forms.Select(attrs={'class': 'form-select'}) ) document_type = forms.ChoiceField( choices=[('', 'All Document Types')] + list(DocumentRequest.DocumentType.choices), required=False, widget=forms.Select(attrs={'class': 'form-select'}) ) status = forms.ChoiceField( choices=[('', 'All Statuses')] + list(DocumentRequest.RequestStatus.choices), required=False, widget=forms.Select(attrs={'class': 'form-select'}) ) language = forms.ChoiceField( choices=[('', 'All Languages')] + list(DocumentRequest.Language.choices), required=False, widget=forms.Select(attrs={'class': 'form-select'}) ) delivery_method = forms.ChoiceField( choices=[('', 'All Delivery Methods')] + list(DocumentRequest.DeliveryMethod.choices), required=False, widget=forms.Select(attrs={'class': 'form-select'}) ) requested_from = forms.DateField( required=False, widget=forms.DateInput(attrs={ 'class': 'form-control', 'type': 'date' }) ) requested_to = forms.DateField( required=False, widget=forms.DateInput(attrs={ 'class': 'form-control', 'type': 'date' }) ) urgent_only = forms.BooleanField( required=False, widget=forms.CheckboxInput(attrs={'class': 'form-check-input'}) ) 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')