2635 lines
98 KiB
Python
2635 lines
98 KiB
Python
"""
|
|
HR app forms with healthcare-focused validation and user experience.
|
|
"""
|
|
|
|
from django import forms
|
|
from django.core.exceptions import ValidationError
|
|
from django.utils import timezone
|
|
from 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')
|