1355 lines
37 KiB
Python
1355 lines
37 KiB
Python
"""
|
|
Operating Theatre app models for hospital management system.
|
|
Provides surgical scheduling, OR management, and perioperative workflows.
|
|
"""
|
|
|
|
import uuid
|
|
from django.db import models
|
|
from django.core.validators import RegexValidator, MinValueValidator, MaxValueValidator
|
|
from django.utils import timezone
|
|
from django.conf import settings
|
|
from datetime import timedelta, datetime, date, time
|
|
from decimal import Decimal
|
|
import json
|
|
|
|
|
|
class OperatingRoom(models.Model):
|
|
"""
|
|
Operating room model for OR configuration and management.
|
|
"""
|
|
|
|
# Tenant relationship
|
|
tenant = models.ForeignKey(
|
|
'core.Tenant',
|
|
on_delete=models.CASCADE,
|
|
related_name='operating_rooms',
|
|
help_text='Organization tenant'
|
|
)
|
|
|
|
# Room Information
|
|
room_id = models.UUIDField(
|
|
default=uuid.uuid4,
|
|
unique=True,
|
|
editable=False,
|
|
help_text='Unique room identifier'
|
|
)
|
|
room_number = models.CharField(
|
|
max_length=20,
|
|
help_text='Operating room number'
|
|
)
|
|
room_name = models.CharField(
|
|
max_length=100,
|
|
help_text='Operating room name'
|
|
)
|
|
|
|
# Room Type and Capabilities
|
|
room_type = models.CharField(
|
|
max_length=30,
|
|
choices=[
|
|
('GENERAL', 'General Surgery'),
|
|
('CARDIAC', 'Cardiac Surgery'),
|
|
('NEURO', 'Neurosurgery'),
|
|
('ORTHOPEDIC', 'Orthopedic Surgery'),
|
|
('TRAUMA', 'Trauma Surgery'),
|
|
('PEDIATRIC', 'Pediatric Surgery'),
|
|
('OBSTETRIC', 'Obstetric Surgery'),
|
|
('OPHTHALMOLOGY', 'Ophthalmology'),
|
|
('ENT', 'ENT Surgery'),
|
|
('UROLOGY', 'Urology'),
|
|
('PLASTIC', 'Plastic Surgery'),
|
|
('VASCULAR', 'Vascular Surgery'),
|
|
('THORACIC', 'Thoracic Surgery'),
|
|
('TRANSPLANT', 'Transplant Surgery'),
|
|
('ROBOTIC', 'Robotic Surgery'),
|
|
('HYBRID', 'Hybrid OR'),
|
|
('AMBULATORY', 'Ambulatory Surgery'),
|
|
('EMERGENCY', 'Emergency Surgery'),
|
|
],
|
|
help_text='Operating room type'
|
|
)
|
|
|
|
# Room Status
|
|
status = models.CharField(
|
|
max_length=20,
|
|
choices=[
|
|
('AVAILABLE', 'Available'),
|
|
('OCCUPIED', 'Occupied'),
|
|
('CLEANING', 'Cleaning'),
|
|
('MAINTENANCE', 'Maintenance'),
|
|
('SETUP', 'Setup'),
|
|
('TURNOVER', 'Turnover'),
|
|
('OUT_OF_ORDER', 'Out of Order'),
|
|
('CLOSED', 'Closed'),
|
|
],
|
|
default='AVAILABLE',
|
|
help_text='Current room status'
|
|
)
|
|
|
|
# Physical Characteristics
|
|
floor_number = models.PositiveIntegerField(
|
|
help_text='Floor number'
|
|
)
|
|
room_size = models.FloatField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Room size in square meters'
|
|
)
|
|
ceiling_height = models.FloatField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Ceiling height in meters'
|
|
)
|
|
|
|
# Environmental Controls
|
|
temperature_min = models.FloatField(
|
|
default=18.0,
|
|
help_text='Minimum temperature in Celsius'
|
|
)
|
|
temperature_max = models.FloatField(
|
|
default=26.0,
|
|
help_text='Maximum temperature in Celsius'
|
|
)
|
|
humidity_min = models.FloatField(
|
|
default=30.0,
|
|
help_text='Minimum humidity percentage'
|
|
)
|
|
humidity_max = models.FloatField(
|
|
default=60.0,
|
|
help_text='Maximum humidity percentage'
|
|
)
|
|
air_changes_per_hour = models.PositiveIntegerField(
|
|
default=20,
|
|
help_text='Air changes per hour'
|
|
)
|
|
positive_pressure = models.BooleanField(
|
|
default=True,
|
|
help_text='Positive pressure room'
|
|
)
|
|
|
|
# Equipment and Features
|
|
equipment_list = models.JSONField(
|
|
default=list,
|
|
help_text='Available equipment list'
|
|
)
|
|
special_features = models.JSONField(
|
|
default=list,
|
|
help_text='Special features and capabilities'
|
|
)
|
|
|
|
# Imaging Capabilities
|
|
has_c_arm = models.BooleanField(
|
|
default=False,
|
|
help_text='C-arm fluoroscopy available'
|
|
)
|
|
has_ct = models.BooleanField(
|
|
default=False,
|
|
help_text='Intraoperative CT available'
|
|
)
|
|
has_mri = models.BooleanField(
|
|
default=False,
|
|
help_text='Intraoperative MRI available'
|
|
)
|
|
has_ultrasound = models.BooleanField(
|
|
default=False,
|
|
help_text='Ultrasound available'
|
|
)
|
|
has_neuromonitoring = models.BooleanField(
|
|
default=False,
|
|
help_text='Neuromonitoring available'
|
|
)
|
|
|
|
# Surgical Capabilities
|
|
supports_robotic = models.BooleanField(
|
|
default=False,
|
|
help_text='Robotic surgery capable'
|
|
)
|
|
supports_laparoscopic = models.BooleanField(
|
|
default=True,
|
|
help_text='Laparoscopic surgery capable'
|
|
)
|
|
supports_microscopy = models.BooleanField(
|
|
default=False,
|
|
help_text='Surgical microscopy available'
|
|
)
|
|
supports_laser = models.BooleanField(
|
|
default=False,
|
|
help_text='Laser surgery capable'
|
|
)
|
|
|
|
# Capacity and Scheduling
|
|
max_case_duration = models.PositiveIntegerField(
|
|
default=480,
|
|
help_text='Maximum case duration in minutes'
|
|
)
|
|
turnover_time = models.PositiveIntegerField(
|
|
default=30,
|
|
help_text='Standard turnover time in minutes'
|
|
)
|
|
cleaning_time = models.PositiveIntegerField(
|
|
default=45,
|
|
help_text='Deep cleaning time in minutes'
|
|
)
|
|
|
|
# Staffing Requirements
|
|
required_nurses = models.PositiveIntegerField(
|
|
default=2,
|
|
help_text='Required number of nurses'
|
|
)
|
|
required_techs = models.PositiveIntegerField(
|
|
default=1,
|
|
help_text='Required number of technicians'
|
|
)
|
|
|
|
# Availability
|
|
is_active = models.BooleanField(
|
|
default=True,
|
|
help_text='Room is active and available for scheduling'
|
|
)
|
|
accepts_emergency = models.BooleanField(
|
|
default=True,
|
|
help_text='Accepts emergency cases'
|
|
)
|
|
|
|
# Location Information
|
|
building = models.CharField(
|
|
max_length=50,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Building name or identifier'
|
|
)
|
|
wing = models.CharField(
|
|
max_length=50,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Wing or section'
|
|
)
|
|
|
|
# Metadata
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
created_by = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='created_operating_rooms',
|
|
help_text='User who created the room'
|
|
)
|
|
|
|
class Meta:
|
|
db_table = 'operating_theatre_operating_room'
|
|
verbose_name = 'Operating Room'
|
|
verbose_name_plural = 'Operating Rooms'
|
|
ordering = ['room_number']
|
|
indexes = [
|
|
models.Index(fields=['tenant', 'is_active']),
|
|
models.Index(fields=['room_type', 'status']),
|
|
models.Index(fields=['floor_number']),
|
|
models.Index(fields=['accepts_emergency']),
|
|
]
|
|
unique_together = ['tenant', 'room_number']
|
|
|
|
def __str__(self):
|
|
return f"OR {self.room_number} - {self.room_name}"
|
|
|
|
@property
|
|
def is_available(self):
|
|
"""
|
|
Check if room is available for scheduling.
|
|
"""
|
|
return self.status == 'AVAILABLE' and self.is_active
|
|
|
|
@property
|
|
def current_case(self):
|
|
"""
|
|
Get current surgical case if room is occupied.
|
|
"""
|
|
if self.status == 'OCCUPIED':
|
|
return self.surgical_cases.filter(
|
|
status='IN_PROGRESS'
|
|
).first()
|
|
return None
|
|
|
|
|
|
class ORBlock(models.Model):
|
|
"""
|
|
OR block model for surgical scheduling and time management.
|
|
"""
|
|
|
|
# Operating Room relationship
|
|
operating_room = models.ForeignKey(
|
|
OperatingRoom,
|
|
on_delete=models.CASCADE,
|
|
related_name='or_blocks',
|
|
help_text='Operating room'
|
|
)
|
|
|
|
# Block Information
|
|
block_id = models.UUIDField(
|
|
default=uuid.uuid4,
|
|
unique=True,
|
|
editable=False,
|
|
help_text='Unique block identifier'
|
|
)
|
|
|
|
# Block Timing
|
|
date = models.DateField(
|
|
help_text='Block date'
|
|
)
|
|
start_time = models.TimeField(
|
|
help_text='Block start time'
|
|
)
|
|
end_time = models.TimeField(
|
|
help_text='Block end time'
|
|
)
|
|
|
|
# Block Type
|
|
block_type = models.CharField(
|
|
max_length=20,
|
|
choices=[
|
|
('SCHEDULED', 'Scheduled Block'),
|
|
('EMERGENCY', 'Emergency Block'),
|
|
('MAINTENANCE', 'Maintenance Block'),
|
|
('CLEANING', 'Deep Cleaning'),
|
|
('RESERVED', 'Reserved'),
|
|
('BLOCKED', 'Blocked'),
|
|
],
|
|
default='SCHEDULED',
|
|
help_text='Block type'
|
|
)
|
|
|
|
# Surgeon Assignment
|
|
primary_surgeon = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.CASCADE,
|
|
related_name='primary_or_blocks',
|
|
help_text='Primary surgeon assigned to block'
|
|
)
|
|
assistant_surgeons = models.ManyToManyField(
|
|
settings.AUTH_USER_MODEL,
|
|
blank=True,
|
|
related_name='assistant_or_blocks',
|
|
help_text='Assistant surgeons'
|
|
)
|
|
|
|
# Service Assignment
|
|
service = models.CharField(
|
|
max_length=30,
|
|
choices=[
|
|
('GENERAL', 'General Surgery'),
|
|
('CARDIAC', 'Cardiac Surgery'),
|
|
('NEURO', 'Neurosurgery'),
|
|
('ORTHOPEDIC', 'Orthopedic Surgery'),
|
|
('TRAUMA', 'Trauma Surgery'),
|
|
('PEDIATRIC', 'Pediatric Surgery'),
|
|
('OBSTETRIC', 'Obstetric Surgery'),
|
|
('OPHTHALMOLOGY', 'Ophthalmology'),
|
|
('ENT', 'ENT Surgery'),
|
|
('UROLOGY', 'Urology'),
|
|
('PLASTIC', 'Plastic Surgery'),
|
|
('VASCULAR', 'Vascular Surgery'),
|
|
('THORACIC', 'Thoracic Surgery'),
|
|
('TRANSPLANT', 'Transplant Surgery'),
|
|
],
|
|
help_text='Surgical service'
|
|
)
|
|
|
|
# Block Status
|
|
status = models.CharField(
|
|
max_length=20,
|
|
choices=[
|
|
('SCHEDULED', 'Scheduled'),
|
|
('ACTIVE', 'Active'),
|
|
('COMPLETED', 'Completed'),
|
|
('CANCELLED', 'Cancelled'),
|
|
('DELAYED', 'Delayed'),
|
|
],
|
|
default='SCHEDULED',
|
|
help_text='Block status'
|
|
)
|
|
|
|
# Utilization
|
|
allocated_minutes = models.PositiveIntegerField(
|
|
help_text='Total allocated minutes'
|
|
)
|
|
used_minutes = models.PositiveIntegerField(
|
|
default=0,
|
|
help_text='Minutes actually used'
|
|
)
|
|
|
|
# Special Requirements
|
|
special_equipment = models.JSONField(
|
|
default=list,
|
|
help_text='Special equipment requirements'
|
|
)
|
|
special_setup = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Special setup requirements'
|
|
)
|
|
|
|
# Notes
|
|
notes = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Block notes and comments'
|
|
)
|
|
|
|
# Metadata
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
created_by = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='created_or_blocks',
|
|
help_text='User who created the block'
|
|
)
|
|
|
|
class Meta:
|
|
db_table = 'operating_theatre_or_block'
|
|
verbose_name = 'OR Block'
|
|
verbose_name_plural = 'OR Blocks'
|
|
ordering = ['date', 'start_time']
|
|
indexes = [
|
|
models.Index(fields=['operating_room', 'date']),
|
|
models.Index(fields=['primary_surgeon', 'date']),
|
|
models.Index(fields=['service', 'date']),
|
|
models.Index(fields=['status']),
|
|
]
|
|
|
|
def __str__(self):
|
|
return f"{self.operating_room.room_number} - {self.date} {self.start_time}-{self.end_time}"
|
|
|
|
def save(self, *args, **kwargs):
|
|
"""
|
|
Calculate allocated minutes from start and end times.
|
|
"""
|
|
if self.start_time and self.end_time:
|
|
start_datetime = datetime.combine(date.today(), self.start_time)
|
|
end_datetime = datetime.combine(date.today(), self.end_time)
|
|
if end_datetime < start_datetime:
|
|
end_datetime += timedelta(days=1)
|
|
delta = end_datetime - start_datetime
|
|
self.allocated_minutes = int(delta.total_seconds() / 60)
|
|
|
|
super().save(*args, **kwargs)
|
|
|
|
@property
|
|
def utilization_percentage(self):
|
|
"""
|
|
Calculate block utilization percentage.
|
|
"""
|
|
if self.allocated_minutes > 0:
|
|
return round((self.used_minutes / self.allocated_minutes) * 100, 1)
|
|
return 0
|
|
|
|
@property
|
|
def tenant(self):
|
|
"""
|
|
Get tenant from operating room.
|
|
"""
|
|
return self.operating_room.tenant
|
|
|
|
|
|
class SurgicalCase(models.Model):
|
|
"""
|
|
Surgical case model for individual surgical procedures.
|
|
"""
|
|
|
|
# OR Block relationship
|
|
or_block = models.ForeignKey(
|
|
ORBlock,
|
|
on_delete=models.CASCADE,
|
|
related_name='surgical_cases',
|
|
help_text='OR block assignment'
|
|
)
|
|
|
|
# Case Information
|
|
case_id = models.UUIDField(
|
|
default=uuid.uuid4,
|
|
unique=True,
|
|
editable=False,
|
|
help_text='Unique case identifier'
|
|
)
|
|
case_number = models.CharField(
|
|
max_length=20,
|
|
unique=True,
|
|
help_text='Surgical case number'
|
|
)
|
|
|
|
# Patient Information
|
|
patient = models.ForeignKey(
|
|
'patients.PatientProfile',
|
|
on_delete=models.CASCADE,
|
|
related_name='surgical_cases',
|
|
help_text='Patient'
|
|
)
|
|
|
|
# Surgical Team
|
|
primary_surgeon = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.CASCADE,
|
|
related_name='primary_surgical_cases',
|
|
help_text='Primary surgeon'
|
|
)
|
|
assistant_surgeons = models.ManyToManyField(
|
|
settings.AUTH_USER_MODEL,
|
|
blank=True,
|
|
related_name='assistant_surgical_cases',
|
|
help_text='Assistant surgeons'
|
|
)
|
|
anesthesiologist = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='anesthesia_cases',
|
|
help_text='Anesthesiologist'
|
|
)
|
|
circulating_nurse = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='circulating_cases',
|
|
help_text='Circulating nurse'
|
|
)
|
|
scrub_nurse = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='scrub_cases',
|
|
help_text='Scrub nurse'
|
|
)
|
|
|
|
# Procedure Information
|
|
primary_procedure = models.CharField(
|
|
max_length=200,
|
|
help_text='Primary surgical procedure'
|
|
)
|
|
secondary_procedures = models.JSONField(
|
|
default=list,
|
|
help_text='Secondary procedures'
|
|
)
|
|
procedure_codes = models.JSONField(
|
|
default=list,
|
|
help_text='CPT procedure codes'
|
|
)
|
|
|
|
# Case Classification
|
|
case_type = models.CharField(
|
|
max_length=20,
|
|
choices=[
|
|
('ELECTIVE', 'Elective'),
|
|
('URGENT', 'Urgent'),
|
|
('EMERGENCY', 'Emergency'),
|
|
('TRAUMA', 'Trauma'),
|
|
('TRANSPLANT', 'Transplant'),
|
|
],
|
|
default='ELECTIVE',
|
|
help_text='Case type'
|
|
)
|
|
|
|
# Surgical Approach
|
|
approach = models.CharField(
|
|
max_length=20,
|
|
choices=[
|
|
('OPEN', 'Open'),
|
|
('LAPAROSCOPIC', 'Laparoscopic'),
|
|
('ROBOTIC', 'Robotic'),
|
|
('ENDOSCOPIC', 'Endoscopic'),
|
|
('PERCUTANEOUS', 'Percutaneous'),
|
|
('HYBRID', 'Hybrid'),
|
|
],
|
|
help_text='Surgical approach'
|
|
)
|
|
|
|
# Anesthesia
|
|
anesthesia_type = models.CharField(
|
|
max_length=20,
|
|
choices=[
|
|
('GENERAL', 'General'),
|
|
('REGIONAL', 'Regional'),
|
|
('LOCAL', 'Local'),
|
|
('SEDATION', 'Sedation'),
|
|
('SPINAL', 'Spinal'),
|
|
('EPIDURAL', 'Epidural'),
|
|
('COMBINED', 'Combined'),
|
|
],
|
|
help_text='Anesthesia type'
|
|
)
|
|
|
|
# Timing
|
|
scheduled_start = models.DateTimeField(
|
|
help_text='Scheduled start time'
|
|
)
|
|
estimated_duration = models.PositiveIntegerField(
|
|
help_text='Estimated duration in minutes'
|
|
)
|
|
actual_start = models.DateTimeField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Actual start time'
|
|
)
|
|
actual_end = models.DateTimeField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Actual end time'
|
|
)
|
|
|
|
# Case Status
|
|
status = models.CharField(
|
|
max_length=20,
|
|
choices=[
|
|
('SCHEDULED', 'Scheduled'),
|
|
('DELAYED', 'Delayed'),
|
|
('IN_PROGRESS', 'In Progress'),
|
|
('COMPLETED', 'Completed'),
|
|
('CANCELLED', 'Cancelled'),
|
|
('POSTPONED', 'Postponed'),
|
|
],
|
|
default='SCHEDULED',
|
|
help_text='Case status'
|
|
)
|
|
|
|
# Clinical Information
|
|
diagnosis = models.CharField(
|
|
max_length=200,
|
|
help_text='Primary diagnosis'
|
|
)
|
|
diagnosis_codes = models.JSONField(
|
|
default=list,
|
|
help_text='ICD-10 diagnosis codes'
|
|
)
|
|
clinical_notes = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Clinical notes and history'
|
|
)
|
|
|
|
# Special Requirements
|
|
special_equipment = models.JSONField(
|
|
default=list,
|
|
help_text='Special equipment requirements'
|
|
)
|
|
blood_products = models.JSONField(
|
|
default=list,
|
|
help_text='Blood product requirements'
|
|
)
|
|
implants = models.JSONField(
|
|
default=list,
|
|
help_text='Implant requirements'
|
|
)
|
|
|
|
# Patient Positioning
|
|
patient_position = models.CharField(
|
|
max_length=20,
|
|
choices=[
|
|
('SUPINE', 'Supine'),
|
|
('PRONE', 'Prone'),
|
|
('LATERAL', 'Lateral'),
|
|
('LITHOTOMY', 'Lithotomy'),
|
|
('TRENDELENBURG', 'Trendelenburg'),
|
|
('REVERSE_TREND', 'Reverse Trendelenburg'),
|
|
('SITTING', 'Sitting'),
|
|
('JACKKNIFE', 'Jackknife'),
|
|
],
|
|
blank=True,
|
|
null=True,
|
|
help_text='Patient positioning'
|
|
)
|
|
|
|
# Complications and Outcomes
|
|
complications = models.JSONField(
|
|
default=list,
|
|
help_text='Intraoperative complications'
|
|
)
|
|
estimated_blood_loss = models.PositiveIntegerField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Estimated blood loss in mL'
|
|
)
|
|
|
|
# Related Information
|
|
encounter = models.ForeignKey(
|
|
'emr.Encounter',
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='surgical_cases',
|
|
help_text='Related encounter'
|
|
)
|
|
admission = models.ForeignKey(
|
|
'inpatients.Admission',
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='surgical_cases',
|
|
help_text='Related admission'
|
|
)
|
|
|
|
# Metadata
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
created_by = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='created_surgical_cases',
|
|
help_text='User who created the case'
|
|
)
|
|
|
|
class Meta:
|
|
db_table = 'operating_theatre_surgical_case'
|
|
verbose_name = 'Surgical Case'
|
|
verbose_name_plural = 'Surgical Cases'
|
|
ordering = ['scheduled_start']
|
|
indexes = [
|
|
models.Index(fields=['or_block', 'status']),
|
|
models.Index(fields=['patient', 'scheduled_start']),
|
|
models.Index(fields=['primary_surgeon', 'scheduled_start']),
|
|
models.Index(fields=['case_type', 'status']),
|
|
models.Index(fields=['scheduled_start']),
|
|
models.Index(fields=['case_number']),
|
|
]
|
|
|
|
def __str__(self):
|
|
return f"{self.case_number} - {self.primary_procedure}"
|
|
|
|
def save(self, *args, **kwargs):
|
|
"""
|
|
Generate case number if not provided.
|
|
"""
|
|
if not self.case_number:
|
|
# Generate case number (simple implementation)
|
|
tenant = self.or_block.operating_room.tenant
|
|
today = timezone.now().date()
|
|
last_case = SurgicalCase.objects.filter(
|
|
or_block__operating_room__tenant=tenant,
|
|
created_at__date=today
|
|
).order_by('-id').first()
|
|
|
|
if last_case:
|
|
last_number = int(last_case.case_number.split('-')[-1])
|
|
self.case_number = f"SURG-{today.strftime('%Y%m%d')}-{last_number + 1:04d}"
|
|
else:
|
|
self.case_number = f"SURG-{today.strftime('%Y%m%d')}-0001"
|
|
|
|
super().save(*args, **kwargs)
|
|
|
|
@property
|
|
def actual_duration(self):
|
|
"""
|
|
Calculate actual case duration in minutes.
|
|
"""
|
|
if self.actual_start and self.actual_end:
|
|
delta = self.actual_end - self.actual_start
|
|
return int(delta.total_seconds() / 60)
|
|
return None
|
|
|
|
@property
|
|
def is_emergency(self):
|
|
"""
|
|
Check if case is emergency.
|
|
"""
|
|
return self.case_type in ['EMERGENCY', 'TRAUMA']
|
|
|
|
@property
|
|
def operating_room(self):
|
|
"""
|
|
Get operating room from OR block.
|
|
"""
|
|
return self.or_block.operating_room
|
|
|
|
@property
|
|
def tenant(self):
|
|
"""
|
|
Get tenant from OR block.
|
|
"""
|
|
return self.or_block.operating_room.tenant
|
|
|
|
|
|
class SurgicalNote(models.Model):
|
|
"""
|
|
Surgical note model for perioperative documentation.
|
|
"""
|
|
|
|
# Surgical Case relationship
|
|
surgical_case = models.OneToOneField(
|
|
SurgicalCase,
|
|
on_delete=models.CASCADE,
|
|
related_name='surgical_note',
|
|
help_text='Related surgical case'
|
|
)
|
|
|
|
# Note Information
|
|
note_id = models.UUIDField(
|
|
default=uuid.uuid4,
|
|
unique=True,
|
|
editable=False,
|
|
help_text='Unique note identifier'
|
|
)
|
|
|
|
# Surgeon Information
|
|
surgeon = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.CASCADE,
|
|
related_name='surgical_notes',
|
|
help_text='Operating surgeon'
|
|
)
|
|
|
|
# Preoperative Information
|
|
preoperative_diagnosis = models.TextField(
|
|
help_text='Preoperative diagnosis'
|
|
)
|
|
planned_procedure = models.TextField(
|
|
help_text='Planned surgical procedure'
|
|
)
|
|
indication = models.TextField(
|
|
help_text='Indication for surgery'
|
|
)
|
|
|
|
# Intraoperative Information
|
|
procedure_performed = models.TextField(
|
|
help_text='Actual procedure performed'
|
|
)
|
|
surgical_approach = models.TextField(
|
|
help_text='Surgical approach and technique'
|
|
)
|
|
findings = models.TextField(
|
|
help_text='Intraoperative findings'
|
|
)
|
|
technique = models.TextField(
|
|
help_text='Detailed surgical technique'
|
|
)
|
|
|
|
# Postoperative Information
|
|
postoperative_diagnosis = models.TextField(
|
|
help_text='Postoperative diagnosis'
|
|
)
|
|
condition = models.CharField(
|
|
max_length=20,
|
|
choices=[
|
|
('STABLE', 'Stable'),
|
|
('CRITICAL', 'Critical'),
|
|
('GUARDED', 'Guarded'),
|
|
('FAIR', 'Fair'),
|
|
('GOOD', 'Good'),
|
|
('EXCELLENT', 'Excellent'),
|
|
],
|
|
help_text='Patient condition post-surgery'
|
|
)
|
|
disposition = models.CharField(
|
|
max_length=30,
|
|
choices=[
|
|
('RECOVERY', 'Recovery Room'),
|
|
('ICU', 'Intensive Care Unit'),
|
|
('WARD', 'Ward'),
|
|
('DISCHARGE', 'Discharge'),
|
|
('MORGUE', 'Morgue'),
|
|
],
|
|
help_text='Patient disposition'
|
|
)
|
|
|
|
# Complications and Blood Loss
|
|
complications = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Intraoperative complications'
|
|
)
|
|
estimated_blood_loss = models.PositiveIntegerField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Estimated blood loss in mL'
|
|
)
|
|
blood_transfusion = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Blood transfusion details'
|
|
)
|
|
|
|
# Specimens and Pathology
|
|
specimens = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Specimens sent to pathology'
|
|
)
|
|
|
|
# Implants and Devices
|
|
implants = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Implants and devices used'
|
|
)
|
|
|
|
# Drains and Tubes
|
|
drains = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Drains and tubes placed'
|
|
)
|
|
|
|
# Closure
|
|
closure = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Wound closure technique'
|
|
)
|
|
|
|
# Postoperative Instructions
|
|
postop_instructions = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Postoperative instructions'
|
|
)
|
|
follow_up = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Follow-up instructions'
|
|
)
|
|
|
|
# Note Status
|
|
status = models.CharField(
|
|
max_length=20,
|
|
choices=[
|
|
('DRAFT', 'Draft'),
|
|
('COMPLETED', 'Completed'),
|
|
('SIGNED', 'Signed'),
|
|
('AMENDED', 'Amended'),
|
|
],
|
|
default='DRAFT',
|
|
help_text='Note status'
|
|
)
|
|
|
|
# Signatures
|
|
signed_datetime = models.DateTimeField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Date and time note was signed'
|
|
)
|
|
|
|
# Template
|
|
template_used = models.ForeignKey(
|
|
'SurgicalNoteTemplate',
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='surgical_notes',
|
|
help_text='Template used for note'
|
|
)
|
|
|
|
# Metadata
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
db_table = 'operating_theatre_surgical_note'
|
|
verbose_name = 'Surgical Note'
|
|
verbose_name_plural = 'Surgical Notes'
|
|
ordering = ['-created_at']
|
|
indexes = [
|
|
models.Index(fields=['surgical_case']),
|
|
models.Index(fields=['surgeon']),
|
|
models.Index(fields=['status']),
|
|
models.Index(fields=['signed_datetime']),
|
|
]
|
|
|
|
def __str__(self):
|
|
return f"Surgical Note - {self.surgical_case.case_number}"
|
|
|
|
@property
|
|
def patient(self):
|
|
"""
|
|
Get patient from surgical case.
|
|
"""
|
|
return self.surgical_case.patient
|
|
|
|
@property
|
|
def is_signed(self):
|
|
"""
|
|
Check if note is signed.
|
|
"""
|
|
return self.status == 'SIGNED' and self.signed_datetime is not None
|
|
|
|
|
|
class EquipmentUsage(models.Model):
|
|
"""
|
|
Equipment usage model for tracking surgical equipment.
|
|
"""
|
|
|
|
# Surgical Case relationship
|
|
surgical_case = models.ForeignKey(
|
|
SurgicalCase,
|
|
on_delete=models.CASCADE,
|
|
related_name='equipment_usage',
|
|
help_text='Related surgical case'
|
|
)
|
|
|
|
# Equipment Information
|
|
usage_id = models.UUIDField(
|
|
default=uuid.uuid4,
|
|
unique=True,
|
|
editable=False,
|
|
help_text='Unique usage identifier'
|
|
)
|
|
equipment_name = models.CharField(
|
|
max_length=100,
|
|
help_text='Equipment name'
|
|
)
|
|
equipment_type = models.CharField(
|
|
max_length=50,
|
|
choices=[
|
|
('SURGICAL_INSTRUMENT', 'Surgical Instrument'),
|
|
('MONITORING_DEVICE', 'Monitoring Device'),
|
|
('ANESTHESIA_MACHINE', 'Anesthesia Machine'),
|
|
('VENTILATOR', 'Ventilator'),
|
|
('ELECTROCAUTERY', 'Electrocautery'),
|
|
('LASER', 'Laser'),
|
|
('MICROSCOPE', 'Microscope'),
|
|
('C_ARM', 'C-Arm'),
|
|
('ULTRASOUND', 'Ultrasound'),
|
|
('ROBOT', 'Surgical Robot'),
|
|
('IMPLANT', 'Implant'),
|
|
('DISPOSABLE', 'Disposable'),
|
|
('OTHER', 'Other'),
|
|
],
|
|
help_text='Equipment type'
|
|
)
|
|
|
|
# Equipment Details
|
|
manufacturer = models.CharField(
|
|
max_length=100,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Equipment manufacturer'
|
|
)
|
|
model = models.CharField(
|
|
max_length=100,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Equipment model'
|
|
)
|
|
serial_number = models.CharField(
|
|
max_length=100,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Equipment serial number'
|
|
)
|
|
|
|
# Usage Information
|
|
quantity_used = models.PositiveIntegerField(
|
|
default=1,
|
|
help_text='Quantity used'
|
|
)
|
|
unit_of_measure = models.CharField(
|
|
max_length=20,
|
|
choices=[
|
|
('EACH', 'Each'),
|
|
('SET', 'Set'),
|
|
('PACK', 'Pack'),
|
|
('BOX', 'Box'),
|
|
('UNIT', 'Unit'),
|
|
('PIECE', 'Piece'),
|
|
],
|
|
default='EACH',
|
|
help_text='Unit of measure'
|
|
)
|
|
|
|
# Timing
|
|
start_time = models.DateTimeField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Equipment usage start time'
|
|
)
|
|
end_time = models.DateTimeField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Equipment usage end time'
|
|
)
|
|
|
|
# Cost Information
|
|
unit_cost = models.DecimalField(
|
|
max_digits=10,
|
|
decimal_places=2,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Unit cost'
|
|
)
|
|
total_cost = models.DecimalField(
|
|
max_digits=10,
|
|
decimal_places=2,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Total cost'
|
|
)
|
|
|
|
# Quality and Safety
|
|
lot_number = models.CharField(
|
|
max_length=50,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Lot or batch number'
|
|
)
|
|
expiration_date = models.DateField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Expiration date'
|
|
)
|
|
sterilization_date = models.DateField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Sterilization date'
|
|
)
|
|
|
|
# Usage Notes
|
|
notes = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Usage notes and comments'
|
|
)
|
|
|
|
# Staff Information
|
|
recorded_by = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='recorded_equipment_usage',
|
|
help_text='Staff member who recorded usage'
|
|
)
|
|
|
|
# Metadata
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
class Meta:
|
|
db_table = 'operating_theatre_equipment_usage'
|
|
verbose_name = 'Equipment Usage'
|
|
verbose_name_plural = 'Equipment Usage'
|
|
ordering = ['-created_at']
|
|
indexes = [
|
|
models.Index(fields=['surgical_case']),
|
|
models.Index(fields=['equipment_type']),
|
|
models.Index(fields=['equipment_name']),
|
|
models.Index(fields=['recorded_by']),
|
|
]
|
|
|
|
def __str__(self):
|
|
return f"{self.equipment_name} - {self.surgical_case.case_number}"
|
|
|
|
def save(self, *args, **kwargs):
|
|
"""
|
|
Calculate total cost from unit cost and quantity.
|
|
"""
|
|
if self.unit_cost and self.quantity_used:
|
|
self.total_cost = self.unit_cost * self.quantity_used
|
|
|
|
super().save(*args, **kwargs)
|
|
|
|
@property
|
|
def duration_minutes(self):
|
|
"""
|
|
Calculate usage duration in minutes.
|
|
"""
|
|
if self.start_time and self.end_time:
|
|
delta = self.end_time - self.start_time
|
|
return int(delta.total_seconds() / 60)
|
|
return None
|
|
|
|
@property
|
|
def patient(self):
|
|
"""
|
|
Get patient from surgical case.
|
|
"""
|
|
return self.surgical_case.patient
|
|
|
|
|
|
class SurgicalNoteTemplate(models.Model):
|
|
"""
|
|
Surgical note template model for standardized documentation.
|
|
"""
|
|
|
|
# Tenant relationship
|
|
tenant = models.ForeignKey(
|
|
'core.Tenant',
|
|
on_delete=models.CASCADE,
|
|
related_name='surgical_note_templates',
|
|
help_text='Organization tenant'
|
|
)
|
|
|
|
# Template Information
|
|
template_id = models.UUIDField(
|
|
default=uuid.uuid4,
|
|
unique=True,
|
|
editable=False,
|
|
help_text='Unique template identifier'
|
|
)
|
|
name = models.CharField(
|
|
max_length=100,
|
|
help_text='Template name'
|
|
)
|
|
description = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Template description'
|
|
)
|
|
|
|
# Template Scope
|
|
procedure_type = models.CharField(
|
|
max_length=100,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Applicable procedure type'
|
|
)
|
|
specialty = models.CharField(
|
|
max_length=30,
|
|
choices=[
|
|
('ALL', 'All Specialties'),
|
|
('GENERAL', 'General Surgery'),
|
|
('CARDIAC', 'Cardiac Surgery'),
|
|
('NEURO', 'Neurosurgery'),
|
|
('ORTHOPEDIC', 'Orthopedic Surgery'),
|
|
('TRAUMA', 'Trauma Surgery'),
|
|
('PEDIATRIC', 'Pediatric Surgery'),
|
|
('OBSTETRIC', 'Obstetric Surgery'),
|
|
('OPHTHALMOLOGY', 'Ophthalmology'),
|
|
('ENT', 'ENT Surgery'),
|
|
('UROLOGY', 'Urology'),
|
|
('PLASTIC', 'Plastic Surgery'),
|
|
('VASCULAR', 'Vascular Surgery'),
|
|
('THORACIC', 'Thoracic Surgery'),
|
|
('TRANSPLANT', 'Transplant Surgery'),
|
|
],
|
|
default='ALL',
|
|
help_text='Applicable specialty'
|
|
)
|
|
|
|
# Template Content
|
|
preoperative_diagnosis_template = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Preoperative diagnosis template'
|
|
)
|
|
planned_procedure_template = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Planned procedure template'
|
|
)
|
|
indication_template = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Indication template'
|
|
)
|
|
procedure_performed_template = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Procedure performed template'
|
|
)
|
|
surgical_approach_template = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Surgical approach template'
|
|
)
|
|
findings_template = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Findings template'
|
|
)
|
|
technique_template = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Technique template'
|
|
)
|
|
postoperative_diagnosis_template = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Postoperative diagnosis template'
|
|
)
|
|
complications_template = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Complications template'
|
|
)
|
|
specimens_template = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Specimens template'
|
|
)
|
|
implants_template = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Implants template'
|
|
)
|
|
closure_template = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Closure template'
|
|
)
|
|
postop_instructions_template = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Postoperative instructions template'
|
|
)
|
|
|
|
# Template Status
|
|
is_active = models.BooleanField(
|
|
default=True,
|
|
help_text='Template is active'
|
|
)
|
|
is_default = models.BooleanField(
|
|
default=False,
|
|
help_text='Default template for specialty'
|
|
)
|
|
|
|
# Usage Statistics
|
|
usage_count = models.PositiveIntegerField(
|
|
default=0,
|
|
help_text='Number of times template has been used'
|
|
)
|
|
|
|
# Metadata
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
created_by = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='created_surgical_note_templates',
|
|
help_text='User who created the template'
|
|
)
|
|
|
|
class Meta:
|
|
db_table = 'operating_theatre_surgical_note_template'
|
|
verbose_name = 'Surgical Note Template'
|
|
verbose_name_plural = 'Surgical Note Templates'
|
|
ordering = ['specialty', 'name']
|
|
indexes = [
|
|
models.Index(fields=['tenant', 'is_active']),
|
|
models.Index(fields=['specialty']),
|
|
models.Index(fields=['is_default']),
|
|
]
|
|
unique_together = ['tenant', 'name']
|
|
|
|
def __str__(self):
|
|
return f"{self.name} ({self.specialty})"
|
|
|
|
def save(self, *args, **kwargs):
|
|
"""
|
|
Ensure only one default template per specialty.
|
|
"""
|
|
if self.is_default:
|
|
# Remove default flag from other templates
|
|
SurgicalNoteTemplate.objects.filter(
|
|
tenant=self.tenant,
|
|
specialty=self.specialty,
|
|
is_default=True
|
|
).exclude(pk=self.pk).update(is_default=False)
|
|
|
|
super().save(*args, **kwargs)
|
|
|