1446 lines
41 KiB
Python
1446 lines
41 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 django.contrib.contenttypes.fields import GenericRelation
|
|
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.
|
|
"""
|
|
|
|
class OperatingRoomType(models.TextChoices):
|
|
GENERAL = 'GENERAL', 'General Surgery'
|
|
CARDIAC = 'CARDIAC', 'Cardiac Surgery'
|
|
NEURO = 'NEURO', 'Neurosurgery'
|
|
ORTHOPEDIC = 'ORTHOPEDIC', 'Orthopedic Surgery'
|
|
TRAUMA = 'TRAUMA', 'Trauma Surgery'
|
|
PEDIATRIC = 'PEDIATRIC', 'Pediatric Surgery'
|
|
OBSTETRIC = 'OBSTETRIC', 'Obstetric Surgery'
|
|
OPHTHALMOLOGY = 'OPHTHALMOLOGY', 'Ophthalmology'
|
|
ENT = 'ENT', 'ENT Surgery'
|
|
UROLOGY = 'UROLOGY', 'Urology'
|
|
PLASTIC = 'PLASTIC', 'Plastic Surgery'
|
|
VASCULAR = 'VASCULAR', 'Vascular Surgery'
|
|
THORACIC = 'THORACIC', 'Thoracic Surgery'
|
|
TRANSPLANT = 'TRANSPLANT', 'Transplant Surgery'
|
|
ROBOTIC = 'ROBOTIC', 'Robotic Surgery'
|
|
HYBRID = 'HYBRID', 'Hybrid OR'
|
|
AMBULATORY = 'AMBULATORY', 'Ambulatory Surgery'
|
|
EMERGENCY = 'EMERGENCY', 'Emergency Surgery'
|
|
|
|
class ORStatus(models.TextChoices):
|
|
AVAILABLE = 'AVAILABLE', 'Available'
|
|
OCCUPIED = 'OCCUPIED', 'Occupied'
|
|
CLEANING = 'CLEANING', 'Cleaning'
|
|
MAINTENANCE = 'MAINTENANCE', 'Maintenance'
|
|
SETUP = 'SETUP', 'Setup'
|
|
TURNOVER = 'TURNOVER', 'Turnover'
|
|
OUT_OF_ORDER = 'OUT_OF_ORDER', 'Out of Order'
|
|
CLOSED = 'CLOSED', 'Closed'
|
|
|
|
# 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=OperatingRoomType.choices,
|
|
help_text='Operating room type'
|
|
)
|
|
|
|
# Room Status
|
|
status = models.CharField(
|
|
max_length=20,
|
|
choices=ORStatus.choices,
|
|
default=ORStatus.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 surgical_cases(self):
|
|
"""
|
|
All surgical cases scheduled/assigned to this operating room
|
|
via its OR blocks.
|
|
"""
|
|
return SurgicalCase.objects.filter(or_block__operating_room=self)
|
|
|
|
# (Optional) a clearer alias if you prefer not to shadow the term "surgical_cases"
|
|
@property
|
|
def cases(self):
|
|
return self.surgical_cases
|
|
|
|
@property
|
|
def current_case(self):
|
|
"""
|
|
Get the in-progress surgical case for this room (if any).
|
|
"""
|
|
return self.surgical_cases.filter(status='IN_PROGRESS').order_by('-scheduled_start').first()
|
|
|
|
|
|
class ORBlock(models.Model):
|
|
"""
|
|
OR block model for surgical scheduling and time management.
|
|
"""
|
|
|
|
class BlockType(models.TextChoices):
|
|
SCHEDULED = 'SCHEDULED', 'Scheduled Block'
|
|
EMERGENCY = 'EMERGENCY', 'Emergency Block'
|
|
MAINTENANCE = 'MAINTENANCE', 'Maintenance Block'
|
|
CLEANING = 'CLEANING', 'Deep Cleaning'
|
|
RESERVED = 'RESERVED', 'Reserved'
|
|
BLOCKED = 'BLOCKED', 'Blocked'
|
|
|
|
class ORService(models.TextChoices):
|
|
GENERAL = 'GENERAL', 'General Surgery'
|
|
CARDIAC = 'CARDIAC', 'Cardiac Surgery'
|
|
NEURO = 'NEURO', 'Neurosurgery'
|
|
ORTHOPEDIC = 'ORTHOPEDIC', 'Orthopedic Surgery'
|
|
TRAUMA = 'TRAUMA', 'Trauma Surgery'
|
|
PEDIATRIC = 'PEDIATRIC', 'Pediatric Surgery'
|
|
OBSTETRIC = 'OBSTETRIC', 'Obstetric Surgery'
|
|
OPHTHALMOLOGY = 'OPHTHALMOLOGY', 'Ophthalmology'
|
|
ENT = 'ENT', 'ENT Surgery'
|
|
UROLOGY = 'UROLOGY', 'Urology'
|
|
PLASTIC = 'PLASTIC', 'Plastic Surgery'
|
|
VASCULAR = 'VASCULAR', 'Vascular Surgery'
|
|
THORACIC = 'THORACIC', 'Thoracic Surgery'
|
|
TRANSPLANT = 'TRANSPLANT', 'Transplant Surgery'
|
|
|
|
class BlockStatus(models.TextChoices):
|
|
SCHEDULED = 'SCHEDULED', 'Scheduled'
|
|
ACTIVE = 'ACTIVE', 'Active'
|
|
COMPLETED = 'COMPLETED', 'Completed'
|
|
CANCELLED = 'CANCELLED', 'Cancelled'
|
|
DELAYED = 'DELAYED', 'Delayed'
|
|
|
|
# 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=BlockType.choices,
|
|
default=BlockType.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=ORService.choices,
|
|
help_text='Surgical service'
|
|
)
|
|
|
|
# Block Status
|
|
status = models.CharField(
|
|
max_length=20,
|
|
choices=BlockStatus.choices,
|
|
default=BlockStatus.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.
|
|
"""
|
|
|
|
class CaseType(models.TextChoices):
|
|
ELECTIVE = 'ELECTIVE', 'Elective'
|
|
URGENT = 'URGENT', 'Urgent'
|
|
EMERGENCY = 'EMERGENCY', 'Emergency'
|
|
TRAUMA = 'TRAUMA', 'Trauma'
|
|
TRANSPLANT = 'TRANSPLANT', 'Transplant'
|
|
|
|
class SurgicalApproach(models.TextChoices):
|
|
OPEN = 'OPEN', 'Open'
|
|
LAPAROSCOPIC = 'LAPAROSCOPIC', 'Laparoscopic'
|
|
ROBOTIC = 'ROBOTIC', 'Robotic'
|
|
ENDOSCOPIC = 'ENDOSCOPIC', 'Endoscopic'
|
|
PERCUTANEOUS = 'PERCUTANEOUS', 'Percutaneous'
|
|
HYBRID = 'HYBRID', 'Hybrid'
|
|
|
|
class AnesthesiaType(models.TextChoices):
|
|
GENERAL = 'GENERAL', 'General'
|
|
REGIONAL = 'REGIONAL', 'Regional'
|
|
LOCAL = 'LOCAL', 'Local'
|
|
SEDATION = 'SEDATION', 'Sedation'
|
|
SPINAL = 'SPINAL', 'Spinal'
|
|
EPIDURAL = 'EPIDURAL', 'Epidural'
|
|
COMBINED = 'COMBINED', 'Combined'
|
|
|
|
class CaseStatus(models.TextChoices):
|
|
SCHEDULED = 'SCHEDULED', 'Scheduled'
|
|
CONFIRMED = 'CONFIRMED', 'Confirmed'
|
|
PREP = 'PREP', 'Pre-operative Prep'
|
|
DELAYED = 'DELAYED', 'Delayed'
|
|
IN_PROGRESS = 'IN_PROGRESS', 'In Progress'
|
|
COMPLETED = 'COMPLETED', 'Completed'
|
|
CANCELLED = 'CANCELLED', 'Cancelled'
|
|
POSTPONED = 'POSTPONED', 'Postponed'
|
|
|
|
class PatientPosition(models.TextChoices):
|
|
SUPINE = 'SUPINE', 'Supine'
|
|
PRONE = 'PRONE', 'Prone'
|
|
LATERAL = 'LATERAL', 'Lateral'
|
|
LITHOTOMY = 'LITHOTOMY', 'Lithotomy'
|
|
TRENDELENBURG = 'TRENDELENBURG', 'Trendelenburg'
|
|
REVERSE_TREND = 'REVERSE_TREND', 'Reverse Trendelenburg'
|
|
SITTING = 'SITTING', 'Sitting'
|
|
JACKKNIFE = 'JACKKNIFE', 'Jackknife'
|
|
|
|
# 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=CaseType.choices,
|
|
default=CaseType.ELECTIVE,
|
|
help_text='Case type'
|
|
)
|
|
|
|
# Surgical Approach
|
|
approach = models.CharField(
|
|
max_length=20,
|
|
choices=SurgicalApproach.choices,
|
|
help_text='Surgical approach'
|
|
)
|
|
|
|
# Anesthesia
|
|
anesthesia_type = models.CharField(
|
|
max_length=20,
|
|
choices=AnesthesiaType.choices,
|
|
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=CaseStatus.choices,
|
|
default=CaseStatus.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=PatientPosition.choices,
|
|
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'
|
|
)
|
|
|
|
# Insurance Approval Integration
|
|
approval_requests = GenericRelation(
|
|
'insurance_approvals.InsuranceApprovalRequest',
|
|
content_type_field='content_type',
|
|
object_id_field='object_id',
|
|
related_query_name='surgical_case'
|
|
)
|
|
|
|
# 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
|
|
|
|
def has_valid_approval(self):
|
|
"""
|
|
Check if surgical case has a valid insurance approval.
|
|
"""
|
|
from django.utils import timezone
|
|
return self.approval_requests.filter(
|
|
status__in=['APPROVED', 'PARTIALLY_APPROVED'],
|
|
expiration_date__gte=timezone.now().date()
|
|
).exists()
|
|
|
|
def get_active_approval(self):
|
|
"""
|
|
Get the active insurance approval for this surgical case.
|
|
"""
|
|
from django.utils import timezone
|
|
return self.approval_requests.filter(
|
|
status__in=['APPROVED', 'PARTIALLY_APPROVED'],
|
|
expiration_date__gte=timezone.now().date()
|
|
).first()
|
|
|
|
def requires_approval(self):
|
|
"""
|
|
Check if surgical case requires insurance approval.
|
|
Returns True if patient has insurance and no valid approval exists.
|
|
Emergency cases may have different requirements.
|
|
"""
|
|
if not self.patient.insurance_info.exists():
|
|
return False
|
|
|
|
# Emergency cases might have expedited approval process
|
|
if self.is_emergency:
|
|
# Check if emergency approval exists
|
|
return not self.has_valid_approval()
|
|
|
|
# Elective cases require approval
|
|
return not self.has_valid_approval()
|
|
|
|
@property
|
|
def approval_status(self):
|
|
"""
|
|
Get current approval status for display.
|
|
"""
|
|
if not self.patient.insurance_info.exists():
|
|
return 'NO_INSURANCE'
|
|
|
|
latest_approval = self.approval_requests.order_by('-created_at').first()
|
|
if not latest_approval:
|
|
if self.is_emergency:
|
|
return 'EMERGENCY_APPROVAL_REQUIRED'
|
|
return 'APPROVAL_REQUIRED'
|
|
|
|
if self.has_valid_approval():
|
|
return 'APPROVED'
|
|
|
|
return latest_approval.status
|
|
|
|
|
|
class SurgicalNote(models.Model):
|
|
"""
|
|
Surgical note model for perioperative documentation.
|
|
"""
|
|
|
|
class PatientCondition(models.TextChoices):
|
|
STABLE = 'STABLE', 'Stable'
|
|
CRITICAL = 'CRITICAL', 'Critical'
|
|
GUARDED = 'GUARDED', 'Guarded'
|
|
FAIR = 'FAIR', 'Fair'
|
|
GOOD = 'GOOD', 'Good'
|
|
EXCELLENT = 'EXCELLENT', 'Excellent'
|
|
|
|
class Disposition(models.TextChoices):
|
|
RECOVERY = 'RECOVERY', 'Recovery Room'
|
|
ICU = 'ICU', 'Intensive Care Unit'
|
|
WARD = 'WARD', 'Ward'
|
|
DISCHARGE = 'DISCHARGE', 'Discharge'
|
|
MORGUE = 'MORGUE', 'Morgue'
|
|
|
|
class NoteStatus(models.TextChoices):
|
|
DRAFT = 'DRAFT', 'Draft'
|
|
COMPLETED = 'COMPLETED', 'Completed'
|
|
SIGNED = 'SIGNED', 'Signed'
|
|
AMENDED = 'AMENDED', 'Amended'
|
|
|
|
# Surgical Case relationship
|
|
surgical_case = models.OneToOneField(
|
|
SurgicalCase,
|
|
on_delete=models.CASCADE,
|
|
related_name='surgical_notes',
|
|
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='surgeon_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=PatientCondition.choices,
|
|
help_text='Patient condition post-surgery'
|
|
)
|
|
disposition = models.CharField(
|
|
max_length=30,
|
|
choices=Disposition.choices,
|
|
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=NoteStatus.choices,
|
|
default=NoteStatus.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.
|
|
"""
|
|
|
|
class EquipmentType(models.TextChoices):
|
|
SURGICAL_INSTRUMENT = 'SURGICAL_INSTRUMENT', 'Surgical Instrument'
|
|
MONITORING_DEVICE = 'MONITORING_DEVICE', 'Monitoring Device'
|
|
ANESTHESIA_MACHINE = 'ANESTHESIA_MACHINE', 'Anesthesia Machine'
|
|
VENTILATOR = 'VENTILATOR', 'Ventilator'
|
|
ELECTROCAUTERY = 'ELECTROCAUTERY', 'Electrocautery'
|
|
LASER = 'LASER', 'Laser'
|
|
MICROSCOPE = 'MICROSCOPE', 'Microscope'
|
|
C_ARM = 'C_ARM', 'C-Arm'
|
|
ULTRASOUND = 'ULTRASOUND', 'Ultrasound'
|
|
ROBOT = 'ROBOT', 'Surgical Robot'
|
|
IMPLANT = 'IMPLANT', 'Implant'
|
|
DISPOSABLE = 'DISPOSABLE', 'Disposable'
|
|
OTHER = 'OTHER', 'Other'
|
|
|
|
class UnitOfMeasure(models.TextChoices): # rename if you already have a global UnitOfMeasure
|
|
EACH = 'EACH', 'Each'
|
|
SET = 'SET', 'Set'
|
|
PACK = 'PACK', 'Pack'
|
|
BOX = 'BOX', 'Box'
|
|
UNIT = 'UNIT', 'Unit'
|
|
PIECE = 'PIECE', 'Piece'
|
|
|
|
# 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=EquipmentType.choices,
|
|
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=UnitOfMeasure.choices,
|
|
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.
|
|
"""
|
|
|
|
class SurgicalSpecialty(models.TextChoices):
|
|
ALL = 'ALL', 'All Specialties' # consider keeping this for UI filters only
|
|
GENERAL = 'GENERAL', 'General Surgery'
|
|
CARDIAC = 'CARDIAC', 'Cardiac Surgery'
|
|
NEURO = 'NEURO', 'Neurosurgery'
|
|
ORTHOPEDIC = 'ORTHOPEDIC', 'Orthopedic Surgery'
|
|
TRAUMA = 'TRAUMA', 'Trauma Surgery'
|
|
PEDIATRIC = 'PEDIATRIC', 'Pediatric Surgery'
|
|
OBSTETRIC = 'OBSTETRIC', 'Obstetric Surgery'
|
|
OPHTHALMOLOGY = 'OPHTHALMOLOGY', 'Ophthalmology'
|
|
ENT = 'ENT', 'ENT Surgery'
|
|
UROLOGY = 'UROLOGY', 'Urology'
|
|
PLASTIC = 'PLASTIC', 'Plastic Surgery'
|
|
VASCULAR = 'VASCULAR', 'Vascular Surgery'
|
|
THORACIC = 'THORACIC', 'Thoracic Surgery'
|
|
TRANSPLANT = 'TRANSPLANT', 'Transplant Surgery'
|
|
|
|
# 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=SurgicalSpecialty.choices,
|
|
default=SurgicalSpecialty.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)
|