1617 lines
46 KiB
Python
1617 lines
46 KiB
Python
"""
|
|
Radiology app models for hospital management system.
|
|
Provides imaging orders, DICOM management, and radiology workflows.
|
|
"""
|
|
|
|
import uuid
|
|
from django.db import models
|
|
from django.core.validators import MinValueValidator, MaxValueValidator
|
|
from django.core.exceptions import ValidationError
|
|
from django.utils import timezone
|
|
from django.conf import settings
|
|
from django.contrib.contenttypes.fields import GenericRelation
|
|
from datetime import timedelta, datetime, date
|
|
from decimal import Decimal
|
|
import json
|
|
|
|
from .constants import RadiologyChoices, RadiologyValidators, RadiologyBusinessRules
|
|
from .managers import (
|
|
ImagingStudyManager, RadiologyReportManager, ImagingOrderManager,
|
|
ImagingSeriesManager, DICOMImageManager, ReportTemplateManager
|
|
)
|
|
|
|
|
|
class ImagingStudy(models.Model):
|
|
"""
|
|
Imaging study model for radiology studies and DICOM management.
|
|
Improved with centralized constants, validation, and optimized structure.
|
|
"""
|
|
# Tenant relationship
|
|
tenant = models.ForeignKey(
|
|
'core.Tenant',
|
|
on_delete=models.CASCADE,
|
|
related_name='imaging_studies',
|
|
help_text='Organization tenant'
|
|
)
|
|
|
|
# Study Information
|
|
study_id = models.UUIDField(
|
|
default=uuid.uuid4,
|
|
unique=True,
|
|
editable=False,
|
|
help_text='Unique study identifier'
|
|
)
|
|
study_instance_uid = models.CharField(
|
|
max_length=64,
|
|
unique=True,
|
|
help_text='DICOM Study Instance UID'
|
|
)
|
|
accession_number = models.CharField(
|
|
max_length=20,
|
|
unique=True,
|
|
help_text='Study accession number'
|
|
)
|
|
|
|
# Patient and Provider
|
|
patient = models.ForeignKey(
|
|
'patients.PatientProfile',
|
|
on_delete=models.CASCADE,
|
|
related_name='imaging_studies',
|
|
help_text='Patient'
|
|
)
|
|
referring_physician = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.CASCADE,
|
|
related_name='referred_studies',
|
|
help_text='Referring physician'
|
|
)
|
|
radiologist = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='interpreted_studies',
|
|
help_text='Interpreting radiologist'
|
|
)
|
|
|
|
# Study Details
|
|
modality = models.CharField(
|
|
max_length=10,
|
|
choices=RadiologyChoices.Modality.choices,
|
|
help_text='Study modality'
|
|
)
|
|
study_description = models.CharField(
|
|
max_length=200,
|
|
help_text='Study description'
|
|
)
|
|
body_part = models.CharField(
|
|
max_length=100,
|
|
choices=RadiologyChoices.BodyPart.choices,
|
|
help_text='Body part examined'
|
|
)
|
|
|
|
# Study Scheduling and Workflow Timestamps
|
|
scheduled_datetime = models.DateTimeField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Date and time study was scheduled'
|
|
)
|
|
study_datetime = models.DateTimeField(
|
|
help_text='Planned study date and time'
|
|
)
|
|
arrived_datetime = models.DateTimeField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Date and time patient arrived for study'
|
|
)
|
|
started_datetime = models.DateTimeField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Date and time study acquisition started'
|
|
)
|
|
completed_datetime = models.DateTimeField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Date and time study acquisition completed'
|
|
)
|
|
interpreted_datetime = models.DateTimeField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Date and time study was interpreted'
|
|
)
|
|
finalized_datetime = models.DateTimeField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Date and time study was finalized'
|
|
)
|
|
|
|
# Clinical Information
|
|
clinical_indication = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Clinical indication for study'
|
|
)
|
|
clinical_history = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Clinical history'
|
|
)
|
|
diagnosis_code = models.CharField(
|
|
max_length=20,
|
|
blank=True,
|
|
null=True,
|
|
help_text='ICD-10 diagnosis code'
|
|
)
|
|
|
|
# Study Status
|
|
status = models.CharField(
|
|
max_length=20,
|
|
choices=RadiologyChoices.StudyStatus.choices,
|
|
default='SCHEDULED',
|
|
help_text='Study status'
|
|
)
|
|
|
|
# Priority
|
|
priority = models.CharField(
|
|
max_length=20,
|
|
choices=RadiologyChoices.Priority.choices,
|
|
default='ROUTINE',
|
|
help_text='Study priority'
|
|
)
|
|
|
|
# Technical Parameters
|
|
kvp = models.FloatField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Peak kilovoltage (kVp)'
|
|
)
|
|
mas = models.FloatField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Milliampere-seconds (mAs)'
|
|
)
|
|
exposure_time = models.FloatField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Exposure time in milliseconds'
|
|
)
|
|
slice_thickness = models.FloatField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Slice thickness in mm'
|
|
)
|
|
|
|
# Equipment Information
|
|
station_name = models.CharField(
|
|
max_length=50,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Acquisition station name'
|
|
)
|
|
manufacturer = models.CharField(
|
|
max_length=50,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Equipment manufacturer'
|
|
)
|
|
model_name = models.CharField(
|
|
max_length=50,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Equipment model name'
|
|
)
|
|
|
|
# Study Metrics
|
|
number_of_series = models.PositiveIntegerField(
|
|
default=0,
|
|
help_text='Number of series in study'
|
|
)
|
|
number_of_instances = models.PositiveIntegerField(
|
|
default=0,
|
|
help_text='Number of instances in study'
|
|
)
|
|
study_size = models.BigIntegerField(
|
|
default=0,
|
|
help_text='Study size in bytes'
|
|
)
|
|
|
|
# Quality and Completion
|
|
image_quality = models.CharField(
|
|
max_length=20,
|
|
choices=RadiologyChoices.ImageQuality.choices,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Image quality assessment'
|
|
)
|
|
completion_status = models.CharField(
|
|
max_length=20,
|
|
choices=RadiologyChoices.CompletionStatus.choices,
|
|
default='COMPLETE',
|
|
help_text='Study completion status'
|
|
)
|
|
|
|
# Related Information
|
|
encounter = models.ForeignKey(
|
|
'emr.Encounter',
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='imaging_studies',
|
|
help_text='Related encounter'
|
|
)
|
|
imaging_order = models.ForeignKey(
|
|
'ImagingOrder',
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='studies',
|
|
help_text='Related imaging order'
|
|
)
|
|
|
|
# PACS Information
|
|
pacs_location = models.CharField(
|
|
max_length=200,
|
|
blank=True,
|
|
null=True,
|
|
help_text='PACS storage location'
|
|
)
|
|
archived = models.BooleanField(
|
|
default=False,
|
|
help_text='Study is archived'
|
|
)
|
|
archive_location = models.CharField(
|
|
max_length=200,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Archive storage location'
|
|
)
|
|
|
|
# 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_studies',
|
|
help_text='User who created the study'
|
|
)
|
|
|
|
# Custom manager
|
|
objects = ImagingStudyManager()
|
|
|
|
class Meta:
|
|
db_table = 'radiology_imaging_study'
|
|
verbose_name = 'Imaging Study'
|
|
verbose_name_plural = 'Imaging Studies'
|
|
ordering = ['-study_datetime']
|
|
indexes = [
|
|
models.Index(fields=['tenant', 'status']),
|
|
models.Index(fields=['patient', 'study_datetime']),
|
|
models.Index(fields=['modality', 'study_datetime']),
|
|
models.Index(fields=['accession_number']),
|
|
models.Index(fields=['study_instance_uid']),
|
|
models.Index(fields=['priority']),
|
|
models.Index(fields=['radiologist']),
|
|
]
|
|
|
|
def __str__(self):
|
|
return f"{self.accession_number} - {self.study_description}"
|
|
|
|
def save(self, *args, **kwargs):
|
|
"""
|
|
Generate accession number if not provided.
|
|
"""
|
|
if not self.accession_number:
|
|
# Generate accession number (simple implementation)
|
|
today = timezone.now().date()
|
|
last_study = ImagingStudy.objects.filter(
|
|
tenant=self.tenant,
|
|
created_at__date=today
|
|
).order_by('-id').first()
|
|
|
|
if last_study:
|
|
last_number = int(last_study.accession_number.split('-')[-1])
|
|
self.accession_number = f"RAD-{today.strftime('%Y%m%d')}-{last_number + 1:04d}"
|
|
else:
|
|
self.accession_number = f"RAD-{today.strftime('%Y%m%d')}-0001"
|
|
|
|
# Set study_datetime from date and time
|
|
# if self.study_datetime:
|
|
# self.study_datetime = timezone.make_aware(
|
|
# datetime.combine(self.study_date, self.study_time)
|
|
# )
|
|
|
|
super().save(*args, **kwargs)
|
|
|
|
@property
|
|
def is_stat(self):
|
|
"""
|
|
Check if study is STAT priority.
|
|
"""
|
|
return self.priority in ['STAT', 'EMERGENCY']
|
|
|
|
@property
|
|
def is_complete(self):
|
|
"""
|
|
Check if study is complete.
|
|
"""
|
|
return self.status in ['COMPLETED', 'INTERPRETED', 'FINALIZED']
|
|
|
|
@property
|
|
def is_final(self):
|
|
"""
|
|
Check if study is finalized.
|
|
"""
|
|
return self.status == 'FINALIZED'
|
|
|
|
@property
|
|
def acquisition_duration(self):
|
|
"""
|
|
Calculate acquisition duration in minutes.
|
|
"""
|
|
if self.started_datetime and self.completed_datetime:
|
|
delta = self.completed_datetime - self.started_datetime
|
|
return int(delta.total_seconds() / 60)
|
|
return None
|
|
|
|
@property
|
|
def total_turnaround_time(self):
|
|
"""
|
|
Calculate total turnaround time from scheduled to finalized in minutes.
|
|
"""
|
|
if self.scheduled_datetime and self.finalized_datetime:
|
|
delta = self.finalized_datetime - self.scheduled_datetime
|
|
return int(delta.total_seconds() / 60)
|
|
return None
|
|
|
|
@property
|
|
def interpretation_turnaround_time(self):
|
|
"""
|
|
Calculate interpretation turnaround time from completed to interpreted in minutes.
|
|
"""
|
|
if self.completed_datetime and self.interpreted_datetime:
|
|
delta = self.interpreted_datetime - self.completed_datetime
|
|
return int(delta.total_seconds() / 60)
|
|
return None
|
|
|
|
@property
|
|
def patient_wait_time(self):
|
|
"""
|
|
Calculate patient wait time from arrival to study start in minutes.
|
|
"""
|
|
if self.arrived_datetime and self.started_datetime:
|
|
delta = self.started_datetime - self.arrived_datetime
|
|
return int(delta.total_seconds() / 60)
|
|
return None
|
|
|
|
@property
|
|
def is_overdue(self):
|
|
"""
|
|
Check if study is overdue based on priority and business rules.
|
|
"""
|
|
if not self.scheduled_datetime or self.is_complete:
|
|
return False
|
|
|
|
now = timezone.now()
|
|
hours_since_scheduled = (now - self.scheduled_datetime).total_seconds() / 3600
|
|
|
|
# Use business rules from constants
|
|
max_hours = RadiologyBusinessRules.PRIORITY_ESCALATION_HOURS.get(self.priority, 24)
|
|
return hours_since_scheduled > max_hours
|
|
|
|
@property
|
|
def workflow_stage(self):
|
|
"""
|
|
Get current workflow stage with timestamp.
|
|
"""
|
|
stages = [
|
|
('scheduled', self.scheduled_datetime),
|
|
('arrived', self.arrived_datetime),
|
|
('started', self.started_datetime),
|
|
('completed', self.completed_datetime),
|
|
('interpreted', self.interpreted_datetime),
|
|
('finalized', self.finalized_datetime),
|
|
]
|
|
|
|
current_stage = 'scheduled'
|
|
for stage, timestamp in stages:
|
|
if timestamp:
|
|
current_stage = stage
|
|
else:
|
|
break
|
|
|
|
return current_stage
|
|
|
|
def clean(self):
|
|
"""
|
|
Validate study data and business rules.
|
|
"""
|
|
super().clean()
|
|
|
|
# Validate timestamp sequence
|
|
timestamps = [
|
|
('scheduled_datetime', self.scheduled_datetime),
|
|
('arrived_datetime', self.arrived_datetime),
|
|
('started_datetime', self.started_datetime),
|
|
('completed_datetime', self.completed_datetime),
|
|
('interpreted_datetime', self.interpreted_datetime),
|
|
('finalized_datetime', self.finalized_datetime),
|
|
]
|
|
|
|
previous_timestamp = None
|
|
for field_name, timestamp in timestamps:
|
|
if timestamp:
|
|
if previous_timestamp and timestamp < previous_timestamp:
|
|
raise ValidationError(f'{field_name} cannot be before previous workflow step')
|
|
previous_timestamp = timestamp
|
|
|
|
# Validate status transitions
|
|
if self.pk: # Only for existing objects
|
|
old_instance = ImagingStudy.objects.get(pk=self.pk)
|
|
valid_transitions = RadiologyBusinessRules.VALID_STATUS_TRANSITIONS.get(old_instance.status, [])
|
|
if self.status != old_instance.status and self.status not in valid_transitions:
|
|
raise ValidationError(f'Invalid status transition from {old_instance.status} to {self.status}')
|
|
|
|
# Validate required fields based on status
|
|
if self.status == 'COMPLETED' and not self.completed_datetime:
|
|
self.completed_datetime = timezone.now()
|
|
|
|
if self.status == 'INTERPRETED' and not self.interpreted_datetime:
|
|
self.interpreted_datetime = timezone.now()
|
|
|
|
if self.status == 'FINALIZED' and not self.finalized_datetime:
|
|
self.finalized_datetime = timezone.now()
|
|
|
|
|
|
class ImagingSeries(models.Model):
|
|
"""
|
|
Imaging series model for DICOM series management.
|
|
"""
|
|
|
|
# Study relationship
|
|
study = models.ForeignKey(
|
|
ImagingStudy,
|
|
on_delete=models.CASCADE,
|
|
related_name='series',
|
|
help_text='Parent imaging study'
|
|
)
|
|
|
|
# Series Information
|
|
series_id = models.UUIDField(
|
|
default=uuid.uuid4,
|
|
unique=True,
|
|
editable=False,
|
|
help_text='Unique series identifier'
|
|
)
|
|
series_instance_uid = models.CharField(
|
|
max_length=64,
|
|
unique=True,
|
|
help_text='DICOM Series Instance UID'
|
|
)
|
|
series_number = models.PositiveIntegerField(
|
|
help_text='Series number within study'
|
|
)
|
|
|
|
# Series Details
|
|
modality = models.CharField(
|
|
max_length=10,
|
|
choices=RadiologyChoices.Modality.choices,
|
|
help_text='Series modality'
|
|
)
|
|
series_description = models.CharField(
|
|
max_length=200,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Series description'
|
|
)
|
|
protocol_name = models.CharField(
|
|
max_length=100,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Protocol name'
|
|
)
|
|
|
|
# Series Workflow Timestamps
|
|
series_datetime = models.DateTimeField(
|
|
help_text='Series acquisition date and time'
|
|
)
|
|
started_datetime = models.DateTimeField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Date and time series acquisition started'
|
|
)
|
|
completed_datetime = models.DateTimeField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Date and time series acquisition completed'
|
|
)
|
|
processed_datetime = models.DateTimeField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Date and time series was processed'
|
|
)
|
|
archived_datetime = models.DateTimeField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Date and time series was archived'
|
|
)
|
|
|
|
# Technical Parameters
|
|
slice_thickness = models.FloatField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Slice thickness in mm'
|
|
)
|
|
spacing_between_slices = models.FloatField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Spacing between slices in mm'
|
|
)
|
|
pixel_spacing = models.CharField(
|
|
max_length=50,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Pixel spacing'
|
|
)
|
|
image_orientation = models.CharField(
|
|
max_length=100,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Image orientation'
|
|
)
|
|
|
|
# Series Metrics
|
|
number_of_instances = models.PositiveIntegerField(
|
|
default=0,
|
|
help_text='Number of instances in series'
|
|
)
|
|
series_size = models.BigIntegerField(
|
|
default=0,
|
|
help_text='Series size in bytes'
|
|
)
|
|
|
|
# Body Part and Position
|
|
body_part = models.CharField(
|
|
max_length=100,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Body part examined'
|
|
)
|
|
patient_position = models.CharField(
|
|
max_length=20,
|
|
choices=[
|
|
('HFP', 'Head First-Prone'),
|
|
('HFS', 'Head First-Supine'),
|
|
('HFDR', 'Head First-Decubitus Right'),
|
|
('HFDL', 'Head First-Decubitus Left'),
|
|
('FFP', 'Feet First-Prone'),
|
|
('FFS', 'Feet First-Supine'),
|
|
('FFDR', 'Feet First-Decubitus Right'),
|
|
('FFDL', 'Feet First-Decubitus Left'),
|
|
],
|
|
blank=True,
|
|
null=True,
|
|
help_text='Patient position'
|
|
)
|
|
|
|
# Contrast Information
|
|
contrast_agent = models.CharField(
|
|
max_length=100,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Contrast agent used'
|
|
)
|
|
contrast_route = models.CharField(
|
|
max_length=20,
|
|
choices=[
|
|
('IV', 'Intravenous'),
|
|
('ORAL', 'Oral'),
|
|
('RECTAL', 'Rectal'),
|
|
('INTRATHECAL', 'Intrathecal'),
|
|
('INTRA_ARTICULAR', 'Intra-articular'),
|
|
('OTHER', 'Other'),
|
|
],
|
|
blank=True,
|
|
null=True,
|
|
help_text='Contrast administration route'
|
|
)
|
|
|
|
# Quality
|
|
image_quality = models.CharField(
|
|
max_length=20,
|
|
choices=[
|
|
('EXCELLENT', 'Excellent'),
|
|
('GOOD', 'Good'),
|
|
('ADEQUATE', 'Adequate'),
|
|
('POOR', 'Poor'),
|
|
('UNACCEPTABLE', 'Unacceptable'),
|
|
],
|
|
blank=True,
|
|
null=True,
|
|
help_text='Image quality assessment'
|
|
)
|
|
|
|
# Metadata
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
# Custom manager
|
|
objects = ImagingSeriesManager()
|
|
|
|
class Meta:
|
|
db_table = 'radiology_imaging_series'
|
|
verbose_name = 'Imaging Series'
|
|
verbose_name_plural = 'Imaging Series'
|
|
ordering = ['study', 'series_number']
|
|
indexes = [
|
|
models.Index(fields=['study', 'series_number']),
|
|
models.Index(fields=['series_instance_uid']),
|
|
models.Index(fields=['modality']),
|
|
models.Index(fields=['series_datetime']),
|
|
]
|
|
unique_together = ['study', 'series_number']
|
|
|
|
def __str__(self):
|
|
return f"Series {self.series_number}: {self.series_description or 'No description'}"
|
|
|
|
def save(self, *args, **kwargs):
|
|
"""
|
|
Set series_datetime if not provided.
|
|
"""
|
|
if not self.series_datetime:
|
|
self.series_datetime = timezone.now()
|
|
|
|
super().save(*args, **kwargs)
|
|
|
|
@property
|
|
def patient(self):
|
|
"""
|
|
Get patient from study.
|
|
"""
|
|
return self.study.patient
|
|
|
|
|
|
class DICOMImage(models.Model):
|
|
"""
|
|
DICOM image model for individual image instances.
|
|
"""
|
|
|
|
# Series relationship
|
|
series = models.ForeignKey(
|
|
ImagingSeries,
|
|
on_delete=models.CASCADE,
|
|
related_name='images',
|
|
help_text='Parent imaging series'
|
|
)
|
|
|
|
# Image Information
|
|
image_id = models.UUIDField(
|
|
default=uuid.uuid4,
|
|
unique=True,
|
|
editable=False,
|
|
help_text='Unique image identifier'
|
|
)
|
|
sop_instance_uid = models.CharField(
|
|
max_length=64,
|
|
unique=True,
|
|
help_text='DICOM SOP Instance UID'
|
|
)
|
|
instance_number = models.PositiveIntegerField(
|
|
help_text='Instance number within series'
|
|
)
|
|
|
|
# Image Details
|
|
image_type = models.CharField(
|
|
max_length=100,
|
|
blank=True,
|
|
null=True,
|
|
help_text='DICOM image type'
|
|
)
|
|
sop_class_uid = models.CharField(
|
|
max_length=64,
|
|
help_text='DICOM SOP Class UID'
|
|
)
|
|
|
|
# Image Dimensions
|
|
rows = models.PositiveIntegerField(
|
|
help_text='Number of rows in image'
|
|
)
|
|
columns = models.PositiveIntegerField(
|
|
help_text='Number of columns in image'
|
|
)
|
|
bits_allocated = models.PositiveIntegerField(
|
|
help_text='Number of bits allocated for each pixel'
|
|
)
|
|
bits_stored = models.PositiveIntegerField(
|
|
help_text='Number of bits stored for each pixel'
|
|
)
|
|
|
|
# Image Position and Orientation
|
|
image_position = models.CharField(
|
|
max_length=100,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Image position (patient)'
|
|
)
|
|
image_orientation = models.CharField(
|
|
max_length=100,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Image orientation (patient)'
|
|
)
|
|
slice_location = models.FloatField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Slice location'
|
|
)
|
|
|
|
# Window/Level Settings
|
|
window_center = models.FloatField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Window center'
|
|
)
|
|
window_width = models.FloatField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Window width'
|
|
)
|
|
|
|
# File Information
|
|
file_path = models.CharField(
|
|
max_length=500,
|
|
help_text='File path on storage system'
|
|
)
|
|
file_size = models.BigIntegerField(
|
|
help_text='File size in bytes'
|
|
)
|
|
transfer_syntax_uid = models.CharField(
|
|
max_length=64,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Transfer syntax UID'
|
|
)
|
|
|
|
# Content Information
|
|
content_date = models.DateField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Content date'
|
|
)
|
|
content_time = models.TimeField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Content time'
|
|
)
|
|
acquisition_datetime = models.DateTimeField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Acquisition date and time'
|
|
)
|
|
|
|
# Processing Workflow Timestamps
|
|
processed_datetime = models.DateTimeField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Date and time image was processed'
|
|
)
|
|
quality_checked_datetime = models.DateTimeField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Date and time image quality was checked'
|
|
)
|
|
archived_datetime = models.DateTimeField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Date and time image was archived'
|
|
)
|
|
|
|
# Quality and Status
|
|
image_quality = models.CharField(
|
|
max_length=20,
|
|
choices=RadiologyChoices.ImageQuality.choices,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Image quality assessment'
|
|
)
|
|
|
|
# Processing Status
|
|
processed = models.BooleanField(
|
|
default=False,
|
|
help_text='Image has been processed'
|
|
)
|
|
archived = models.BooleanField(
|
|
default=False,
|
|
help_text='Image is archived'
|
|
)
|
|
|
|
# Metadata
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
# Custom manager
|
|
objects = DICOMImageManager()
|
|
|
|
class Meta:
|
|
db_table = 'radiology_dicom_image'
|
|
verbose_name = 'DICOM Image'
|
|
verbose_name_plural = 'DICOM Images'
|
|
ordering = ['series', 'instance_number']
|
|
indexes = [
|
|
models.Index(fields=['series', 'instance_number']),
|
|
models.Index(fields=['sop_instance_uid']),
|
|
models.Index(fields=['sop_class_uid']),
|
|
models.Index(fields=['processed']),
|
|
models.Index(fields=['archived']),
|
|
]
|
|
unique_together = ['series', 'instance_number']
|
|
|
|
def __str__(self):
|
|
return f"Image {self.instance_number} ({self.sop_instance_uid})"
|
|
|
|
@property
|
|
def study(self):
|
|
"""
|
|
Get study from series.
|
|
"""
|
|
return self.series.study
|
|
|
|
@property
|
|
def patient(self):
|
|
"""
|
|
Get patient from study.
|
|
"""
|
|
return self.series.study.patient
|
|
|
|
@property
|
|
def file_size_mb(self):
|
|
"""
|
|
Get file size in MB.
|
|
"""
|
|
return round(self.file_size / (1024 * 1024), 2)
|
|
|
|
def generate_dicom_file(self, pixel_data=None):
|
|
"""
|
|
Generate DICOM file from this model instance.
|
|
|
|
Args:
|
|
pixel_data: Optional numpy array for pixel data
|
|
|
|
Returns:
|
|
str: Path to generated DICOM file
|
|
"""
|
|
from .services import DICOMGenerator
|
|
generator = DICOMGenerator()
|
|
return generator.generate_dicom_from_model(self, pixel_data)
|
|
|
|
def validate_for_dicom_generation(self):
|
|
"""
|
|
Validate this instance for DICOM generation.
|
|
|
|
Returns:
|
|
dict: Validation results
|
|
"""
|
|
from .services import DICOMValidator
|
|
return DICOMValidator.validate_dicom_image_model(self)
|
|
|
|
def has_dicom_file(self):
|
|
"""
|
|
Check if DICOM file exists on disk.
|
|
|
|
Returns:
|
|
bool: True if file exists
|
|
"""
|
|
import os
|
|
return bool(self.file_path and os.path.exists(self.file_path))
|
|
|
|
def get_dicom_metadata(self):
|
|
"""
|
|
Extract metadata from DICOM file if it exists.
|
|
|
|
Returns:
|
|
dict: DICOM metadata or None if file doesn't exist
|
|
"""
|
|
if not self.has_dicom_file():
|
|
return None
|
|
|
|
from .services import DICOMValidator
|
|
return DICOMValidator.validate_dicom_file(self.file_path)
|
|
|
|
|
|
class RadiologyReport(models.Model):
|
|
"""
|
|
Radiology report model for study interpretation and reporting.
|
|
"""
|
|
|
|
# Study relationship
|
|
study = models.OneToOneField(
|
|
ImagingStudy,
|
|
on_delete=models.CASCADE,
|
|
related_name='report',
|
|
help_text='Related imaging study'
|
|
)
|
|
|
|
# Report Information
|
|
report_id = models.UUIDField(
|
|
default=uuid.uuid4,
|
|
unique=True,
|
|
editable=False,
|
|
help_text='Unique report identifier'
|
|
)
|
|
|
|
# Radiologist Information
|
|
radiologist = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.CASCADE,
|
|
related_name='radiology_reports',
|
|
help_text='Interpreting radiologist'
|
|
)
|
|
dictated_by = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='dictated_reports',
|
|
help_text='Radiologist who dictated the report'
|
|
)
|
|
transcribed_by = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='transcribed_reports',
|
|
help_text='Person who transcribed the report'
|
|
)
|
|
|
|
# Report Content
|
|
clinical_history = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Clinical history and indication'
|
|
)
|
|
technique = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Imaging technique and protocol'
|
|
)
|
|
findings = models.TextField(
|
|
help_text='Imaging findings'
|
|
)
|
|
impression = models.TextField(
|
|
help_text='Radiologist impression and conclusion'
|
|
)
|
|
recommendations = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Recommendations for follow-up'
|
|
)
|
|
|
|
# Report Status
|
|
status = models.CharField(
|
|
max_length=20,
|
|
choices=[
|
|
('DRAFT', 'Draft'),
|
|
('PRELIMINARY', 'Preliminary'),
|
|
('FINAL', 'Final'),
|
|
('AMENDED', 'Amended'),
|
|
('CORRECTED', 'Corrected'),
|
|
],
|
|
default='DRAFT',
|
|
help_text='Report status'
|
|
)
|
|
|
|
# Critical Findings
|
|
critical_finding = models.BooleanField(
|
|
default=False,
|
|
help_text='Report contains critical findings'
|
|
)
|
|
critical_communicated = models.BooleanField(
|
|
default=False,
|
|
help_text='Critical findings have been communicated'
|
|
)
|
|
critical_communicated_to = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.SET_NULL,
|
|
related_name='critical_communicated_reports',
|
|
blank=True,
|
|
null=True,
|
|
help_text='Person critical findings were communicated to'
|
|
)
|
|
critical_communicated_datetime = models.DateTimeField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Date and time critical findings were communicated'
|
|
)
|
|
|
|
# Report Dates
|
|
dictated_datetime = models.DateTimeField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Date and time report was dictated'
|
|
)
|
|
transcribed_datetime = models.DateTimeField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Date and time report was transcribed'
|
|
)
|
|
verified_datetime = models.DateTimeField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Date and time report was verified'
|
|
)
|
|
finalized_datetime = models.DateTimeField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Date and time report was finalized'
|
|
)
|
|
|
|
# Template and Structured Reporting
|
|
template_used = models.ForeignKey(
|
|
'ReportTemplate',
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='reports',
|
|
help_text='Report template used'
|
|
)
|
|
structured_data = models.JSONField(
|
|
default=dict,
|
|
help_text='Structured reporting data'
|
|
)
|
|
|
|
# Quality and Metrics
|
|
report_length = models.PositiveIntegerField(
|
|
default=0,
|
|
help_text='Report length in characters'
|
|
)
|
|
turnaround_time = models.PositiveIntegerField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Turnaround time in minutes'
|
|
)
|
|
|
|
# Addenda
|
|
addendum = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Report addendum'
|
|
)
|
|
addendum_datetime = models.DateTimeField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Date and time addendum was added'
|
|
)
|
|
|
|
# Metadata
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
# Custom manager
|
|
objects = RadiologyReportManager()
|
|
|
|
class Meta:
|
|
db_table = 'radiology_radiology_report'
|
|
verbose_name = 'Radiology Report'
|
|
verbose_name_plural = 'Radiology Reports'
|
|
ordering = ['-created_at']
|
|
indexes = [
|
|
models.Index(fields=['study']),
|
|
models.Index(fields=['radiologist']),
|
|
models.Index(fields=['status']),
|
|
models.Index(fields=['critical_finding']),
|
|
models.Index(fields=['finalized_datetime']),
|
|
]
|
|
|
|
def __str__(self):
|
|
return f"Report for {self.study.accession_number}"
|
|
|
|
def save(self, *args, **kwargs):
|
|
"""
|
|
Calculate report length and turnaround time.
|
|
"""
|
|
# Calculate report length
|
|
content = f"{self.findings} {self.impression}"
|
|
self.report_length = len(content)
|
|
|
|
# Calculate turnaround time
|
|
if self.finalized_datetime and self.study.study_datetime:
|
|
delta = self.finalized_datetime - self.study.study_datetime
|
|
self.turnaround_time = int(delta.total_seconds() / 60)
|
|
|
|
# Set finalized datetime if status is final
|
|
if self.status == 'FINAL' and not self.finalized_datetime:
|
|
self.finalized_datetime = timezone.now()
|
|
|
|
super().save(*args, **kwargs)
|
|
|
|
@property
|
|
def patient(self):
|
|
"""
|
|
Get patient from study.
|
|
"""
|
|
return self.study.patient
|
|
|
|
@property
|
|
def is_final(self):
|
|
"""
|
|
Check if report is final.
|
|
"""
|
|
return self.status == 'FINAL'
|
|
|
|
@property
|
|
def is_critical(self):
|
|
"""
|
|
Check if report has critical findings.
|
|
"""
|
|
return self.critical_finding
|
|
|
|
|
|
class ReportTemplate(models.Model):
|
|
"""
|
|
Report template model for standardized reporting.
|
|
"""
|
|
|
|
# Tenant relationship
|
|
tenant = models.ForeignKey(
|
|
'core.Tenant',
|
|
on_delete=models.CASCADE,
|
|
related_name='report_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
|
|
modality = models.CharField(
|
|
max_length=10,
|
|
choices=[
|
|
('ALL', 'All Modalities'),
|
|
('CR', 'Computed Radiography'),
|
|
('CT', 'Computed Tomography'),
|
|
('MR', 'Magnetic Resonance'),
|
|
('US', 'Ultrasound'),
|
|
('XA', 'X-Ray Angiography'),
|
|
('RF', 'Radiofluoroscopy'),
|
|
('DX', 'Digital Radiography'),
|
|
('MG', 'Mammography'),
|
|
('NM', 'Nuclear Medicine'),
|
|
('PT', 'Positron Emission Tomography'),
|
|
],
|
|
default='ALL',
|
|
help_text='Applicable modality'
|
|
)
|
|
body_part = models.CharField(
|
|
max_length=100,
|
|
choices=[
|
|
('ALL', 'All Body Parts'),
|
|
('HEAD', 'Head'),
|
|
('NECK', 'Neck'),
|
|
('CHEST', 'Chest'),
|
|
('ABDOMEN', 'Abdomen'),
|
|
('PELVIS', 'Pelvis'),
|
|
('SPINE', 'Spine'),
|
|
('EXTREMITY', 'Extremity'),
|
|
('BREAST', 'Breast'),
|
|
('HEART', 'Heart'),
|
|
('BRAIN', 'Brain'),
|
|
],
|
|
default='ALL',
|
|
help_text='Applicable body part'
|
|
)
|
|
|
|
# Template Content
|
|
clinical_history_template = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Clinical history template'
|
|
)
|
|
technique_template = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Technique template'
|
|
)
|
|
findings_template = models.TextField(
|
|
help_text='Findings template'
|
|
)
|
|
impression_template = models.TextField(
|
|
help_text='Impression template'
|
|
)
|
|
recommendations_template = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Recommendations template'
|
|
)
|
|
|
|
# Structured Reporting
|
|
structured_fields = models.JSONField(
|
|
default=dict,
|
|
help_text='Structured reporting field definitions'
|
|
)
|
|
|
|
# Template Status
|
|
is_active = models.BooleanField(
|
|
default=True,
|
|
help_text='Template is active'
|
|
)
|
|
is_default = models.BooleanField(
|
|
default=False,
|
|
help_text='Default template for modality/body part'
|
|
)
|
|
|
|
# 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_report_templates',
|
|
help_text='User who created the template'
|
|
)
|
|
|
|
# Custom manager
|
|
objects = ReportTemplateManager()
|
|
|
|
class Meta:
|
|
db_table = 'radiology_report_template'
|
|
verbose_name = 'Report Template'
|
|
verbose_name_plural = 'Report Templates'
|
|
ordering = ['modality', 'body_part', 'name']
|
|
indexes = [
|
|
models.Index(fields=['tenant', 'is_active']),
|
|
models.Index(fields=['modality', 'body_part']),
|
|
models.Index(fields=['is_default']),
|
|
]
|
|
unique_together = ['tenant', 'name']
|
|
|
|
def __str__(self):
|
|
return f"{self.name} ({self.modality}/{self.body_part})"
|
|
|
|
def save(self, *args, **kwargs):
|
|
"""
|
|
Ensure only one default template per modality/body part.
|
|
"""
|
|
if self.is_default:
|
|
# Remove default flag from other templates
|
|
ReportTemplate.objects.filter(
|
|
tenant=self.tenant,
|
|
modality=self.modality,
|
|
body_part=self.body_part,
|
|
is_default=True
|
|
).exclude(pk=self.pk).update(is_default=False)
|
|
|
|
super().save(*args, **kwargs)
|
|
|
|
|
|
class ImagingOrder(models.Model):
|
|
"""
|
|
Imaging order model for radiology order management.
|
|
"""
|
|
MODALITY_CHOICES = [
|
|
('CR', 'Computed Radiography'),
|
|
('CT', 'Computed Tomography'),
|
|
('MR', 'Magnetic Resonance'),
|
|
('US', 'Ultrasound'),
|
|
('XA', 'X-Ray Angiography'),
|
|
('RF', 'Radiofluoroscopy'),
|
|
('DX', 'Digital Radiography'),
|
|
('MG', 'Mammography'),
|
|
('NM', 'Nuclear Medicine'),
|
|
('PT', 'Positron Emission Tomography'),
|
|
]
|
|
PRIORITY_CHOICES = [
|
|
('ROUTINE', 'Routine'),
|
|
('URGENT', 'Urgent'),
|
|
('STAT', 'STAT'),
|
|
('EMERGENCY', 'Emergency'),
|
|
]
|
|
BODY_PART_CHOICES = [
|
|
('HEAD', 'Head'),
|
|
('NECK', 'Neck'),
|
|
('CHEST', 'Chest'),
|
|
('ABDOMEN', 'Abdomen'),
|
|
('PELVIS', 'Pelvis'),
|
|
('SPINE', 'Spine'),
|
|
('EXTREMITY', 'Extremity'),
|
|
('BREAST', 'Breast'),
|
|
('HEART', 'Heart'),
|
|
('BRAIN', 'Brain'),
|
|
('WHOLE_BODY', 'Whole Body'),
|
|
('OTHER', 'Other'),
|
|
]
|
|
CONTRAST_ROUTE_CHOICES = [
|
|
('IV', 'Intravenous'),
|
|
('ORAL', 'Oral'),
|
|
('RECTAL', 'Rectal'),
|
|
('INTRATHECAL', 'Intrathecal'),
|
|
('INTRA_ARTICULAR', 'Intra-articular'),
|
|
('OTHER', 'Other'),
|
|
]
|
|
STATUS_CHOICES = [
|
|
('PENDING', 'Pending'),
|
|
('SCHEDULED', 'Scheduled'),
|
|
('IN_PROGRESS', 'In Progress'),
|
|
('COMPLETED', 'Completed'),
|
|
('CANCELLED', 'Cancelled'),
|
|
('ON_HOLD', 'On Hold'),
|
|
]
|
|
# Tenant relationship
|
|
tenant = models.ForeignKey(
|
|
'core.Tenant',
|
|
on_delete=models.CASCADE,
|
|
related_name='imaging_orders',
|
|
help_text='Organization tenant'
|
|
)
|
|
|
|
# Order Information
|
|
order_id = models.UUIDField(
|
|
default=uuid.uuid4,
|
|
unique=True,
|
|
editable=False,
|
|
help_text='Unique order identifier'
|
|
)
|
|
order_number = models.CharField(
|
|
max_length=20,
|
|
unique=True,
|
|
help_text='Imaging order number'
|
|
)
|
|
|
|
# Patient and Provider
|
|
patient = models.ForeignKey(
|
|
'patients.PatientProfile',
|
|
on_delete=models.CASCADE,
|
|
related_name='imaging_orders',
|
|
help_text='Patient'
|
|
)
|
|
ordering_provider = models.ForeignKey(
|
|
settings.AUTH_USER_MODEL,
|
|
on_delete=models.CASCADE,
|
|
related_name='ordered_imaging_studies',
|
|
help_text='Ordering provider'
|
|
)
|
|
|
|
# Order Details
|
|
order_datetime = models.DateTimeField(
|
|
default=timezone.now,
|
|
help_text='Date and time order was placed'
|
|
)
|
|
priority = models.CharField(
|
|
max_length=20,
|
|
choices=PRIORITY_CHOICES,
|
|
default='ROUTINE',
|
|
help_text='Order priority'
|
|
)
|
|
|
|
# Imaging Details
|
|
modality = models.CharField(
|
|
max_length=10,
|
|
choices=MODALITY_CHOICES,
|
|
help_text='Requested modality'
|
|
)
|
|
study_description = models.CharField(
|
|
max_length=200,
|
|
help_text='Study description'
|
|
)
|
|
body_part = models.CharField(
|
|
max_length=100,
|
|
choices=BODY_PART_CHOICES,
|
|
help_text='Body part to be examined'
|
|
)
|
|
|
|
# Clinical Information
|
|
clinical_indication = models.TextField(
|
|
help_text='Clinical indication for imaging'
|
|
)
|
|
clinical_history = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Relevant clinical history'
|
|
)
|
|
diagnosis_code = models.CharField(
|
|
max_length=20,
|
|
blank=True,
|
|
null=True,
|
|
help_text='ICD-10 diagnosis code'
|
|
)
|
|
|
|
# Contrast Information
|
|
contrast_required = models.BooleanField(
|
|
default=False,
|
|
help_text='Contrast agent required'
|
|
)
|
|
contrast_type = models.CharField(
|
|
max_length=50,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Type of contrast agent'
|
|
)
|
|
contrast_route = models.CharField(
|
|
max_length=20,
|
|
choices=CONTRAST_ROUTE_CHOICES,
|
|
blank=True,
|
|
null=True,
|
|
help_text='Contrast administration route'
|
|
)
|
|
|
|
# Order Workflow Timestamps
|
|
requested_datetime = models.DateTimeField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Requested study date and time'
|
|
)
|
|
approved_datetime = models.DateTimeField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Date and time order was approved'
|
|
)
|
|
scheduled_datetime = models.DateTimeField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Scheduled study date and time'
|
|
)
|
|
cancelled_datetime = models.DateTimeField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Date and time order was cancelled'
|
|
)
|
|
completed_datetime = models.DateTimeField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Date and time order was completed'
|
|
)
|
|
|
|
# Status
|
|
status = models.CharField(
|
|
max_length=20,
|
|
choices=STATUS_CHOICES,
|
|
default='PENDING',
|
|
help_text='Order status'
|
|
)
|
|
|
|
# Related Information
|
|
encounter = models.ForeignKey(
|
|
'emr.Encounter',
|
|
on_delete=models.SET_NULL,
|
|
null=True,
|
|
blank=True,
|
|
related_name='imaging_orders',
|
|
help_text='Related encounter'
|
|
)
|
|
|
|
# Special Instructions
|
|
special_instructions = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Special instructions for imaging'
|
|
)
|
|
patient_preparation = models.TextField(
|
|
blank=True,
|
|
null=True,
|
|
help_text='Patient preparation instructions'
|
|
)
|
|
|
|
# Insurance Approval Integration
|
|
approval_requests = GenericRelation(
|
|
'insurance_approvals.InsuranceApprovalRequest',
|
|
content_type_field='content_type',
|
|
object_id_field='object_id',
|
|
related_query_name='imaging_order'
|
|
)
|
|
|
|
# Metadata
|
|
created_at = models.DateTimeField(auto_now_add=True)
|
|
updated_at = models.DateTimeField(auto_now=True)
|
|
|
|
# Custom manager
|
|
objects = ImagingOrderManager()
|
|
|
|
class Meta:
|
|
db_table = 'radiology_imaging_order'
|
|
verbose_name = 'Imaging Order'
|
|
verbose_name_plural = 'Imaging Orders'
|
|
ordering = ['-order_datetime']
|
|
indexes = [
|
|
models.Index(fields=['tenant', 'status']),
|
|
models.Index(fields=['patient', 'status']),
|
|
models.Index(fields=['ordering_provider']),
|
|
models.Index(fields=['order_datetime']),
|
|
models.Index(fields=['order_number']),
|
|
models.Index(fields=['priority']),
|
|
models.Index(fields=['modality']),
|
|
]
|
|
|
|
def __str__(self):
|
|
return f"{self.order_number} - {self.study_description}"
|
|
|
|
def save(self, *args, **kwargs):
|
|
"""
|
|
Generate order number if not provided.
|
|
"""
|
|
if not self.order_number:
|
|
# Generate order number (simple implementation)
|
|
last_order = ImagingOrder.objects.filter(tenant=self.tenant).order_by('-id').first()
|
|
if last_order:
|
|
last_number = int(last_order.order_number.split('-')[-1])
|
|
self.order_number = f"IMG-{self.tenant.id}-{last_number + 1:06d}"
|
|
else:
|
|
self.order_number = f"IMG-{self.tenant.id}-000001"
|
|
|
|
super().save(*args, **kwargs)
|
|
|
|
@property
|
|
def is_stat(self):
|
|
"""
|
|
Check if order is STAT priority.
|
|
"""
|
|
return self.priority in ['STAT', 'EMERGENCY']
|
|
|
|
def has_valid_approval(self):
|
|
"""
|
|
Check if order 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 order.
|
|
"""
|
|
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 order requires insurance approval.
|
|
Returns True if patient has insurance and no valid approval exists.
|
|
"""
|
|
if not self.patient.insurance_info.exists():
|
|
return False
|
|
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:
|
|
return 'APPROVAL_REQUIRED'
|
|
|
|
if self.has_valid_approval():
|
|
return 'APPROVED'
|
|
|
|
return latest_approval.status
|