875 lines
25 KiB
Python
875 lines
25 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 decimal import Decimal
|
|
from datetime import date
|
|
from .models import (
|
|
Employee, Department, Schedule, ScheduleAssignment,
|
|
TimeEntry, PerformanceReview, TrainingRecord
|
|
)
|
|
|
|
|
|
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 = [
|
|
'training_name', 'training_date', 'completion_date',
|
|
'status', 'passed'
|
|
]
|
|
readonly_fields = ['record_id']
|
|
|
|
|
|
@admin.register(Employee)
|
|
class EmployeeAdmin(admin.ModelAdmin):
|
|
"""
|
|
Admin interface for employees.
|
|
"""
|
|
list_display = [
|
|
'employee_number', '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_number', 'first_name', 'last_name',
|
|
'email', 'job_title'
|
|
]
|
|
readonly_fields = [
|
|
'employee_id', 'age', 'years_of_service',
|
|
'is_license_expired', 'full_address',
|
|
'created_at', 'updated_at'
|
|
]
|
|
fieldsets = [
|
|
('Employee Information', {
|
|
'fields': [
|
|
'employee_id', 'tenant', 'employee_number', 'user'
|
|
]
|
|
}),
|
|
('Personal Information', {
|
|
'fields': [
|
|
'first_name', 'last_name', 'middle_name', 'preferred_name'
|
|
]
|
|
}),
|
|
('Contact Information', {
|
|
'fields': [
|
|
'email', 'phone', 'mobile_phone'
|
|
]
|
|
}),
|
|
('Address Information', {
|
|
'fields': [
|
|
'address_line_1', 'address_line_2', 'city',
|
|
'state', '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', 'certifications'
|
|
]
|
|
}),
|
|
('Emergency Contact', {
|
|
'fields': [
|
|
'emergency_contact_name', 'emergency_contact_relationship',
|
|
'emergency_contact_phone'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Calculated Fields', {
|
|
'fields': [
|
|
'age', 'years_of_service', 'is_license_expired', 'full_address'
|
|
],
|
|
'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 = [
|
|
'department_code', 'name', 'department_type',
|
|
'department_head', 'employee_count_display',
|
|
'total_fte_display', 'is_active'
|
|
]
|
|
list_filter = [
|
|
'tenant', 'department_type', 'is_active'
|
|
]
|
|
search_fields = [
|
|
'department_code', 'name', 'description'
|
|
]
|
|
readonly_fields = [
|
|
'department_id', 'employee_count', 'total_fte',
|
|
'created_at', 'updated_at'
|
|
]
|
|
fieldsets = [
|
|
('Department Information', {
|
|
'fields': [
|
|
'department_id', 'tenant', 'department_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_number', '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_number'
|
|
]
|
|
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_number'
|
|
]
|
|
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',
|
|
'employee__employee_number'
|
|
]
|
|
readonly_fields = [
|
|
'review_id', 'tenant', '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')
|
|
|
|
|
|
@admin.register(TrainingRecord)
|
|
class TrainingRecordAdmin(admin.ModelAdmin):
|
|
"""
|
|
Admin interface for training records.
|
|
"""
|
|
list_display = [
|
|
'employee_name', 'training_name', 'training_type',
|
|
'training_date', 'completion_date', 'status',
|
|
'passed', 'expiry_status_display'
|
|
]
|
|
list_filter = [
|
|
'training_type', 'status', 'passed', 'training_date'
|
|
]
|
|
search_fields = [
|
|
'employee__first_name', 'employee__last_name',
|
|
'employee__employee_number', 'training_name'
|
|
]
|
|
readonly_fields = [
|
|
'record_id', 'tenant', 'is_expired', 'days_to_expiry',
|
|
'is_due_for_renewal', 'created_at', 'updated_at'
|
|
]
|
|
fieldsets = [
|
|
('Training Information', {
|
|
'fields': [
|
|
'record_id', 'employee', 'training_name', 'training_description'
|
|
]
|
|
}),
|
|
('Training Type', {
|
|
'fields': [
|
|
'training_type'
|
|
]
|
|
}),
|
|
('Training Provider', {
|
|
'fields': [
|
|
'training_provider', 'instructor'
|
|
]
|
|
}),
|
|
('Training Dates', {
|
|
'fields': [
|
|
'training_date', 'completion_date', 'expiry_date'
|
|
]
|
|
}),
|
|
('Training Details', {
|
|
'fields': [
|
|
'duration_hours', 'credits_earned'
|
|
]
|
|
}),
|
|
('Training Status', {
|
|
'fields': [
|
|
'status'
|
|
]
|
|
}),
|
|
('Results', {
|
|
'fields': [
|
|
'score', 'passed'
|
|
]
|
|
}),
|
|
('Certification Information', {
|
|
'fields': [
|
|
'certificate_number', 'certification_body'
|
|
]
|
|
}),
|
|
('Cost Information', {
|
|
'fields': [
|
|
'training_cost'
|
|
]
|
|
}),
|
|
('Expiry Information', {
|
|
'fields': [
|
|
'is_expired', 'days_to_expiry', 'is_due_for_renewal'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Notes', {
|
|
'fields': [
|
|
'notes'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Related Information', {
|
|
'fields': [
|
|
'tenant'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Metadata', {
|
|
'fields': [
|
|
'created_at', 'updated_at', 'created_by'
|
|
],
|
|
'classes': ['collapse']
|
|
})
|
|
]
|
|
date_hierarchy = 'training_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.is_due_for_renewal:
|
|
return format_html('<span style="color: orange;">⚠️ Due 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', 'created_by')
|
|
|
|
|
|
# Customize admin site
|
|
admin.site.site_header = "Hospital Management System - HR"
|
|
admin.site.site_title = "HR Admin"
|
|
admin.site.index_title = "Human Resources Administration"
|
|
|