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

1382 lines
40 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')
# ============================================================================
# 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"