# scripts/hr_data_generator.py """ Comprehensive HR Data Generator for Hospital Management System Generates realistic Saudi healthcare HR data including: - Employees with complete profiles - Departments with organizational structure - Work schedules and assignments - Time tracking entries - Performance reviews - Complete training management system """ import os import django # Set up Django environment os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'hospital_management.settings') try: django.setup() except Exception as e: print(f"Django setup failed: {e}") print("Please run this script using: python manage.py shell -c \"exec(open('hr_data.py').read())\"") exit(1) import random from datetime import datetime, timedelta, date, time from decimal import Decimal from django.utils import timezone as django_timezone from django.core.exceptions import ValidationError from hr.models import ( Employee, Department, Schedule, ScheduleAssignment, TimeEntry, PerformanceReview, TrainingPrograms, TrainingSession, TrainingRecord, ProgramModule, ProgramPrerequisite, TrainingAttendance, TrainingAssessment, TrainingCertificates, LeaveType, LeaveBalance, LeaveRequest, LeaveApproval, LeaveDelegate, SalaryInformation, SalaryAdjustment, DocumentRequest, DocumentTemplate ) from accounts.models import User from core.models import Tenant # ---------------------------- # Saudi-specific data constants # ---------------------------- SAUDI_FIRST_NAMES_MALE = [ 'Mohammed', 'Ahmed', 'Abdullah', 'Khalid', 'Omar', 'Ali', 'Hassan', 'Ibrahim', 'Yousef', 'Fahd', 'Faisal', 'Saud', 'Nasser', 'Abdulaziz', 'Abdulrahman', 'Majid', 'Saeed', 'Waleed', 'Tariq', 'Mansour', 'Sultan', 'Bandar', 'Turki', 'Nawaf', 'Rayan', 'Ziad', 'Adel', 'Salman', 'Fares', 'Amjad' ] SAUDI_FIRST_NAMES_FEMALE = [ 'Fatima', 'Aisha', 'Khadija', 'Maryam', 'Zahra', 'Noor', 'Layla', 'Sara', 'Hala', 'Noura', 'Reem', 'Lina', 'Dina', 'Rana', 'Jana', 'Maya', 'Amal', 'Hanan', 'Widad', 'Nada', 'Rawan', 'Ghada', 'Samar', 'Hind', 'Munira', 'Rahma', 'Najla', 'Dalal', 'Abeer', 'Manal' ] SAUDI_LAST_NAMES = [ 'Al-Rashid', 'Al-Otaibi', 'Al-Dosari', 'Al-Harbi', 'Al-Zahrani', 'Al-Ghamdi', 'Al-Qahtani', 'Al-Maliki', 'Al-Shammari', 'Al-Mutairi', 'Al-Subai', 'Al-Shamsi', 'Al-Faisal', 'Al-Saud', 'Al-Thani', 'Al-Fahd', 'Al-Sultan', 'Al-Nasser', 'Al-Mansour', 'Al-Khalil', 'Al-Ibrahim', 'Al-Hassan', 'Al-Ali', 'Al-Omar', 'Al-Ahmed', 'Al-Mohammed' ] SAUDI_CITIES = [ 'Riyadh', 'Jeddah', 'Mecca', 'Medina', 'Dammam', 'Khobar', 'Dhahran', 'Taif', 'Tabuk', 'Buraidah', 'Khamis Mushait', 'Hail', 'Hofuf', 'Najran', 'Jazan', 'Yanbu', 'Abha', 'Arar', 'Sakaka', 'Qatif' ] SAUDI_DEPARTMENTS = [ ('EMERGENCY', 'Emergency Department', 'Emergency medical services'), ('ICU', 'Intensive Care Unit', 'Critical care services'), ('CARDIOLOGY', 'Cardiology Department', 'Heart and cardiovascular care'), ('SURGERY', 'General Surgery', 'Surgical services'), ('ORTHOPEDICS', 'Orthopedics Department', 'Bone and joint care'), ('PEDIATRICS', 'Pediatrics Department', 'Children healthcare'), ('OBSTETRICS', 'Obstetrics & Gynecology', 'Women and maternity care'), ('RADIOLOGY', 'Radiology Department', 'Medical imaging services'), ('LABORATORY', 'Laboratory Services', 'Diagnostic testing'), ('PHARMACY', 'Pharmacy Department', 'Medication services'), ('NURSING', 'Nursing Services', 'Patient care services'), ('ADMINISTRATION', 'Administration', 'Hospital administration'), ('FINANCE', 'Finance Department', 'Financial management'), ('HR', 'Human Resources', 'Staff management'), ('IT', 'Information Technology', 'Technology services'), ('MAINTENANCE', 'Maintenance Services', 'Facility maintenance'), ('SECURITY', 'Security Department', 'Hospital security'), ('HOUSEKEEPING', 'Housekeeping Services', 'Cleaning services'), ('FOOD_SERVICE', 'Food Services', 'Dietary services'), ('SOCIAL_WORK', 'Social Work', 'Patient social services') ] SAUDI_TRAINING_PROGRAMS = [ 'Basic Life Support (BLS)', 'Advanced Cardiac Life Support (ACLS)', 'Pediatric Advanced Life Support (PALS)', 'Infection Control', 'Patient Safety', 'Fire Safety', 'Emergency Procedures', 'HIPAA Compliance', 'Cultural Sensitivity', 'Arabic Language', 'Islamic Healthcare Ethics', 'Medication Administration', 'Wound Care Management', 'Electronic Health Records', 'Quality Improvement', 'Customer Service Excellence' ] # ---------------------------- # Helpers # ---------------------------- def e164_ksa_mobile() -> str: """Return +9665XXXXXXXX to satisfy Employee.e164_ksa_regex.""" return f"+9665{random.randint(10000000, 99999999)}" def ensure_departments(tenant): """ Ensure Department objects exist for this tenant; return a list. If Department is global (no tenant field), operate globally. """ dept_fields = {f.name for f in Department._meta.fields} is_tenant_scoped = 'tenant' in dept_fields qset = Department.objects.filter(tenant=tenant) if is_tenant_scoped else Department.objects.all() existing_codes = set(qset.values_list('code', flat=True)) created = [] for code, name, desc in SAUDI_DEPARTMENTS: if code in existing_codes: continue kwargs = dict(code=code, name=name, description=desc, department_type=('CLINICAL' if code in ['EMERGENCY', 'ICU', 'CARDIOLOGY', 'SURGERY', 'ORTHOPEDICS', 'PEDIATRICS', 'OBSTETRICS', 'RADIOLOGY', 'LABORATORY', 'PHARMACY', 'NURSING'] else 'ADMINISTRATIVE' if code in ['ADMINISTRATION', 'FINANCE', 'HR', 'IT'] else 'SUPPORT' if code in ['MAINTENANCE', 'SECURITY', 'HOUSEKEEPING', 'FOOD_SERVICE'] else 'ANCILLARY'), annual_budget=Decimal(str(random.randint(500000, 5000000))), cost_center=f"CC-{code}", location=f"{random.choice(['Building A', 'Building B', 'Main Building'])}, Floor {random.randint(1, 5)}", is_active=True, notes=f"Primary {name.lower()} for {tenant.name}", created_at=django_timezone.now() - timedelta(days=random.randint(30, 365)), updated_at=django_timezone.now() - timedelta(days=random.randint(0, 30))) if is_tenant_scoped: kwargs['tenant'] = tenant try: created.append(Department.objects.create(**kwargs)) except Exception as e: print(f"Error creating department {code} for {tenant.name}: {e}") # Return all for this tenant (or global) qset = Department.objects.filter(tenant=tenant) if is_tenant_scoped else Department.objects.all() print(f"Departments ready for {tenant.name}: {qset.count()}") return list(qset) def tenant_scoped_unique_username(tenant, base_username: str) -> str: """Make username unique within a tenant (AUTH is tenant-scoped).""" uname = base_username i = 1 while User.objects.filter(tenant=tenant, username=uname).exists(): i += 1 uname = f"{base_username}{i}" return uname def pick_job_title_for_department(department) -> str: dtype = getattr(department, 'department_type', 'ADMINISTRATIVE') or 'ADMINISTRATIVE' if dtype == 'CLINICAL': return random.choice([ 'Consultant Physician', 'Senior Physician', 'Physician', 'Resident Physician', 'Intern', 'Chief Nurse', 'Nurse Manager', 'Senior Nurse', 'Staff Nurse', 'Nurse Practitioner', 'Clinical Nurse Specialist', 'Charge Nurse', 'Pharmacist', 'Clinical Pharmacist', 'Pharmacy Technician', 'Radiologist', 'Radiology Technician', 'Medical Technologist', 'Laboratory Technician', 'Physical Therapist', 'Respiratory Therapist' ]) if dtype == 'SUPPORT': return random.choice([ 'Security Officer', 'Security Guard', 'Maintenance Technician', 'Housekeeping Supervisor', 'Housekeeper', 'Food Service Manager', 'Cook', 'Kitchen Assistant', 'Transport Aide', 'Receptionist' ]) return random.choice([ 'Chief Executive Officer', 'Chief Operating Officer', 'Administrator', 'Assistant Administrator', 'Department Manager', 'Supervisor', 'Administrative Assistant', 'Secretary', 'Clerk', 'Coordinator' ]) def infer_role_from_title(job_title: str) -> str: jt = (job_title or '').lower() if 'physician' in jt: return Employee.Role.PHYSICIAN if 'nurse practitioner' in jt: return Employee.Role.NURSE_PRACTITIONER if 'nurse' in jt: return Employee.Role.NURSE if 'pharmac' in jt: return Employee.Role.PHARMACIST if 'radiolog' in jt and 'techn' not in jt: return Employee.Role.RADIOLOGIST if 'radiolog' in jt and 'techn' in jt: return Employee.Role.RAD_TECH if 'laborator' in jt: return Employee.Role.LAB_TECH if any(k in jt for k in ['chief', 'director', 'manager', 'admin']): return Employee.Role.ADMIN return Employee.Role.CLERICAL # ---------------------------- # Employee creation / update # ---------------------------- def create_or_update_saudi_employees(tenants, departments_by_tenant, employees_per_tenant=150): """ Ensure each tenant has ~employees_per_tenant Employees. - For existing Users (with Employee via signal), update their Employee fields. - If we still need more, create additional Users (signal creates Employee), then update. """ all_employees = [] for tenant in tenants: depts = departments_by_tenant[tenant] if not depts: print(f"No departments for {tenant.name}, skipping employees...") continue # Existing users -> employees tenant_users = list(User.objects.filter(tenant=tenant).order_by('user_id')) employees_existing = [u.employee_profile for u in tenant_users if hasattr(u, 'employee_profile')] num_existing = len(employees_existing) # Create more users if we need more employees to_create = max(0, employees_per_tenant - num_existing) print(f"Creating {to_create} new employees for {tenant.name} (existing: {num_existing})") for i in range(to_create): try: gender = random.choice([Employee.Gender.MALE, Employee.Gender.FEMALE]) first = random.choice(SAUDI_FIRST_NAMES_MALE if gender == Employee.Gender.MALE else SAUDI_FIRST_NAMES_FEMALE) father = random.choice(SAUDI_FIRST_NAMES_MALE) grandfather = random.choice(SAUDI_FIRST_NAMES_MALE) last = random.choice(SAUDI_LAST_NAMES) base_username = f"{first.lower()}.{last.lower().replace('-', '').replace('al', '')}" username = tenant_scoped_unique_username(tenant, base_username) email = f"{username}@{tenant.name.lower().replace(' ', '').replace('-', '')}.sa" u = User.objects.create( tenant=tenant, username=username, email=email, first_name=first, last_name=last, is_active=True, ) u.set_password('Hospital@123') u.save() tenant_users.append(u) # signal creates Employee if (i + 1) % 50 == 0: print(f" Created {i + 1}/{to_create} users for {tenant.name}") except Exception as e: print(f"Error creating user {i+1} for {tenant.name}: {e}") continue # Now (re)populate employee HR data print(f"Updating employee profiles for {tenant.name}...") for i, u in enumerate(tenant_users): try: emp = getattr(u, 'employee_profile', None) if not emp: continue # Basic personal if not emp.first_name: emp.first_name = u.first_name or emp.first_name if not emp.last_name: emp.last_name = u.last_name or emp.last_name if not emp.email: emp.email = u.email # Add father and grandfather names if not emp.father_name: emp.father_name = random.choice(SAUDI_FIRST_NAMES_MALE) if not emp.grandfather_name: emp.grandfather_name = random.choice(SAUDI_FIRST_NAMES_MALE) # ID information if not emp.identification_number: emp.identification_number = f"{random.randint(1000000000, 9999999999)}" emp.id_type = random.choice([Employee.IdNumberTypes.NATIONAL_ID, Employee.IdNumberTypes.IQAMA]) # Demographics if not emp.gender: emp.gender = random.choice([Employee.Gender.MALE, Employee.Gender.FEMALE]) if not emp.marital_status: emp.marital_status = random.choice([ Employee.MaritalStatus.SINGLE, Employee.MaritalStatus.MARRIED, Employee.MaritalStatus.DIVORCED, Employee.MaritalStatus.WIDOWED, Employee.MaritalStatus.SEPARATED ]) if not emp.date_of_birth: # 22–55 years old emp.date_of_birth = (django_timezone.now().date() - timedelta(days=random.randint(22*365, 55*365))) # Contact E.164 (both are mobiles by model design) if not emp.phone: emp.phone = e164_ksa_mobile() if not emp.mobile_phone: emp.mobile_phone = e164_ksa_mobile() # Address if not emp.address_line_1: emp.address_line_1 = f"{random.randint(1, 999)} {random.choice(['King Fahd Rd', 'Prince Sultan St', 'Olaya St', 'King Abdul Aziz Rd', 'Tahlia St'])}" if not emp.address_line_2: if random.choice([True, False]): emp.address_line_2 = f"Apt {random.randint(1, 50)}" if not emp.city: emp.city = random.choice(SAUDI_CITIES) if not emp.postal_code: emp.postal_code = f"{random.randint(10000, 99999)}" if not emp.country: emp.country = 'Saudi Arabia' # Org if not emp.department: emp.department = random.choice(depts) if not emp.job_title: emp.job_title = pick_job_title_for_department(emp.department) # Role (derive from job title if not set) if not emp.role or emp.role == Employee.Role.GUEST: emp.role = infer_role_from_title(emp.job_title) # Employment if not emp.employment_type: emp.employment_type = random.choices([ Employee.EmploymentType.FULL_TIME, Employee.EmploymentType.PART_TIME, Employee.EmploymentType.CONTRACT, Employee.EmploymentType.TEMPORARY, Employee.EmploymentType.INTERN, Employee.EmploymentType.CONSULTANT ], weights=[70, 15, 8, 4, 2, 1])[0] if not emp.hire_date: emp.hire_date = django_timezone.now().date() - timedelta(days=random.randint(30, 2000)) if not emp.employment_status: emp.employment_status = random.choices( [Employee.EmploymentStatus.ACTIVE, Employee.EmploymentStatus.INACTIVE, Employee.EmploymentStatus.LEAVE], weights=[85, 10, 5] )[0] # If terminated, set a termination_date after hire_date if emp.employment_status == Employee.EmploymentStatus.TERMINATED and not emp.termination_date: emp.termination_date = emp.hire_date + timedelta(days=random.randint(30, 1000)) # Compensation if not emp.hourly_rate and emp.employment_type in [Employee.EmploymentType.PART_TIME, Employee.EmploymentType.PER_DIEM]: emp.hourly_rate = Decimal(str(random.randint(50, 300))) if not emp.annual_salary and emp.employment_type == Employee.EmploymentType.FULL_TIME: emp.annual_salary = Decimal(str(random.randint(60000, 500000))) if not emp.fte_percentage: if emp.employment_type == Employee.EmploymentType.PART_TIME: emp.fte_percentage = Decimal(str(random.choice([25, 50, 75]))) else: emp.fte_percentage = Decimal('100.00') # Licensure (optional by role/title) jt_lower = (emp.job_title or '').lower() if not emp.license_number and any(k in jt_lower for k in ['physician', 'nurse', 'pharmacist', 'radiolog']): emp.license_number = f"LIC-{random.randint(100000, 999999)}" emp.license_expiry_date = django_timezone.now().date() + timedelta(days=random.randint(180, 1095)) emp.license_state = random.choice(['Riyadh Province', 'Makkah Province', 'Eastern Province', 'Asir Province']) if 'physician' in jt_lower and not emp.npi_number: emp.npi_number = f"SA{random.randint(1000000, 9999999)}" # Emergency contact if not emp.emergency_contact_name: contact_gender = random.choice([Employee.Gender.MALE, Employee.Gender.FEMALE]) contact_first = random.choice(SAUDI_FIRST_NAMES_MALE if contact_gender == Employee.Gender.MALE else SAUDI_FIRST_NAMES_FEMALE) contact_last = random.choice(SAUDI_LAST_NAMES) emp.emergency_contact_name = f"{contact_first} {contact_last}" emp.emergency_contact_relationship = random.choice(['Spouse', 'Parent', 'Sibling', 'Child', 'Friend']) emp.emergency_contact_phone = e164_ksa_mobile() # Bio for senior staff if any(k in jt_lower for k in ['chief', 'director', 'manager', 'senior', 'consultant']): if not emp.bio: years_exp = random.randint(5, 25) emp.bio = f"Experienced healthcare professional with {years_exp} years in {emp.department.name if emp.department else 'healthcare'}. Specialized in {random.choice(['patient care', 'clinical excellence', 'team leadership', 'quality improvement'])}." # Preferences emp.user_timezone = 'Asia/Riyadh' if not emp.language: emp.language = random.choices(['ar', 'en'], weights=[70, 30])[0] if not emp.theme: emp.theme = random.choice([Employee.Theme.LIGHT, Employee.Theme.DARK, Employee.Theme.AUTO]) # Approval status for active employees if emp.employment_status == Employee.EmploymentStatus.ACTIVE: emp.is_verified = True emp.is_approved = True emp.approval_date = emp.hire_date + timedelta(days=random.randint(1, 30)) emp.save() all_employees.append(emp) if (i + 1) % 100 == 0: print(f" Updated {i + 1}/{len(tenant_users)} employee profiles for {tenant.name}") except Exception as e: print(f"Error updating employee profile for user {u.username}: {e}") continue print(f"Employees ready for {tenant.name}: {len([e for e in all_employees if e.tenant == tenant])}") return all_employees # ---------------------------- # Scheduling # ---------------------------- def create_saudi_schedules(employees, schedules_per_employee=2): """Create work schedules for employees based on department type.""" schedules = [] schedule_patterns = { 'DAY_SHIFT': { 'sunday': {'start': '07:00', 'end': '19:00'}, 'monday': {'start': '07:00', 'end': '19:00'}, 'tuesday': {'start': '07:00', 'end': '19:00'}, 'wednesday': {'start': '07:00', 'end': '19:00'}, 'thursday': {'start': '07:00', 'end': '19:00'}, 'friday': {'start': '07:00', 'end': '19:00'}, 'saturday': {'start': '07:00', 'end': '19:00'}, }, 'NIGHT_SHIFT': { 'sunday': {'start': '19:00', 'end': '07:00'}, 'monday': {'start': '19:00', 'end': '07:00'}, 'tuesday': {'start': '19:00', 'end': '07:00'}, 'wednesday': {'start': '19:00', 'end': '07:00'}, 'thursday': {'start': '19:00', 'end': '07:00'}, 'friday': {'start': '19:00', 'end': '07:00'}, 'saturday': {'start': '19:00', 'end': '07:00'}, }, 'ADMIN_HOURS': { 'sunday': {'start': '08:00', 'end': '17:00'}, 'monday': {'start': '08:00', 'end': '17:00'}, 'tuesday': {'start': '08:00', 'end': '17:00'}, 'wednesday': {'start': '08:00', 'end': '17:00'}, 'thursday': {'start': '08:00', 'end': '16:00'}, 'friday': 'off', 'saturday': 'off', } } clinical = [e for e in employees if e.department and getattr(e.department, 'department_type', '') == 'CLINICAL' and e.employment_status == Employee.EmploymentStatus.ACTIVE] admin = [e for e in employees if e.department and getattr(e.department, 'department_type', '') in ['ADMINISTRATIVE', 'SUPPORT'] and e.employment_status == Employee.EmploymentStatus.ACTIVE] # Clinical staff: mix of day/night for emp in clinical: for i in range(schedules_per_employee): pattern_name = random.choice(['DAY_SHIFT', 'NIGHT_SHIFT']) effective_date = django_timezone.now().date() - timedelta(days=random.randint(0, 180)) end_date = effective_date + timedelta(days=random.randint(90, 365)) if random.choice([True, False]) else None try: s = Schedule.objects.create( employee=emp, name=f"{emp.get_full_name()} - {pattern_name.replace('_', ' ').title()}", description=f"{pattern_name.replace('_', ' ').title()} schedule for {emp.job_title}", schedule_type=random.choice(['REGULAR', 'ROTATING']), effective_date=effective_date, end_date=end_date, schedule_pattern=schedule_patterns[pattern_name], is_active=(i == 0), # first one active notes=f"Standard {pattern_name.replace('_', ' ').lower()} for clinical staff", created_at=django_timezone.now() - timedelta(days=random.randint(1, 90)), updated_at=django_timezone.now() - timedelta(days=random.randint(0, 30)) ) schedules.append(s) except Exception as e: print(f"Error creating schedule for {emp.get_full_name()}: {e}") # Admin/support: admin hours for emp in admin: try: s = Schedule.objects.create( employee=emp, name=f"{emp.get_full_name()} - Administrative Hours", description="Standard administrative working hours", schedule_type='REGULAR', effective_date=django_timezone.now().date() - timedelta(days=random.randint(0, 180)), end_date=None, schedule_pattern=schedule_patterns['ADMIN_HOURS'], is_active=True, notes="Standard administrative schedule", created_at=django_timezone.now() - timedelta(days=random.randint(1, 90)), updated_at=django_timezone.now() - timedelta(days=random.randint(0, 30)) ) schedules.append(s) except Exception as e: print(f"Error creating schedule for {emp.get_full_name()}: {e}") print(f"Created {len(schedules)} employee schedules") return schedules def create_schedule_assignments(schedules, days_back=30): """Create specific schedule assignments for active schedules.""" assignments = [] for s in schedules: if not s.is_active: continue start_date = django_timezone.now().date() - timedelta(days=days_back) for day_offset in range(days_back): assignment_date = start_date + timedelta(days=day_offset) weekday = assignment_date.strftime('%A').lower() day_pat = s.schedule_pattern.get(weekday) if not day_pat or day_pat == 'off': continue try: start_time = datetime.strptime(day_pat['start'], '%H:%M').time() end_time = datetime.strptime(day_pat['end'], '%H:%M').time() except Exception: continue # Infer shift type from hours shift_type = 'NIGHT' if end_time <= start_time else 'DAY' status = random.choices( ['COMPLETED', 'NO_SHOW', 'CANCELLED'] if assignment_date < django_timezone.now().date() else ['SCHEDULED', 'CONFIRMED'], weights=[90, 5, 5] if assignment_date < django_timezone.now().date() else [70, 30] )[0] try: a = ScheduleAssignment.objects.create( schedule=s, assignment_date=assignment_date, start_time=start_time, end_time=end_time, shift_type=shift_type, department=s.employee.department, location=s.employee.department.location if s.employee.department else None, status=status, break_minutes=15 if shift_type == 'DAY' else 30, lunch_minutes=30 if shift_type == 'DAY' else 0, notes=f"{shift_type.title()} shift assignment", created_at=django_timezone.now() - timedelta(days=random.randint(0, 7)), updated_at=django_timezone.now() - timedelta(days=random.randint(0, 3)) ) assignments.append(a) except Exception as e: print(f"Error creating assignment for {s.employee.get_full_name()}: {e}") print(f"Created {len(assignments)} schedule assignments") return assignments def create_time_entries(employees, days_back=30): """Create time entries for ACTIVE employees.""" entries = [] active_emps = [e for e in employees if e.employment_status == Employee.EmploymentStatus.ACTIVE and e.hire_date] for emp in active_emps: start_date = django_timezone.now().date() - timedelta(days=days_back) for d in range(days_back): work_date = start_date + timedelta(days=d) # Skip admin weekend days (Fri/Sat) if emp.department and getattr(emp.department, 'department_type', '') == 'ADMINISTRATIVE' and work_date.weekday() in [4, 5]: continue work_probability = 0.85 if emp.department and getattr(emp.department, 'department_type', '') == 'CLINICAL' else 0.90 if random.random() > work_probability: continue # Typical shift windows if emp.department and getattr(emp.department, 'department_type', '') == 'CLINICAL': shift_opts = [ (time(7, 0), time(15, 0)), (time(15, 0), time(23, 0)), (time(23, 0), time(7, 0)), ] start_t, end_t = random.choice(shift_opts) else: start_t = time(8, 0) end_t = time(17, 0) if work_date.weekday() != 4 else time(12, 0) cin_var = random.randint(-15, 15) cout_var = random.randint(-15, 15) clock_in = datetime.combine(work_date, start_t) + timedelta(minutes=cin_var) clock_out = datetime.combine(work_date, end_t) + timedelta(minutes=cout_var) if end_t < start_t: clock_out += timedelta(days=1) break_start = clock_in + timedelta(hours=2, minutes=random.randint(0, 60)) break_end = break_start + timedelta(minutes=15) lunch_start = clock_in + timedelta(hours=4, minutes=random.randint(0, 60)) lunch_end = lunch_start + timedelta(minutes=30) entry_type = random.choices(['REGULAR', 'OVERTIME', 'HOLIDAY'], weights=[85, 10, 5])[0] status = random.choices(['APPROVED', 'SUBMITTED', 'DRAFT'], weights=[70, 20, 10])[0] try: e = TimeEntry.objects.create( employee=emp, work_date=work_date, clock_in_time=clock_in, clock_out_time=clock_out, break_start_time=break_start, break_end_time=break_end, lunch_start_time=lunch_start, lunch_end_time=lunch_end, entry_type=entry_type, department=emp.department, location=emp.department.location if emp.department else None, status=status, notes=f"{entry_type.title()} work shift", created_at=django_timezone.now() - timedelta(days=random.randint(0, 7)), updated_at=django_timezone.now() - timedelta(days=random.randint(0, 3)) ) entries.append(e) except Exception as ex: print(f"Error creating time entry for {emp.get_full_name()}: {ex}") print(f"Created {len(entries)} time entries") return entries def create_performance_reviews(employees): """Create 1–2 performance reviews for employees with ≥ 6 months service.""" reviews = [] today = django_timezone.now().date() eligible = [e for e in employees if e.employment_status == Employee.EmploymentStatus.ACTIVE and e.hire_date and (today - e.hire_date).days >= 180] competency_areas = [ 'Clinical Skills', 'Communication', 'Teamwork', 'Professionalism', 'Quality of Work', 'Productivity', 'Problem Solving', 'Initiative', 'Reliability', 'Cultural Competency', 'Patient Care', 'Safety Compliance' ] for emp in eligible: for _ in range(random.randint(1, 2)): review_date = today - timedelta(days=random.randint(0, 90)) period_start = review_date - timedelta(days=random.randint(180, 360)) period_end = review_date - timedelta(days=30) review_type = random.choices(['ANNUAL', 'PROBATIONARY', 'MID_YEAR'], weights=[60, 20, 20])[0] overall = Decimal(str(random.choices([2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0], weights=[5, 10, 20, 25, 25, 10, 5])[0])) ratings = {c: float(random.choices([2.0, 2.5, 3.0, 3.5, 4.0, 4.5, 5.0], weights=[5, 10, 20, 25, 25, 10, 5])[0]) for c in random.sample(competency_areas, random.randint(6, 10))} status = random.choices(['COMPLETED', 'ACKNOWLEDGED', 'IN_PROGRESS'], weights=[60, 30, 10])[0] try: r = PerformanceReview.objects.create( employee=emp, review_period_start=period_start, review_period_end=period_end, review_date=review_date, review_type=review_type, overall_rating=overall, competency_ratings=ratings, goals_achieved=f"{emp.first_name} contributed to department objectives.", goals_not_achieved=("Needs improvement in documentation timeliness." if overall < 4 else None), future_goals="Maintain high standards of patient care; grow leadership skills.", strengths=f"Strong {random.choice(['clinical skills', 'communication', 'teamwork'])}.", areas_for_improvement=("Leadership and mentoring junior staff." if overall < 4.5 else None), development_plan="Advanced training and leadership opportunities recommended.", training_recommendations=f"Suggested training: {random.choice(SAUDI_TRAINING_PROGRAMS)}", employee_comments=("Appreciate the feedback and committed to improvement." if status == 'ACKNOWLEDGED' else None), employee_signature_date=(review_date + timedelta(days=random.randint(1, 14)) if status == 'ACKNOWLEDGED' else None), status=status, notes=f"{review_type.title()} performance review completed", created_at=django_timezone.now() - timedelta(days=random.randint(1, 60)), updated_at=django_timezone.now() - timedelta(days=random.randint(0, 30)) ) reviews.append(r) except Exception as ex: print(f"Error creating performance review for {emp.get_full_name()}: {ex}") print(f"Created {len(reviews)} performance reviews") return reviews def create_training_programs(tenants, employees): """Create training programs for each tenant.""" programs = [] training_data = [ ('Orientation', 'ORIENTATION', 'New employee orientation program', 8, False, None), ('Fire Safety', 'SAFETY', 'Fire safety and emergency evacuation procedures', 2, False, None), ('Emergency Procedures', 'MANDATORY', 'Hospital emergency response procedures', 4, False, None), ('HIPAA Compliance', 'COMPLIANCE', 'Patient privacy and data protection training', 3, False, None), ('Patient Safety', 'MANDATORY', 'Patient safety protocols and best practices', 6, False, None), ('Basic Life Support (BLS)', 'CERTIFICATION', 'Basic life support certification', 8, True, 730), ('Advanced Cardiac Life Support (ACLS)', 'CERTIFICATION', 'Advanced cardiac life support certification', 16, True, 730), ('Pediatric Advanced Life Support (PALS)', 'CERTIFICATION', 'Pediatric advanced life support certification', 14, True, 730), ('Infection Control', 'SAFETY', 'Infection prevention and control measures', 4, False, None), ('Medication Administration', 'SKILLS', 'Safe medication administration practices', 6, False, None), ('Wound Care Management', 'SKILLS', 'Advanced wound care techniques', 8, False, None), ('IV Therapy', 'SKILLS', 'Intravenous therapy administration', 6, False, None), ('Pain Management', 'CONTINUING_ED', 'Pain assessment and management strategies', 4, False, None), ('Electronic Health Records', 'TECHNICAL', 'EHR system training and best practices', 4, False, None), ('Customer Service Excellence', 'OTHER', 'Patient and family service excellence', 3, False, None), ('Quality Improvement', 'CONTINUING_ED', 'Healthcare quality improvement methodologies', 6, False, None), ('Arabic Language', 'OTHER', 'Arabic language skills for healthcare', 20, False, None), ('Cultural Sensitivity', 'OTHER', 'Cultural competency in healthcare', 4, False, None), ('Islamic Healthcare Ethics', 'MANDATORY', 'Islamic principles in healthcare practice', 6, False, None), ('Leadership Development', 'LEADERSHIP', 'Leadership skills for healthcare managers', 12, False, None), ] providers = [ 'Saudi Healthcare Training Institute', 'Ministry of Health Training Center', 'King Fahd Medical Training Academy', 'Internal Training Department', 'Saudi Commission for Health Specialties' ] # Get potential instructors potential_instructors = [e for e in employees if e.employment_status == Employee.EmploymentStatus.ACTIVE and any(k in (e.job_title or '').lower() for k in ['senior', 'chief', 'manager', 'director', 'physician'])] for tenant in tenants: tenant_instructors = [e for e in potential_instructors if e.tenant == tenant] for name, ptype, description, hours, is_certified, validity_days in training_data: # Set program dates start_date = django_timezone.now().date() - timedelta(days=random.randint(30, 365)) end_date = start_date + timedelta(days=random.randint(30, 180)) if random.choice([True, False]) else None try: # Check if program already exists for this tenant existing = TrainingPrograms.objects.filter(tenant=tenant, name=name).first() if existing: print(f"Training program {name} already exists for {tenant.name}, skipping...") programs.append(existing) continue program = TrainingPrograms.objects.create( tenant=tenant, name=name, description=description, program_type=ptype, program_provider=random.choice(providers), instructor=random.choice(tenant_instructors) if tenant_instructors and random.choice([True, False]) else None, start_date=start_date, end_date=end_date, duration_hours=Decimal(str(hours)), cost=Decimal(str(random.randint(500, 5000))), is_certified=is_certified, validity_days=validity_days, notify_before_days=30 if is_certified else None, created_at=django_timezone.now() - timedelta(days=random.randint(30, 365)), updated_at=django_timezone.now() - timedelta(days=random.randint(0, 30)) ) programs.append(program) print(f"Created training program: {name} for {tenant.name}") except Exception as e: print(f"Error creating training program {name} for {tenant.name}: {e}") import traceback traceback.print_exc() print(f"Created {len(programs)} training programs") return programs def create_training_sessions(programs, employees): """Create training sessions for programs.""" sessions = [] # Get instructors (employees who could teach) potential_instructors = [e for e in employees if e.employment_status == Employee.EmploymentStatus.ACTIVE and any(k in (e.job_title or '').lower() for k in ['senior', 'chief', 'manager', 'director', 'physician'])] for program in programs: # Create 1-3 sessions per program num_sessions = random.randint(1, 3) for i in range(num_sessions): # Schedule sessions in the past and future days_offset = random.randint(-90, 90) start_date = django_timezone.now().date() + timedelta(days=days_offset) # Session duration based on program hours session_hours = program.duration_hours if session_hours <= 4: # Half day session start_time = time(9, 0) if random.choice([True, False]) else time(14, 0) end_time = time(13, 0) if start_time == time(9, 0) else time(18, 0) else: # Full day or multi-day start_time = time(9, 0) end_time = time(17, 0) if session_hours > 8: # Multi-day, adjust end date days_needed = int(session_hours / 8) + (1 if session_hours % 8 > 0 else 0) start_date = start_date start_datetime = django_timezone.make_aware(datetime.combine(start_date, start_time)) end_datetime = django_timezone.make_aware(datetime.combine(start_date, end_time)) # Adjust for multi-day sessions if session_hours > 8: days_needed = int(session_hours / 8) end_datetime = end_datetime + timedelta(days=days_needed) instructor = random.choice(potential_instructors) if potential_instructors else None try: session = TrainingSession.objects.create( program=program, title=f"{program.name} - Session {i+1}", instructor=instructor, delivery_method=random.choice(['IN_PERSON', 'VIRTUAL', 'HYBRID']), start_at=start_datetime, end_at=end_datetime, location=random.choice([ 'Training Room A', 'Training Room B', 'Conference Hall', 'Simulation Lab', 'Skills Lab', 'Auditorium' ]), capacity=random.randint(10, 50), created_at=django_timezone.now() - timedelta(days=random.randint(1, 30)) ) sessions.append(session) except Exception as e: print(f"Error creating session for {program.name}: {e}") print(f"Created {len(sessions)} training sessions") return sessions def create_training_records(employees, sessions): """Create training enrollment records for employees.""" records = [] # Define which roles need which training training_requirements = { 'ALL': ['Orientation', 'Fire Safety', 'Emergency Procedures', 'HIPAA Compliance', 'Patient Safety'], 'CLINICAL': ['Basic Life Support (BLS)', 'Infection Control', 'Medication Administration'], 'PHYSICIAN': ['Advanced Cardiac Life Support (ACLS)', 'Pediatric Advanced Life Support (PALS)'], 'NURSE': ['IV Therapy', 'Pain Management', 'Wound Care Management'], 'ADMIN': ['Customer Service Excellence', 'Quality Improvement', 'Arabic Language'], 'SUPPORT': ['Cultural Sensitivity', 'Customer Service Excellence'] } active_employees = [e for e in employees if e.employment_status == Employee.EmploymentStatus.ACTIVE] for emp in active_employees: # Determine required training based on role and department required_programs = set(training_requirements['ALL']) if emp.department: dtype = getattr(emp.department, 'department_type', '') if dtype == 'CLINICAL': required_programs.update(training_requirements['CLINICAL']) jt = (emp.job_title or '').lower() if 'physician' in jt: required_programs.update(training_requirements['PHYSICIAN']) elif 'nurse' in jt: required_programs.update(training_requirements['NURSE']) elif dtype == 'ADMINISTRATIVE': required_programs.update(training_requirements['ADMIN']) else: required_programs.update(training_requirements['SUPPORT']) # Add some random additional training all_program_names = [s.program.name for s in sessions] additional_programs = random.sample([p for p in all_program_names if p not in required_programs], min(3, len([p for p in all_program_names if p not in required_programs]))) required_programs.update(additional_programs) # Find sessions for required programs for program_name in required_programs: matching_sessions = [s for s in sessions if s.program.name == program_name and s.program.tenant == emp.tenant] if matching_sessions: session = random.choice(matching_sessions) # Determine enrollment status based on session timing now = django_timezone.now() if session.start_at < now - timedelta(days=7): status = random.choices(['COMPLETED', 'NO_SHOW', 'FAILED'], weights=[85, 10, 5])[0] elif session.start_at < now: status = random.choices(['IN_PROGRESS', 'COMPLETED'], weights=[30, 70])[0] else: status = random.choices(['SCHEDULED', 'WAITLISTED'], weights=[90, 10])[0] # Generate scores and completion data score = None passed = False completion_date = None expiry_date = None credits_earned = Decimal('0.00') if status == 'COMPLETED': score = Decimal(str(random.randint(70, 100))) passed = score >= 70 completion_date = session.end_at.date() credits_earned = session.program.duration_hours # Set expiry date for certified programs if session.program.is_certified and session.program.validity_days: expiry_date = completion_date + timedelta(days=session.program.validity_days) elif status == 'FAILED': score = Decimal(str(random.randint(40, 69))) passed = False completion_date = session.end_at.date() try: record = TrainingRecord.objects.create( employee=emp, program=session.program, session=session, started_at=session.start_at if status in ['IN_PROGRESS', 'COMPLETED', 'FAILED', 'NO_SHOW'] else None, completion_date=completion_date, expiry_date=expiry_date, status=status, credits_earned=credits_earned, score=score, passed=passed, notes=f"{session.program.name} - {status.replace('_', ' ').title()}", cost_paid=session.program.cost if random.choice([True, False]) else None, created_at=django_timezone.now() - timedelta(days=random.randint(1, 30)), updated_at=django_timezone.now() - timedelta(days=random.randint(0, 15)) ) records.append(record) except Exception as e: print(f"Error creating training record for {emp.get_full_name()}: {e}") print(f"Created {len(records)} training records") return records def create_program_modules(programs): """Create modules for training programs.""" modules = [] # Define module templates for different program types module_templates = { 'ORIENTATION': [ 'Welcome and Introduction', 'Company Policies and Procedures', 'Safety and Security', 'Benefits and HR Information', 'Department Overview', 'Q&A Session' ], 'BLS': [ 'Basic Life Support Overview', 'CPR Techniques', 'AED Usage', 'Choking Response', 'Hands-on Practice', 'Assessment and Certification' ], 'ACLS': [ 'Advanced Cardiac Life Support Overview', 'Cardiac Arrest Management', 'Arrhythmia Recognition', 'Pharmacology', 'Team Dynamics', 'Simulation Scenarios', 'Written and Practical Exam' ], 'INFECTION_CONTROL': [ 'Infection Prevention Principles', 'Hand Hygiene', 'Personal Protective Equipment', 'Isolation Precautions', 'Cleaning and Disinfection' ] } for program in programs: # Determine modules based on program name/type if 'BLS' in program.name: module_list = module_templates['BLS'] elif 'ACLS' in program.name: module_list = module_templates['ACLS'] elif 'Orientation' in program.name: module_list = module_templates['ORIENTATION'] elif 'Infection Control' in program.name: module_list = module_templates['INFECTION_CONTROL'] else: # Generic modules for other programs module_list = [ 'Introduction and Overview', 'Core Concepts', 'Practical Application', 'Assessment' ] # Create modules for this program total_hours = float(program.duration_hours) hours_per_module = total_hours / len(module_list) for order, title in enumerate(module_list, 1): try: module = ProgramModule.objects.create( program=program, title=title, order=order, hours=Decimal(str(round(hours_per_module, 2))) ) modules.append(module) except Exception as e: print(f"Error creating module {title} for {program.name}: {e}") print(f"Created {len(modules)} program modules") return modules def create_program_prerequisites(programs): """Create prerequisites between training programs.""" prerequisites = [] # Define prerequisite relationships prerequisite_rules = { 'Advanced Cardiac Life Support (ACLS)': ['Basic Life Support (BLS)'], 'Pediatric Advanced Life Support (PALS)': ['Basic Life Support (BLS)'], 'Wound Care Management': ['Infection Control'], 'IV Therapy': ['Medication Administration'], 'Pain Management': ['Medication Administration'], 'Leadership Development': ['Customer Service Excellence'] } # Group programs by tenant for easier lookup programs_by_tenant = {} for program in programs: if program.tenant not in programs_by_tenant: programs_by_tenant[program.tenant] = {} programs_by_tenant[program.tenant][program.name] = program for tenant, tenant_programs in programs_by_tenant.items(): for program_name, required_program_names in prerequisite_rules.items(): if program_name in tenant_programs: program = tenant_programs[program_name] for required_name in required_program_names: if required_name in tenant_programs: required_program = tenant_programs[required_name] try: prerequisite = ProgramPrerequisite.objects.create( program=program, required_program=required_program ) prerequisites.append(prerequisite) except Exception as e: print(f"Error creating prerequisite {required_name} -> {program_name}: {e}") print(f"Created {len(prerequisites)} program prerequisites") return prerequisites def create_training_attendance(training_records): """Create attendance records for training sessions.""" attendance_records = [] # Only create attendance for records that have started started_records = [r for r in training_records if r.started_at and r.status in ['IN_PROGRESS', 'COMPLETED', 'FAILED', 'NO_SHOW']] for record in started_records: # Determine attendance status based on training record status if record.status == 'NO_SHOW': attendance_status = 'ABSENT' elif record.status == 'COMPLETED': attendance_status = random.choices(['PRESENT', 'LATE'], weights=[90, 10])[0] elif record.status == 'FAILED': attendance_status = random.choices(['PRESENT', 'LATE', 'ABSENT'], weights=[70, 20, 10])[0] else: # IN_PROGRESS attendance_status = random.choices(['PRESENT', 'LATE'], weights=[85, 15])[0] # Set check-in/out times based on session times and attendance status session = record.session checked_in_at = None checked_out_at = None if attendance_status in ['PRESENT', 'LATE']: if attendance_status == 'LATE': # Late arrival (5-30 minutes after start) late_minutes = random.randint(5, 30) checked_in_at = session.start_at + timedelta(minutes=late_minutes) else: # On time or early arrival early_minutes = random.randint(-10, 5) checked_in_at = session.start_at + timedelta(minutes=early_minutes) # Check out time (if session is completed) if record.status in ['COMPLETED', 'FAILED']: checkout_variance = random.randint(-15, 15) checked_out_at = session.end_at + timedelta(minutes=checkout_variance) notes = None if attendance_status == 'LATE': notes = f"Arrived {late_minutes} minutes late" elif attendance_status == 'ABSENT': notes = "Did not attend session" elif attendance_status == 'EXCUSED': notes = "Excused absence" try: attendance = TrainingAttendance.objects.create( enrollment=record, checked_in_at=checked_in_at, checked_out_at=checked_out_at, status=attendance_status, notes=notes ) attendance_records.append(attendance) except Exception as e: print(f"Error creating attendance for {record.employee.get_full_name()}: {e}") print(f"Created {len(attendance_records)} attendance records") return attendance_records def create_training_assessments(training_records): """Create assessments for training records.""" assessments = [] # Only create assessments for completed or failed records assessed_records = [r for r in training_records if r.status in ['COMPLETED', 'FAILED'] and r.score is not None] assessment_types = { 'CERTIFICATION': ['Written Exam', 'Practical Assessment'], 'SKILLS': ['Practical Assessment', 'Skills Demonstration'], 'MANDATORY': ['Knowledge Check'], 'SAFETY': ['Safety Quiz'], 'COMPLIANCE': ['Compliance Test'], 'OTHER': ['Final Assessment'] } for record in assessed_records: program_type = record.program.program_type possible_assessments = assessment_types.get(program_type, ['Final Assessment']) # Create 1-2 assessments per record num_assessments = random.randint(1, min(2, len(possible_assessments))) selected_assessments = random.sample(possible_assessments, num_assessments) for assessment_name in selected_assessments: # Determine max score based on assessment type if 'Practical' in assessment_name: max_score = Decimal('100.00') elif 'Written' in assessment_name or 'Exam' in assessment_name: max_score = Decimal('100.00') else: max_score = Decimal('50.00') # Generate score based on training record score base_score = float(record.score) if record.score else 70 # Add some variance to the assessment score score_variance = random.randint(-10, 10) assessment_score = max(0, min(float(max_score), base_score + score_variance)) passed = assessment_score >= (float(max_score) * 0.7) # 70% passing # Set taken date around the completion date if record.completion_date: taken_at = django_timezone.make_aware( datetime.combine(record.completion_date, time(random.randint(9, 17), random.randint(0, 59))) ) else: taken_at = record.session.end_at try: assessment = TrainingAssessment.objects.create( enrollment=record, name=assessment_name, max_score=max_score, score=Decimal(str(assessment_score)), passed=passed, taken_at=taken_at, notes=f"{assessment_name} for {record.program.name}" ) assessments.append(assessment) except Exception as e: print(f"Error creating assessment for {record.employee.get_full_name()}: {e}") print(f"Created {len(assessments)} training assessments") return assessments def create_training_certificates(training_records, employees): """Create certificates for completed certified training.""" certificates = [] # Only create certificates for completed certified programs eligible_records = [r for r in training_records if r.status == 'COMPLETED' and r.passed and r.program.is_certified] # Get potential signers (senior staff) potential_signers = [e for e in employees if e.employment_status == Employee.EmploymentStatus.ACTIVE and any(k in (e.job_title or '').lower() for k in ['chief', 'director', 'manager'])] for record in eligible_records: # Generate certificate details certificate_name = f"{record.program.name} Certificate" certificate_number = f"CERT-{record.program.tenant.id}-{random.randint(100000, 999999)}" # Determine certification body based on program type if 'BLS' in record.program.name or 'ACLS' in record.program.name or 'PALS' in record.program.name: certification_body = 'Saudi Heart Association' elif record.program.program_type == 'SAFETY': certification_body = 'Saudi Occupational Safety and Health Administration' elif record.program.program_type == 'COMPLIANCE': certification_body = 'Saudi Ministry of Health' else: certification_body = 'Saudi Commission for Health Specialties' # Set expiry date expiry_date = None if record.program.validity_days: expiry_date = record.completion_date + timedelta(days=record.program.validity_days) # Select signer from same tenant tenant_signers = [s for s in potential_signers if s.tenant == record.employee.tenant] signer_employee = random.choice(tenant_signers) if tenant_signers else None signer_user = signer_employee.user if signer_employee else None try: certificate = TrainingCertificates.objects.create( program=record.program, employee=record.employee, enrollment=record, certificate_name=certificate_name, certificate_number=certificate_number, certification_body=certification_body, expiry_date=expiry_date, signed_by=signer_user, created_by=signer_user, created_at=django_timezone.now() - timedelta(days=random.randint(0, 7)), updated_at=django_timezone.now() - timedelta(days=random.randint(0, 3)) ) certificates.append(certificate) print(f"Created certificate {certificate_number} for {record.employee.get_full_name()}") except Exception as e: print(f"Error creating certificate for {record.employee.get_full_name()}: {e}") import traceback traceback.print_exc() print(f"Created {len(certificates)} training certificates") return certificates # ---------------------------- # Leave Management # ---------------------------- def create_leave_types(tenants): """Create standard leave types for each tenant.""" leave_types = [] # Saudi-specific leave types with configurations leave_type_configs = [ { 'name': 'Annual Leave', 'code': 'AL', 'description': 'Annual vacation leave', 'is_paid': True, 'requires_approval': True, 'requires_documentation': False, 'accrual_method': 'ANNUAL', 'annual_entitlement': Decimal('21.00'), 'max_carry_over': Decimal('5.00'), 'max_consecutive_days': 21, 'min_notice_days': 7, 'gender_specific': None, }, { 'name': 'Sick Leave', 'code': 'SL', 'description': 'Medical sick leave', 'is_paid': True, 'requires_approval': True, 'requires_documentation': True, 'accrual_method': 'ANNUAL', 'annual_entitlement': Decimal('10.00'), 'max_carry_over': Decimal('0.00'), 'max_consecutive_days': None, 'min_notice_days': 0, 'gender_specific': None, }, { 'name': 'Emergency Leave', 'code': 'EL', 'description': 'Emergency personal leave', 'is_paid': True, 'requires_approval': True, 'requires_documentation': False, 'accrual_method': 'ANNUAL', 'annual_entitlement': Decimal('5.00'), 'max_carry_over': Decimal('0.00'), 'max_consecutive_days': 3, 'min_notice_days': 0, 'gender_specific': None, }, { 'name': 'Maternity Leave', 'code': 'ML', 'description': 'Maternity leave for childbirth', 'is_paid': True, 'requires_approval': True, 'requires_documentation': True, 'accrual_method': 'NONE', 'annual_entitlement': Decimal('70.00'), 'max_carry_over': Decimal('0.00'), 'max_consecutive_days': 70, 'min_notice_days': 30, 'gender_specific': Employee.Gender.FEMALE, }, { 'name': 'Paternity Leave', 'code': 'PL', 'description': 'Paternity leave for childbirth', 'is_paid': True, 'requires_approval': True, 'requires_documentation': True, 'accrual_method': 'NONE', 'annual_entitlement': Decimal('3.00'), 'max_carry_over': Decimal('0.00'), 'max_consecutive_days': 3, 'min_notice_days': 7, 'gender_specific': Employee.Gender.MALE, }, { 'name': 'Hajj Leave', 'code': 'HL', 'description': 'Leave for Hajj pilgrimage', 'is_paid': True, 'requires_approval': True, 'requires_documentation': False, 'accrual_method': 'NONE', 'annual_entitlement': Decimal('10.00'), 'max_carry_over': Decimal('0.00'), 'max_consecutive_days': 10, 'min_notice_days': 30, 'gender_specific': None, }, { 'name': 'Bereavement Leave', 'code': 'BL', 'description': 'Leave for family bereavement', 'is_paid': True, 'requires_approval': True, 'requires_documentation': False, 'accrual_method': 'NONE', 'annual_entitlement': Decimal('3.00'), 'max_carry_over': Decimal('0.00'), 'max_consecutive_days': 3, 'min_notice_days': 0, 'gender_specific': None, }, ] for tenant in tenants: for config in leave_type_configs: try: # Check if leave type already exists existing = LeaveType.objects.filter(tenant=tenant, code=config['code']).first() if existing: leave_types.append(existing) continue leave_type = LeaveType.objects.create( tenant=tenant, **config, is_active=True, available_for_all=True, created_at=django_timezone.now() - timedelta(days=random.randint(30, 365)), updated_at=django_timezone.now() - timedelta(days=random.randint(0, 30)) ) leave_types.append(leave_type) print(f"Created leave type: {config['name']} for {tenant.name}") except Exception as e: print(f"Error creating leave type {config['name']} for {tenant.name}: {e}") print(f"Created {len(leave_types)} leave types") return leave_types def create_leave_balances(employees, leave_types): """Create leave balances for all active employees.""" balances = [] current_year = django_timezone.now().year active_employees = [e for e in employees if e.employment_status == Employee.EmploymentStatus.ACTIVE] for emp in active_employees: # Get leave types for this tenant tenant_leave_types = [lt for lt in leave_types if lt.tenant == emp.tenant] for leave_type in tenant_leave_types: # Skip gender-specific leave types if not applicable if leave_type.gender_specific and leave_type.gender_specific != emp.gender: continue try: # Check if balance already exists existing = LeaveBalance.objects.filter( employee=emp, leave_type=leave_type, year=current_year ).first() if existing: balances.append(existing) continue # Calculate entitlement based on hire date and accrual method if emp.hire_date: months_employed = ((current_year - emp.hire_date.year) * 12 + (12 - emp.hire_date.month + 1)) if leave_type.accrual_method == 'PRORATED' and emp.hire_date.year == current_year: # Pro-rate based on months employed accrued = (leave_type.annual_entitlement / 12) * min(months_employed, 12) elif leave_type.accrual_method == 'MONTHLY': # Monthly accrual accrued = (leave_type.annual_entitlement / 12) * min(months_employed, 12) else: # Annual allocation accrued = leave_type.annual_entitlement else: accrued = leave_type.annual_entitlement # Opening balance (carry-over from previous year) opening_balance = Decimal('0.00') if leave_type.max_carry_over > 0 and random.choice([True, False]): opening_balance = Decimal(str(random.uniform(0, float(leave_type.max_carry_over)))) # Used balance (some employees have already used leave) max_used = min(float(accrued + opening_balance), float(leave_type.annual_entitlement)) used = Decimal(str(random.uniform(0, max_used * 0.6))) # Up to 60% used # Pending balance (some requests pending approval) max_pending = float(accrued + opening_balance - used) pending = Decimal(str(random.uniform(0, min(max_pending * 0.2, 5)))) # Up to 20% pending balance = LeaveBalance.objects.create( employee=emp, leave_type=leave_type, year=current_year, opening_balance=opening_balance, accrued=accrued, used=used, pending=pending, adjusted=Decimal('0.00'), last_accrual_date=django_timezone.now().date(), created_at=django_timezone.now() - timedelta(days=random.randint(30, 365)), updated_at=django_timezone.now() - timedelta(days=random.randint(0, 30)) ) balances.append(balance) except Exception as e: print(f"Error creating leave balance for {emp.get_full_name()}: {e}") print(f"Created {len(balances)} leave balances") return balances def create_leave_requests(employees, leave_types, balances): """Create realistic leave requests for employees.""" requests = [] active_employees = [e for e in employees if e.employment_status == Employee.EmploymentStatus.ACTIVE] for emp in active_employees: # Each employee gets 2-5 leave requests (past, current, future) num_requests = random.randint(2, 5) # Get employee's balances emp_balances = [b for b in balances if b.employee == emp] for _ in range(num_requests): if not emp_balances: continue # Select a leave type with available balance balance = random.choice(emp_balances) leave_type = balance.leave_type # Generate request dates (mix of past, current, future) days_offset = random.randint(-180, 180) start_date = django_timezone.now().date() + timedelta(days=days_offset) # Duration based on leave type if leave_type.code == 'ML': duration = random.randint(60, 70) elif leave_type.code in ['PL', 'BL', 'EL']: duration = random.randint(1, 3) elif leave_type.code == 'HL': duration = random.randint(7, 10) else: duration = random.randint(1, min(14, int(balance.available))) end_date = start_date + timedelta(days=duration - 1) # Day types (mostly full days) start_day_type = random.choices( ['FULL_DAY', 'HALF_DAY_AM', 'HALF_DAY_PM'], weights=[85, 10, 5] )[0] end_day_type = 'FULL_DAY' if duration > 1 else start_day_type # Calculate total days total_days = Decimal(str(duration)) if start_day_type != 'FULL_DAY': total_days -= Decimal('0.5') if end_day_type != 'FULL_DAY' and duration > 1: total_days -= Decimal('0.5') # Status based on date if start_date < django_timezone.now().date() - timedelta(days=30): status = random.choices( ['APPROVED', 'REJECTED', 'CANCELLED'], weights=[70, 20, 10] )[0] elif start_date < django_timezone.now().date(): status = random.choices( ['APPROVED', 'PENDING', 'REJECTED'], weights=[80, 15, 5] )[0] else: status = random.choices( ['PENDING', 'APPROVED', 'DRAFT'], weights=[60, 30, 10] )[0] # Reason reasons = { 'AL': ['Family vacation', 'Personal time off', 'Rest and relaxation', 'Travel plans'], 'SL': ['Medical treatment', 'Doctor appointment', 'Recovery from illness', 'Medical procedure'], 'EL': ['Family emergency', 'Urgent personal matter', 'Unexpected situation'], 'ML': ['Maternity leave for childbirth'], 'PL': ['Paternity leave for childbirth'], 'HL': ['Hajj pilgrimage'], 'BL': ['Family bereavement'], } reason = random.choice(reasons.get(leave_type.code, ['Personal leave'])) try: request = LeaveRequest.objects.create( employee=emp, leave_type=leave_type, start_date=start_date, end_date=end_date, start_day_type=start_day_type, end_day_type=end_day_type, total_days=total_days, reason=reason, contact_number=emp.mobile_phone if random.choice([True, False]) else None, emergency_contact=emp.emergency_contact_name if random.choice([True, False]) else None, status=status, submitted_at=django_timezone.now() - timedelta(days=abs(days_offset) + random.randint(1, 7)) if status != 'DRAFT' else None, current_approver=emp.supervisor if status == 'PENDING' else None, final_approver=emp.supervisor if status == 'APPROVED' else None, approved_at=django_timezone.now() - timedelta(days=abs(days_offset) + random.randint(1, 5)) if status == 'APPROVED' else None, rejected_at=django_timezone.now() - timedelta(days=abs(days_offset) + random.randint(1, 5)) if status == 'REJECTED' else None, rejection_reason='Insufficient staffing during requested period' if status == 'REJECTED' else None, cancelled_at=django_timezone.now() - timedelta(days=random.randint(1, 10)) if status == 'CANCELLED' else None, cancellation_reason='Personal circumstances changed' if status == 'CANCELLED' else None, created_at=django_timezone.now() - timedelta(days=abs(days_offset) + random.randint(7, 14)), updated_at=django_timezone.now() - timedelta(days=random.randint(0, 7)) ) requests.append(request) except Exception as e: print(f"Error creating leave request for {emp.get_full_name()}: {e}") print(f"Created {len(requests)} leave requests") return requests def create_leave_approvals(leave_requests, employees): """Create approval records for processed leave requests.""" approvals = [] # Only create approvals for non-draft requests processed_requests = [r for r in leave_requests if r.status != 'DRAFT'] for request in processed_requests: # Level 1 approval (supervisor) if request.employee.supervisor: action = 'APPROVED' if request.status == 'APPROVED' else 'REJECTED' if request.status == 'REJECTED' else 'PENDING' try: approval = LeaveApproval.objects.create( leave_request=request, level=1, approver=request.employee.supervisor, action=action, comments=f"{'Approved' if action == 'APPROVED' else 'Rejected' if action == 'REJECTED' else 'Pending review'} - Level 1", action_date=request.approved_at or request.rejected_at if action != 'PENDING' else None, created_at=request.submitted_at or django_timezone.now(), updated_at=django_timezone.now() ) approvals.append(approval) except Exception as e: print(f"Error creating approval for request {request.request_id}: {e}") print(f"Created {len(approvals)} leave approvals") return approvals def create_leave_delegations(employees): """Create delegation records for supervisors.""" delegations = [] # Get supervisors (employees who have direct reports) supervisors = [e for e in employees if e.employment_status == Employee.EmploymentStatus.ACTIVE and any(k in (e.job_title or '').lower() for k in ['manager', 'director', 'chief', 'head', 'supervisor'])] for supervisor in supervisors: # 30% chance of having a delegation if random.random() > 0.3: continue # Find potential delegates (peers or senior staff in same department) potential_delegates = [e for e in employees if e.employment_status == Employee.EmploymentStatus.ACTIVE and e.department == supervisor.department and e.id != supervisor.id and any(k in (e.job_title or '').lower() for k in ['senior', 'manager', 'supervisor'])] if not potential_delegates: continue delegate = random.choice(potential_delegates) # Delegation period (past, current, or future) days_offset = random.randint(-90, 90) start_date = django_timezone.now().date() + timedelta(days=days_offset) duration = random.randint(7, 30) end_date = start_date + timedelta(days=duration) # Status is_active = start_date <= django_timezone.now().date() <= end_date reasons = [ 'Annual leave coverage', 'Business travel', 'Training program attendance', 'Temporary assignment', 'Medical leave coverage' ] try: delegation = LeaveDelegate.objects.create( delegator=supervisor, delegate=delegate, start_date=start_date, end_date=end_date, reason=random.choice(reasons), is_active=is_active, created_at=django_timezone.now() - timedelta(days=abs(days_offset) + random.randint(7, 14)), updated_at=django_timezone.now() - timedelta(days=random.randint(0, 7)) ) delegations.append(delegation) except Exception as e: print(f"Error creating delegation for {supervisor.get_full_name()}: {e}") print(f"Created {len(delegations)} leave delegations") return delegations # ---------------------------- # Salary and Document Management # ---------------------------- def create_salary_information(employees): """Create salary records for active employees.""" salary_records = [] active_employees = [e for e in employees if e.employment_status == Employee.EmploymentStatus.ACTIVE and e.hire_date] for emp in active_employees: # Determine salary based on job title and department jt_lower = (emp.job_title or '').lower() # Base salary ranges (SAR per month) if 'chief' in jt_lower or 'director' in jt_lower: basic_salary = Decimal(str(random.randint(25000, 45000))) elif 'consultant' in jt_lower or 'senior physician' in jt_lower: basic_salary = Decimal(str(random.randint(20000, 35000))) elif 'physician' in jt_lower or 'doctor' in jt_lower: basic_salary = Decimal(str(random.randint(15000, 25000))) elif 'manager' in jt_lower: basic_salary = Decimal(str(random.randint(12000, 22000))) elif 'pharmacist' in jt_lower or 'radiologist' in jt_lower: basic_salary = Decimal(str(random.randint(10000, 18000))) elif 'nurse practitioner' in jt_lower or 'senior nurse' in jt_lower: basic_salary = Decimal(str(random.randint(9000, 15000))) elif 'nurse' in jt_lower: basic_salary = Decimal(str(random.randint(6000, 12000))) elif 'technician' in jt_lower or 'technologist' in jt_lower: basic_salary = Decimal(str(random.randint(5000, 10000))) else: basic_salary = Decimal(str(random.randint(4000, 8000))) # Allowances (typical Saudi structure) housing_allowance = basic_salary * Decimal('0.25') # 25% of basic transportation_allowance = Decimal(str(random.randint(500, 1500))) food_allowance = Decimal(str(random.randint(300, 800))) # Other allowances (JSON field) other_allowances = {} if random.choice([True, False]): other_allowances['mobile_allowance'] = float(random.randint(200, 500)) if 'manager' in jt_lower or 'director' in jt_lower or 'chief' in jt_lower: other_allowances['management_allowance'] = float(random.randint(2000, 5000)) # Bank details bank_names = ['Al Rajhi Bank', 'Saudi National Bank', 'Riyad Bank', 'Al Bilad Bank', 'Bank AlJazira'] bank_name = random.choice(bank_names) account_number = f"{random.randint(1000000000, 9999999999)}" iban = f"SA{random.randint(10, 99)}{random.randint(10000000000000000000, 99999999999999999999)}" # Effective date (hire date or later) effective_date = emp.hire_date try: salary = SalaryInformation.objects.create( employee=emp, basic_salary=basic_salary, housing_allowance=housing_allowance, transportation_allowance=transportation_allowance, food_allowance=food_allowance, other_allowances=other_allowances, currency='SAR', payment_frequency='MONTHLY', bank_name=bank_name, account_number=account_number, iban=iban, effective_date=effective_date, is_active=True, created_by=emp.user ) salary_records.append(salary) except Exception as e: print(f"Error creating salary for {emp.get_full_name()}: {e}") print(f"Created {len(salary_records)} salary records") return salary_records def create_salary_adjustments(employees, salary_records): """Create salary adjustment records for some employees.""" adjustments = [] # 30% of employees get salary adjustments eligible_employees = random.sample( [e for e in employees if e.employment_status == Employee.EmploymentStatus.ACTIVE], int(len([e for e in employees if e.employment_status == Employee.EmploymentStatus.ACTIVE]) * 0.3) ) for emp in eligible_employees: # Get employee's salary records emp_salaries = [s for s in salary_records if s.employee == emp] if len(emp_salaries) < 1: continue # Create a new salary record (adjustment) previous_salary = emp_salaries[0] # Adjustment type and percentage adjustment_type = random.choice([ 'ANNUAL_INCREMENT', 'PROMOTION', 'MERIT_INCREASE', 'COST_OF_LIVING', 'MARKET_ADJUSTMENT' ]) if adjustment_type == 'PROMOTION': increase_pct = Decimal(str(random.uniform(15, 30))) elif adjustment_type == 'ANNUAL_INCREMENT': increase_pct = Decimal(str(random.uniform(3, 8))) elif adjustment_type == 'MERIT_INCREASE': increase_pct = Decimal(str(random.uniform(5, 15))) else: increase_pct = Decimal(str(random.uniform(2, 10))) # Calculate new salary new_basic = previous_salary.basic_salary * (Decimal('1') + increase_pct / Decimal('100')) new_housing = new_basic * Decimal('0.25') # Adjustment date (3-12 months after hire) adjustment_date = previous_salary.effective_date + timedelta(days=random.randint(90, 365)) try: # Create new salary record new_salary = SalaryInformation.objects.create( employee=emp, basic_salary=new_basic, housing_allowance=new_housing, transportation_allowance=previous_salary.transportation_allowance, food_allowance=previous_salary.food_allowance, other_allowances=previous_salary.other_allowances, currency=previous_salary.currency, payment_frequency=previous_salary.payment_frequency, bank_name=previous_salary.bank_name, account_number=previous_salary.account_number, iban=previous_salary.iban, effective_date=adjustment_date, is_active=True, created_by=emp.user ) # Deactivate previous salary previous_salary.is_active = False previous_salary.end_date = adjustment_date previous_salary.save() # Create adjustment record adjustment = SalaryAdjustment.objects.create( employee=emp, previous_salary=previous_salary, new_salary=new_salary, adjustment_type=adjustment_type, effective_date=adjustment_date, adjustment_reason=f"{adjustment_type.replace('_', ' ').title()} - Performance and market review", approved_by=emp.supervisor.user if emp.supervisor else None, approval_date=django_timezone.now() - timedelta(days=random.randint(1, 30)) ) adjustments.append(adjustment) salary_records.append(new_salary) except Exception as e: print(f"Error creating salary adjustment for {emp.get_full_name()}: {e}") print(f"Created {len(adjustments)} salary adjustments") return adjustments def create_document_templates(tenants): """Create document templates for each tenant.""" templates = [] template_configs = [ { 'name': 'Salary Certificate - English', 'document_type': 'SALARY_CERTIFICATE', 'language': 'EN', 'description': 'Standard salary certificate in English', 'template_content': '''
Date: {{current_date}}
To Whom It May Concern,
This is to certify that {{employee_name}} (ID: {{employee_id}}) is employed with {{company_name}} as {{job_title}} in the {{department}} department since {{hire_date}}.
The employee's current monthly salary breakdown is as follows:
This certificate is issued upon the employee's request for official purposes.
''', 'header_content': '{{company_address}}
HR Department
Date: {{current_date}}
To Whom It May Concern,
This is to certify that {{employee_name}} (ID: {{employee_id}}) has been employed with {{company_name}} since {{hire_date}}.
Current Position: {{job_title}}
Department: {{department}}
This certificate is issued upon the employee's request for official purposes.
''', 'header_content': '{{company_address}}
HR Department