1412 lines
37 KiB
Python
1412 lines
37 KiB
Python
"""
|
|
HR app models for hospital management system.
|
|
Provides staff management, scheduling, performance management, and training tracking.
|
|
"""
|
|
|
|
import uuid
|
|
from django.db import models
|
|
from django.core.validators import RegexValidator, MinValueValidator, MaxValueValidator
|
|
from django.utils import timezone
|
|
from django.conf import settings
|
|
from datetime import timedelta, datetime, date, time
|
|
from decimal import Decimal
|
|
import json
|
|
|
|
|
|
class Employee(models.Model):
|
|
"""
|
|
Employee model for hospital staff management.
|
|
"""
|
|
GENDER_CHOICES = [
|
|
('MALE', 'Male'),
|
|
('FEMALE', 'Female'),
|
|
('OTHER', 'Other'),
|
|
('UNKNOWN', 'Unknown'),
|
|
]
|
|
MARITAL_STATUS_CHOICES = [
|
|
('SINGLE', 'Single'),
|
|
('MARRIED', 'Married'),
|
|
('DIVORCED', 'Divorced'),
|
|
('WIDOWED', 'Widowed'),
|
|
('SEPARATED', 'Separated'),
|
|
('OTHER', 'Other'),
|
|
]
|
|
EMPLOYMENT_TYPE_CHOICES = [
|
|
('FULL_TIME', 'Full Time'),
|
|
('PART_TIME', 'Part Time'),
|
|
('CONTRACT', 'Contract'),
|
|
('TEMPORARY', 'Temporary'),
|
|
('INTERN', 'Intern'),
|
|
('VOLUNTEER', 'Volunteer'),
|
|
('PER_DIEM', 'Per Diem'),
|
|
('CONSULTANT', 'Consultant'),
|
|
]
|
|
EMPLOYMENT_STATUS_CHOICES = [
|
|
('ACTIVE', 'Active'),
|
|
('INACTIVE', 'Inactive'),
|
|
('TERMINATED', 'Terminated'),
|
|
('SUSPENDED', 'Suspended'),
|
|
('LEAVE', 'On Leave'),
|
|
('RETIRED', 'Retired'),
|
|
]
|
|
# Tenant relationship
|
|
tenant = models.ForeignKey(
|
|
'core.Tenant',
|
|
on_delete=models.CASCADE,
|
|
related_name='employees',
|
|
help_text='Organization tenant'
|
|
)
|
|
|
|
# User relationship (optional - for employees who have system access)
|
|
user = models.OneToOneField(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='employee_profile',
|
|
help_text='Associated user account'
|
|
)
|
|
|
|
# Employee Information
|
|
employee_id = models.UUIDField(
|
|
default=uuid.uuid4,
|
|
unique=True,
|
|
editable=False,
|
|
help_text='Unique employee identifier'
|
|
)
|
|
employee_number = models.CharField(
|
|
max_length=20,
|
|
help_text='Employee number'
|
|
)
|
|
|
|
# Personal Information
|
|
first_name = models.CharField(
|
|
max_length=50,
|
|
help_text='First name'
|
|
)
|
|
last_name = models.CharField(
|
|
max_length=50,
|
|
help_text='Last name'
|
|
)
|
|
middle_name = models.CharField(
|
|
max_length=50,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Middle name'
|
|
)
|
|
preferred_name = models.CharField(
|
|
max_length=50,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Preferred name'
|
|
)
|
|
|
|
# Contact Information
|
|
email = models.EmailField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Email address'
|
|
)
|
|
phone = models.CharField(
|
|
max_length=20,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Phone number'
|
|
)
|
|
mobile_phone = models.CharField(
|
|
max_length=20,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Mobile phone number'
|
|
)
|
|
|
|
# Address Information
|
|
address_line_1 = models.CharField(
|
|
max_length=100,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Address line 1'
|
|
)
|
|
address_line_2 = models.CharField(
|
|
max_length=100,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Address line 2'
|
|
)
|
|
city = models.CharField(
|
|
max_length=50,
|
|
blank=True,
|
|
null=True,
|
|
help_text='City'
|
|
)
|
|
state = models.CharField(
|
|
max_length=50,
|
|
blank=True,
|
|
null=True,
|
|
help_text='State/Province'
|
|
)
|
|
postal_code = models.CharField(
|
|
max_length=20,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Postal/ZIP code'
|
|
)
|
|
country = models.CharField(
|
|
max_length=50,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Country'
|
|
)
|
|
|
|
# Personal Details
|
|
date_of_birth = models.DateField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Date of birth'
|
|
)
|
|
gender = models.CharField(
|
|
max_length=10,
|
|
choices=GENDER_CHOICES,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Gender'
|
|
)
|
|
marital_status = models.CharField(
|
|
max_length=20,
|
|
choices=MARITAL_STATUS_CHOICES,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Marital status'
|
|
)
|
|
|
|
# Employment Information
|
|
department = models.ForeignKey(
|
|
'Department',
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='employees',
|
|
help_text='Department'
|
|
)
|
|
job_title = models.CharField(
|
|
max_length=100,
|
|
help_text='Job title'
|
|
)
|
|
employment_type = models.CharField(
|
|
max_length=20,
|
|
choices=EMPLOYMENT_TYPE_CHOICES,
|
|
help_text='Employment type'
|
|
)
|
|
employment_status = models.CharField(
|
|
max_length=20,
|
|
choices=EMPLOYMENT_STATUS_CHOICES,
|
|
default='ACTIVE',
|
|
help_text='Employment status'
|
|
)
|
|
|
|
# Employment Dates
|
|
hire_date = models.DateField(
|
|
help_text='Hire date'
|
|
)
|
|
termination_date = models.DateField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Termination date'
|
|
)
|
|
|
|
# Supervisor Information
|
|
supervisor = models.ForeignKey(
|
|
'self',
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='direct_reports',
|
|
help_text='Direct supervisor'
|
|
)
|
|
|
|
# Work Schedule Information
|
|
standard_hours_per_week = models.DecimalField(
|
|
max_digits=5,
|
|
decimal_places=2,
|
|
default=Decimal('40.00'),
|
|
help_text='Standard hours per week'
|
|
)
|
|
fte_percentage = models.DecimalField(
|
|
max_digits=5,
|
|
decimal_places=2,
|
|
default=Decimal('100.00'),
|
|
validators=[MinValueValidator(0), MaxValueValidator(100)],
|
|
help_text='FTE percentage'
|
|
)
|
|
|
|
# Compensation Information
|
|
hourly_rate = models.DecimalField(
|
|
max_digits=10,
|
|
decimal_places=2,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Hourly rate'
|
|
)
|
|
annual_salary = models.DecimalField(
|
|
max_digits=12,
|
|
decimal_places=2,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Annual salary'
|
|
)
|
|
|
|
# Professional Information
|
|
license_number = models.CharField(
|
|
max_length=50,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Professional license number'
|
|
)
|
|
license_expiry_date = models.DateField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='License expiry date'
|
|
)
|
|
certifications = models.JSONField(
|
|
default=list,
|
|
help_text='Professional certifications'
|
|
)
|
|
|
|
# Emergency Contact
|
|
emergency_contact_name = models.CharField(
|
|
max_length=100,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Emergency contact name'
|
|
)
|
|
emergency_contact_relationship = models.CharField(
|
|
max_length=50,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Emergency contact relationship'
|
|
)
|
|
emergency_contact_phone = models.CharField(
|
|
max_length=20,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Emergency contact phone'
|
|
)
|
|
|
|
# Notes
|
|
notes = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Employee notes'
|
|
)
|
|
|
|
# Metadata
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
created_by = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='created_employees',
|
|
help_text='User who created the employee record'
|
|
)
|
|
|
|
class Meta:
|
|
db_table = 'hr_employee'
|
|
verbose_name = 'Employee'
|
|
verbose_name_plural = 'Employees'
|
|
ordering = ['last_name', 'first_name']
|
|
indexes = [
|
|
models.Index(fields=['tenant', 'employment_status']),
|
|
models.Index(fields=['employee_number']),
|
|
models.Index(fields=['last_name', 'first_name']),
|
|
models.Index(fields=['department']),
|
|
models.Index(fields=['hire_date']),
|
|
]
|
|
unique_together = ['tenant', 'employee_number']
|
|
|
|
def __str__(self):
|
|
return f"{self.employee_number} - {self.get_full_name()}"
|
|
|
|
def get_full_name(self):
|
|
"""
|
|
Get employee's full name.
|
|
"""
|
|
if self.middle_name:
|
|
return f"{self.first_name} {self.middle_name} {self.last_name}"
|
|
return f"{self.first_name} {self.last_name}"
|
|
|
|
def get_display_name(self):
|
|
"""
|
|
Get employee's display name (preferred name if available).
|
|
"""
|
|
if self.preferred_name:
|
|
return f"{self.preferred_name} {self.last_name}"
|
|
return self.get_full_name()
|
|
|
|
@property
|
|
def age(self):
|
|
"""
|
|
Calculate employee's age.
|
|
"""
|
|
if self.date_of_birth:
|
|
today = date.today()
|
|
return today.year - self.date_of_birth.year - ((today.month, today.day) < (self.date_of_birth.month, self.date_of_birth.day))
|
|
return None
|
|
|
|
@property
|
|
def years_of_service(self):
|
|
"""
|
|
Calculate years of service.
|
|
"""
|
|
if self.hire_date:
|
|
end_date = self.termination_date or date.today()
|
|
return (end_date - self.hire_date).days / 365.25
|
|
return 0
|
|
|
|
@property
|
|
def is_license_expired(self):
|
|
"""
|
|
Check if professional license is expired.
|
|
"""
|
|
if self.license_expiry_date:
|
|
return self.license_expiry_date < date.today()
|
|
return False
|
|
|
|
@property
|
|
def full_address(self):
|
|
"""
|
|
Get full address.
|
|
"""
|
|
parts = [
|
|
self.address_line_1,
|
|
self.address_line_2,
|
|
f"{self.city}, {self.state} {self.postal_code}",
|
|
self.country
|
|
]
|
|
return "\n".join([part for part in parts if part])
|
|
|
|
|
|
class Department(models.Model):
|
|
"""
|
|
Department model for organizational structure.
|
|
"""
|
|
|
|
# Tenant relationship
|
|
tenant = models.ForeignKey(
|
|
'core.Tenant',
|
|
on_delete=models.CASCADE,
|
|
related_name='hr_departments',
|
|
help_text='Organization tenant'
|
|
)
|
|
|
|
# Department Information
|
|
department_id = models.UUIDField(
|
|
default=uuid.uuid4,
|
|
unique=True,
|
|
editable=False,
|
|
help_text='Unique department identifier'
|
|
)
|
|
department_code = models.CharField(
|
|
max_length=20,
|
|
help_text='Department code'
|
|
)
|
|
name = models.CharField(
|
|
max_length=100,
|
|
help_text='Department name'
|
|
)
|
|
description = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Department description'
|
|
)
|
|
|
|
# Department Type
|
|
department_type = models.CharField(
|
|
max_length=20,
|
|
choices=[
|
|
('CLINICAL', 'Clinical'),
|
|
('ADMINISTRATIVE', 'Administrative'),
|
|
('SUPPORT', 'Support'),
|
|
('ANCILLARY', 'Ancillary'),
|
|
('EXECUTIVE', 'Executive'),
|
|
],
|
|
help_text='Department type'
|
|
)
|
|
|
|
# Hierarchy
|
|
parent_department = models.ForeignKey(
|
|
'self',
|
|
on_delete=models.CASCADE,
|
|
null=True,
|
|
blank=True,
|
|
related_name='child_departments',
|
|
help_text='Parent department'
|
|
)
|
|
|
|
# Management
|
|
department_head = models.ForeignKey(
|
|
Employee,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='headed_departments',
|
|
help_text='Department head'
|
|
)
|
|
|
|
# Budget Information
|
|
annual_budget = models.DecimalField(
|
|
max_digits=12,
|
|
decimal_places=2,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Annual budget'
|
|
)
|
|
cost_center = models.CharField(
|
|
max_length=20,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Cost center code'
|
|
)
|
|
|
|
# Location Information
|
|
location = models.CharField(
|
|
max_length=100,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Department location'
|
|
)
|
|
|
|
# Department Status
|
|
is_active = models.BooleanField(
|
|
default=True,
|
|
help_text='Department is active'
|
|
)
|
|
|
|
# Notes
|
|
notes = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Department notes'
|
|
)
|
|
|
|
# Metadata
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
created_by = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='created_hr_departments',
|
|
help_text='User who created the department'
|
|
)
|
|
|
|
class Meta:
|
|
db_table = 'hr_department'
|
|
verbose_name = 'Department'
|
|
verbose_name_plural = 'Departments'
|
|
ordering = ['name']
|
|
indexes = [
|
|
models.Index(fields=['tenant', 'department_type']),
|
|
models.Index(fields=['department_code']),
|
|
models.Index(fields=['name']),
|
|
models.Index(fields=['is_active']),
|
|
]
|
|
unique_together = ['tenant', 'department_code']
|
|
|
|
def __str__(self):
|
|
return f"{self.department_code} - {self.name}"
|
|
|
|
@property
|
|
def employee_count(self):
|
|
"""
|
|
Get number of employees in department.
|
|
"""
|
|
return self.employees.filter(employment_status='ACTIVE').count()
|
|
|
|
@property
|
|
def total_fte(self):
|
|
"""
|
|
Calculate total FTE for department.
|
|
"""
|
|
return sum(emp.fte_percentage for emp in self.employees.filter(employment_status='ACTIVE')) / 100
|
|
|
|
|
|
class Schedule(models.Model):
|
|
"""
|
|
Schedule model for employee work schedules.
|
|
"""
|
|
|
|
# Employee relationship
|
|
employee = models.ForeignKey(
|
|
Employee,
|
|
on_delete=models.CASCADE,
|
|
related_name='schedules',
|
|
help_text='Employee'
|
|
)
|
|
|
|
# Schedule Information
|
|
schedule_id = models.UUIDField(
|
|
default=uuid.uuid4,
|
|
unique=True,
|
|
editable=False,
|
|
help_text='Unique schedule identifier'
|
|
)
|
|
name = models.CharField(
|
|
max_length=100,
|
|
help_text='Schedule name'
|
|
)
|
|
description = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Schedule description'
|
|
)
|
|
|
|
# Schedule Type
|
|
schedule_type = models.CharField(
|
|
max_length=20,
|
|
choices=[
|
|
('REGULAR', 'Regular Schedule'),
|
|
('ROTATING', 'Rotating Schedule'),
|
|
('FLEXIBLE', 'Flexible Schedule'),
|
|
('ON_CALL', 'On-Call Schedule'),
|
|
('TEMPORARY', 'Temporary Schedule'),
|
|
],
|
|
help_text='Schedule type'
|
|
)
|
|
|
|
# Schedule Dates
|
|
effective_date = models.DateField(
|
|
help_text='Effective date'
|
|
)
|
|
end_date = models.DateField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='End date'
|
|
)
|
|
|
|
# Schedule Pattern (JSON)
|
|
schedule_pattern = models.JSONField(
|
|
default=dict,
|
|
help_text='Schedule pattern configuration'
|
|
)
|
|
|
|
# Schedule Status
|
|
is_active = models.BooleanField(
|
|
default=True,
|
|
help_text='Schedule is active'
|
|
)
|
|
|
|
# Approval Information
|
|
approved_by = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='approved_schedules',
|
|
help_text='User who approved the schedule'
|
|
)
|
|
approval_date = models.DateTimeField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Approval date and time'
|
|
)
|
|
|
|
# Notes
|
|
notes = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Schedule notes'
|
|
)
|
|
|
|
# Metadata
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
created_by = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='created_schedules',
|
|
help_text='User who created the schedule'
|
|
)
|
|
|
|
class Meta:
|
|
db_table = 'hr_schedule'
|
|
verbose_name = 'Schedule'
|
|
verbose_name_plural = 'Schedules'
|
|
ordering = ['-effective_date']
|
|
indexes = [
|
|
models.Index(fields=['employee', 'effective_date']),
|
|
models.Index(fields=['schedule_type']),
|
|
models.Index(fields=['is_active']),
|
|
]
|
|
|
|
def __str__(self):
|
|
return f"{self.employee.get_full_name()} - {self.name}"
|
|
|
|
@property
|
|
def tenant(self):
|
|
"""
|
|
Get tenant from employee.
|
|
"""
|
|
return self.employee.tenant
|
|
|
|
@property
|
|
def is_current(self):
|
|
"""
|
|
Check if schedule is currently active.
|
|
"""
|
|
today = date.today()
|
|
if self.end_date:
|
|
return self.effective_date <= today <= self.end_date
|
|
return self.effective_date <= today
|
|
|
|
|
|
class ScheduleAssignment(models.Model):
|
|
"""
|
|
Schedule assignment model for specific shift assignments.
|
|
"""
|
|
|
|
# Schedule relationship
|
|
schedule = models.ForeignKey(
|
|
Schedule,
|
|
on_delete=models.CASCADE,
|
|
related_name='assignments',
|
|
help_text='Schedule'
|
|
)
|
|
|
|
# Assignment Information
|
|
assignment_id = models.UUIDField(
|
|
default=uuid.uuid4,
|
|
unique=True,
|
|
editable=False,
|
|
help_text='Unique assignment identifier'
|
|
)
|
|
|
|
# Date and Time
|
|
assignment_date = models.DateField(
|
|
help_text='Assignment date'
|
|
)
|
|
start_time = models.TimeField(
|
|
help_text='Start time'
|
|
)
|
|
end_time = models.TimeField(
|
|
help_text='End time'
|
|
)
|
|
|
|
# Shift Information
|
|
shift_type = models.CharField(
|
|
max_length=20,
|
|
choices=[
|
|
('DAY', 'Day Shift'),
|
|
('EVENING', 'Evening Shift'),
|
|
('NIGHT', 'Night Shift'),
|
|
('WEEKEND', 'Weekend Shift'),
|
|
('HOLIDAY', 'Holiday Shift'),
|
|
('ON_CALL', 'On-Call'),
|
|
('OVERTIME', 'Overtime'),
|
|
],
|
|
help_text='Shift type'
|
|
)
|
|
|
|
# Location Information
|
|
department = models.ForeignKey(
|
|
Department,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='schedule_assignments',
|
|
help_text='Department'
|
|
)
|
|
location = models.CharField(
|
|
max_length=100,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Specific location'
|
|
)
|
|
|
|
# Assignment Status
|
|
status = models.CharField(
|
|
max_length=20,
|
|
choices=[
|
|
('SCHEDULED', 'Scheduled'),
|
|
('CONFIRMED', 'Confirmed'),
|
|
('COMPLETED', 'Completed'),
|
|
('CANCELLED', 'Cancelled'),
|
|
('NO_SHOW', 'No Show'),
|
|
],
|
|
default='SCHEDULED',
|
|
help_text='Assignment status'
|
|
)
|
|
|
|
# Break Information
|
|
break_minutes = models.PositiveIntegerField(
|
|
default=0,
|
|
help_text='Break time in minutes'
|
|
)
|
|
lunch_minutes = models.PositiveIntegerField(
|
|
default=0,
|
|
help_text='Lunch time in minutes'
|
|
)
|
|
|
|
# Notes
|
|
notes = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Assignment notes'
|
|
)
|
|
|
|
# Metadata
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
db_table = 'hr_schedule_assignment'
|
|
verbose_name = 'Schedule Assignment'
|
|
verbose_name_plural = 'Schedule Assignments'
|
|
ordering = ['assignment_date', 'start_time']
|
|
indexes = [
|
|
models.Index(fields=['schedule', 'assignment_date']),
|
|
models.Index(fields=['assignment_date']),
|
|
models.Index(fields=['status']),
|
|
]
|
|
|
|
def __str__(self):
|
|
return f"{self.schedule.employee.get_full_name()} - {self.assignment_date} {self.start_time}-{self.end_time}"
|
|
|
|
@property
|
|
def tenant(self):
|
|
"""
|
|
Get tenant from schedule.
|
|
"""
|
|
return self.schedule.tenant
|
|
|
|
@property
|
|
def employee(self):
|
|
"""
|
|
Get employee from schedule.
|
|
"""
|
|
return self.schedule.employee
|
|
|
|
@property
|
|
def total_hours(self):
|
|
"""
|
|
Calculate total hours for assignment.
|
|
"""
|
|
start_datetime = datetime.combine(self.assignment_date, self.start_time)
|
|
end_datetime = datetime.combine(self.assignment_date, self.end_time)
|
|
|
|
# Handle overnight shifts
|
|
if self.end_time < self.start_time:
|
|
end_datetime += timedelta(days=1)
|
|
|
|
total_minutes = (end_datetime - start_datetime).total_seconds() / 60
|
|
total_minutes -= (self.break_minutes + self.lunch_minutes)
|
|
|
|
return total_minutes / 60
|
|
|
|
|
|
class TimeEntry(models.Model):
|
|
"""
|
|
Time entry model for tracking actual work hours.
|
|
"""
|
|
|
|
# Employee relationship
|
|
employee = models.ForeignKey(
|
|
Employee,
|
|
on_delete=models.CASCADE,
|
|
related_name='time_entries',
|
|
help_text='Employee'
|
|
)
|
|
|
|
# Time Entry Information
|
|
entry_id = models.UUIDField(
|
|
default=uuid.uuid4,
|
|
unique=True,
|
|
editable=False,
|
|
help_text='Unique time entry identifier'
|
|
)
|
|
|
|
# Date and Time
|
|
work_date = models.DateField(
|
|
help_text='Work date'
|
|
)
|
|
clock_in_time = models.DateTimeField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Clock in time'
|
|
)
|
|
clock_out_time = models.DateTimeField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Clock out time'
|
|
)
|
|
|
|
# Break Times
|
|
break_start_time = models.DateTimeField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Break start time'
|
|
)
|
|
break_end_time = models.DateTimeField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Break end time'
|
|
)
|
|
lunch_start_time = models.DateTimeField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Lunch start time'
|
|
)
|
|
lunch_end_time = models.DateTimeField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Lunch end time'
|
|
)
|
|
|
|
# Hours Information
|
|
regular_hours = models.DecimalField(
|
|
max_digits=5,
|
|
decimal_places=2,
|
|
default=Decimal('0.00'),
|
|
help_text='Regular hours worked'
|
|
)
|
|
overtime_hours = models.DecimalField(
|
|
max_digits=5,
|
|
decimal_places=2,
|
|
default=Decimal('0.00'),
|
|
help_text='Overtime hours worked'
|
|
)
|
|
total_hours = models.DecimalField(
|
|
max_digits=5,
|
|
decimal_places=2,
|
|
default=Decimal('0.00'),
|
|
help_text='Total hours worked'
|
|
)
|
|
|
|
# Entry Type
|
|
entry_type = models.CharField(
|
|
max_length=20,
|
|
choices=[
|
|
('REGULAR', 'Regular Time'),
|
|
('OVERTIME', 'Overtime'),
|
|
('HOLIDAY', 'Holiday'),
|
|
('VACATION', 'Vacation'),
|
|
('SICK', 'Sick Leave'),
|
|
('PERSONAL', 'Personal Time'),
|
|
('BEREAVEMENT', 'Bereavement'),
|
|
('JURY_DUTY', 'Jury Duty'),
|
|
('TRAINING', 'Training'),
|
|
],
|
|
default='REGULAR',
|
|
help_text='Entry type'
|
|
)
|
|
|
|
# Department and Location
|
|
department = models.ForeignKey(
|
|
Department,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='time_entries',
|
|
help_text='Department'
|
|
)
|
|
location = models.CharField(
|
|
max_length=100,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Work location'
|
|
)
|
|
|
|
# Approval Information
|
|
approved_by = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='approved_time_entries',
|
|
help_text='User who approved the time entry'
|
|
)
|
|
approval_date = models.DateTimeField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Approval date and time'
|
|
)
|
|
|
|
# Entry Status
|
|
status = models.CharField(
|
|
max_length=20,
|
|
choices=[
|
|
('DRAFT', 'Draft'),
|
|
('SUBMITTED', 'Submitted'),
|
|
('APPROVED', 'Approved'),
|
|
('REJECTED', 'Rejected'),
|
|
('PAID', 'Paid'),
|
|
],
|
|
default='DRAFT',
|
|
help_text='Entry status'
|
|
)
|
|
|
|
# Notes
|
|
notes = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Time entry notes'
|
|
)
|
|
|
|
# Metadata
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
db_table = 'hr_time_entry'
|
|
verbose_name = 'Time Entry'
|
|
verbose_name_plural = 'Time Entries'
|
|
ordering = ['-work_date']
|
|
indexes = [
|
|
models.Index(fields=['employee', 'work_date']),
|
|
models.Index(fields=['work_date']),
|
|
models.Index(fields=['status']),
|
|
models.Index(fields=['entry_type']),
|
|
]
|
|
|
|
def __str__(self):
|
|
return f"{self.employee.get_full_name()} - {self.work_date}"
|
|
|
|
def save(self, *args, **kwargs):
|
|
"""
|
|
Calculate hours automatically.
|
|
"""
|
|
if self.clock_in_time and self.clock_out_time:
|
|
# Calculate total time worked
|
|
total_minutes = (self.clock_out_time - self.clock_in_time).total_seconds() / 60
|
|
|
|
# Subtract break time
|
|
if self.break_start_time and self.break_end_time:
|
|
break_minutes = (self.break_end_time - self.break_start_time).total_seconds() / 60
|
|
total_minutes -= break_minutes
|
|
|
|
# Subtract lunch time
|
|
if self.lunch_start_time and self.lunch_end_time:
|
|
lunch_minutes = (self.lunch_end_time - self.lunch_start_time).total_seconds() / 60
|
|
total_minutes -= lunch_minutes
|
|
|
|
self.total_hours = Decimal(str(total_minutes / 60))
|
|
|
|
# Calculate regular vs overtime hours (assuming 8 hours is regular)
|
|
if self.total_hours <= 8:
|
|
self.regular_hours = self.total_hours
|
|
self.overtime_hours = Decimal('0.00')
|
|
else:
|
|
self.regular_hours = Decimal('8.00')
|
|
self.overtime_hours = self.total_hours - Decimal('8.00')
|
|
|
|
super().save(*args, **kwargs)
|
|
|
|
@property
|
|
def tenant(self):
|
|
"""
|
|
Get tenant from employee.
|
|
"""
|
|
return self.employee.tenant
|
|
|
|
@property
|
|
def is_approved(self):
|
|
"""
|
|
Check if time entry is approved.
|
|
"""
|
|
return self.status == 'APPROVED'
|
|
|
|
|
|
class PerformanceReview(models.Model):
|
|
"""
|
|
Performance review model for employee evaluations.
|
|
"""
|
|
|
|
# Employee relationship
|
|
employee = models.ForeignKey(
|
|
Employee,
|
|
on_delete=models.CASCADE,
|
|
related_name='performance_reviews',
|
|
help_text='Employee being reviewed'
|
|
)
|
|
|
|
# Review Information
|
|
review_id = models.UUIDField(
|
|
default=uuid.uuid4,
|
|
unique=True,
|
|
editable=False,
|
|
help_text='Unique review identifier'
|
|
)
|
|
|
|
# Review Period
|
|
review_period_start = models.DateField(
|
|
help_text='Review period start date'
|
|
)
|
|
review_period_end = models.DateField(
|
|
help_text='Review period end date'
|
|
)
|
|
review_date = models.DateField(
|
|
help_text='Review date'
|
|
)
|
|
|
|
# Review Type
|
|
review_type = models.CharField(
|
|
max_length=20,
|
|
choices=[
|
|
('ANNUAL', 'Annual Review'),
|
|
('PROBATIONARY', 'Probationary Review'),
|
|
('MID_YEAR', 'Mid-Year Review'),
|
|
('PROJECT', 'Project Review'),
|
|
('DISCIPLINARY', 'Disciplinary Review'),
|
|
('PROMOTION', 'Promotion Review'),
|
|
],
|
|
help_text='Review type'
|
|
)
|
|
|
|
# Reviewer Information
|
|
reviewer = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='conducted_reviews',
|
|
help_text='Reviewer'
|
|
)
|
|
|
|
# Overall Rating
|
|
overall_rating = models.DecimalField(
|
|
max_digits=3,
|
|
decimal_places=1,
|
|
validators=[MinValueValidator(1), MaxValueValidator(5)],
|
|
help_text='Overall rating (1-5)'
|
|
)
|
|
|
|
# Competency Ratings (JSON)
|
|
competency_ratings = models.JSONField(
|
|
default=dict,
|
|
help_text='Individual competency ratings'
|
|
)
|
|
|
|
# Goals and Objectives
|
|
goals_achieved = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Goals achieved during review period'
|
|
)
|
|
goals_not_achieved = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Goals not achieved during review period'
|
|
)
|
|
future_goals = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Goals for next review period'
|
|
)
|
|
|
|
# Strengths and Areas for Improvement
|
|
strengths = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Employee strengths'
|
|
)
|
|
areas_for_improvement = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Areas for improvement'
|
|
)
|
|
|
|
# Development Plan
|
|
development_plan = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Professional development plan'
|
|
)
|
|
training_recommendations = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Training recommendations'
|
|
)
|
|
|
|
# Employee Comments
|
|
employee_comments = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Employee comments'
|
|
)
|
|
employee_signature_date = models.DateField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Employee signature date'
|
|
)
|
|
|
|
# Review Status
|
|
status = models.CharField(
|
|
max_length=20,
|
|
choices=[
|
|
('DRAFT', 'Draft'),
|
|
('IN_PROGRESS', 'In Progress'),
|
|
('COMPLETED', 'Completed'),
|
|
('ACKNOWLEDGED', 'Acknowledged by Employee'),
|
|
('DISPUTED', 'Disputed'),
|
|
],
|
|
default='DRAFT',
|
|
help_text='Review status'
|
|
)
|
|
|
|
# Notes
|
|
notes = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Additional notes'
|
|
)
|
|
|
|
# Metadata
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
db_table = 'hr_performance_review'
|
|
verbose_name = 'Performance Review'
|
|
verbose_name_plural = 'Performance Reviews'
|
|
ordering = ['-review_date']
|
|
indexes = [
|
|
models.Index(fields=['employee', 'review_date']),
|
|
models.Index(fields=['review_type']),
|
|
models.Index(fields=['status']),
|
|
models.Index(fields=['overall_rating']),
|
|
]
|
|
|
|
def __str__(self):
|
|
return f"{self.employee.get_full_name()} - {self.review_type} ({self.review_date})"
|
|
|
|
@property
|
|
def tenant(self):
|
|
"""
|
|
Get tenant from employee.
|
|
"""
|
|
return self.employee.tenant
|
|
|
|
@property
|
|
def is_overdue(self):
|
|
"""
|
|
Check if review is overdue.
|
|
"""
|
|
if self.status in ['DRAFT', 'IN_PROGRESS']:
|
|
return self.review_date < date.today()
|
|
return False
|
|
|
|
|
|
class TrainingRecord(models.Model):
|
|
"""
|
|
Training record model for employee training and certifications.
|
|
"""
|
|
|
|
# Employee relationship
|
|
employee = models.ForeignKey(
|
|
Employee,
|
|
on_delete=models.CASCADE,
|
|
related_name='training_records',
|
|
help_text='Employee'
|
|
)
|
|
|
|
# Training Information
|
|
record_id = models.UUIDField(
|
|
default=uuid.uuid4,
|
|
unique=True,
|
|
editable=False,
|
|
help_text='Unique training record identifier'
|
|
)
|
|
training_name = models.CharField(
|
|
max_length=200,
|
|
help_text='Training name'
|
|
)
|
|
training_description = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Training description'
|
|
)
|
|
|
|
# Training Type
|
|
training_type = models.CharField(
|
|
max_length=20,
|
|
choices=[
|
|
('ORIENTATION', 'Orientation'),
|
|
('MANDATORY', 'Mandatory Training'),
|
|
('CONTINUING_ED', 'Continuing Education'),
|
|
('CERTIFICATION', 'Certification'),
|
|
('SKILLS', 'Skills Training'),
|
|
('SAFETY', 'Safety Training'),
|
|
('COMPLIANCE', 'Compliance Training'),
|
|
('LEADERSHIP', 'Leadership Development'),
|
|
('TECHNICAL', 'Technical Training'),
|
|
('OTHER', 'Other'),
|
|
],
|
|
help_text='Training type'
|
|
)
|
|
|
|
# Training Provider
|
|
training_provider = models.CharField(
|
|
max_length=200,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Training provider'
|
|
)
|
|
instructor = models.CharField(
|
|
max_length=100,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Instructor name'
|
|
)
|
|
|
|
# Training Dates
|
|
training_date = models.DateField(
|
|
help_text='Training date'
|
|
)
|
|
completion_date = models.DateField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Completion date'
|
|
)
|
|
expiry_date = models.DateField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Certification expiry date'
|
|
)
|
|
|
|
# Training Details
|
|
duration_hours = models.DecimalField(
|
|
max_digits=5,
|
|
decimal_places=2,
|
|
default=Decimal('0.00'),
|
|
help_text='Training duration in hours'
|
|
)
|
|
credits_earned = models.DecimalField(
|
|
max_digits=5,
|
|
decimal_places=2,
|
|
default=Decimal('0.00'),
|
|
help_text='Credits earned'
|
|
)
|
|
|
|
# Training Status
|
|
status = models.CharField(
|
|
max_length=20,
|
|
choices=[
|
|
('SCHEDULED', 'Scheduled'),
|
|
('IN_PROGRESS', 'In Progress'),
|
|
('COMPLETED', 'Completed'),
|
|
('CANCELLED', 'Cancelled'),
|
|
('NO_SHOW', 'No Show'),
|
|
('FAILED', 'Failed'),
|
|
],
|
|
default='SCHEDULED',
|
|
help_text='Training status'
|
|
)
|
|
|
|
# Results
|
|
score = models.DecimalField(
|
|
max_digits=5,
|
|
decimal_places=2,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Training score/grade'
|
|
)
|
|
passed = models.BooleanField(
|
|
default=False,
|
|
help_text='Training passed'
|
|
)
|
|
|
|
# Certification Information
|
|
certificate_number = models.CharField(
|
|
max_length=50,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Certificate number'
|
|
)
|
|
certification_body = models.CharField(
|
|
max_length=200,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Certification body'
|
|
)
|
|
|
|
# Cost Information
|
|
training_cost = models.DecimalField(
|
|
max_digits=10,
|
|
decimal_places=2,
|
|
default=Decimal('0.00'),
|
|
help_text='Training cost'
|
|
)
|
|
|
|
# Notes
|
|
notes = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Training notes'
|
|
)
|
|
|
|
# Metadata
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
created_by = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='created_training_records',
|
|
help_text='User who created the training record'
|
|
)
|
|
|
|
class Meta:
|
|
db_table = 'hr_training_record'
|
|
verbose_name = 'Training Record'
|
|
verbose_name_plural = 'Training Records'
|
|
ordering = ['-training_date']
|
|
indexes = [
|
|
models.Index(fields=['employee', 'training_date']),
|
|
models.Index(fields=['training_type']),
|
|
models.Index(fields=['status']),
|
|
models.Index(fields=['expiry_date']),
|
|
]
|
|
|
|
def __str__(self):
|
|
return f"{self.employee.get_full_name()} - {self.training_name}"
|
|
|
|
@property
|
|
def tenant(self):
|
|
"""
|
|
Get tenant from employee.
|
|
"""
|
|
return self.employee.tenant
|
|
|
|
@property
|
|
def is_expired(self):
|
|
"""
|
|
Check if certification is expired.
|
|
"""
|
|
if self.expiry_date:
|
|
return self.expiry_date < date.today()
|
|
return False
|
|
|
|
@property
|
|
def days_to_expiry(self):
|
|
"""
|
|
Calculate days to expiry.
|
|
"""
|
|
if self.expiry_date:
|
|
return (self.expiry_date - date.today()).days
|
|
return None
|
|
|
|
@property
|
|
def is_due_for_renewal(self):
|
|
"""
|
|
Check if certification is due for renewal (within 30 days).
|
|
"""
|
|
if self.expiry_date:
|
|
return (self.expiry_date - date.today()).days <= 30
|
|
return False
|
|
|