import random import uuid from datetime import datetime, date, time, timedelta from collections import defaultdict from django.core.management.base import BaseCommand, CommandError from django.db import transaction, IntegrityError from django.db.models import Q from django.conf import settings from django.utils import timezone # Import your models from core.models import Tenant from operating_theatre.models import ( OperatingRoom, ORBlock, SurgicalCase, SurgicalNote, EquipmentUsage, SurgicalNoteTemplate ) from patients.models import PatientProfile from emr.models import Encounter from inpatients.models import Admission from django.contrib.auth import get_user_model User = get_user_model() # ---------- Saudi-influenced constants (English only) ---------- SAUDI_BUILDINGS = [ "King Abdulaziz Tower A", "King Abdulaziz Tower B", "Riyadh Medical Pavilion", "Jeddah Surgical Center", "Makkah Clinical Wing", "Dammam Health Complex", "Madina Specialty Institute", "Qassim Medical Block", ] SAUDI_WINGS = ["North Wing", "South Wing", "East Wing", "West Wing", "Royal Wing"] # Services / specialties mapped to common procedure examples SERVICE_TO_PROCEDURES = { "GENERAL": [ ("Laparoscopic Cholecystectomy", ["SILS Appendectomy"], ["47562"]), ("Open Appendectomy", ["Diagnostic Laparoscopy"], ["44950"]), ("Inguinal Hernia Repair", ["Umbilical Hernia Repair"], ["49505"]), ], "CARDIAC": [ ("CABG x3", ["Saphenous Vein Harvest"], ["33512"]), ("Mitral Valve Repair", ["Left Atrial Appendage Closure"], ["33426"]), ("Aortic Valve Replacement", ["Ascending Aorta Repair"], ["33405"]), ], "NEURO": [ ("Craniotomy for Tumor", ["External Ventricular Drain"], ["61510"]), ("Lumbar Microdiscectomy", ["Foraminotomy"], ["63030"]), ("Aneurysm Clipping", ["Intraop Angiography"], ["61697"]), ], "ORTHOPEDIC": [ ("Total Knee Arthroplasty", ["Patellar Resurfacing"], ["27447"]), ("Hip Hemiarthroplasty", ["Acetabular Revision"], ["27125"]), ("ACL Reconstruction", ["Meniscectomy"], ["29888"]), ], "TRAUMA": [ ("Open Reduction Internal Fixation - Tibia", ["External Fixator Removal"], ["27758"]), ("Exploratory Laparotomy", ["Splenectomy"], ["49000"]), ("Thoracotomy for Hemothorax", ["Rib Fixation"], ["32100"]), ], "PEDIATRIC": [ ("Pyloromyotomy", ["Umbilical Hernia Repair"], ["43520"]), ("Orchiopexy", ["Inguinal Hernia Repair"], ["54640"]), ("Tonsillectomy & Adenoidectomy", ["Myringotomy"], ["42820"]), ], "OBSTETRIC": [ ("Emergency Cesarean Section", ["B-Lynch Suture"], ["59510"]), ("Elective Cesarean Section", ["Tubal Ligation"], ["59514"]), ("Manual Removal of Placenta", ["Uterine Curettage"], ["59414"]), ], "OPHTHALMOLOGY": [ ("Phacoemulsification with IOL", ["Trabeculectomy"], ["66984"]), ("Retinal Detachment Repair", ["Laser Photocoagulation"], ["67108"]), ("Strabismus Surgery", ["Adjustable Sutures"], ["67311"]), ], "ENT": [ ("Functional Endoscopic Sinus Surgery", ["Septoplasty"], ["31253"]), ("Microlaryngoscopy", ["Polypectomy"], ["31526"]), ("Tympanoplasty", ["Mastoidectomy"], ["69631"]), ], "UROLOGY": [ ("TURP", ["Cystoscopy"], ["52601"]), ("PCNL", ["DJ Stent Placement"], ["50080"]), ("Laparoscopic Nephrectomy", ["Adrenalectomy"], ["50545"]), ], "PLASTIC": [ ("Split-Thickness Skin Graft", ["Debridement"], ["15100"]), ("Breast Reconstruction (DIEP)", ["Implant Exchange"], ["19357"]), ("Cleft Lip Repair", ["Cleft Palate Repair"], ["40701"]), ], "VASCULAR": [ ("AV Fistula Creation", ["Angioplasty"], ["36821"]), ("Carotid Endarterectomy", ["Patch Angioplasty"], ["35301"]), ("EVAR", ["Iliac Extension"], ["34802"]), ], "THORACIC": [ ("VATS Lobectomy", ["Lymph Node Dissection"], ["32663"]), ("Esophagectomy", ["Feeding Jejunostomy"], ["43107"]), ("Mediastinoscopy", ["Bronchoscopy"], ["39402"]), ], "TRANSPLANT": [ ("Living Donor Kidney Transplant", ["Ureteric Stent"], ["50360"]), ("Liver Transplant", ["Biliary Reconstruction"], ["47135"]), ("Pancreas Transplant", ["Enteric Drainage"], ["48554"]), ], } ROOM_FEATURES = [ "Laminar Flow", "HEPA Filtration", "Lead-Lined Walls", "Integrated Boom", "Ceiling-Mounted Monitors", "Shadowless Lighting", "Dedicated Anesthesia Column", "Sterile Core Access", "Hybrid Imaging Suite", "Negative Pressure Anteroom" ] EQUIPMENT_BANK = [ # (name, type, manufacturer, model) ("Dräger Anesthesia Machine", "ANESTHESIA_MACHINE", "Dräger", "Fabius Plus"), ("GE Patient Monitor", "MONITORING_DEVICE", "GE Healthcare", "CARESCAPE B650"), ("Valleylab Electrocautery", "ELECTROCAUTERY", "Medtronic", "ForceTriad"), ("Karl Storz Laparoscopy Set", "SURGICAL_INSTRUMENT", "Karl Storz", "Image1 S"), ("Stryker 1688 AIM Camera", "SURGICAL_INSTRUMENT", "Stryker", "1688 AIM"), ("Zeiss Surgical Microscope", "MICROSCOPE", "Zeiss", "OPMI Pentero"), ("C-Arm Fluoroscopy", "C_ARM", "Siemens", "Cios Fusion"), ("Portable Ultrasound", "ULTRASOUND", "Mindray", "M9"), ("Da Vinci Surgical Robot", "ROBOT", "Intuitive Surgical", "Xi"), ] BLOOD_PRODUCTS = ["PRBC", "FFP", "Platelets", "Cryoprecipitate"] IMPLANT_EXAMPLES = ["Titanium Plate", "Pedicle Screws", "Hemashield Patch", "Stent Graft", "Knee Prosthesis"] DIAGNOSES = [ ("Cholelithiasis with Cholecystitis", ["K80.10"]), ("Degenerative Joint Disease Knee", ["M17.10"]), ("Intracranial Neoplasm", ["D49.6"]), ("Coronary Artery Disease", ["I25.10"]), ("Ureteric Calculus", ["N20.1"]), ("Pelvic Organ Prolapse", ["N81.4"]), ("Retinal Detachment", ["H33.2"]), ("Carotid Stenosis", ["I65.21"]), ] # Operating hours (typical KSA elective schedule) and emergency window ELECTIVE_START = time(7, 30) ELECTIVE_END = time(15, 30) EMERGENCY_START = time(16, 0) EMERGENCY_END = time(22, 0) # ---------- Helpers ---------- def choose_staff(candidates, k=1, exclude=None): qs = candidates if exclude: qs = qs.exclude(id__in=[u.id for u in exclude]) lst = list(qs) if not lst: return [] random.shuffle(lst) return lst[:k] def dt_in_ksa(d, t): """Return a timezone-aware datetime in the current Django timezone (e.g., Asia/Riyadh).""" tz = timezone.get_current_timezone() naive_dt = datetime.combine(d, t) # If Django gave us an aware time elsewhere, be safe: if timezone.is_naive(naive_dt): return timezone.make_aware(naive_dt, tz) return naive_dt.astimezone(tz) def minutes_between(t1: time, t2: time): d1 = datetime.combine(date.today(), t1) d2 = datetime.combine(date.today(), t2) if d2 < d1: d2 += timedelta(days=1) return int((d2 - d1).total_seconds() // 60) def safe_get_encounter(patient): # Prefer start_datetime; fall back to created_at, then id return ( Encounter.objects .filter(patient=patient) .order_by('-start_datetime', '-created_at', '-id') .first() ) def safe_get_admission(patient): # We don’t know your Admission timestamp field; use created_at/id as stable fallbacks return ( Admission.objects .filter(patient=patient) .order_by('-created_at', '-id') .first() ) def probable_case_type(service: str): # Slight emergency skew for Trauma/Transplant/Obstetric if service in {"TRAUMA", "TRANSPLANT", "OBSTETRIC"}: return random.choices( ["EMERGENCY", "URGENT", "ELECTIVE", "TRAUMA"], weights=[40, 25, 20, 15], k=1 )[0] return random.choices( ["ELECTIVE", "URGENT", "EMERGENCY"], weights=[70, 20, 10], k=1 )[0] def pick_approach(service: str): options = ["OPEN", "LAPAROSCOPIC", "ROBOTIC", "ENDOSCOPIC", "PERCUTANEOUS", "HYBRID"] if service in {"GENERAL", "UROLOGY", "GYNE", "OBSTETRIC"}: weights = [25, 50, 10, 10, 3, 2] elif service in {"ORTHOPEDIC", "NEURO", "CARDIAC", "THORACIC"}: weights = [55, 10, 15, 5, 5, 10] else: weights = [50, 15, 10, 15, 5, 5] return random.choices(options, weights=weights, k=1)[0] def pick_anesthesia(): return random.choice(["GENERAL", "REGIONAL", "LOCAL", "SEDATION", "SPINAL", "EPIDURAL", "COMBINED"]) def pick_position(service: str): mapping = { "GENERAL": ["SUPINE", "LITHOTOMY", "TRENDELENBURG", "REVERSE_TREND"], "ORTHOPEDIC": ["SUPINE", "PRONE", "LATERAL", "SITTING"], "NEURO": ["SUPINE", "PRONE", "SITTING"], "CARDIAC": ["SUPINE", "REVERSE_TREND"], "UROLOGY": ["SUPINE", "LITHOTOMY"], "ENT": ["SUPINE"], "OPHTHALMOLOGY": ["SUPINE"], "THORACIC": ["LATERAL", "SUPINE"], "TRAUMA": ["SUPINE", "PRONE", "LATERAL"], "PEDIATRIC": ["SUPINE"], "OBSTETRIC": ["SUPINE", "LITHOTOMY"], "PLASTIC": ["SUPINE", "PRONE", "LATERAL"], "VASCULAR": ["SUPINE"], "TRANSPLANT": ["SUPINE"], } return random.choice(mapping.get(service, ["SUPINE"])) def random_equipment_set(service: str, robotic=False, has_c_arm=False, has_ultrasound=False): chosen = [] pool = list(EQUIPMENT_BANK) random.shuffle(pool) # Always include a monitor & cautery core = [e for e in pool if e[1] in {"MONITORING_DEVICE", "ELECTROCAUTERY", "ANESTHESIA_MACHINE"}] chosen.extend(core[:3]) if has_c_arm: chosen += [e for e in pool if e[1] == "C_ARM"][:1] if has_ultrasound: chosen += [e for e in pool if e[1] == "ULTRASOUND"][:1] if robotic: chosen += [e for e in pool if e[1] == "ROBOT"][:1] # Add service-specific instrument set instr = [e for e in pool if e[1] == "SURGICAL_INSTRUMENT"] chosen.extend(instr[:2]) # Deduplicate by name final = [] seen = set() for e in chosen: if e[0] not in seen: final.append(e) seen.add(e[0]) return final # ---------- Command ---------- class Command(BaseCommand): help = "Generate Saudi-influenced operating theatre data (English only)." def add_arguments(self, parser): parser.add_argument("--tenant-id", type=str, help="Tenant UUID or integer PK") parser.add_argument("--days", type=int, default=5, help="Number of days to populate (default: 5)") parser.add_argument("--rooms", type=int, default=6, help="Number of operating rooms to create if none exist") parser.add_argument("--elective-blocks", type=int, default=6, help="Elective blocks per day across rooms (approx)") parser.add_argument("--emergency-blocks", type=int, default=2, help="Emergency blocks per day (approx)") parser.add_argument("--min-cases", type=int, default=1, help="Min cases per block") parser.add_argument("--max-cases", type=int, default=3, help="Max cases per block") parser.add_argument("--seed", type=int, default=None, help="Random seed for reproducibility") parser.add_argument("--start-date", type=str, default=None, help="YYYY-MM-DD (defaults to today)") parser.add_argument("--dry-run", action="store_true", help="Show what would be created without committing") def handle(self, *args, **options): if options["seed"] is not None: random.seed(options["seed"]) # Resolve tenant tenant_id = options.get("tenant_id") tenant = self._get_tenant(tenant_id) # Resolve date range if options["start_date"]: try: start_date = date.fromisoformat(options["start_date"]) except Exception: raise CommandError("Invalid --start-date. Use YYYY-MM-DD.") else: start_date = timezone.localdate() days = max(1, options["days"]) rooms_target = max(1, options["rooms"]) elective_blocks = max(0, options["elective_blocks"]) emergency_blocks = max(0, options["emergency_blocks"]) min_cases = max(1, options["min_cases"]) max_cases = max(min_cases, options["max_cases"]) # Staff & patients pool surgeons = User.objects.filter(is_active=True) anesth_pool = User.objects.filter(is_active=True) nurses_pool = User.objects.filter(is_active=True) patients = PatientProfile.objects.filter(tenant=tenant) if not patients.exists(): raise CommandError("No patients found for the selected tenant.") created_stats = defaultdict(int) @transaction.atomic def do_work(): # Ensure rooms rooms = list(OperatingRoom.objects.filter(tenant=tenant)) if not rooms: rooms = self._create_operating_rooms(tenant, rooms_target) created_stats["OperatingRoom"] += len(rooms) # Ensure some templates created_templates = self._ensure_templates(tenant) created_stats["SurgicalNoteTemplate"] += created_templates # For each day, create a mix of elective and emergency blocks and cases for i in range(days): the_day = start_date + timedelta(days=i) # Shuffle rooms daily so distribution varies random.shuffle(rooms) # Elective blocks created_stats["ORBlock"] += self._create_day_blocks( tenant=tenant, the_day=the_day, rooms=rooms, count=elective_blocks, block_type="SCHEDULED", surgeons=surgeons, anesth_pool=anesth_pool, time_window=(ELECTIVE_START, ELECTIVE_END), ) # Emergency blocks created_stats["ORBlock"] += self._create_day_blocks( tenant=tenant, the_day=the_day, rooms=rooms, count=emergency_blocks, block_type="EMERGENCY", surgeons=surgeons, anesth_pool=anesth_pool, time_window=(EMERGENCY_START, EMERGENCY_END), ) # Fill cases into blocks blocks = ORBlock.objects.filter(operating_room__tenant=tenant, date=the_day) for block in blocks: per_block = random.randint(min_cases, max_cases) created, created_eu, created_notes = self._populate_cases_for_block( tenant=tenant, block=block, count=per_block, surgeons=surgeons, anesth_pool=anesth_pool, nurses_pool=nurses_pool, patients=patients, ) created_stats["SurgicalCase"] += created created_stats["EquipmentUsage"] += created_eu created_stats["SurgicalNote"] += created_notes if options["dry_run"]: self.stdout.write(self.style.WARNING("DRY RUN: No changes will be committed.")) with transaction.atomic(): do_work() raise transaction.TransactionManagementError("DRY RUN rollback") else: do_work() # Summary self.stdout.write(self.style.SUCCESS("Data generation completed.")) for k, v in sorted(created_stats.items()): self.stdout.write(f" - {k}: {v}") # ---------- Internal methods ---------- def _get_tenant(self, tenant_id): if tenant_id: try: # Try by UUID or PK try: return Tenant.objects.get(tenant_id=tenant_id) except Exception: return Tenant.objects.get(pk=tenant_id) except Tenant.DoesNotExist: raise CommandError("Tenant not found. Provide a valid --tenant-id (UUID or PK).") # If only one tenant, pick it, else require explicit count = Tenant.objects.count() if count == 1: return Tenant.objects.first() raise CommandError("Multiple tenants found. Please provide --tenant-id.") def _next_case_number(self, tenant, day): """ Return a unique case number like SURG-YYYYMMDD-0001 scoped to tenant+day. Uses DB ordering to find the current max suffix under the prefix. """ prefix = f"SURG-{day.strftime('%Y%m%d')}-" # Find the lexicographically max case_number under this prefix for this tenant last_cn = ( SurgicalCase.objects .filter(or_block__operating_room__tenant=tenant, case_number__startswith=prefix) .order_by('-case_number') .values_list('case_number', flat=True) .first() ) if not last_cn: return f"{prefix}0001" try: last_num = int(last_cn.split('-')[-1]) except Exception: # Fallback if an unexpected format is found last_num = 0 return f"{prefix}{last_num + 1:04d}" def _create_operating_rooms(self, tenant, rooms_target): rooms = [] room_types = [choice[0] for choice in OperatingRoom.ROOM_TYPE_CHOICES] for idx in range(1, rooms_target + 1): rt = random.choice(room_types) building = random.choice(SAUDI_BUILDINGS) wing = random.choice(SAUDI_WINGS) has_c_arm = rt in {"ORTHOPEDIC", "TRAUMA", "VASCULAR", "THORACIC", "UROLOGY"} supports_robotic = rt in {"UROLOGY", "GENERAL", "GYNE", "ORTHOPEDIC", "CARDIAC", "TRANSPLANT", "THORACIC", "NEURO", "PLASTIC"} has_ultrasound = rt in {"GENERAL", "UROLOGY", "OBSTETRIC", "VASCULAR"} equipment_list = [e[0] for e in random_equipment_set(rt, supports_robotic, has_c_arm, has_ultrasound)] features = random.sample(ROOM_FEATURES, k=min(len(ROOM_FEATURES), random.randint(3, 6))) room = OperatingRoom( tenant=tenant, room_number=f"OR-{idx:02d}", room_name=f"Operating Room {idx}", room_type=rt, status="AVAILABLE", floor_number=random.randint(1, 4), room_size=round(random.uniform(35.0, 70.0), 1), ceiling_height=round(random.uniform(2.8, 3.5), 1), temperature_min=18.0, temperature_max=26.0, humidity_min=30.0, humidity_max=60.0, air_changes_per_hour=random.choice([20, 25, 30]), positive_pressure=True, equipment_list=equipment_list, special_features=features, has_c_arm=has_c_arm, has_ct=False, has_mri=False, has_ultrasound=has_ultrasound, has_neuromonitoring=rt in {"NEURO", "ORTHOPEDIC", "SPINE"}, supports_robotic=supports_robotic, supports_laparoscopic=True, supports_microscopy=rt in {"NEURO", "PLASTIC", "ENT"}, supports_laser=rt in {"UROLOGY", "ENT", "OPHTHALMOLOGY"}, max_case_duration=random.choice([480, 540, 600]), turnover_time=random.choice([25, 30, 35]), cleaning_time=random.choice([40, 45, 50]), required_nurses=random.choice([2, 3, 4]), required_techs=random.choice([1, 2]), is_active=True, accepts_emergency=True, building=building, wing=wing, ) rooms.append(room) OperatingRoom.objects.bulk_create(rooms) return list(OperatingRoom.objects.filter(tenant=tenant)) def _ensure_templates(self, tenant): created = 0 for specialty_code, specialty_label in SurgicalNoteTemplate.SPECIALTY_CHOICES: name = f"Default {specialty_label} Template" if not SurgicalNoteTemplate.objects.filter(tenant=tenant, name=name).exists(): SurgicalNoteTemplate.objects.create( tenant=tenant, name=name, description=f"Standardized note template for {specialty_label}.", procedure_type=None, specialty=specialty_code, preoperative_diagnosis_template="Preoperative diagnosis: {{ diagnosis }}.", planned_procedure_template="Planned procedure: {{ primary_procedure }}.", indication_template="Indications include symptom severity and failed conservative management.", procedure_performed_template="Procedure performed as planned with appropriate variations.", surgical_approach_template="Approach: {{ approach }} with standard sterile technique.", findings_template="Key intraoperative findings noted.", technique_template="Step-wise technique documented with hemostasis achieved.", postoperative_diagnosis_template="Postoperative diagnosis similar to preoperative with noted changes.", complications_template="No complications unless specified.", specimens_template="Specimens sent to pathology as listed.", implants_template="Implants, if any, listed with lot numbers.", closure_template="Layered closure with appropriate sutures.", postop_instructions_template="Routine postoperative instructions and follow-up plan.", is_active=True, is_default=(specialty_code == "ALL"), usage_count=0, created_by=None, ) created += 1 return created def _create_day_blocks(self, tenant, the_day, rooms, count, block_type, surgeons, anesth_pool, time_window): if count <= 0 or not rooms: return 0 created = 0 start_t, end_t = time_window window_minutes = minutes_between(start_t, end_t) # Each block ~ 90–180 minutes blocks_to_make = count room_idx = 0 while blocks_to_make > 0 and room_idx < len(rooms): room = rooms[room_idx] room_idx += 1 # Decide number of blocks for this room for the day per_room = random.randint(1, min(3, blocks_to_make)) # Build non-overlapping blocks cursor_minute = 0 for _ in range(per_room): if cursor_minute + 90 > window_minutes: break dur = random.choice([90, 120, 150, 180]) if cursor_minute + dur > window_minutes: dur = window_minutes - cursor_minute if dur < 60: break block_start = (datetime.combine(the_day, start_t) + timedelta(minutes=cursor_minute)).time() block_end_dt = (datetime.combine(the_day, start_t) + timedelta(minutes=cursor_minute + dur)) block_end = block_end_dt.time() # Assign primary surgeon primary = choose_staff(surgeons, k=1) if not primary: continue primary = primary[0] service = random.choice(list(SERVICE_TO_PROCEDURES.keys())) ob = ORBlock( operating_room=room, date=the_day, start_time=block_start, end_time=block_end, block_type=block_type, primary_surgeon=primary, service=service, status="SCHEDULED", allocated_minutes=0, # computed in save() used_minutes=0, special_equipment=[], special_setup=None, notes=f"{block_type.title()} block for {service} service.", created_by=None, ) ob.save() created += 1 # Occasionally add assistant surgeons assistants = choose_staff(surgeons, k=random.choice([0, 1, 2]), exclude=[primary]) if assistants: ob.assistant_surgeons.add(*assistants) # Add special equipment hints if room.supports_robotic and random.random() < 0.2: ob.special_equipment.append("Robot Instruments Set") if room.has_c_arm and random.random() < 0.3: ob.special_equipment.append("C-Arm Ready") if room.has_ultrasound and random.random() < 0.3: ob.special_equipment.append("Intraop Ultrasound Probe") ob.save() cursor_minute += dur + room.turnover_time if created >= count: break if created >= count: break return created def _populate_cases_for_block(self, tenant, block, count, surgeons, anesth_pool, nurses_pool, patients): created_cases = 0 created_usage = 0 created_notes = 0 # Time slicing within the block for each case block_start_dt = dt_in_ksa(block.date, block.start_time) block_end_dt = dt_in_ksa(block.date, block.end_time) available_minutes = int((block_end_dt - block_start_dt).total_seconds() // 60) # Estimate per-case minutes per_case_estimates = [] remaining = available_minutes for i in range(count): est = random.choice([60, 90, 120, 150]) if i == count - 1: est = max(45, remaining) per_case_estimates.append(est) remaining -= est if remaining < 45 and i < count - 1: # Reduce case count if not enough room count = i + 1 per_case_estimates = per_case_estimates[:count] break current_start = block_start_dt for i in range(count): est_minutes = per_case_estimates[i] sched_start = current_start # Keep end within block est_end = sched_start + timedelta(minutes=est_minutes) if est_end > block_end_dt: est_end = block_end_dt # Staff primary_surgeon = block.primary_surgeon assistants = choose_staff(surgeons, k=random.choice([0, 1, 2]), exclude=[primary_surgeon]) anesthesiologist = choose_staff(anesth_pool, k=1, exclude=[primary_surgeon] + assistants) anesthesiologist = anesthesiologist[0] if anesthesiologist else None circ_nurse = choose_staff(nurses_pool, k=1, exclude=[primary_surgeon] + assistants + ([anesthesiologist] if anesthesiologist else [])) circ_nurse = circ_nurse[0] if circ_nurse else None scrub_nurse = choose_staff(nurses_pool, k=1, exclude=[primary_surgeon] + assistants + ([anesthesiologist] if anesthesiologist else []) + ([circ_nurse] if circ_nurse else [])) scrub_nurse = scrub_nurse[0] if scrub_nurse else None # Procedure selection by service service = block.service proc_triplet = random.choice(SERVICE_TO_PROCEDURES[service]) primary_proc, secondary_proc_list, cpts = proc_triplet case_type = probable_case_type(service) approach = pick_approach(service) anesthesia = pick_anesthesia() position = pick_position(service) diagnosis, icd10s = random.choice(DIAGNOSES) patient = patients.order_by('?').first() encounter = safe_get_encounter(patient) admission = safe_get_admission(patient) if case_type != "ELECTIVE" and random.random() < 0.5 else None day = sched_start.date() for attempt in range(5): try: case_number = self._next_case_number(tenant, day) sc = SurgicalCase( or_block=block, case_number=case_number, # <-- set explicitly (non-empty) patient=patient, primary_surgeon=primary_surgeon, # assistants added after save anesthesiologist=anesthesiologist, circulating_nurse=circ_nurse, scrub_nurse=scrub_nurse, primary_procedure=primary_proc, secondary_procedures=secondary_proc_list[:], procedure_codes=cpts[:], case_type=case_type, approach=approach, anesthesia_type=anesthesia, scheduled_start=sched_start, estimated_duration=est_minutes, actual_start=None, actual_end=None, status="SCHEDULED", diagnosis=diagnosis, diagnosis_codes=icd10s[:], clinical_notes="Patient optimized preoperatively. Informed consent obtained.", special_equipment=[], blood_products=random.sample(BLOOD_PRODUCTS, k=random.choice([0, 1, 2])), implants=random.sample(IMPLANT_EXAMPLES, k=random.choice([0, 1, 2])), patient_position=position, complications=[], estimated_blood_loss=None, encounter=encounter, admission=admission, created_by=None, ) sc.save() break # success except IntegrityError: # Someone else grabbed that number; try again with the next one if attempt == 4: raise continue # Optional: set realistic actual times (simulate some completed cases) if random.random() < 0.4: delay = random.choice([0, 5, 10, 15]) overrun = random.choice([-10, 0, 10, 20, 30]) sc.actual_start = sc.scheduled_start + timedelta(minutes=delay) sc.actual_end = sc.actual_start + timedelta(minutes=max(45, est_minutes + overrun)) sc.status = "COMPLETED" sc.estimated_blood_loss = random.choice([100, 150, 200, 300, 500]) sc.save() created_cases += 1 # Equipment usage entries (a few per case) eu_set = random_equipment_set( service=service, robotic=block.operating_room.supports_robotic and approach == "ROBOTIC", has_c_arm=block.operating_room.has_c_arm, has_ultrasound=block.operating_room.has_ultrasound, ) for (eq_name, eq_type, manu, model) in random.sample(eu_set, k=min(len(eu_set), random.randint(2, 5))): started = sc.scheduled_start + timedelta(minutes=random.choice([0, 5, 10, 15])) ended = started + timedelta(minutes=random.choice([20, 30, 45, 60])) eu = EquipmentUsage( surgical_case=sc, equipment_name=eq_name, equipment_type=eq_type, manufacturer=manu, model=model, quantity_used=random.choice([1, 1, 2]), unit_of_measure="EACH", start_time=started if random.random() < 0.8 else None, end_time=ended if random.random() < 0.7 else None, unit_cost=random.choice([None, 250.00, 500.00, 1200.00]), total_cost=None, lot_number=str(uuid.uuid4())[:8] if random.random() < 0.4 else None, expiration_date=None, sterilization_date=block.date - timedelta(days=random.randint(1, 14)) if random.random() < 0.5 else None, notes=None, recorded_by=primary_surgeon if random.random() < 0.3 else None, ) eu.save() created_usage += 1 # Surgical Note (draft or completed) note_status = random.choice(["DRAFT", "COMPLETED", "SIGNED"]) tmpl = SurgicalNoteTemplate.objects.filter(tenant=tenant, is_active=True).order_by('?').first() sn = SurgicalNote( surgical_case=sc, surgeon=primary_surgeon, preoperative_diagnosis=f"{diagnosis}.", planned_procedure=primary_proc, indication="Worsening symptoms with failure of conservative management.", procedure_performed=f"{primary_proc} performed; secondary procedures as indicated.", surgical_approach=f"Approach: {approach}. Standard sterile preparation and draping.", findings="Findings consistent with preoperative diagnosis.", technique="Procedure completed in steps with meticulous hemostasis.", postoperative_diagnosis=diagnosis, condition=random.choice([c[0] for c in SurgicalNote.CONDITION_CHOICES]), disposition=random.choice([d[0] for d in SurgicalNote.DISPOSITION_CHOICES]), complications=None if sc.status == "COMPLETED" and random.random() < 0.7 else "Minor bleeding controlled with cautery.", estimated_blood_loss=sc.estimated_blood_loss if sc.estimated_blood_loss else random.choice([None, 150, 200]), blood_transfusion=None if random.random() < 0.7 else "1 unit PRBC transfused intraoperatively.", specimens="Specimen sent to pathology as required." if random.random() < 0.5 else None, implants=", ".join(sc.implants) if sc.implants else None, drains="Closed-suction drain placed to dependent region." if random.random() < 0.3 else None, closure="Layered closure with absorbable sutures; skin staples as needed.", postop_instructions="Early ambulation, incentive spirometry, pain control, DVT prophylaxis.", follow_up="Outpatient clinic in 1–2 weeks; sooner if concerns.", status=note_status, signed_datetime=( timezone.now() if note_status == "SIGNED" else None ), template_used=tmpl, ) sn.save() created_notes += 1 # Move pointer for next case current_start = est_end + timedelta(minutes=block.operating_room.turnover_time) # Update block used_minutes based on completed cases used = 0 for case in block.surgical_cases.all(): if case.actual_start and case.actual_end: used += int((case.actual_end - case.actual_start).total_seconds() // 60) else: used += case.estimated_duration block.used_minutes = min(used, block.allocated_minutes or used) block.status = "ACTIVE" if block.used_minutes > 0 else block.status block.save() return created_cases, created_usage, created_notes