2025-08-12 13:33:25 +03:00

1367 lines
37 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 RegexValidator, MinValueValidator, MaxValueValidator
from django.utils import timezone
from django.conf import settings
from datetime import timedelta, datetime, date
from decimal import Decimal
import json
class ImagingStudy(models.Model):
"""
Imaging study model for radiology studies and DICOM 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'),
('OT', 'Other'),
]
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'),
]
STATUS_CHOICES=[
('SCHEDULED', 'Scheduled'),
('ARRIVED', 'Arrived'),
('IN_PROGRESS', 'In Progress'),
('COMPLETED', 'Completed'),
('INTERPRETED', 'Interpreted'),
('FINALIZED', 'Finalized'),
('CANCELLED', 'Cancelled'),
]
PRIORITY_CHOICES = [
('ROUTINE', 'Routine'),
('URGENT', 'Urgent'),
('STAT', 'STAT'),
('EMERGENCY', 'Emergency'),
]
IMAGE_QUALITY_CHOICES = [
('EXCELLENT', 'Excellent'),
('GOOD', 'Good'),
('ADEQUATE', 'Adequate'),
('POOR', 'Poor'),
('UNACCEPTABLE', 'Unacceptable'),
]
COMPLETION_STATUS_CHOICES = [
('COMPLETE', 'Complete'),
('PARTIAL', 'Partial'),
('INCOMPLETE', 'Incomplete'),
]
# 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=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=BODY_PART_CHOICES,
help_text='Body part examined'
)
# Study Dates and Times
study_date = models.DateField(
help_text='Study date'
)
study_time = models.TimeField(
help_text='Study time'
)
study_datetime = models.DateTimeField(
help_text='Study date and time'
)
# 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=STATUS_CHOICES,
default='SCHEDULED',
help_text='Study status'
)
# Priority
priority = models.CharField(
max_length=20,
choices=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=IMAGE_QUALITY_CHOICES,
blank=True,
null=True,
help_text='Image quality assessment'
)
completion_status = models.CharField(
max_length=20,
choices=COMPLETION_STATUS_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'
)
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_date']),
models.Index(fields=['modality', 'study_date']),
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_date and self.study_time:
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']
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=[
('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'),
('OT', 'Other'),
],
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 Dates and Times
series_date = models.DateField(
help_text='Series date'
)
series_time = models.TimeField(
help_text='Series time'
)
series_datetime = models.DateTimeField(
help_text='Series date and time'
)
# 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)
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 from date and time.
"""
if self.series_date and self.series_time:
self.series_datetime = timezone.make_aware(
datetime.combine(self.series_date, self.series_time)
)
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'
)
# Quality and Status
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'
)
# 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)
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)
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.CharField(
max_length=100,
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)
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'
)
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'
)
# Scheduling Information
requested_datetime = models.DateTimeField(
blank=True,
null=True,
help_text='Requested study date and time'
)
scheduled_datetime = models.DateTimeField(
blank=True,
null=True,
help_text='Scheduled study date and time'
)
# 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'
)
# Metadata
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
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']