""" 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