2202 lines
65 KiB
Python
2202 lines
65 KiB
Python
"""
|
|
HR app admin configuration.
|
|
"""
|
|
|
|
from django.contrib import admin
|
|
from django.utils.html import format_html
|
|
from django.urls import reverse
|
|
from django.utils.safestring import mark_safe
|
|
from django.db.models import Count, Avg, Sum
|
|
from decimal import Decimal
|
|
from datetime import date, timedelta
|
|
from .models import *
|
|
|
|
|
|
# ============================================================================
|
|
# INLINE ADMIN CLASSES
|
|
# ============================================================================
|
|
|
|
class ScheduleInline(admin.TabularInline):
|
|
"""
|
|
Inline admin for employee schedules.
|
|
"""
|
|
model = Schedule
|
|
extra = 0
|
|
fields = [
|
|
'name', 'schedule_type', 'effective_date',
|
|
'end_date', 'is_active'
|
|
]
|
|
readonly_fields = ['schedule_id']
|
|
|
|
|
|
class TimeEntryInline(admin.TabularInline):
|
|
"""
|
|
Inline admin for employee time entries.
|
|
"""
|
|
model = TimeEntry
|
|
extra = 0
|
|
fields = [
|
|
'work_date', 'clock_in_time', 'clock_out_time',
|
|
'total_hours', 'entry_type', 'status'
|
|
]
|
|
readonly_fields = ['entry_id', 'total_hours']
|
|
|
|
|
|
class PerformanceReviewInline(admin.TabularInline):
|
|
"""
|
|
Inline admin for employee performance reviews.
|
|
"""
|
|
model = PerformanceReview
|
|
extra = 0
|
|
fields = [
|
|
'review_date', 'review_type', 'overall_rating', 'status'
|
|
]
|
|
readonly_fields = ['review_id']
|
|
|
|
|
|
class TrainingRecordInline(admin.TabularInline):
|
|
"""
|
|
Inline admin for employee training records.
|
|
"""
|
|
model = TrainingRecord
|
|
extra = 0
|
|
fields = [
|
|
'program', 'session', 'completion_date',
|
|
'status', 'passed'
|
|
]
|
|
readonly_fields = ['record_id']
|
|
|
|
|
|
class ProgramModuleInline(admin.TabularInline):
|
|
"""
|
|
Inline admin for training program modules.
|
|
"""
|
|
model = ProgramModule
|
|
extra = 0
|
|
fields = ['order', 'title', 'hours']
|
|
ordering = ['order']
|
|
|
|
|
|
class ProgramPrerequisiteInline(admin.TabularInline):
|
|
"""
|
|
Inline admin for training program prerequisites.
|
|
"""
|
|
model = ProgramPrerequisite
|
|
extra = 0
|
|
fk_name = 'program'
|
|
fields = ['required_program']
|
|
|
|
|
|
class TrainingSessionInline(admin.TabularInline):
|
|
"""
|
|
Inline admin for training sessions.
|
|
"""
|
|
model = TrainingSession
|
|
extra = 0
|
|
fields = [
|
|
'title', 'start_at', 'end_at', 'instructor',
|
|
'delivery_method', 'capacity'
|
|
]
|
|
readonly_fields = ['session_id']
|
|
|
|
|
|
class TrainingAttendanceInline(admin.TabularInline):
|
|
"""
|
|
Inline admin for training attendance.
|
|
"""
|
|
model = TrainingAttendance
|
|
extra = 0
|
|
fields = [
|
|
'checked_in_at', 'checked_out_at', 'status', 'notes'
|
|
]
|
|
|
|
|
|
class TrainingAssessmentInline(admin.TabularInline):
|
|
"""
|
|
Inline admin for training assessments.
|
|
"""
|
|
model = TrainingAssessment
|
|
extra = 0
|
|
fields = [
|
|
'name', 'max_score', 'score', 'passed', 'taken_at'
|
|
]
|
|
|
|
|
|
@admin.register(Employee)
|
|
class EmployeeAdmin(admin.ModelAdmin):
|
|
"""
|
|
Admin interface for employees.
|
|
"""
|
|
list_display = [
|
|
'employee_id', 'get_full_name', 'job_title',
|
|
'department', 'employment_type', 'employment_status',
|
|
'hire_date', 'years_of_service_display', 'license_status_display'
|
|
]
|
|
list_filter = [
|
|
'tenant', 'department', 'employment_type', 'employment_status',
|
|
'gender', 'hire_date'
|
|
]
|
|
search_fields = [
|
|
'employee_id', 'first_name', 'last_name',
|
|
'email', 'job_title'
|
|
]
|
|
readonly_fields = [
|
|
'age', 'years_of_service',
|
|
'is_license_expired',
|
|
'created_at', 'updated_at'
|
|
]
|
|
fieldsets = [
|
|
('Employee Information', {
|
|
'fields': [
|
|
'tenant', 'user'
|
|
]
|
|
}),
|
|
('Personal Information', {
|
|
'fields': [
|
|
'first_name', 'father_name', 'grandfather_name', 'last_name',
|
|
]
|
|
}),
|
|
('Contact Information', {
|
|
'fields': [
|
|
'email', 'phone', 'mobile_phone', 'profile_picture'
|
|
]
|
|
}),
|
|
('Address Information', {
|
|
'fields': [
|
|
'address_line_1', 'address_line_2', 'city', 'postal_code', 'country'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Personal Details', {
|
|
'fields': [
|
|
'date_of_birth', 'gender', 'marital_status'
|
|
]
|
|
}),
|
|
('Employment Information', {
|
|
'fields': [
|
|
'department', 'job_title', 'employment_type', 'employment_status'
|
|
]
|
|
}),
|
|
('Employment Dates', {
|
|
'fields': [
|
|
'hire_date', 'termination_date'
|
|
]
|
|
}),
|
|
('Supervisor Information', {
|
|
'fields': [
|
|
'supervisor'
|
|
]
|
|
}),
|
|
('Work Schedule Information', {
|
|
'fields': [
|
|
'standard_hours_per_week', 'fte_percentage'
|
|
]
|
|
}),
|
|
('Compensation Information', {
|
|
'fields': [
|
|
'hourly_rate', 'annual_salary'
|
|
]
|
|
}),
|
|
('Professional Information', {
|
|
'fields': [
|
|
'license_number', 'license_expiry_date',
|
|
]
|
|
}),
|
|
('Emergency Contact', {
|
|
'fields': [
|
|
'emergency_contact_name', 'emergency_contact_relationship',
|
|
'emergency_contact_phone'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Calculated Fields', {
|
|
'fields': [
|
|
'age', 'years_of_service', 'is_license_expired',
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Notes', {
|
|
'fields': [
|
|
'notes'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Metadata', {
|
|
'fields': [
|
|
'created_at', 'updated_at', 'created_by'
|
|
],
|
|
'classes': ['collapse']
|
|
})
|
|
]
|
|
inlines = [ScheduleInline, TimeEntryInline, PerformanceReviewInline, TrainingRecordInline]
|
|
date_hierarchy = 'hire_date'
|
|
|
|
def years_of_service_display(self, obj):
|
|
"""Display years of service."""
|
|
years = obj.years_of_service
|
|
if years:
|
|
return f"{years:.1f} years"
|
|
return "0 years"
|
|
years_of_service_display.short_description = 'Years of Service'
|
|
|
|
def license_status_display(self, obj):
|
|
"""Display license status with color coding."""
|
|
if not obj.license_number:
|
|
return format_html('<span style="color: gray;">No License</span>')
|
|
|
|
if obj.is_license_expired:
|
|
return format_html('<span style="color: red;">⚠️ Expired</span>')
|
|
|
|
if obj.license_expiry_date:
|
|
days_to_expiry = (obj.license_expiry_date - date.today()).days
|
|
if days_to_expiry <= 30:
|
|
return format_html('<span style="color: orange;">⚠️ Expires Soon</span>')
|
|
|
|
return format_html('<span style="color: green;">✓ Valid</span>')
|
|
license_status_display.short_description = 'License Status'
|
|
|
|
def get_queryset(self, request):
|
|
"""Filter by user's tenant."""
|
|
qs = super().get_queryset(request)
|
|
if hasattr(request.user, 'tenant'):
|
|
qs = qs.filter(tenant=request.user.tenant)
|
|
return qs.select_related('department', 'supervisor')
|
|
|
|
|
|
@admin.register(Department)
|
|
class DepartmentAdmin(admin.ModelAdmin):
|
|
"""
|
|
Admin interface for departments.
|
|
"""
|
|
list_display = [
|
|
'code', 'name', 'department_type',
|
|
'department_head', 'employee_count_display',
|
|
'total_fte_display', 'is_active'
|
|
]
|
|
list_filter = [
|
|
'tenant', 'department_type', 'is_active'
|
|
]
|
|
search_fields = [
|
|
'code', 'name', 'description'
|
|
]
|
|
readonly_fields = [
|
|
'department_id', 'employee_count', 'total_fte',
|
|
'created_at', 'updated_at'
|
|
]
|
|
fieldsets = [
|
|
('Department Information', {
|
|
'fields': [
|
|
'department_id', 'tenant', 'code', 'name', 'description'
|
|
]
|
|
}),
|
|
('Department Type', {
|
|
'fields': [
|
|
'department_type'
|
|
]
|
|
}),
|
|
('Hierarchy', {
|
|
'fields': [
|
|
'parent_department'
|
|
]
|
|
}),
|
|
('Management', {
|
|
'fields': [
|
|
'department_head'
|
|
]
|
|
}),
|
|
('Budget Information', {
|
|
'fields': [
|
|
'annual_budget', 'cost_center'
|
|
]
|
|
}),
|
|
('Location Information', {
|
|
'fields': [
|
|
'location'
|
|
]
|
|
}),
|
|
('Department Status', {
|
|
'fields': [
|
|
'is_active'
|
|
]
|
|
}),
|
|
('Summary Information', {
|
|
'fields': [
|
|
'employee_count', 'total_fte'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Notes', {
|
|
'fields': [
|
|
'notes'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Metadata', {
|
|
'fields': [
|
|
'created_at', 'updated_at', 'created_by'
|
|
],
|
|
'classes': ['collapse']
|
|
})
|
|
]
|
|
|
|
def employee_count_display(self, obj):
|
|
"""Display employee count."""
|
|
return obj.employee_count
|
|
employee_count_display.short_description = 'Employees'
|
|
|
|
def total_fte_display(self, obj):
|
|
"""Display total FTE."""
|
|
return f"{obj.total_fte:.1f}"
|
|
total_fte_display.short_description = 'Total FTE'
|
|
|
|
def get_queryset(self, request):
|
|
"""Filter by user's tenant."""
|
|
qs = super().get_queryset(request)
|
|
if hasattr(request.user, 'tenant'):
|
|
qs = qs.filter(tenant=request.user.tenant)
|
|
return qs.select_related('department_head')
|
|
|
|
|
|
class ScheduleAssignmentInline(admin.TabularInline):
|
|
"""
|
|
Inline admin for schedule assignments.
|
|
"""
|
|
model = ScheduleAssignment
|
|
extra = 0
|
|
fields = [
|
|
'assignment_date', 'start_time', 'end_time',
|
|
'shift_type', 'status'
|
|
]
|
|
readonly_fields = ['assignment_id', 'total_hours']
|
|
|
|
|
|
@admin.register(Schedule)
|
|
class ScheduleAdmin(admin.ModelAdmin):
|
|
"""
|
|
Admin interface for schedules.
|
|
"""
|
|
list_display = [
|
|
'employee_name', 'name', 'schedule_type',
|
|
'effective_date', 'end_date', 'is_current_display', 'is_active'
|
|
]
|
|
list_filter = [
|
|
'schedule_type', 'effective_date', 'is_active'
|
|
]
|
|
search_fields = [
|
|
'employee__first_name', 'employee__last_name',
|
|
'employee__employee_id', 'name'
|
|
]
|
|
readonly_fields = [
|
|
'schedule_id', 'tenant', 'is_current',
|
|
'created_at', 'updated_at'
|
|
]
|
|
fieldsets = [
|
|
('Schedule Information', {
|
|
'fields': [
|
|
'schedule_id', 'employee', 'name', 'description'
|
|
]
|
|
}),
|
|
('Schedule Type', {
|
|
'fields': [
|
|
'schedule_type'
|
|
]
|
|
}),
|
|
('Schedule Dates', {
|
|
'fields': [
|
|
'effective_date', 'end_date'
|
|
]
|
|
}),
|
|
('Schedule Pattern', {
|
|
'fields': [
|
|
'schedule_pattern'
|
|
]
|
|
}),
|
|
('Schedule Status', {
|
|
'fields': [
|
|
'is_active'
|
|
]
|
|
}),
|
|
('Approval Information', {
|
|
'fields': [
|
|
'approved_by', 'approval_date'
|
|
]
|
|
}),
|
|
('Status Information', {
|
|
'fields': [
|
|
'is_current'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Notes', {
|
|
'fields': [
|
|
'notes'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Related Information', {
|
|
'fields': [
|
|
'tenant'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Metadata', {
|
|
'fields': [
|
|
'created_at', 'updated_at', 'created_by'
|
|
],
|
|
'classes': ['collapse']
|
|
})
|
|
]
|
|
inlines = [ScheduleAssignmentInline]
|
|
date_hierarchy = 'effective_date'
|
|
|
|
def employee_name(self, obj):
|
|
"""Display employee name."""
|
|
return obj.employee.get_full_name()
|
|
employee_name.short_description = 'Employee'
|
|
|
|
def is_current_display(self, obj):
|
|
"""Display current status with icon."""
|
|
if obj.is_current:
|
|
return format_html('<span style="color: green;">✓ Current</span>')
|
|
return format_html('<span style="color: gray;">Not Current</span>')
|
|
is_current_display.short_description = 'Current'
|
|
|
|
def get_queryset(self, request):
|
|
"""Filter by user's tenant."""
|
|
qs = super().get_queryset(request)
|
|
if hasattr(request.user, 'tenant'):
|
|
qs = qs.filter(employee__tenant=request.user.tenant)
|
|
return qs.select_related('employee', 'approved_by')
|
|
|
|
|
|
@admin.register(ScheduleAssignment)
|
|
class ScheduleAssignmentAdmin(admin.ModelAdmin):
|
|
"""
|
|
Admin interface for schedule assignments.
|
|
"""
|
|
list_display = [
|
|
'employee_name', 'assignment_date', 'start_time',
|
|
'end_time', 'shift_type', 'total_hours_display', 'status'
|
|
]
|
|
list_filter = [
|
|
'shift_type', 'status', 'assignment_date'
|
|
]
|
|
search_fields = [
|
|
'schedule__employee__first_name', 'schedule__employee__last_name',
|
|
'schedule__employee__employee_id'
|
|
]
|
|
readonly_fields = [
|
|
'assignment_id', 'tenant', 'employee', 'total_hours',
|
|
'created_at', 'updated_at'
|
|
]
|
|
fieldsets = [
|
|
('Assignment Information', {
|
|
'fields': [
|
|
'assignment_id', 'schedule'
|
|
]
|
|
}),
|
|
('Date and Time', {
|
|
'fields': [
|
|
'assignment_date', 'start_time', 'end_time'
|
|
]
|
|
}),
|
|
('Shift Information', {
|
|
'fields': [
|
|
'shift_type'
|
|
]
|
|
}),
|
|
('Location Information', {
|
|
'fields': [
|
|
'department', 'location'
|
|
]
|
|
}),
|
|
('Assignment Status', {
|
|
'fields': [
|
|
'status'
|
|
]
|
|
}),
|
|
('Break Information', {
|
|
'fields': [
|
|
'break_minutes', 'lunch_minutes'
|
|
]
|
|
}),
|
|
('Calculated Information', {
|
|
'fields': [
|
|
'total_hours'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Notes', {
|
|
'fields': [
|
|
'notes'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Related Information', {
|
|
'fields': [
|
|
'tenant', 'employee'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Metadata', {
|
|
'fields': [
|
|
'created_at', 'updated_at'
|
|
],
|
|
'classes': ['collapse']
|
|
})
|
|
]
|
|
date_hierarchy = 'assignment_date'
|
|
|
|
def employee_name(self, obj):
|
|
"""Display employee name."""
|
|
return obj.employee.get_full_name()
|
|
employee_name.short_description = 'Employee'
|
|
|
|
def total_hours_display(self, obj):
|
|
"""Display total hours."""
|
|
return f"{obj.total_hours:.2f}"
|
|
total_hours_display.short_description = 'Hours'
|
|
|
|
def get_queryset(self, request):
|
|
"""Filter by user's tenant."""
|
|
qs = super().get_queryset(request)
|
|
if hasattr(request.user, 'tenant'):
|
|
qs = qs.filter(schedule__employee__tenant=request.user.tenant)
|
|
return qs.select_related('schedule__employee', 'department')
|
|
|
|
|
|
@admin.register(TimeEntry)
|
|
class TimeEntryAdmin(admin.ModelAdmin):
|
|
"""
|
|
Admin interface for time entries.
|
|
"""
|
|
list_display = [
|
|
'employee_name', 'work_date', 'clock_in_time',
|
|
'clock_out_time', 'total_hours', 'entry_type',
|
|
'status', 'approval_status_display'
|
|
]
|
|
list_filter = [
|
|
'entry_type', 'status', 'work_date'
|
|
]
|
|
search_fields = [
|
|
'employee__first_name', 'employee__last_name',
|
|
'employee__employee_id'
|
|
]
|
|
readonly_fields = [
|
|
'entry_id', 'tenant', 'regular_hours', 'overtime_hours',
|
|
'total_hours', 'is_approved', 'created_at', 'updated_at'
|
|
]
|
|
fieldsets = [
|
|
('Time Entry Information', {
|
|
'fields': [
|
|
'entry_id', 'employee'
|
|
]
|
|
}),
|
|
('Date and Time', {
|
|
'fields': [
|
|
'work_date', 'clock_in_time', 'clock_out_time'
|
|
]
|
|
}),
|
|
('Break Times', {
|
|
'fields': [
|
|
'break_start_time', 'break_end_time',
|
|
'lunch_start_time', 'lunch_end_time'
|
|
]
|
|
}),
|
|
('Hours Information', {
|
|
'fields': [
|
|
'regular_hours', 'overtime_hours', 'total_hours'
|
|
]
|
|
}),
|
|
('Entry Type', {
|
|
'fields': [
|
|
'entry_type'
|
|
]
|
|
}),
|
|
('Department and Location', {
|
|
'fields': [
|
|
'department', 'location'
|
|
]
|
|
}),
|
|
('Approval Information', {
|
|
'fields': [
|
|
'approved_by', 'approval_date'
|
|
]
|
|
}),
|
|
('Entry Status', {
|
|
'fields': [
|
|
'status'
|
|
]
|
|
}),
|
|
('Status Information', {
|
|
'fields': [
|
|
'is_approved'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Notes', {
|
|
'fields': [
|
|
'notes'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Related Information', {
|
|
'fields': [
|
|
'tenant'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Metadata', {
|
|
'fields': [
|
|
'created_at', 'updated_at'
|
|
],
|
|
'classes': ['collapse']
|
|
})
|
|
]
|
|
date_hierarchy = 'work_date'
|
|
|
|
def employee_name(self, obj):
|
|
"""Display employee name."""
|
|
return obj.employee.get_full_name()
|
|
employee_name.short_description = 'Employee'
|
|
|
|
def approval_status_display(self, obj):
|
|
"""Display approval status with icon."""
|
|
if obj.is_approved:
|
|
return format_html('<span style="color: green;">✓ Approved</span>')
|
|
elif obj.status == 'REJECTED':
|
|
return format_html('<span style="color: red;">✗ Rejected</span>')
|
|
elif obj.status == 'SUBMITTED':
|
|
return format_html('<span style="color: orange;">⏳ Pending</span>')
|
|
return format_html('<span style="color: gray;">Draft</span>')
|
|
approval_status_display.short_description = 'Approval'
|
|
|
|
def get_queryset(self, request):
|
|
"""Filter by user's tenant."""
|
|
qs = super().get_queryset(request)
|
|
if hasattr(request.user, 'tenant'):
|
|
qs = qs.filter(employee__tenant=request.user.tenant)
|
|
return qs.select_related('employee', 'department', 'approved_by')
|
|
|
|
|
|
@admin.register(PerformanceReview)
|
|
class PerformanceReviewAdmin(admin.ModelAdmin):
|
|
"""
|
|
Admin interface for performance reviews.
|
|
"""
|
|
list_display = [
|
|
'employee_name', 'review_date', 'review_type',
|
|
'overall_rating', 'status', 'is_overdue_display'
|
|
]
|
|
list_filter = [
|
|
'review_type', 'status', 'overall_rating', 'review_date'
|
|
]
|
|
search_fields = [
|
|
'employee__first_name', 'employee__last_name',
|
|
]
|
|
readonly_fields = [
|
|
'review_id', 'is_overdue',
|
|
'created_at', 'updated_at'
|
|
]
|
|
fieldsets = [
|
|
('Review Information', {
|
|
'fields': [
|
|
'review_id', 'employee'
|
|
]
|
|
}),
|
|
('Review Period', {
|
|
'fields': [
|
|
'review_period_start', 'review_period_end', 'review_date'
|
|
]
|
|
}),
|
|
('Review Type', {
|
|
'fields': [
|
|
'review_type'
|
|
]
|
|
}),
|
|
('Reviewer Information', {
|
|
'fields': [
|
|
'reviewer'
|
|
]
|
|
}),
|
|
('Overall Rating', {
|
|
'fields': [
|
|
'overall_rating'
|
|
]
|
|
}),
|
|
('Competency Ratings', {
|
|
'fields': [
|
|
'competency_ratings'
|
|
]
|
|
}),
|
|
('Goals and Objectives', {
|
|
'fields': [
|
|
'goals_achieved', 'goals_not_achieved', 'future_goals'
|
|
]
|
|
}),
|
|
('Strengths and Areas for Improvement', {
|
|
'fields': [
|
|
'strengths', 'areas_for_improvement'
|
|
]
|
|
}),
|
|
('Development Plan', {
|
|
'fields': [
|
|
'development_plan', 'training_recommendations'
|
|
]
|
|
}),
|
|
('Employee Comments', {
|
|
'fields': [
|
|
'employee_comments', 'employee_signature_date'
|
|
]
|
|
}),
|
|
('Review Status', {
|
|
'fields': [
|
|
'status'
|
|
]
|
|
}),
|
|
('Status Information', {
|
|
'fields': [
|
|
'is_overdue'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Notes', {
|
|
'fields': [
|
|
'notes'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Related Information', {
|
|
'fields': [
|
|
'tenant'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Metadata', {
|
|
'fields': [
|
|
'created_at', 'updated_at'
|
|
],
|
|
'classes': ['collapse']
|
|
})
|
|
]
|
|
date_hierarchy = 'review_date'
|
|
|
|
def employee_name(self, obj):
|
|
"""Display employee name."""
|
|
return obj.employee.get_full_name()
|
|
employee_name.short_description = 'Employee'
|
|
|
|
def is_overdue_display(self, obj):
|
|
"""Display overdue status with icon."""
|
|
if obj.is_overdue:
|
|
return format_html('<span style="color: red;">⚠️ Overdue</span>')
|
|
return format_html('<span style="color: green;">✓ On Time</span>')
|
|
is_overdue_display.short_description = 'Status'
|
|
|
|
def get_queryset(self, request):
|
|
"""Filter by user's tenant."""
|
|
qs = super().get_queryset(request)
|
|
if hasattr(request.user, 'tenant'):
|
|
qs = qs.filter(employee__tenant=request.user.tenant)
|
|
return qs.select_related('employee', 'reviewer')
|
|
|
|
|
|
# ============================================================================
|
|
# TRAINING ADMIN CLASSES
|
|
# ============================================================================
|
|
|
|
@admin.register(TrainingPrograms)
|
|
class TrainingProgramsAdmin(admin.ModelAdmin):
|
|
"""
|
|
Admin interface for training programs.
|
|
"""
|
|
list_display = [
|
|
'name', 'program_type', 'program_provider', 'instructor',
|
|
'duration_hours', 'cost', 'is_certified', 'validity_display'
|
|
]
|
|
list_filter = [
|
|
'tenant', 'program_type', 'is_certified', 'program_provider'
|
|
]
|
|
search_fields = [
|
|
'name', 'description', 'program_provider'
|
|
]
|
|
readonly_fields = [
|
|
'program_id', 'created_at', 'updated_at'
|
|
]
|
|
fieldsets = [
|
|
('Program Information', {
|
|
'fields': [
|
|
'program_id', 'tenant', 'name', 'description'
|
|
]
|
|
}),
|
|
('Program Details', {
|
|
'fields': [
|
|
'program_type', 'program_provider', 'instructor'
|
|
]
|
|
}),
|
|
('Schedule Information', {
|
|
'fields': [
|
|
'start_date', 'end_date', 'duration_hours'
|
|
]
|
|
}),
|
|
('Cost Information', {
|
|
'fields': [
|
|
'cost'
|
|
]
|
|
}),
|
|
('Certification Information', {
|
|
'fields': [
|
|
'is_certified', 'validity_days', 'notify_before_days'
|
|
]
|
|
}),
|
|
('Metadata', {
|
|
'fields': [
|
|
'created_at', 'updated_at', 'created_by'
|
|
],
|
|
'classes': ['collapse']
|
|
})
|
|
]
|
|
inlines = [ProgramModuleInline, ProgramPrerequisiteInline, TrainingSessionInline]
|
|
date_hierarchy = 'start_date'
|
|
|
|
def validity_display(self, obj):
|
|
"""Display validity information."""
|
|
if obj.is_certified and obj.validity_days:
|
|
return f"{obj.validity_days} days"
|
|
return "No expiry"
|
|
validity_display.short_description = 'Validity'
|
|
|
|
def get_queryset(self, request):
|
|
"""Filter by user's tenant."""
|
|
qs = super().get_queryset(request)
|
|
if hasattr(request.user, 'tenant'):
|
|
qs = qs.filter(tenant=request.user.tenant)
|
|
return qs.select_related('instructor', 'created_by')
|
|
|
|
|
|
@admin.register(ProgramModule)
|
|
class ProgramModuleAdmin(admin.ModelAdmin):
|
|
"""
|
|
Admin interface for program modules.
|
|
"""
|
|
list_display = [
|
|
'program', 'order', 'title', 'hours'
|
|
]
|
|
list_filter = [
|
|
'program__tenant', 'program'
|
|
]
|
|
search_fields = [
|
|
'title', 'program__name'
|
|
]
|
|
ordering = ['program', 'order']
|
|
|
|
def get_queryset(self, request):
|
|
"""Filter by user's tenant."""
|
|
qs = super().get_queryset(request)
|
|
if hasattr(request.user, 'tenant'):
|
|
qs = qs.filter(program__tenant=request.user.tenant)
|
|
return qs.select_related('program')
|
|
|
|
|
|
@admin.register(ProgramPrerequisite)
|
|
class ProgramPrerequisiteAdmin(admin.ModelAdmin):
|
|
"""
|
|
Admin interface for program prerequisites.
|
|
"""
|
|
list_display = [
|
|
'program', 'required_program'
|
|
]
|
|
list_filter = [
|
|
'program__tenant', 'program'
|
|
]
|
|
search_fields = [
|
|
'program__name', 'required_program__name'
|
|
]
|
|
|
|
def get_queryset(self, request):
|
|
"""Filter by user's tenant."""
|
|
qs = super().get_queryset(request)
|
|
if hasattr(request.user, 'tenant'):
|
|
qs = qs.filter(program__tenant=request.user.tenant)
|
|
return qs.select_related('program', 'required_program')
|
|
|
|
|
|
@admin.register(TrainingSession)
|
|
class TrainingSessionAdmin(admin.ModelAdmin):
|
|
"""
|
|
Admin interface for training sessions.
|
|
"""
|
|
list_display = [
|
|
'program', 'title_display', 'instructor', 'start_at',
|
|
'end_at', 'delivery_method', 'capacity', 'enrollment_count'
|
|
]
|
|
list_filter = [
|
|
'program__tenant', 'delivery_method', 'start_at'
|
|
]
|
|
search_fields = [
|
|
'title', 'program__name', 'instructor__first_name', 'instructor__last_name'
|
|
]
|
|
readonly_fields = [
|
|
'session_id', 'enrollment_count', 'created_at'
|
|
]
|
|
fieldsets = [
|
|
('Session Information', {
|
|
'fields': [
|
|
'session_id', 'program', 'title'
|
|
]
|
|
}),
|
|
('Instructor Information', {
|
|
'fields': [
|
|
'instructor'
|
|
]
|
|
}),
|
|
('Schedule Information', {
|
|
'fields': [
|
|
'start_at', 'end_at'
|
|
]
|
|
}),
|
|
('Delivery Information', {
|
|
'fields': [
|
|
'delivery_method', 'location'
|
|
]
|
|
}),
|
|
('Capacity Information', {
|
|
'fields': [
|
|
'capacity', 'enrollment_count'
|
|
]
|
|
}),
|
|
('Cost Override', {
|
|
'fields': [
|
|
'cost_override', 'hours_override'
|
|
]
|
|
}),
|
|
('Metadata', {
|
|
'fields': [
|
|
'created_at', 'created_by'
|
|
],
|
|
'classes': ['collapse']
|
|
})
|
|
]
|
|
date_hierarchy = 'start_at'
|
|
|
|
def title_display(self, obj):
|
|
"""Display session title or program name."""
|
|
return obj.title or obj.program.name
|
|
title_display.short_description = 'Title'
|
|
|
|
def enrollment_count(self, obj):
|
|
"""Display enrollment count."""
|
|
return obj.enrollments.count()
|
|
enrollment_count.short_description = 'Enrollments'
|
|
|
|
def get_queryset(self, request):
|
|
"""Filter by user's tenant."""
|
|
qs = super().get_queryset(request)
|
|
if hasattr(request.user, 'tenant'):
|
|
qs = qs.filter(program__tenant=request.user.tenant)
|
|
return qs.select_related('program', 'instructor', 'created_by')
|
|
|
|
|
|
@admin.register(TrainingRecord)
|
|
class TrainingRecordAdmin(admin.ModelAdmin):
|
|
"""
|
|
Admin interface for training records (enrollments).
|
|
"""
|
|
list_display = [
|
|
'employee_name', 'program', 'session', 'enrolled_at',
|
|
'completion_date', 'status', 'passed', 'expiry_status_display'
|
|
]
|
|
list_filter = [
|
|
'employee__tenant', 'status', 'passed', 'program__program_type',
|
|
'enrolled_at', 'completion_date'
|
|
]
|
|
search_fields = [
|
|
'employee__first_name', 'employee__last_name',
|
|
'employee__employee_id', 'program__name'
|
|
]
|
|
readonly_fields = [
|
|
'record_id', 'enrolled_at', 'hours', 'effective_cost',
|
|
'eligible_for_certificate', 'completion_percentage',
|
|
'created_at', 'updated_at'
|
|
]
|
|
fieldsets = [
|
|
('Enrollment Information', {
|
|
'fields': [
|
|
'record_id', 'employee', 'program', 'session'
|
|
]
|
|
}),
|
|
('Enrollment Dates', {
|
|
'fields': [
|
|
'enrolled_at', 'started_at', 'completion_date', 'expiry_date'
|
|
]
|
|
}),
|
|
('Status Information', {
|
|
'fields': [
|
|
'status', 'passed'
|
|
]
|
|
}),
|
|
('Performance Information', {
|
|
'fields': [
|
|
'credits_earned', 'score'
|
|
]
|
|
}),
|
|
('Cost Information', {
|
|
'fields': [
|
|
'cost_paid', 'effective_cost'
|
|
]
|
|
}),
|
|
('Calculated Information', {
|
|
'fields': [
|
|
'hours', 'eligible_for_certificate', 'completion_percentage'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Notes', {
|
|
'fields': [
|
|
'notes'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Metadata', {
|
|
'fields': [
|
|
'created_at', 'updated_at', 'created_by'
|
|
],
|
|
'classes': ['collapse']
|
|
})
|
|
]
|
|
inlines = [TrainingAttendanceInline, TrainingAssessmentInline]
|
|
date_hierarchy = 'enrolled_at'
|
|
|
|
def employee_name(self, obj):
|
|
"""Display employee name."""
|
|
return obj.employee.get_full_name()
|
|
employee_name.short_description = 'Employee'
|
|
|
|
def expiry_status_display(self, obj):
|
|
"""Display expiry status with color coding."""
|
|
if not obj.expiry_date:
|
|
return format_html('<span style="color: gray;">No Expiry</span>')
|
|
|
|
days_to_expiry = (obj.expiry_date - date.today()).days
|
|
if days_to_expiry < 0:
|
|
return format_html('<span style="color: red;">⚠️ Expired</span>')
|
|
elif days_to_expiry <= 30:
|
|
return format_html('<span style="color: orange;">⚠️ Expires Soon</span>')
|
|
else:
|
|
return format_html('<span style="color: green;">✓ Valid</span>')
|
|
expiry_status_display.short_description = 'Expiry Status'
|
|
|
|
def get_queryset(self, request):
|
|
"""Filter by user's tenant."""
|
|
qs = super().get_queryset(request)
|
|
if hasattr(request.user, 'tenant'):
|
|
qs = qs.filter(employee__tenant=request.user.tenant)
|
|
return qs.select_related('employee', 'program', 'session', 'created_by')
|
|
|
|
|
|
@admin.register(TrainingAttendance)
|
|
class TrainingAttendanceAdmin(admin.ModelAdmin):
|
|
"""
|
|
Admin interface for training attendance.
|
|
"""
|
|
list_display = [
|
|
'employee_name', 'program_name', 'checked_in_at',
|
|
'checked_out_at', 'status', 'duration_display'
|
|
]
|
|
list_filter = [
|
|
'enrollment__employee__tenant', 'status', 'checked_in_at'
|
|
]
|
|
search_fields = [
|
|
'enrollment__employee__first_name', 'enrollment__employee__last_name',
|
|
'enrollment__program__name'
|
|
]
|
|
readonly_fields = [
|
|
'duration_display'
|
|
]
|
|
fieldsets = [
|
|
('Attendance Information', {
|
|
'fields': [
|
|
'enrollment'
|
|
]
|
|
}),
|
|
('Check-in/out Times', {
|
|
'fields': [
|
|
'checked_in_at', 'checked_out_at'
|
|
]
|
|
}),
|
|
('Status Information', {
|
|
'fields': [
|
|
'status'
|
|
]
|
|
}),
|
|
('Duration Information', {
|
|
'fields': [
|
|
'duration_display'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Notes', {
|
|
'fields': [
|
|
'notes'
|
|
],
|
|
'classes': ['collapse']
|
|
})
|
|
]
|
|
date_hierarchy = 'checked_in_at'
|
|
|
|
def employee_name(self, obj):
|
|
"""Display employee name."""
|
|
return obj.enrollment.employee.get_full_name()
|
|
employee_name.short_description = 'Employee'
|
|
|
|
def program_name(self, obj):
|
|
"""Display program name."""
|
|
return obj.enrollment.program.name
|
|
program_name.short_description = 'Program'
|
|
|
|
def duration_display(self, obj):
|
|
"""Display attendance duration."""
|
|
if obj.checked_in_at and obj.checked_out_at:
|
|
duration = obj.checked_out_at - obj.checked_in_at
|
|
hours = duration.total_seconds() / 3600
|
|
return f"{hours:.2f} hours"
|
|
return "Incomplete"
|
|
duration_display.short_description = 'Duration'
|
|
|
|
def get_queryset(self, request):
|
|
"""Filter by user's tenant."""
|
|
qs = super().get_queryset(request)
|
|
if hasattr(request.user, 'tenant'):
|
|
qs = qs.filter(enrollment__employee__tenant=request.user.tenant)
|
|
return qs.select_related('enrollment__employee', 'enrollment__program')
|
|
|
|
|
|
@admin.register(TrainingAssessment)
|
|
class TrainingAssessmentAdmin(admin.ModelAdmin):
|
|
"""
|
|
Admin interface for training assessments.
|
|
"""
|
|
list_display = [
|
|
'employee_name', 'program_name', 'name',
|
|
'score', 'max_score', 'percentage_display', 'passed', 'taken_at'
|
|
]
|
|
list_filter = [
|
|
'enrollment__employee__tenant', 'passed', 'taken_at'
|
|
]
|
|
search_fields = [
|
|
'enrollment__employee__first_name', 'enrollment__employee__last_name',
|
|
'enrollment__program__name', 'name'
|
|
]
|
|
readonly_fields = [
|
|
'percentage_display'
|
|
]
|
|
fieldsets = [
|
|
('Assessment Information', {
|
|
'fields': [
|
|
'enrollment', 'name'
|
|
]
|
|
}),
|
|
('Score Information', {
|
|
'fields': [
|
|
'max_score', 'score', 'percentage_display', 'passed'
|
|
]
|
|
}),
|
|
('Date Information', {
|
|
'fields': [
|
|
'taken_at'
|
|
]
|
|
}),
|
|
('Notes', {
|
|
'fields': [
|
|
'notes'
|
|
],
|
|
'classes': ['collapse']
|
|
})
|
|
]
|
|
date_hierarchy = 'taken_at'
|
|
|
|
def employee_name(self, obj):
|
|
"""Display employee name."""
|
|
return obj.enrollment.employee.get_full_name()
|
|
employee_name.short_description = 'Employee'
|
|
|
|
def program_name(self, obj):
|
|
"""Display program name."""
|
|
return obj.enrollment.program.name
|
|
program_name.short_description = 'Program'
|
|
|
|
def percentage_display(self, obj):
|
|
"""Display score percentage."""
|
|
if obj.score is not None and obj.max_score > 0:
|
|
percentage = (obj.score / obj.max_score) * 100
|
|
color = 'green' if percentage >= 70 else 'orange' if percentage >= 50 else 'red'
|
|
return format_html(
|
|
'<span style="color: {};">{:.1f}%</span>',
|
|
color, percentage
|
|
)
|
|
return "N/A"
|
|
percentage_display.short_description = 'Percentage'
|
|
|
|
def get_queryset(self, request):
|
|
"""Filter by user's tenant."""
|
|
qs = super().get_queryset(request)
|
|
if hasattr(request.user, 'tenant'):
|
|
qs = qs.filter(enrollment__employee__tenant=request.user.tenant)
|
|
return qs.select_related('enrollment__employee', 'enrollment__program')
|
|
|
|
|
|
@admin.register(TrainingCertificates)
|
|
class TrainingCertificatesAdmin(admin.ModelAdmin):
|
|
"""
|
|
Admin interface for training certificates.
|
|
"""
|
|
list_display = [
|
|
'employee_name', 'certificate_name', 'program',
|
|
'issued_date', 'expiry_date', 'expiry_status_display',
|
|
'certificate_number'
|
|
]
|
|
list_filter = [
|
|
'employee__tenant', 'program', 'issued_date', 'expiry_date'
|
|
]
|
|
search_fields = [
|
|
'employee__first_name', 'employee__last_name',
|
|
'certificate_name', 'certificate_number', 'program__name'
|
|
]
|
|
readonly_fields = [
|
|
'certificate_id', 'is_expired', 'days_to_expiry',
|
|
'created_at', 'updated_at'
|
|
]
|
|
fieldsets = [
|
|
('Certificate Information', {
|
|
'fields': [
|
|
'certificate_id', 'employee', 'program', 'enrollment'
|
|
]
|
|
}),
|
|
('Certificate Details', {
|
|
'fields': [
|
|
'certificate_name', 'certificate_number', 'certification_body'
|
|
]
|
|
}),
|
|
('Date Information', {
|
|
'fields': [
|
|
'issued_date', 'expiry_date'
|
|
]
|
|
}),
|
|
('File Information', {
|
|
'fields': [
|
|
'file'
|
|
]
|
|
}),
|
|
('Signature Information', {
|
|
'fields': [
|
|
'created_by', 'signed_by'
|
|
]
|
|
}),
|
|
('Status Information', {
|
|
'fields': [
|
|
'is_expired', 'days_to_expiry'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Metadata', {
|
|
'fields': [
|
|
'created_at', 'updated_at'
|
|
],
|
|
'classes': ['collapse']
|
|
})
|
|
]
|
|
date_hierarchy = 'issued_date'
|
|
|
|
def employee_name(self, obj):
|
|
"""Display employee name."""
|
|
return obj.employee.get_full_name()
|
|
employee_name.short_description = 'Employee'
|
|
|
|
def expiry_status_display(self, obj):
|
|
"""Display expiry status with color coding."""
|
|
if not obj.expiry_date:
|
|
return format_html('<span style="color: gray;">No Expiry</span>')
|
|
|
|
if obj.is_expired:
|
|
return format_html('<span style="color: red;">⚠️ Expired</span>')
|
|
|
|
if obj.days_to_expiry is not None and obj.days_to_expiry <= 30:
|
|
return format_html('<span style="color: orange;">⚠️ Expires Soon</span>')
|
|
|
|
return format_html('<span style="color: green;">✓ Valid</span>')
|
|
expiry_status_display.short_description = 'Expiry Status'
|
|
|
|
def get_queryset(self, request):
|
|
"""Filter by user's tenant."""
|
|
qs = super().get_queryset(request)
|
|
if hasattr(request.user, 'tenant'):
|
|
qs = qs.filter(employee__tenant=request.user.tenant)
|
|
return qs.select_related('employee', 'program', 'enrollment', 'created_by', 'signed_by')
|
|
|
|
|
|
# ============================================================================
|
|
# LEAVE MANAGEMENT ADMIN CLASSES
|
|
# ============================================================================
|
|
|
|
@admin.register(LeaveType)
|
|
class LeaveTypeAdmin(admin.ModelAdmin):
|
|
"""
|
|
Admin interface for leave types.
|
|
"""
|
|
list_display = [
|
|
'code', 'name', 'is_paid', 'requires_approval',
|
|
'annual_entitlement', 'accrual_method', 'is_active'
|
|
]
|
|
list_filter = [
|
|
'tenant', 'is_paid', 'requires_approval', 'accrual_method',
|
|
'is_active', 'gender_specific'
|
|
]
|
|
search_fields = [
|
|
'code', 'name', 'description'
|
|
]
|
|
readonly_fields = [
|
|
'leave_type_id', 'created_at', 'updated_at'
|
|
]
|
|
fieldsets = [
|
|
('Leave Type Information', {
|
|
'fields': [
|
|
'leave_type_id', 'tenant', 'name', 'code', 'description'
|
|
]
|
|
}),
|
|
('Leave Configuration', {
|
|
'fields': [
|
|
'is_paid', 'requires_approval', 'requires_documentation'
|
|
]
|
|
}),
|
|
('Accrual Settings', {
|
|
'fields': [
|
|
'accrual_method', 'annual_entitlement', 'max_carry_over',
|
|
'max_consecutive_days', 'min_notice_days'
|
|
]
|
|
}),
|
|
('Availability', {
|
|
'fields': [
|
|
'is_active', 'available_for_all', 'gender_specific'
|
|
]
|
|
}),
|
|
('Metadata', {
|
|
'fields': [
|
|
'created_at', 'updated_at', 'created_by'
|
|
],
|
|
'classes': ['collapse']
|
|
})
|
|
]
|
|
|
|
def get_queryset(self, request):
|
|
"""Filter by user's tenant."""
|
|
qs = super().get_queryset(request)
|
|
if hasattr(request.user, 'tenant'):
|
|
qs = qs.filter(tenant=request.user.tenant)
|
|
return qs
|
|
|
|
|
|
@admin.register(LeaveBalance)
|
|
class LeaveBalanceAdmin(admin.ModelAdmin):
|
|
"""
|
|
Admin interface for leave balances.
|
|
"""
|
|
list_display = [
|
|
'employee_name', 'leave_type', 'year', 'total_entitled_display',
|
|
'used', 'pending', 'available_display'
|
|
]
|
|
list_filter = [
|
|
'employee__tenant', 'leave_type', 'year'
|
|
]
|
|
search_fields = [
|
|
'employee__first_name', 'employee__last_name',
|
|
'employee__employee_id', 'leave_type__name'
|
|
]
|
|
readonly_fields = [
|
|
'balance_id', 'available', 'total_entitled',
|
|
'created_at', 'updated_at'
|
|
]
|
|
fieldsets = [
|
|
('Balance Information', {
|
|
'fields': [
|
|
'balance_id', 'employee', 'leave_type', 'year'
|
|
]
|
|
}),
|
|
('Balance Tracking', {
|
|
'fields': [
|
|
'opening_balance', 'accrued', 'used', 'pending', 'adjusted'
|
|
]
|
|
}),
|
|
('Calculated Fields', {
|
|
'fields': [
|
|
'available', 'total_entitled'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Metadata', {
|
|
'fields': [
|
|
'last_accrual_date', 'created_at', 'updated_at'
|
|
],
|
|
'classes': ['collapse']
|
|
})
|
|
]
|
|
|
|
def employee_name(self, obj):
|
|
"""Display employee name."""
|
|
return obj.employee.get_full_name()
|
|
employee_name.short_description = 'Employee'
|
|
|
|
def total_entitled_display(self, obj):
|
|
"""Display total entitled days."""
|
|
return f"{obj.total_entitled:.2f}"
|
|
total_entitled_display.short_description = 'Entitled'
|
|
|
|
def available_display(self, obj):
|
|
"""Display available balance with color coding."""
|
|
available = obj.available
|
|
color = 'green' if available > 5 else 'orange' if available > 0 else 'red'
|
|
return format_html(
|
|
'<span style="color: {};">{:.2f}</span>',
|
|
color, available
|
|
)
|
|
available_display.short_description = 'Available'
|
|
|
|
def get_queryset(self, request):
|
|
"""Filter by user's tenant."""
|
|
qs = super().get_queryset(request)
|
|
if hasattr(request.user, 'tenant'):
|
|
qs = qs.filter(employee__tenant=request.user.tenant)
|
|
return qs.select_related('employee', 'leave_type')
|
|
|
|
|
|
class LeaveApprovalInline(admin.TabularInline):
|
|
"""
|
|
Inline admin for leave approvals.
|
|
"""
|
|
model = LeaveApproval
|
|
extra = 0
|
|
fields = [
|
|
'level', 'approver', 'action', 'comments', 'action_date'
|
|
]
|
|
readonly_fields = ['approval_id']
|
|
|
|
|
|
@admin.register(LeaveRequest)
|
|
class LeaveRequestAdmin(admin.ModelAdmin):
|
|
"""
|
|
Admin interface for leave requests.
|
|
"""
|
|
list_display = [
|
|
'employee_name', 'leave_type', 'start_date', 'end_date',
|
|
'total_days', 'status', 'current_approver', 'submitted_at'
|
|
]
|
|
list_filter = [
|
|
'employee__tenant', 'leave_type', 'status', 'start_date'
|
|
]
|
|
search_fields = [
|
|
'employee__first_name', 'employee__last_name',
|
|
'employee__employee_id', 'reason'
|
|
]
|
|
readonly_fields = [
|
|
'request_id', 'tenant', 'total_days', 'is_pending',
|
|
'is_approved', 'can_cancel', 'can_edit',
|
|
'created_at', 'updated_at'
|
|
]
|
|
fieldsets = [
|
|
('Request Information', {
|
|
'fields': [
|
|
'request_id', 'employee', 'leave_type'
|
|
]
|
|
}),
|
|
('Leave Details', {
|
|
'fields': [
|
|
'start_date', 'end_date', 'start_day_type',
|
|
'end_day_type', 'total_days'
|
|
]
|
|
}),
|
|
('Request Information', {
|
|
'fields': [
|
|
'reason', 'contact_number', 'emergency_contact'
|
|
]
|
|
}),
|
|
('Supporting Documents', {
|
|
'fields': [
|
|
'attachment'
|
|
]
|
|
}),
|
|
('Status and Workflow', {
|
|
'fields': [
|
|
'status', 'submitted_at'
|
|
]
|
|
}),
|
|
('Approval Information', {
|
|
'fields': [
|
|
'current_approver', 'final_approver',
|
|
'approved_at', 'rejected_at', 'rejection_reason'
|
|
]
|
|
}),
|
|
('Cancellation', {
|
|
'fields': [
|
|
'cancelled_at', 'cancelled_by', 'cancellation_reason'
|
|
]
|
|
}),
|
|
('Status Checks', {
|
|
'fields': [
|
|
'is_pending', 'is_approved', 'can_cancel', 'can_edit'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Notes', {
|
|
'fields': [
|
|
'notes'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Related Information', {
|
|
'fields': [
|
|
'tenant'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Metadata', {
|
|
'fields': [
|
|
'created_at', 'updated_at', 'created_by'
|
|
],
|
|
'classes': ['collapse']
|
|
})
|
|
]
|
|
inlines = [LeaveApprovalInline]
|
|
date_hierarchy = 'start_date'
|
|
|
|
def employee_name(self, obj):
|
|
"""Display employee name."""
|
|
return obj.employee.get_full_name()
|
|
employee_name.short_description = 'Employee'
|
|
|
|
def get_queryset(self, request):
|
|
"""Filter by user's tenant."""
|
|
qs = super().get_queryset(request)
|
|
if hasattr(request.user, 'tenant'):
|
|
qs = qs.filter(employee__tenant=request.user.tenant)
|
|
return qs.select_related(
|
|
'employee', 'leave_type', 'current_approver',
|
|
'final_approver', 'cancelled_by', 'created_by'
|
|
)
|
|
|
|
|
|
@admin.register(LeaveApproval)
|
|
class LeaveApprovalAdmin(admin.ModelAdmin):
|
|
"""
|
|
Admin interface for leave approvals.
|
|
"""
|
|
list_display = [
|
|
'leave_request_display', 'level', 'approver',
|
|
'action', 'action_date', 'is_delegated_display'
|
|
]
|
|
list_filter = [
|
|
'action', 'level', 'action_date'
|
|
]
|
|
search_fields = [
|
|
'leave_request__employee__first_name',
|
|
'leave_request__employee__last_name',
|
|
'approver__first_name', 'approver__last_name'
|
|
]
|
|
readonly_fields = [
|
|
'approval_id', 'is_pending', 'is_delegated_approval',
|
|
'created_at', 'updated_at'
|
|
]
|
|
fieldsets = [
|
|
('Approval Information', {
|
|
'fields': [
|
|
'approval_id', 'leave_request', 'level', 'approver'
|
|
]
|
|
}),
|
|
('Delegation', {
|
|
'fields': [
|
|
'delegated_by'
|
|
]
|
|
}),
|
|
('Approval Details', {
|
|
'fields': [
|
|
'action', 'comments', 'action_date'
|
|
]
|
|
}),
|
|
('Status Information', {
|
|
'fields': [
|
|
'is_pending', 'is_delegated_approval'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Metadata', {
|
|
'fields': [
|
|
'created_at', 'updated_at'
|
|
],
|
|
'classes': ['collapse']
|
|
})
|
|
]
|
|
|
|
def leave_request_display(self, obj):
|
|
"""Display leave request summary."""
|
|
return f"{obj.leave_request.employee.get_full_name()} - {obj.leave_request.leave_type.name}"
|
|
leave_request_display.short_description = 'Leave Request'
|
|
|
|
def is_delegated_display(self, obj):
|
|
"""Display delegation status."""
|
|
if obj.is_delegated_approval:
|
|
return format_html(
|
|
'<span style="color: orange;">✓ Delegated from {}</span>',
|
|
obj.delegated_by.get_full_name()
|
|
)
|
|
return format_html('<span style="color: gray;">Direct</span>')
|
|
is_delegated_display.short_description = 'Delegation'
|
|
|
|
def get_queryset(self, request):
|
|
"""Filter by user's tenant."""
|
|
qs = super().get_queryset(request)
|
|
if hasattr(request.user, 'tenant'):
|
|
qs = qs.filter(leave_request__employee__tenant=request.user.tenant)
|
|
return qs.select_related(
|
|
'leave_request__employee', 'leave_request__leave_type',
|
|
'approver', 'delegated_by'
|
|
)
|
|
|
|
|
|
@admin.register(LeaveDelegate)
|
|
class LeaveDelegateAdmin(admin.ModelAdmin):
|
|
"""
|
|
Admin interface for leave delegations.
|
|
"""
|
|
list_display = [
|
|
'delegator', 'delegate', 'start_date', 'end_date',
|
|
'is_current_display', 'is_active'
|
|
]
|
|
list_filter = [
|
|
'delegator__tenant', 'is_active', 'start_date', 'end_date'
|
|
]
|
|
search_fields = [
|
|
'delegator__first_name', 'delegator__last_name',
|
|
'delegate__first_name', 'delegate__last_name'
|
|
]
|
|
readonly_fields = [
|
|
'delegation_id', 'tenant', 'is_current',
|
|
'created_at', 'updated_at'
|
|
]
|
|
fieldsets = [
|
|
('Delegation Information', {
|
|
'fields': [
|
|
'delegation_id', 'delegator', 'delegate'
|
|
]
|
|
}),
|
|
('Delegation Period', {
|
|
'fields': [
|
|
'start_date', 'end_date'
|
|
]
|
|
}),
|
|
('Delegation Scope', {
|
|
'fields': [
|
|
'reason', 'is_active'
|
|
]
|
|
}),
|
|
('Status Information', {
|
|
'fields': [
|
|
'is_current'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Related Information', {
|
|
'fields': [
|
|
'tenant'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Metadata', {
|
|
'fields': [
|
|
'created_at', 'updated_at', 'created_by'
|
|
],
|
|
'classes': ['collapse']
|
|
})
|
|
]
|
|
date_hierarchy = 'start_date'
|
|
|
|
def is_current_display(self, obj):
|
|
"""Display current status."""
|
|
if obj.is_current:
|
|
return format_html('<span style="color: green;">✓ Active</span>')
|
|
return format_html('<span style="color: gray;">Inactive</span>')
|
|
is_current_display.short_description = 'Current'
|
|
|
|
def get_queryset(self, request):
|
|
"""Filter by user's tenant."""
|
|
qs = super().get_queryset(request)
|
|
if hasattr(request.user, 'tenant'):
|
|
qs = qs.filter(delegator__tenant=request.user.tenant)
|
|
return qs.select_related('delegator', 'delegate', 'created_by')
|
|
|
|
|
|
# ============================================================================
|
|
# CUSTOM ADMIN ACTIONS
|
|
# ============================================================================
|
|
|
|
def mark_training_completed(modeladmin, request, queryset):
|
|
"""Mark selected training records as completed."""
|
|
updated = queryset.update(status='COMPLETED', completion_date=date.today())
|
|
modeladmin.message_user(request, f'{updated} training records marked as completed.')
|
|
mark_training_completed.short_description = "Mark selected training as completed"
|
|
|
|
def mark_training_passed(modeladmin, request, queryset):
|
|
"""Mark selected training records as passed."""
|
|
updated = queryset.update(passed=True)
|
|
modeladmin.message_user(request, f'{updated} training records marked as passed.')
|
|
mark_training_passed.short_description = "Mark selected training as passed"
|
|
|
|
def generate_certificates(modeladmin, request, queryset):
|
|
"""Generate certificates for completed training."""
|
|
count = 0
|
|
for record in queryset.filter(status='COMPLETED', passed=True):
|
|
if record.program.is_certified and not hasattr(record, 'certificate'):
|
|
TrainingCertificates.objects.create(
|
|
program=record.program,
|
|
employee=record.employee,
|
|
enrollment=record,
|
|
certificate_name=f"{record.program.name} Certificate",
|
|
expiry_date=TrainingCertificates.compute_expiry(record.program, date.today()),
|
|
created_by=request.user
|
|
)
|
|
count += 1
|
|
modeladmin.message_user(request, f'{count} certificates generated.')
|
|
generate_certificates.short_description = "Generate certificates for completed training"
|
|
|
|
# Add actions to TrainingRecord admin
|
|
TrainingRecordAdmin.actions = [mark_training_completed, mark_training_passed, generate_certificates]
|
|
|
|
|
|
# ============================================================================
|
|
# ADMIN SITE CUSTOMIZATION
|
|
# ============================================================================
|
|
|
|
# Customize admin site
|
|
admin.site.site_header = "Hospital Management System - HR"
|
|
admin.site.site_title = "HR Admin"
|
|
admin.site.index_title = "Human Resources Administration"
|
|
|
|
|
|
# ============================================================================
|
|
# SALARY & COMPENSATION ADMIN CLASSES
|
|
# ============================================================================
|
|
|
|
@admin.register(SalaryInformation)
|
|
class SalaryInformationAdmin(admin.ModelAdmin):
|
|
"""
|
|
Admin interface for salary information.
|
|
"""
|
|
list_display = [
|
|
'employee_name', 'effective_date', 'total_salary_display',
|
|
'currency', 'payment_frequency', 'is_active', 'created_at'
|
|
]
|
|
list_filter = [
|
|
'employee__tenant', 'is_active', 'currency',
|
|
'payment_frequency', 'effective_date'
|
|
]
|
|
search_fields = [
|
|
'employee__first_name', 'employee__last_name',
|
|
'employee__employee_id'
|
|
]
|
|
readonly_fields = [
|
|
'salary_id', 'total_salary', 'tenant',
|
|
'created_at', 'updated_at'
|
|
]
|
|
fieldsets = [
|
|
('Employee Information', {
|
|
'fields': [
|
|
'salary_id', 'employee', 'effective_date', 'end_date', 'is_active'
|
|
]
|
|
}),
|
|
('Salary Components', {
|
|
'fields': [
|
|
'basic_salary', 'housing_allowance',
|
|
'transportation_allowance', 'food_allowance',
|
|
'other_allowances', 'total_salary'
|
|
]
|
|
}),
|
|
('Payment Details', {
|
|
'fields': [
|
|
'currency', 'payment_frequency',
|
|
'bank_name', 'account_number', 'iban', 'swift_code'
|
|
]
|
|
}),
|
|
('Additional Information', {
|
|
'fields': [
|
|
'notes'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Related Information', {
|
|
'fields': [
|
|
'tenant'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Metadata', {
|
|
'fields': [
|
|
'created_at', 'updated_at', 'created_by'
|
|
],
|
|
'classes': ['collapse']
|
|
})
|
|
]
|
|
date_hierarchy = 'effective_date'
|
|
|
|
def employee_name(self, obj):
|
|
"""Display employee name."""
|
|
return obj.employee.get_full_name()
|
|
employee_name.short_description = 'Employee'
|
|
|
|
def total_salary_display(self, obj):
|
|
"""Display total salary with formatting."""
|
|
return format_html(
|
|
'<strong>{:,.2f} {}</strong>',
|
|
obj.total_salary,
|
|
obj.currency
|
|
)
|
|
total_salary_display.short_description = 'Total Salary'
|
|
|
|
def save_model(self, request, obj, form, change):
|
|
"""Set created_by on creation."""
|
|
if not change:
|
|
obj.created_by = request.user
|
|
super().save_model(request, obj, form, change)
|
|
|
|
def get_queryset(self, request):
|
|
"""Filter by user's tenant."""
|
|
qs = super().get_queryset(request)
|
|
if hasattr(request.user, 'tenant'):
|
|
qs = qs.filter(employee__tenant=request.user.tenant)
|
|
return qs.select_related('employee', 'created_by')
|
|
|
|
|
|
@admin.register(SalaryAdjustment)
|
|
class SalaryAdjustmentAdmin(admin.ModelAdmin):
|
|
"""
|
|
Admin interface for salary adjustments.
|
|
"""
|
|
list_display = [
|
|
'employee_name', 'adjustment_type', 'effective_date',
|
|
'adjustment_amount_display', 'adjustment_percentage_display',
|
|
'approved_by', 'approval_date'
|
|
]
|
|
list_filter = [
|
|
'employee__tenant', 'adjustment_type', 'effective_date', 'approved_by'
|
|
]
|
|
search_fields = [
|
|
'employee__first_name', 'employee__last_name',
|
|
'employee__employee_id', 'adjustment_reason'
|
|
]
|
|
readonly_fields = [
|
|
'adjustment_id', 'tenant', 'adjustment_amount',
|
|
'adjustment_percentage', 'created_at', 'updated_at'
|
|
]
|
|
fieldsets = [
|
|
('Adjustment Information', {
|
|
'fields': [
|
|
'adjustment_id', 'employee'
|
|
]
|
|
}),
|
|
('Salary References', {
|
|
'fields': [
|
|
'previous_salary', 'new_salary'
|
|
]
|
|
}),
|
|
('Adjustment Details', {
|
|
'fields': [
|
|
'adjustment_type', 'adjustment_reason',
|
|
'adjustment_amount', 'adjustment_percentage'
|
|
]
|
|
}),
|
|
('Effective Date', {
|
|
'fields': [
|
|
'effective_date'
|
|
]
|
|
}),
|
|
('Approval', {
|
|
'fields': [
|
|
'approved_by', 'approval_date'
|
|
]
|
|
}),
|
|
('Notes', {
|
|
'fields': [
|
|
'notes'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Related Information', {
|
|
'fields': [
|
|
'tenant'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Metadata', {
|
|
'fields': [
|
|
'created_at', 'updated_at', 'created_by'
|
|
],
|
|
'classes': ['collapse']
|
|
})
|
|
]
|
|
date_hierarchy = 'effective_date'
|
|
|
|
def employee_name(self, obj):
|
|
"""Display employee name."""
|
|
return obj.employee.get_full_name()
|
|
employee_name.short_description = 'Employee'
|
|
|
|
def adjustment_amount_display(self, obj):
|
|
"""Display adjustment amount with color coding."""
|
|
color = 'green' if obj.adjustment_amount >= 0 else 'red'
|
|
return format_html(
|
|
'<span style="color: {};">{:+,.2f}</span>',
|
|
color,
|
|
obj.adjustment_amount
|
|
)
|
|
adjustment_amount_display.short_description = 'Amount Change'
|
|
|
|
def adjustment_percentage_display(self, obj):
|
|
"""Display adjustment percentage."""
|
|
if obj.adjustment_percentage:
|
|
color = 'green' if obj.adjustment_percentage >= 0 else 'red'
|
|
return format_html(
|
|
'<span style="color: {};">{:+.2f}%</span>',
|
|
color,
|
|
obj.adjustment_percentage
|
|
)
|
|
return 'N/A'
|
|
adjustment_percentage_display.short_description = 'Percentage'
|
|
|
|
def save_model(self, request, obj, form, change):
|
|
"""Set created_by on creation."""
|
|
if not change:
|
|
obj.created_by = request.user
|
|
super().save_model(request, obj, form, change)
|
|
|
|
def get_queryset(self, request):
|
|
"""Filter by user's tenant."""
|
|
qs = super().get_queryset(request)
|
|
if hasattr(request.user, 'tenant'):
|
|
qs = qs.filter(employee__tenant=request.user.tenant)
|
|
return qs.select_related(
|
|
'employee', 'previous_salary', 'new_salary',
|
|
'approved_by', 'created_by'
|
|
)
|
|
|
|
|
|
# ============================================================================
|
|
# DOCUMENT REQUEST ADMIN CLASSES
|
|
# ============================================================================
|
|
|
|
@admin.register(DocumentRequest)
|
|
class DocumentRequestAdmin(admin.ModelAdmin):
|
|
"""
|
|
Admin interface for document requests.
|
|
"""
|
|
list_display = [
|
|
'employee_name', 'document_type', 'status', 'requested_date',
|
|
'required_by_date', 'urgency_indicator', 'processed_by'
|
|
]
|
|
list_filter = [
|
|
'employee__tenant', 'status', 'document_type', 'language',
|
|
'delivery_method', 'requested_date', 'required_by_date'
|
|
]
|
|
search_fields = [
|
|
'employee__first_name', 'employee__last_name',
|
|
'employee__employee_id', 'purpose', 'document_number'
|
|
]
|
|
readonly_fields = [
|
|
'request_id', 'tenant', 'requested_date', 'document_number',
|
|
'is_urgent', 'is_overdue', 'can_cancel',
|
|
'created_at', 'updated_at'
|
|
]
|
|
list_editable = ['status']
|
|
|
|
actions = ['mark_as_ready', 'mark_as_delivered', 'mark_as_rejected']
|
|
|
|
fieldsets = [
|
|
('Request Information', {
|
|
'fields': [
|
|
'request_id', 'employee', 'document_type', 'custom_document_name'
|
|
]
|
|
}),
|
|
('Request Details', {
|
|
'fields': [
|
|
'purpose', 'addressee', 'include_salary'
|
|
]
|
|
}),
|
|
('Language and Delivery', {
|
|
'fields': [
|
|
'language', 'delivery_method', 'delivery_address', 'delivery_email'
|
|
]
|
|
}),
|
|
('Dates', {
|
|
'fields': [
|
|
'requested_date', 'required_by_date'
|
|
]
|
|
}),
|
|
('Status', {
|
|
'fields': [
|
|
'status'
|
|
]
|
|
}),
|
|
('Processing', {
|
|
'fields': [
|
|
'processed_by', 'processed_date'
|
|
]
|
|
}),
|
|
('Generated Document', {
|
|
'fields': [
|
|
'generated_document', 'document_number'
|
|
]
|
|
}),
|
|
('Rejection', {
|
|
'fields': [
|
|
'rejection_reason'
|
|
]
|
|
}),
|
|
('Additional Information', {
|
|
'fields': [
|
|
'additional_notes'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Status Checks', {
|
|
'fields': [
|
|
'is_urgent', 'is_overdue', 'can_cancel'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Related Information', {
|
|
'fields': [
|
|
'tenant'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Metadata', {
|
|
'fields': [
|
|
'created_at', 'updated_at', 'created_by'
|
|
],
|
|
'classes': ['collapse']
|
|
})
|
|
]
|
|
date_hierarchy = 'requested_date'
|
|
|
|
def employee_name(self, obj):
|
|
"""Display employee name."""
|
|
return obj.employee.get_full_name()
|
|
employee_name.short_description = 'Employee'
|
|
|
|
def urgency_indicator(self, obj):
|
|
"""Display urgency indicator."""
|
|
if obj.is_urgent:
|
|
return format_html('<span style="color: red;">🔴 Urgent</span>')
|
|
elif obj.is_overdue:
|
|
return format_html('<span style="color: orange;">⚠️ Overdue</span>')
|
|
return '✓'
|
|
urgency_indicator.short_description = 'Urgency'
|
|
|
|
def mark_as_ready(self, request, queryset):
|
|
"""Mark selected requests as ready."""
|
|
updated = queryset.update(status='READY')
|
|
self.message_user(request, f'{updated} requests marked as ready.')
|
|
mark_as_ready.short_description = 'Mark selected as Ready'
|
|
|
|
def mark_as_delivered(self, request, queryset):
|
|
"""Mark selected requests as delivered."""
|
|
updated = queryset.update(status='DELIVERED')
|
|
self.message_user(request, f'{updated} requests marked as delivered.')
|
|
mark_as_delivered.short_description = 'Mark selected as Delivered'
|
|
|
|
def mark_as_rejected(self, request, queryset):
|
|
"""Mark selected requests as rejected."""
|
|
updated = queryset.update(status='REJECTED')
|
|
self.message_user(request, f'{updated} requests marked as rejected.')
|
|
mark_as_rejected.short_description = 'Mark selected as Rejected'
|
|
|
|
def save_model(self, request, obj, form, change):
|
|
"""Set created_by on creation."""
|
|
if not change:
|
|
obj.created_by = request.user
|
|
super().save_model(request, obj, form, change)
|
|
|
|
def get_queryset(self, request):
|
|
"""Filter by user's tenant."""
|
|
qs = super().get_queryset(request)
|
|
if hasattr(request.user, 'tenant'):
|
|
qs = qs.filter(employee__tenant=request.user.tenant)
|
|
return qs.select_related('employee', 'processed_by', 'created_by')
|
|
|
|
|
|
@admin.register(DocumentTemplate)
|
|
class DocumentTemplateAdmin(admin.ModelAdmin):
|
|
"""
|
|
Admin interface for document templates.
|
|
"""
|
|
list_display = [
|
|
'name', 'document_type', 'language', 'is_active',
|
|
'is_default', 'requires_approval', 'created_at'
|
|
]
|
|
list_filter = [
|
|
'tenant', 'document_type', 'language', 'is_active', 'is_default'
|
|
]
|
|
search_fields = [
|
|
'name', 'description', 'template_content'
|
|
]
|
|
readonly_fields = [
|
|
'template_id', 'created_at', 'updated_at'
|
|
]
|
|
fieldsets = [
|
|
('Template Information', {
|
|
'fields': [
|
|
'template_id', 'tenant', 'name', 'description'
|
|
]
|
|
}),
|
|
('Document Type', {
|
|
'fields': [
|
|
'document_type', 'language'
|
|
]
|
|
}),
|
|
('Template Content', {
|
|
'fields': [
|
|
'template_content', 'header_content', 'footer_content'
|
|
]
|
|
}),
|
|
('Placeholders', {
|
|
'fields': [
|
|
'available_placeholders'
|
|
]
|
|
}),
|
|
('Settings', {
|
|
'fields': [
|
|
'is_active', 'is_default', 'requires_approval'
|
|
]
|
|
}),
|
|
('Styling', {
|
|
'fields': [
|
|
'css_styles'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Metadata', {
|
|
'fields': [
|
|
'created_at', 'updated_at', 'created_by'
|
|
],
|
|
'classes': ['collapse']
|
|
})
|
|
]
|
|
|
|
def save_model(self, request, obj, form, change):
|
|
"""Set created_by on creation."""
|
|
if not change:
|
|
obj.created_by = request.user
|
|
super().save_model(request, obj, form, change)
|
|
|
|
def get_queryset(self, request):
|
|
"""Filter by user's tenant."""
|
|
qs = super().get_queryset(request)
|
|
if hasattr(request.user, 'tenant'):
|
|
qs = qs.filter(tenant=request.user.tenant)
|
|
return qs.select_related('created_by')
|