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