2237 lines
70 KiB
Python
2237 lines
70 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.db.models import TextChoices
|
||
from django.utils import timezone
|
||
from django.conf import settings
|
||
from datetime import timedelta, datetime, date, time
|
||
from decimal import Decimal
|
||
import json
|
||
from django.core.exceptions import ValidationError
|
||
|
||
|
||
class Employee(models.Model):
|
||
"""
|
||
Employee profile for all non-auth user data.
|
||
Auto-created via accounts.signals when a User is created.
|
||
"""
|
||
class MaritalStatus(models.TextChoices):
|
||
SINGLE = 'SINGLE', 'Single'
|
||
MARRIED = 'MARRIED', 'Married'
|
||
DIVORCED = 'DIVORCED', 'Divorced'
|
||
WIDOWED = 'WIDOWED', 'Widowed'
|
||
SEPARATED = 'SEPARATED', 'Separated'
|
||
OTHER = 'OTHER', 'Other'
|
||
|
||
|
||
class EmploymentType(models.TextChoices):
|
||
FULL_TIME = 'FULL_TIME', 'Full Time'
|
||
PART_TIME = 'PART_TIME', 'Part Time'
|
||
CONTRACT = 'CONTRACT', 'Contract'
|
||
TEMPORARY = 'TEMPORARY', 'Temporary'
|
||
INTERN = 'INTERN', 'Intern'
|
||
VOLUNTEER = 'VOLUNTEER', 'Volunteer'
|
||
PER_DIEM = 'PER_DIEM', 'Per Diem'
|
||
CONSULTANT = 'CONSULTANT', 'Consultant'
|
||
|
||
class EmploymentStatus(models.TextChoices):
|
||
ACTIVE = 'ACTIVE', 'Active'
|
||
INACTIVE = 'INACTIVE', 'Inactive'
|
||
TERMINATED = 'TERMINATED', 'Terminated'
|
||
SUSPENDED = 'SUSPENDED', 'Suspended'
|
||
LEAVE = 'LEAVE', 'On Leave'
|
||
RETIRED = 'RETIRED', 'Retired'
|
||
|
||
class Gender(models.TextChoices):
|
||
MALE = 'MALE', 'Male'
|
||
FEMALE = 'FEMALE', 'Female'
|
||
OTHER = 'OTHER', 'Other'
|
||
|
||
class Role(models.TextChoices):
|
||
SUPER_ADMIN = 'SUPER_ADMIN', 'Super Administrator'
|
||
ADMIN = 'ADMIN', 'Administrator'
|
||
PHYSICIAN = 'PHYSICIAN', 'Physician'
|
||
SURGEON = 'SURGEON', 'Surgeon'
|
||
NURSE = 'NURSE', 'Nurse'
|
||
NURSE_PRACTITIONER = 'NURSE_PRACTITIONER', 'Nurse Practitioner'
|
||
PHYSICIAN_ASSISTANT = 'PHYSICIAN_ASSISTANT', 'Physician Assistant'
|
||
SURGICAL_TECHNICIAN = 'SURGICAL_TECHNICIAN', 'Surgical Technician'
|
||
ANESTHESIOLOGIST = 'ANESTHESIOLOGIST', 'Anesthesiologist'
|
||
ANESTHESIOLOGIST_ASSOCIATE = 'ANESTHESIOLOGIST_ASSOCIATE', 'Anesthesiologist Associate'
|
||
CLINICAL_NURSE_ASSOCIATE = 'CLINICAL_NURSE_ASSOCIATE', 'Clinical Nurse Associate'
|
||
CLINICAL_NURSE_SPECIALIST = 'CLINICAL_NURSE_SPECIALIST', 'Clinical Nurse Specialist'
|
||
CLINICAL_NURSE_MANAGER = 'CLINICAL_NURSE_MANAGER', 'Clinical Nurse Manager'
|
||
CLINICAL_NURSE_TECHNICIAN = 'CLINICAL_NURSE_TECHNICIAN', 'Clinical Nurse Technician'
|
||
CLINICAL_NURSE_COORDINATOR = 'CLINICAL_NURSE_COORDINATOR', 'Clinical Nurse Coordinator'
|
||
FELLOW = 'FELLOW', 'Fellow'
|
||
INTERN = 'INTERN', 'Intern'
|
||
INTERNSHIP = 'INTERNSHIP', 'Internship'
|
||
RESIDENT = 'RESIDENT', 'Resident'
|
||
WORK_FROM_HOME = 'WORK_FROM_HOME', 'Work from Home'
|
||
WORK_FROM_HOME_PART_TIME = 'WORK_FROM_HOME_PART_TIME', 'Work from Home Part-time'
|
||
PHARMACIST = 'PHARMACIST', 'Pharmacist'
|
||
PHARMACY_TECH = 'PHARMACY_TECH', 'Pharmacy Technician'
|
||
LAB_TECH = 'LAB_TECH', 'Laboratory Technician'
|
||
RADIOLOGIST = 'RADIOLOGIST', 'Radiologist'
|
||
RAD_TECH = 'RAD_TECH', 'Radiology Technician'
|
||
THERAPIST = 'THERAPIST', 'Therapist'
|
||
SOCIAL_WORKER = 'SOCIAL_WORKER', 'Social Worker'
|
||
CASE_MANAGER = 'CASE_MANAGER', 'Case Manager'
|
||
BILLING_SPECIALIST = 'BILLING_SPECIALIST', 'Billing Specialist'
|
||
REGISTRATION = 'REGISTRATION', 'Registration Staff'
|
||
SCHEDULER = 'SCHEDULER', 'Scheduler'
|
||
MEDICAL_ASSISTANT = 'MEDICAL_ASSISTANT', 'Medical Assistant'
|
||
CLERICAL = 'CLERICAL', 'Clerical Staff'
|
||
IT_SUPPORT = 'IT_SUPPORT', 'IT Support'
|
||
QUALITY_ASSURANCE = 'QUALITY_ASSURANCE', 'Quality Assurance'
|
||
COMPLIANCE = 'COMPLIANCE', 'Compliance Officer'
|
||
SECURITY = 'SECURITY', 'Security'
|
||
MAINTENANCE = 'MAINTENANCE', 'Maintenance'
|
||
VOLUNTEER = 'VOLUNTEER', 'Volunteer'
|
||
STUDENT = 'STUDENT', 'Student'
|
||
RESEARCHER = 'RESEARCHER', 'Researcher'
|
||
CONSULTANT = 'CONSULTANT', 'Consultant'
|
||
VENDOR = 'VENDOR', 'Vendor'
|
||
GUEST = 'GUEST', 'Guest'
|
||
|
||
class Theme(models.TextChoices):
|
||
LIGHT = 'LIGHT', 'Light'
|
||
DARK = 'DARK', 'Dark'
|
||
AUTO = 'AUTO', 'Auto'
|
||
|
||
|
||
|
||
tenant = models.ForeignKey(
|
||
'core.Tenant',
|
||
on_delete=models.CASCADE,
|
||
related_name='employees',
|
||
help_text='Organization tenant'
|
||
)
|
||
|
||
user = models.OneToOneField(
|
||
settings.AUTH_USER_MODEL,
|
||
on_delete=models.CASCADE,
|
||
related_name='employee_profile',
|
||
help_text='Associated user account'
|
||
)
|
||
employee_id = models.UUIDField(default=uuid.uuid4, unique=True, editable=False)
|
||
employee_number = models.CharField(max_length=20, help_text='Employee number')
|
||
id_number = models.CharField(max_length=20, help_text='ID number')
|
||
first_name = models.CharField(max_length=50, help_text='First name', blank=True, default='')
|
||
last_name = models.CharField(max_length=50, help_text='Last name', blank=True, default='')
|
||
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 (moved from User)
|
||
e164_ksa_regex = RegexValidator(
|
||
regex=r'^\+?9665\d{8}$',
|
||
message='Use E.164 format: +9665XXXXXXXX'
|
||
)
|
||
email = models.EmailField(blank=True, null=True, help_text='Work email')
|
||
phone = models.CharField(max_length=16, blank=True, null=True, validators=[e164_ksa_regex], help_text='Primary phone')
|
||
mobile_phone = models.CharField(max_length=16, blank=True, null=True, validators=[e164_ksa_regex], help_text='Mobile phone')
|
||
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')
|
||
postal_code = models.CharField(max_length=10, blank=True, null=True, help_text='Postal code')
|
||
country = models.CharField(max_length=50, blank=True, null=True, help_text='Country')
|
||
|
||
date_of_birth = models.DateField(blank=True, null=True, help_text='Date of birth')
|
||
gender = models.CharField(max_length=20, choices=Gender.choices, blank=True, null=True, help_text='Gender')
|
||
marital_status = models.CharField(max_length=20, choices=MaritalStatus.choices, blank=True, null=True, help_text='Marital status')
|
||
|
||
user_timezone = models.CharField(max_length=50, default='Asia/Riyadh', help_text='Timezone')
|
||
language = models.CharField(max_length=10, default='en', help_text='Preferred language')
|
||
theme = models.CharField(max_length=20, choices=Theme.choices, default=Theme.LIGHT)
|
||
role = models.CharField(max_length=50, choices=Role.choices, default=Role.GUEST)
|
||
|
||
department = models.ForeignKey(
|
||
'hr.Department',
|
||
on_delete=models.SET_NULL,
|
||
null=True,
|
||
blank=True,
|
||
related_name='employees',
|
||
help_text='Department'
|
||
)
|
||
job_title = models.CharField(max_length=100, blank=True, null=True, help_text='Job title')
|
||
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')
|
||
license_state = models.CharField(max_length=50, blank=True, null=True, help_text='Issuing state/authority')
|
||
dea_number = models.CharField(max_length=20, blank=True, null=True, help_text='DEA number (if applicable)')
|
||
npi_number = models.CharField(max_length=10, blank=True, null=True, help_text='NPI (if applicable)')
|
||
employment_status = models.CharField(
|
||
max_length=20,
|
||
choices=EmploymentStatus.choices,
|
||
default=EmploymentStatus.ACTIVE,
|
||
help_text='Employment status'
|
||
)
|
||
employment_type = models.CharField(max_length=20, choices=EmploymentType.choices, blank=True, null=True, help_text='Employment type')
|
||
hire_date = models.DateField(blank=True, null=True, help_text='Hire date')
|
||
termination_date = models.DateField(blank=True, null=True, help_text='Termination date')
|
||
supervisor = models.ForeignKey(
|
||
'self',
|
||
on_delete=models.SET_NULL,
|
||
null=True,
|
||
blank=True,
|
||
related_name='direct_reports',
|
||
help_text='Direct supervisor'
|
||
)
|
||
hourly_rate = models.DecimalField(
|
||
max_digits=10,
|
||
decimal_places=2,
|
||
blank=True,
|
||
null=True,
|
||
help_text='Hourly rate'
|
||
)
|
||
standard_hours_per_week = models.DecimalField(
|
||
max_digits=5,
|
||
decimal_places=2,
|
||
default=Decimal('40.00'),
|
||
help_text='Standard hours per week'
|
||
)
|
||
annual_salary = models.DecimalField(
|
||
max_digits=12,
|
||
decimal_places=2,
|
||
blank=True,
|
||
null=True,
|
||
help_text='Annual salary'
|
||
)
|
||
fte_percentage = models.DecimalField(
|
||
max_digits=5,
|
||
decimal_places=2,
|
||
default=Decimal('100.00'),
|
||
validators=[MinValueValidator(0), MaxValueValidator(100)],
|
||
help_text='FTE percentage'
|
||
)
|
||
|
||
profile_picture = models.ImageField(
|
||
upload_to='profile_pictures/',
|
||
blank=True,
|
||
null=True,
|
||
help_text='Profile picture'
|
||
)
|
||
bio = models.TextField(blank=True, null=True, help_text='Professional bio')
|
||
|
||
# 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 = models.TextField(blank=True, null=True, help_text='Employee notes')
|
||
is_verified = models.BooleanField(default=False, help_text='Account verified')
|
||
is_approved = models.BooleanField(default=False, help_text='Account approved')
|
||
approval_date = models.DateTimeField(blank=True, null=True, help_text='Approval date')
|
||
approved_by = models.ForeignKey(
|
||
settings.AUTH_USER_MODEL,
|
||
on_delete=models.SET_NULL,
|
||
null=True,
|
||
blank=True,
|
||
related_name='approved_employees',
|
||
help_text='User who approved this employee'
|
||
)
|
||
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', 'employee_number']),
|
||
models.Index(fields=['tenant', 'role']),
|
||
models.Index(fields=['tenant', 'department']),
|
||
models.Index(fields=['tenant', 'employment_status']),
|
||
]
|
||
|
||
def __str__(self):
|
||
return f"{self.employee_number} - {self.get_display_name()}"
|
||
|
||
# ---- Convenience ----
|
||
def get_full_name(self):
|
||
if self.middle_name:
|
||
return f"{self.first_name} {self.middle_name} {self.last_name}".strip()
|
||
return f"{self.first_name} {self.last_name}".strip()
|
||
|
||
def get_display_name(self):
|
||
return f"{self.preferred_name} {self.last_name}".strip() if self.preferred_name else 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):
|
||
return bool(self.license_expiry_date and self.license_expiry_date < date.today())
|
||
|
||
def clean(self):
|
||
# Ensure tenant alignment: Employee.tenant MUST match User.tenant
|
||
if self.user_id and self.tenant_id and self.user.tenant_id != self.tenant_id:
|
||
raise ValidationError({'tenant': 'Employee.tenant must match User.tenant.'})
|
||
# Dates sanity
|
||
if self.termination_date and self.hire_date and self.termination_date < self.hire_date:
|
||
raise ValidationError({'termination_date': 'Termination date cannot be before hire date.'})
|
||
|
||
|
||
class Department(models.Model):
|
||
"""
|
||
Department model for organizational structure.
|
||
"""
|
||
|
||
class DepartmentType(models.TextChoices):
|
||
CLINICAL = 'CLINICAL', 'Clinical'
|
||
ADMINISTRATIVE = 'ADMINISTRATIVE', 'Administrative'
|
||
SUPPORT = 'SUPPORT', 'Support'
|
||
ANCILLARY = 'ANCILLARY', 'Ancillary'
|
||
EXECUTIVE = 'EXECUTIVE', 'Executive'
|
||
|
||
# Tenant relationship
|
||
tenant = models.ForeignKey(
|
||
'core.Tenant',
|
||
on_delete=models.CASCADE,
|
||
related_name='departments',
|
||
help_text='Organization tenant'
|
||
)
|
||
|
||
# Department Information
|
||
department_id = models.UUIDField(
|
||
default=uuid.uuid4,
|
||
unique=True,
|
||
editable=False,
|
||
help_text='Unique department identifier'
|
||
)
|
||
code = models.CharField(
|
||
max_length=20,
|
||
help_text='Department code (e.g., CARD, EMER, SURG)'
|
||
)
|
||
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=DepartmentType.choices,
|
||
help_text='Department type'
|
||
)
|
||
|
||
# Hierarchy
|
||
parent_department = models.ForeignKey(
|
||
'self',
|
||
on_delete=models.CASCADE,
|
||
null=True,
|
||
blank=True,
|
||
related_name='sub_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'
|
||
)
|
||
# Contact Information
|
||
phone = models.CharField(
|
||
max_length=20,
|
||
blank=True,
|
||
null=True,
|
||
help_text='Department phone number'
|
||
)
|
||
extension = models.CharField(
|
||
max_length=10,
|
||
blank=True,
|
||
null=True,
|
||
help_text='Phone extension'
|
||
)
|
||
email = models.EmailField(
|
||
blank=True,
|
||
null=True,
|
||
help_text='Department email'
|
||
)
|
||
|
||
# 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'
|
||
)
|
||
authorized_positions = models.PositiveIntegerField(
|
||
default=0,
|
||
help_text='Number of authorized positions'
|
||
)
|
||
# Location Information
|
||
location = models.CharField(
|
||
max_length=100,
|
||
blank=True,
|
||
null=True,
|
||
help_text='Department location'
|
||
)
|
||
|
||
# Operational Information
|
||
is_active = models.BooleanField(
|
||
default=True,
|
||
help_text='Department is active'
|
||
)
|
||
is_24_hour = models.BooleanField(
|
||
default=False,
|
||
help_text='Department operates 24 hours'
|
||
)
|
||
operating_hours = models.JSONField(
|
||
default=dict,
|
||
blank=True,
|
||
help_text='Operating hours by day of week'
|
||
)
|
||
|
||
# Quality and Compliance
|
||
accreditation_required = models.BooleanField(
|
||
default=False,
|
||
help_text='Department requires special accreditation'
|
||
)
|
||
accreditation_body = models.CharField(
|
||
max_length=100,
|
||
blank=True,
|
||
null=True,
|
||
help_text='Accrediting body (e.g., Joint Commission, CAP)'
|
||
)
|
||
last_inspection_date = models.DateField(
|
||
blank=True,
|
||
null=True,
|
||
help_text='Last inspection date'
|
||
)
|
||
next_inspection_date = models.DateField(
|
||
blank=True,
|
||
null=True,
|
||
help_text='Next scheduled inspection date'
|
||
)
|
||
|
||
# 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=['code']),
|
||
models.Index(fields=['name']),
|
||
models.Index(fields=['is_active']),
|
||
]
|
||
unique_together = ['tenant', 'code']
|
||
|
||
def __str__(self):
|
||
return f"{self.code} - {self.name}"
|
||
|
||
@property
|
||
def full_name(self):
|
||
"""Return full department name with parent if applicable"""
|
||
if self.parent_department:
|
||
return f"{self.parent_department.name} - {self.name}"
|
||
return 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
|
||
|
||
@property
|
||
def staffing_percentage(self):
|
||
"""Calculate current staffing percentage"""
|
||
if self.authorized_positions > 0:
|
||
return (self.employee_count / self.authorized_positions) * 100
|
||
return 0
|
||
|
||
def get_all_sub_departments(self):
|
||
"""Get all sub-departments recursively"""
|
||
sub_departments = []
|
||
for sub_dept in self.sub_departments.all():
|
||
sub_departments.append(sub_dept)
|
||
sub_departments.extend(sub_dept.get_all_sub_departments())
|
||
return sub_departments
|
||
|
||
|
||
class Schedule(models.Model):
|
||
"""
|
||
Schedule model for employee work schedules.
|
||
"""
|
||
|
||
class ScheduleType(models.TextChoices):
|
||
REGULAR = 'REGULAR', 'Regular'
|
||
ROTATING = 'ROTATING', 'Rotating'
|
||
FLEXIBLE = 'FLEXIBLE', 'Flexible'
|
||
ON_CALL = 'ON_CALL', 'On-Call'
|
||
TEMPORARY = 'TEMPORARY', 'Temporary'
|
||
|
||
# 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=ScheduleType.choices,
|
||
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.
|
||
"""
|
||
|
||
class ShiftType(models.TextChoices):
|
||
DAY = 'DAY', 'Day Shift'
|
||
EVENING = 'EVENING', 'Evening Shift'
|
||
NIGHT = 'NIGHT', 'Night Shift'
|
||
WEEKEND = 'WEEKEND', 'Weekend Shift'
|
||
HOLIDAY = 'HOLIDAY', 'Holiday Shift'
|
||
ON_CALL = 'ON_CALL', 'On-Call'
|
||
OVERTIME = 'OVERTIME', 'Overtime'
|
||
|
||
class ShiftStatus(models.TextChoices):
|
||
SCHEDULED = 'SCHEDULED', 'Scheduled'
|
||
CONFIRMED = 'CONFIRMED', 'Confirmed'
|
||
COMPLETED = 'COMPLETED', 'Completed'
|
||
CANCELLED = 'CANCELLED', 'Cancelled'
|
||
NO_SHOW = 'NO_SHOW', 'No Show'
|
||
|
||
|
||
# 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=ShiftType.choices,
|
||
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=ShiftStatus.choices,
|
||
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.
|
||
"""
|
||
|
||
class EntryStatus(models.TextChoices):
|
||
DRAFT = 'DRAFT', 'Draft'
|
||
SUBMITTED = 'SUBMITTED', 'Submitted'
|
||
APPROVED = 'APPROVED', 'Approved'
|
||
REJECTED = 'REJECTED', 'Rejected'
|
||
PAID = 'PAID', 'Paid'
|
||
|
||
class EntryType(models.TextChoices):
|
||
REGULAR = 'REGULAR', 'Regular Time'
|
||
OVERTIME = 'OVERTIME', 'Overtime'
|
||
HOLIDAY = 'HOLIDAY', 'Holiday'
|
||
VACATION = 'VACATION', 'Vacation'
|
||
SICK = 'SICK', 'Sick Leave'
|
||
PERSONAL = 'PERSONAL', 'Personal Time'
|
||
TRAINING = 'TRAINING', 'Training'
|
||
|
||
# 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=EntryType.choices,
|
||
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=EntryStatus.choices,
|
||
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.
|
||
"""
|
||
|
||
class ReviewType(models.TextChoices):
|
||
ANNUAL = 'ANNUAL', 'Annual Review'
|
||
PROBATIONARY = 'PROBATIONARY', 'Probationary Review'
|
||
MID_YEAR = 'MID_YEAR', 'Mid-Year Review'
|
||
PROJECT = 'PROJECT', 'Project Review'
|
||
DISCIPLINARY = 'DISCIPLINARY', 'Disciplinary Review'
|
||
PROMOTION = 'PROMOTION', 'Promotion Review'
|
||
OTHER = 'OTHER', 'Other Review'
|
||
|
||
class ReviewStatus(models.TextChoices):
|
||
DRAFT = 'DRAFT', 'Draft'
|
||
SUBMITTED = 'SUBMITTED', 'Submitted'
|
||
IN_PROGRESS = 'IN_PROGRESS', 'In Progress'
|
||
COMPLETED = 'COMPLETED', 'Completed'
|
||
ACKNOWLEDGED = 'ACKNOWLEDGED', 'Acknowledged by Employee'
|
||
DISPUTED = 'DISPUTED', 'Disputed'
|
||
|
||
|
||
# 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=ReviewType.choices,
|
||
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=ReviewStatus.choices,
|
||
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.
|
||
"""
|
||
|
||
class TrainingType(models.TextChoices):
|
||
ORIENTATION = 'ORIENTATION', 'Orientation'
|
||
MANDATORY = 'MANDATORY', 'Mandatory Training'
|
||
CONTINUING_ED = 'CONTINUING_ED', 'Continuing Education'
|
||
CERTIFICATION = 'CERTIFICATION', 'Certification'
|
||
SKILLS = 'SKILLS', 'Skills Training'
|
||
SAFETY = 'SAFETY', 'Safety Training'
|
||
COMPLIANCE = 'COMPLIANCE', 'Compliance Training'
|
||
LEADERSHIP = 'LEADERSHIP', 'Leadership Development'
|
||
TECHNICAL = 'TECHNICAL', 'Technical Training'
|
||
OTHER = 'OTHER', 'Other'
|
||
|
||
class TrainingStatus(models.TextChoices):
|
||
SCHEDULED = 'SCHEDULED', 'Scheduled'
|
||
IN_PROGRESS = 'IN_PROGRESS', 'In Progress'
|
||
COMPLETED = 'COMPLETED', 'Completed'
|
||
CANCELLED = 'CANCELLED', 'Cancelled'
|
||
NO_SHOW = 'NO_SHOW', 'No Show'
|
||
FAILED = 'FAILED', 'Failed'
|
||
|
||
# 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=TrainingType.choices,
|
||
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=TrainingStatus.choices,
|
||
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'
|
||
)
|
||
is_certified = models.BooleanField(
|
||
default=False,
|
||
help_text='Training is certified'
|
||
)
|
||
# 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
|
||
|
||
|
||
# class Certification(models.Model):
|
||
# tenant = models.ForeignKey('core.Tenant', on_delete=models.PROTECT, related_name='certifications')
|
||
# name = models.CharField(max_length=100)
|
||
# issuer = models.CharField(max_length=150, blank=True, null=True)
|
||
# is_clinical = models.BooleanField(default=False)
|
||
#
|
||
# class Meta:
|
||
# unique_together = [('tenant', 'name')]
|
||
#
|
||
# class EmployeeCertification(models.Model):
|
||
# employee = models.ForeignKey(Employee, on_delete=models.CASCADE, related_name='employee_certifications')
|
||
# certification = models.ForeignKey(Certification, on_delete=models.PROTECT)
|
||
# credential_id = models.CharField(max_length=100, blank=True, null=True)
|
||
# issued_on = models.DateField(blank=True, null=True)
|
||
# expires_on = models.DateField(blank=True, null=True)
|
||
#
|
||
# class Meta:
|
||
# constraints = [
|
||
# models.UniqueConstraint(fields=['employee', 'certification'], name='uq_employee_cert_once')
|
||
# ]
|
||
|
||
|
||
|
||
# class TrainingPrograms(models.Model):
|
||
# PROGRAM_TYPE_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'),
|
||
# ]
|
||
#
|
||
# # Multi-tenancy
|
||
# tenant = models.ForeignKey(
|
||
# 'core.Tenant', on_delete=models.CASCADE, related_name='training_programs'
|
||
# )
|
||
#
|
||
# program_id = models.UUIDField(default=uuid.uuid4, unique=True, editable=False)
|
||
# name = models.CharField(max_length=200)
|
||
# description = models.TextField(blank=True, null=True)
|
||
# program_type = models.CharField(max_length=20, choices=PROGRAM_TYPE_CHOICES)
|
||
#
|
||
# # Provider/Instructor at the program (defaults; sessions can override)
|
||
# program_provider = models.CharField(max_length=200, blank=True, null=True)
|
||
# default_instructor = models.ForeignKey(
|
||
# Employee, on_delete=models.SET_NULL, null=True, blank=True,
|
||
# related_name='default_instructor_programs',
|
||
# help_text='Default instructor; sessions may override'
|
||
# )
|
||
#
|
||
# # Optional “program window” (e.g., for long initiatives)
|
||
# start_date = models.DateField(help_text='Program start date', blank=True, null=True)
|
||
# end_date = models.DateField(help_text='Program end date', blank=True, null=True)
|
||
#
|
||
# duration_hours = models.DecimalField(max_digits=5, decimal_places=2,
|
||
# default=Decimal('0.00'))
|
||
# cost = models.DecimalField(max_digits=10, decimal_places=2,
|
||
# default=Decimal('0.00'))
|
||
# is_certified = models.BooleanField(default=False)
|
||
#
|
||
# # Renewal/expiry policy (applies if is_certified)
|
||
# validity_days = models.PositiveIntegerField(
|
||
# blank=True, null=True,
|
||
# help_text='Days certificate is valid from completion (e.g., 365).'
|
||
# )
|
||
# notify_before_days = models.PositiveIntegerField(
|
||
# blank=True, null=True,
|
||
# help_text='Days before expiry to flag for renewal.'
|
||
# )
|
||
#
|
||
# # 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_programs'
|
||
# )
|
||
#
|
||
# class Meta:
|
||
# db_table = 'hr_training_program'
|
||
# ordering = ['name']
|
||
# unique_together = [('tenant', 'name')]
|
||
# indexes = [
|
||
# models.Index(fields=['tenant', 'program_type']),
|
||
# models.Index(fields=['tenant', 'is_certified']),
|
||
# ]
|
||
#
|
||
# def clean(self):
|
||
# if self.start_date and self.end_date and self.end_date < self.start_date:
|
||
# raise ValidationError(_('Program end_date cannot be before start_date.'))
|
||
# if self.is_certified and not self.validity_days:
|
||
# # Not hard error—could be open-ended—but warn as best practice.
|
||
# pass
|
||
#
|
||
# def __str__(self):
|
||
# return f'{self.name} ({self.get_program_type_display()})'
|
||
#
|
||
#
|
||
# class ProgramModule(models.Model):
|
||
# """Optional content structure for a program."""
|
||
# program = models.ForeignKey(
|
||
# TrainingPrograms, on_delete=models.CASCADE, related_name='modules'
|
||
# )
|
||
# title = models.CharField(max_length=200)
|
||
# order = models.PositiveIntegerField(default=1)
|
||
# hours = models.DecimalField(max_digits=5, decimal_places=2,
|
||
# default=Decimal('0.00'))
|
||
#
|
||
# class Meta:
|
||
# db_table = 'hr_training_program_module'
|
||
# ordering = ['program', 'order']
|
||
# unique_together = [('program', 'order')]
|
||
# indexes = [models.Index(fields=['program', 'order'])]
|
||
#
|
||
# def __str__(self):
|
||
# return f'{self.program.name} · {self.order}. {self.title}'
|
||
#
|
||
#
|
||
# class ProgramPrerequisite(models.Model):
|
||
# """A program may require completion of other program(s)."""
|
||
# program = models.ForeignKey(
|
||
# TrainingPrograms, on_delete=models.CASCADE, related_name='prerequisites'
|
||
# )
|
||
# required_program = models.ForeignKey(
|
||
# TrainingPrograms, on_delete=models.CASCADE, related_name='unlocking_programs'
|
||
# )
|
||
#
|
||
# class Meta:
|
||
# db_table = 'hr_training_program_prerequisite'
|
||
# unique_together = [('program', 'required_program')]
|
||
#
|
||
# def clean(self):
|
||
# if self.program_id == self.required_program_id:
|
||
# raise ValidationError(_('Program cannot require itself.'))
|
||
#
|
||
#
|
||
# class TrainingSession(models.Model):
|
||
# """
|
||
# A scheduled run of a program (cohort/class).
|
||
# """
|
||
# DELIVERY_CHOICES = [
|
||
# ('IN_PERSON', 'In-person'),
|
||
# ('VIRTUAL', 'Virtual'),
|
||
# ('HYBRID', 'Hybrid'),
|
||
# ('SELF_PACED', 'Self-paced'),
|
||
# ]
|
||
#
|
||
# tenant = models.ForeignKey(
|
||
# Tenant, on_delete=models.CASCADE, related_name='training_sessions'
|
||
# )
|
||
# session_id = models.UUIDField(default=uuid.uuid4, unique=True, editable=False)
|
||
# program = models.ForeignKey(
|
||
# TrainingPrograms, on_delete=models.CASCADE, related_name='sessions'
|
||
# )
|
||
# title = models.CharField(
|
||
# max_length=200, blank=True, null=True,
|
||
# help_text='Optional run title; falls back to program name'
|
||
# )
|
||
# instructor = models.ForeignKey(
|
||
# Employee, on_delete=models.SET_NULL, null=True, blank=True,
|
||
# related_name='instructed_sessions'
|
||
# )
|
||
# delivery_method = models.CharField(max_length=12, choices=DELIVERY_CHOICES, default='IN_PERSON')
|
||
#
|
||
# # Schedule
|
||
# start_at = models.DateTimeField()
|
||
# end_at = models.DateTimeField()
|
||
# location = models.CharField(max_length=200, blank=True, null=True)
|
||
# capacity = models.PositiveIntegerField(default=0)
|
||
#
|
||
# # Overrides
|
||
# cost_override = models.DecimalField(max_digits=10, decimal_places=2,
|
||
# blank=True, null=True)
|
||
# hours_override = models.DecimalField(max_digits=5, decimal_places=2,
|
||
# blank=True, null=True)
|
||
#
|
||
# # Metadata
|
||
# created_at = models.DateTimeField(auto_now_add=True)
|
||
# created_by = models.ForeignKey(
|
||
# settings.AUTH_USER_MODEL, on_delete=models.SET_NULL,
|
||
# null=True, blank=True, related_name='created_training_sessions'
|
||
# )
|
||
#
|
||
# class Meta:
|
||
# db_table = 'hr_training_session'
|
||
# ordering = ['-start_at']
|
||
# indexes = [
|
||
# models.Index(fields=['tenant', 'start_at']),
|
||
# models.Index(fields=['tenant', 'program']),
|
||
# ]
|
||
# constraints = [
|
||
# models.CheckConstraint(
|
||
# check=models.Q(end_at__gt=models.F('start_at')),
|
||
# name='session_end_after_start'
|
||
# ),
|
||
# ]
|
||
#
|
||
# def __str__(self):
|
||
# return self.title or f'{self.program.name} @ {self.start_at:%Y-%m-%d}'
|
||
#
|
||
#
|
||
# class TrainingRecord(models.Model):
|
||
# """
|
||
# Enrollment/participation record (renamed semantic, kept class name).
|
||
# Each row = an employee participating in a specific session of a program.
|
||
# """
|
||
# STATUS_CHOICES = [
|
||
# ('SCHEDULED', 'Scheduled'),
|
||
# ('IN_PROGRESS', 'In Progress'),
|
||
# ('COMPLETED', 'Completed'),
|
||
# ('CANCELLED', 'Cancelled'),
|
||
# ('NO_SHOW', 'No Show'),
|
||
# ('FAILED', 'Failed'),
|
||
# ('WAITLISTED', 'Waitlisted'),
|
||
# ]
|
||
#
|
||
# tenant = models.ForeignKey(
|
||
# Tenant, on_delete=models.CASCADE, related_name='training_records'
|
||
# )
|
||
# record_id = models.UUIDField(default=uuid.uuid4, unique=True, editable=False)
|
||
#
|
||
# # Core links
|
||
# employee = models.ForeignKey(
|
||
# Employee, on_delete=models.CASCADE, related_name='training_records'
|
||
# )
|
||
# program = models.ForeignKey(
|
||
# TrainingPrograms, on_delete=models.PROTECT, related_name='training_records'
|
||
# )
|
||
# session = models.ForeignKey(
|
||
# TrainingSession, on_delete=models.PROTECT, related_name='enrollments',
|
||
# help_text='The specific run the employee is enrolled in.'
|
||
# )
|
||
#
|
||
# # Timeline
|
||
# enrolled_at = models.DateTimeField(auto_now_add=True)
|
||
# started_at = models.DateTimeField(blank=True, null=True)
|
||
# completion_date = models.DateField(blank=True, null=True)
|
||
#
|
||
# # Outcomes
|
||
# status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='SCHEDULED')
|
||
# credits_earned = models.DecimalField(max_digits=5, decimal_places=2,
|
||
# default=Decimal('0.00'))
|
||
# score = models.DecimalField(max_digits=5, decimal_places=2, blank=True, null=True)
|
||
# passed = models.BooleanField(default=False)
|
||
#
|
||
# # Notes/Cost
|
||
# notes = models.TextField(blank=True, null=True)
|
||
# cost_paid = models.DecimalField(max_digits=10, decimal_places=2,
|
||
# blank=True, null=True)
|
||
#
|
||
# 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'
|
||
# )
|
||
#
|
||
# class Meta:
|
||
# db_table = 'hr_training_record'
|
||
# verbose_name = 'Training Enrollment'
|
||
# verbose_name_plural = 'Training Enrollments'
|
||
# ordering = ['-enrolled_at']
|
||
# unique_together = [('employee', 'session')]
|
||
# indexes = [
|
||
# models.Index(fields=['tenant', 'employee']),
|
||
# models.Index(fields=['tenant', 'program']),
|
||
# models.Index(fields=['tenant', 'session']),
|
||
# models.Index(fields=['tenant', 'status']),
|
||
# models.Index(fields=['tenant', 'completion_date']),
|
||
# ]
|
||
#
|
||
# def clean(self):
|
||
# # Tenancy alignment
|
||
# if self.program and self.tenant_id != self.program.tenant_id:
|
||
# raise ValidationError(_('Tenant mismatch between record and program.'))
|
||
# if self.session and self.tenant_id != self.session.tenant_id:
|
||
# raise ValidationError(_('Tenant mismatch between record and session.'))
|
||
# if self.employee and self.tenant_id != self.employee.tenant_id:
|
||
# raise ValidationError(_('Tenant mismatch between record and employee.'))
|
||
#
|
||
# # Prevent enrolling into sessions of a different program (shouldn’t happen)
|
||
# if self.session and self.program and self.session.program_id != self.program_id:
|
||
# raise ValidationError(_('Session does not belong to the selected program.'))
|
||
#
|
||
# if self.completion_date and self.status not in ('COMPLETED', 'FAILED'):
|
||
# raise ValidationError(_('Completion date requires status COMPLETED or FAILED.'))
|
||
#
|
||
# def __str__(self):
|
||
# return f'{self.employee} → {self.program.name} ({self.get_status_display()})'
|
||
#
|
||
# # Helper properties
|
||
# @property
|
||
# def hours(self):
|
||
# return self.session.hours_override or self.program.duration_hours
|
||
#
|
||
# @property
|
||
# def effective_cost(self):
|
||
# return self.cost_paid if self.cost_paid is not None else \
|
||
# (self.session.cost_override if self.session.cost_override is not None
|
||
# else self.program.cost)
|
||
#
|
||
# @property
|
||
# def eligible_for_certificate(self):
|
||
# return self.status == 'COMPLETED' and self.passed and self.program.is_certified
|
||
#
|
||
#
|
||
# class SessionAttendance(models.Model):
|
||
# """
|
||
# Optional check-in/out per participant per session (or per day if multi-day).
|
||
# If you want per-day granularity, add a "session_day" field.
|
||
# """
|
||
# ATTENDANCE_STATUS = [
|
||
# ('PRESENT', 'Present'),
|
||
# ('LATE', 'Late'),
|
||
# ('ABSENT', 'Absent'),
|
||
# ('EXCUSED', 'Excused'),
|
||
# ]
|
||
#
|
||
# enrollment = models.ForeignKey(
|
||
# TrainingRecord, on_delete=models.CASCADE, related_name='attendance'
|
||
# )
|
||
# checked_in_at = models.DateTimeField(blank=True, null=True)
|
||
# checked_out_at = models.DateTimeField(blank=True, null=True)
|
||
# status = models.CharField(max_length=10, choices=ATTENDANCE_STATUS, default='PRESENT')
|
||
# notes = models.CharField(max_length=255, blank=True, null=True)
|
||
#
|
||
# class Meta:
|
||
# db_table = 'hr_training_attendance'
|
||
# ordering = ['enrollment_id', 'checked_in_at']
|
||
# indexes = [models.Index(fields=['enrollment'])]
|
||
#
|
||
#
|
||
# class SessionAssessment(models.Model):
|
||
# """
|
||
# Optional evaluation (quiz/exam) tied to an enrollment.
|
||
# """
|
||
# enrollment = models.ForeignKey(
|
||
# TrainingRecord, on_delete=models.CASCADE, related_name='assessments'
|
||
# )
|
||
# name = models.CharField(max_length=200)
|
||
# max_score = models.DecimalField(max_digits=7, decimal_places=2, default=100)
|
||
# score = models.DecimalField(max_digits=7, decimal_places=2, blank=True, null=True)
|
||
# passed = models.BooleanField(default=False)
|
||
# taken_at = models.DateTimeField(blank=True, null=True)
|
||
#
|
||
# class Meta:
|
||
# db_table = 'hr_training_assessment'
|
||
# ordering = ['-taken_at']
|
||
# indexes = [models.Index(fields=['enrollment'])]
|
||
#
|
||
#
|
||
# class TrainingCertificates(models.Model):
|
||
# """
|
||
# Issued certificates on completion.
|
||
# Usually tied to a program and the enrollment that produced it.
|
||
# """
|
||
# tenant = models.ForeignKey(
|
||
# Tenant, on_delete=models.CASCADE, related_name='training_certificates'
|
||
# )
|
||
# certificate_id = models.UUIDField(default=uuid.uuid4, unique=True, editable=False)
|
||
#
|
||
# program = models.ForeignKey(
|
||
# TrainingPrograms, on_delete=models.PROTECT, related_name='certificates'
|
||
# )
|
||
# employee = models.ForeignKey(
|
||
# Employee, on_delete=models.CASCADE, related_name='training_certificates'
|
||
# )
|
||
# enrollment = models.OneToOneField(
|
||
# TrainingRecord, on_delete=models.CASCADE, related_name='certificate',
|
||
# help_text='The enrollment that generated this certificate.'
|
||
# )
|
||
#
|
||
# certificate_name = models.CharField(max_length=200)
|
||
# certificate_number = models.CharField(max_length=50, blank=True, null=True)
|
||
# certification_body = models.CharField(max_length=200, blank=True, null=True)
|
||
# issued_date = models.DateField(auto_now_add=True)
|
||
# expiry_date = models.DateField(blank=True, null=True)
|
||
# file = models.FileField(upload_to='certificates/', blank=True, null=True)
|
||
#
|
||
# created_at = models.DateTimeField(auto_now_add=True)
|
||
# created_by = models.ForeignKey(
|
||
# settings.AUTH_USER_MODEL, on_delete=models.SET_NULL,
|
||
# null=True, blank=True, related_name='created_training_certificates'
|
||
# )
|
||
#
|
||
# class Meta:
|
||
# db_table = 'hr_training_certificate'
|
||
# ordering = ['-issued_date']
|
||
# unique_together = [('employee', 'program', 'enrollment')]
|
||
# indexes = [
|
||
# models.Index(fields=['tenant', 'employee']),
|
||
# models.Index(fields=['tenant', 'program']),
|
||
# models.Index(fields=['tenant', 'expiry_date']),
|
||
# models.Index(fields=['certificate_number']),
|
||
# ]
|
||
#
|
||
# def clean(self):
|
||
# # tenancy alignment
|
||
# if self.program and self.tenant_id != self.program.tenant_id:
|
||
# raise ValidationError(_('Tenant mismatch between certificate and program.'))
|
||
# if self.employee and self.tenant_id != self.employee.tenant_id:
|
||
# raise ValidationError(_('Tenant mismatch between certificate and employee.'))
|
||
# if self.enrollment and self.tenant_id != self.enrollment.tenant_id:
|
||
# raise ValidationError(_('Tenant mismatch between certificate and enrollment.'))
|
||
# if self.enrollment and self.enrollment.program_id != self.program_id:
|
||
# raise ValidationError(_('Enrollment does not belong to this program.'))
|
||
#
|
||
# def __str__(self):
|
||
# return f'{self.certificate_name} - {self.employee}'
|
||
#
|
||
# @property
|
||
# def is_expired(self):
|
||
# return bool(self.expiry_date and self.expiry_date < date.today())
|
||
#
|
||
# @property
|
||
# def days_to_expiry(self):
|
||
# return (self.expiry_date - date.today()).days if self.expiry_date else None
|
||
#
|
||
# @classmethod
|
||
# def compute_expiry(cls, program: TrainingPrograms, issued_on: date) -> date | None:
|
||
# if program.is_certified and program.validity_days:
|
||
# return issued_on + timedelta(days=program.validity_days)
|
||
# return None
|
||
|
||
|
||
# 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'
|
||
# )
|
||
# national_id = models.CharField(
|
||
# max_length=10,
|
||
# blank=True,
|
||
# null=True,
|
||
# unique=True,
|
||
# help_text='National ID'
|
||
# )
|
||
# # 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]) |