""" Laboratory app models for hospital management system. Provides lab test ordering, specimen management, and result processing. """ import uuid from django.db import models from django.core.validators import RegexValidator, MinValueValidator, MaxValueValidator from django.utils import timezone from django.conf import settings from django.contrib.contenttypes.fields import GenericRelation from datetime import timedelta, datetime, date from decimal import Decimal import json class LabTest(models.Model): """ Lab test model for test catalog and configuration. """ class TestCategory(models.TextChoices): CHEMISTRY = 'CHEMISTRY', 'Chemistry' HEMATOLOGY = 'HEMATOLOGY', 'Hematology' MICROBIOLOGY = 'MICROBIOLOGY', 'Microbiology' IMMUNOLOGY = 'IMMUNOLOGY', 'Immunology' MOLECULAR = 'MOLECULAR', 'Molecular' PATHOLOGY = 'PATHOLOGY', 'Pathology' TOXICOLOGY = 'TOXICOLOGY', 'Toxicology' ENDOCRINOLOGY = 'ENDOCRINOLOGY', 'Endocrinology' CARDIOLOGY = 'CARDIOLOGY', 'Cardiology' ONCOLOGY = 'ONCOLOGY', 'Oncology' GENETICS = 'GENETICS', 'Genetics' COAGULATION = 'COAGULATION', 'Coagulation' URINALYSIS = 'URINALYSIS', 'Urinalysis' OTHER = 'OTHER', 'Other' class TestType(models.TextChoices): QUANTITATIVE = 'QUANTITATIVE', 'Quantitative' QUALITATIVE = 'QUALITATIVE', 'Qualitative' SEMI_QUANTITATIVE = 'SEMI_QUANTITATIVE', 'Semi-Quantitative' CULTURE = 'CULTURE', 'Culture' MICROSCOPY = 'MICROSCOPY', 'Microscopy' MOLECULAR = 'MOLECULAR', 'Molecular' IMMUNOASSAY = 'IMMUNOASSAY', 'Immunoassay' OTHER = 'OTHER', 'Other' class SpecimenType(models.TextChoices): BLOOD = 'BLOOD', 'Blood' SERUM = 'SERUM', 'Serum' PLASMA = 'PLASMA', 'Plasma' URINE = 'URINE', 'Urine' STOOL = 'STOOL', 'Stool' CSF = 'CSF', 'Cerebrospinal Fluid' SPUTUM = 'SPUTUM', 'Sputum' SWAB = 'SWAB', 'Swab' TISSUE = 'TISSUE', 'Tissue' FLUID = 'FLUID', 'Body Fluid' SALIVA = 'SALIVA', 'Saliva' HAIR = 'HAIR', 'Hair' NAIL = 'NAIL', 'Nail' OTHER = 'OTHER', 'Other' class StorageTemperature(models.TextChoices): ROOM_TEMP = 'ROOM_TEMP', 'Room Temperature' REFRIGERATED = 'REFRIGERATED', 'Refrigerated (2-8°C)' FROZEN = 'FROZEN', 'Frozen (-20°C)' DEEP_FROZEN = 'DEEP_FROZEN', 'Deep Frozen (-80°C)' ICE = 'ICE', 'On Ice' AMBIENT = 'AMBIENT', 'Ambient' class QCFrequency(models.TextChoices): DAILY = 'DAILY', 'Daily' WEEKLY = 'WEEKLY', 'Weekly' MONTHLY = 'MONTHLY', 'Monthly' PER_BATCH = 'PER_BATCH', 'Per Batch' CONTINUOUS = 'CONTINUOUS', 'Continuous' class Department(models.TextChoices): CHEMISTRY = 'CHEMISTRY', 'Chemistry' HEMATOLOGY = 'HEMATOLOGY', 'Hematology' MICROBIOLOGY = 'MICROBIOLOGY', 'Microbiology' IMMUNOLOGY = 'IMMUNOLOGY', 'Immunology' MOLECULAR = 'MOLECULAR', 'Molecular' PATHOLOGY = 'PATHOLOGY', 'Pathology' BLOOD_BANK = 'BLOOD_BANK', 'Blood Bank' CYTOLOGY = 'CYTOLOGY', 'Cytology' HISTOLOGY = 'HISTOLOGY', 'Histology' TOXICOLOGY = 'TOXICOLOGY', 'Toxicology' # Tenant relationship tenant = models.ForeignKey( 'core.Tenant', on_delete=models.CASCADE, related_name='lab_tests', help_text='Organization tenant' ) # Test Information test_id = models.UUIDField( default=uuid.uuid4, unique=True, editable=False, help_text='Unique test identifier' ) # Test Identification test_code = models.CharField( max_length=20, help_text='Laboratory test code' ) test_name = models.CharField( max_length=200, help_text='Test name' ) test_description = models.TextField( blank=True, null=True, help_text='Detailed test description' ) # Medical Coding loinc_code = models.CharField( max_length=20, blank=True, null=True, help_text='LOINC code' ) cpt_code = models.CharField( max_length=10, blank=True, null=True, help_text='CPT code for billing' ) snomed_code = models.CharField( max_length=20, blank=True, null=True, help_text='SNOMED CT code' ) # Test Classification test_category = models.CharField( max_length=50, choices=TestCategory.choices, help_text='Test category' ) test_type = models.CharField( max_length=30, choices= TestType.choices, help_text='Type of test' ) # Specimen Requirements specimen_type = models.CharField( max_length=30, choices=SpecimenType, help_text='Required specimen type' ) specimen_volume = models.CharField( max_length=50, blank=True, null=True, help_text='Required specimen volume' ) collection_container = models.CharField( max_length=100, blank=True, null=True, help_text='Collection container type' ) collection_instructions = models.TextField( blank=True, null=True, help_text='Specimen collection instructions' ) # Processing Requirements processing_time = models.PositiveIntegerField( help_text='Processing time in minutes' ) turnaround_time = models.PositiveIntegerField( help_text='Expected turnaround time in hours' ) stat_available = models.BooleanField( default=False, help_text='STAT processing available' ) stat_turnaround_time = models.PositiveIntegerField( blank=True, null=True, help_text='STAT turnaround time in hours' ) # Storage and Transport storage_temperature = models.CharField( max_length=30, choices=StorageTemperature.choices, default=StorageTemperature.ROOM_TEMP, help_text='Storage temperature requirement' ) transport_requirements = models.TextField( blank=True, null=True, help_text='Transport requirements' ) stability_time = models.PositiveIntegerField( blank=True, null=True, help_text='Specimen stability time in hours' ) # Clinical Information clinical_significance = models.TextField( blank=True, null=True, help_text='Clinical significance of test' ) indications = models.TextField( blank=True, null=True, help_text='Clinical indications' ) contraindications = models.TextField( blank=True, null=True, help_text='Contraindications' ) # Patient Preparation patient_preparation = models.TextField( blank=True, null=True, help_text='Patient preparation instructions' ) fasting_required = models.BooleanField( default=False, help_text='Fasting required' ) fasting_hours = models.PositiveIntegerField( blank=True, null=True, help_text='Required fasting hours' ) # Methodology methodology = models.CharField( max_length=100, blank=True, null=True, help_text='Test methodology' ) analyzer = models.CharField( max_length=100, blank=True, null=True, help_text='Analyzer/instrument used' ) # Quality Control qc_frequency = models.CharField( max_length=20, choices=QCFrequency.choices, default=QCFrequency.DAILY, help_text='Quality control frequency' ) # Pricing cost = models.DecimalField( max_digits=10, decimal_places=2, blank=True, null=True, help_text='Test cost' ) # Availability is_active = models.BooleanField( default=True, help_text='Test is active and available' ) is_orderable = models.BooleanField( default=True, help_text='Test can be ordered' ) # Department department = models.CharField( max_length=50, choices=Department.choices, help_text='Laboratory department' ) # 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_lab_tests', help_text='User who created the test' ) class Meta: db_table = 'laboratory_lab_test' verbose_name = 'Lab Test' verbose_name_plural = 'Lab Tests' ordering = ['test_name'] indexes = [ models.Index(fields=['tenant', 'is_active']), models.Index(fields=['test_code']), models.Index(fields=['test_category']), models.Index(fields=['department']), models.Index(fields=['loinc_code']), models.Index(fields=['cpt_code']), ] unique_together = ['tenant', 'test_code'] def __str__(self): return f"{self.test_code} - {self.test_name}" @property def display_name(self): """ Get display name for test. """ return str(self) class LabOrder(models.Model): """ Lab order model for test ordering and management. """ class LabPriority(models.TextChoices): ROUTINE = 'ROUTINE', 'Routine' URGENT = 'URGENT', 'Urgent' STAT = 'STAT', 'STAT' ASAP = 'ASAP', 'ASAP' TIMED = 'TIMED', 'Timed' class LabStatus(models.TextChoices): PENDING = 'PENDING', 'Pending' SCHEDULED = 'SCHEDULED', 'Scheduled' COLLECTED = 'COLLECTED', 'Collected' IN_PROGRESS = 'IN_PROGRESS', 'In Progress' COMPLETED = 'COMPLETED', 'Completed' CANCELLED = 'CANCELLED', 'Cancelled' ON_HOLD = 'ON_HOLD', 'On Hold' class FastingStatus(models.TextChoices): FASTING = 'FASTING', 'Fasting' NON_FASTING = 'NON_FASTING', 'Non-Fasting' UNKNOWN = 'UNKNOWN', 'Unknown' # Tenant relationship tenant = models.ForeignKey( 'core.Tenant', on_delete=models.CASCADE, related_name='lab_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='Lab order number' ) # Patient and Provider patient = models.ForeignKey( 'patients.PatientProfile', on_delete=models.CASCADE, related_name='lab_orders', help_text='Patient' ) ordering_provider = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='ordered_lab_tests', help_text='Ordering provider' ) # Tests tests = models.ManyToManyField( LabTest, related_name='orders', help_text='Ordered tests' ) # Collection scheduling scheduled_collection = models.DateTimeField( blank=True, null=True, help_text='Scheduled collection date and time' ) # Order Details order_datetime = models.DateTimeField( default=timezone.now, help_text='Date and time order was placed' ) priority = models.CharField( max_length=20, choices=LabPriority.choices, default=LabPriority.ROUTINE, help_text='Order priority' ) # Clinical Information clinical_indication = models.TextField( blank=True, null=True, help_text='Clinical indication for tests' ) diagnosis_code = models.CharField( max_length=20, blank=True, null=True, help_text='ICD-10 diagnosis code' ) clinical_notes = models.TextField( blank=True, null=True, help_text='Clinical notes' ) # Collection Information collection_datetime = models.DateTimeField( blank=True, null=True, help_text='Requested collection date and time' ) collection_location = models.CharField( max_length=100, blank=True, null=True, help_text='Collection location' ) fasting_status = models.CharField( max_length=20, choices=FastingStatus.choices, default=FastingStatus.UNKNOWN, help_text='Patient fasting status' ) # Status status = models.CharField( max_length=20, choices=LabStatus.choices, default=LabStatus.PENDING, help_text='Order status' ) # Related Information encounter = models.ForeignKey( 'emr.Encounter', on_delete=models.SET_NULL, null=True, blank=True, related_name='lab_orders', help_text='Related encounter' ) # Special Instructions special_instructions = models.TextField( blank=True, null=True, help_text='Special instructions for collection or processing' ) # Insurance Approval Integration approval_requests = GenericRelation( 'insurance_approvals.InsuranceApprovalRequest', content_type_field='content_type', object_id_field='object_id', related_query_name='lab_order' ) # Metadata created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = 'laboratory_lab_order' verbose_name = 'Lab Order' verbose_name_plural = 'Lab 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']), ] def __str__(self): return f"{self.order_number} - {self.patient.get_full_name()}" def save(self, *args, **kwargs): """ Generate order number if not provided. """ if not self.order_number: # Generate order number (simple implementation) last_order = LabOrder.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"LAB-{self.tenant.id}-{last_number + 1:06d}" else: self.order_number = f"LAB-{self.tenant.id}-000001" super().save(*args, **kwargs) @property def test_count(self): """ Get number of tests in order. """ return self.tests.count() @property def is_stat(self): """ Check if order is STAT priority. """ return self.priority == 'STAT' 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 class Specimen(models.Model): """ Specimen model for specimen tracking and management. """ class SpecimenType(models.TextChoices): BLOOD = 'BLOOD', 'Blood' SERUM = 'SERUM', 'Serum' PLASMA = 'PLASMA', 'Plasma' URINE = 'URINE', 'Urine' STOOL = 'STOOL', 'Stool' CSF = 'CSF', 'Cerebrospinal Fluid' SPUTUM = 'SPUTUM', 'Sputum' SWAB = 'SWAB', 'Swab' TISSUE = 'TISSUE', 'Tissue' FLUID = 'FLUID', 'Body Fluid' SALIVA = 'SALIVA', 'Saliva' HAIR = 'HAIR', 'Hair' NAIL = 'NAIL', 'Nail' OTHER = 'OTHER', 'Other' class SpecimenQuality(models.TextChoices): ACCEPTABLE = 'ACCEPTABLE', 'Acceptable' SUBOPTIMAL = 'SUBOPTIMAL', 'Suboptimal' REJECTED = 'REJECTED', 'Rejected' class RejectionReason(models.TextChoices): HEMOLYZED = 'HEMOLYZED', 'Hemolyzed' CLOTTED = 'CLOTTED', 'Clotted' INSUFFICIENT_VOLUME = 'INSUFFICIENT_VOLUME', 'Insufficient Volume' CONTAMINATED = 'CONTAMINATED', 'Contaminated' MISLABELED = 'MISLABELED', 'Mislabeled' EXPIRED = 'EXPIRED', 'Expired' IMPROPER_STORAGE = 'IMPROPER_STORAGE', 'Improper Storage' DAMAGED_CONTAINER = 'DAMAGED_CONTAINER', 'Damaged Container' OTHER = 'OTHER', 'Other' class StorageTemperature(models.TextChoices): ROOM_TEMP = 'ROOM_TEMP', 'Room Temperature' REFRIGERATED = 'REFRIGERATED', 'Refrigerated (2-8°C)' FROZEN = 'FROZEN', 'Frozen (-20°C)' DEEP_FROZEN = 'DEEP_FROZEN', 'Deep Frozen (-80°C)' class SpecimenStatus(models.TextChoices): COLLECTED = 'COLLECTED', 'Collected' IN_TRANSIT = 'IN_TRANSIT', 'In Transit' RECEIVED = 'RECEIVED', 'Received' PROCESSING = 'PROCESSING', 'Processing' COMPLETED = 'COMPLETED', 'Completed' REJECTED = 'REJECTED', 'Rejected' DISPOSED = 'DISPOSED', 'Disposed' # Order relationship order = models.ForeignKey( LabOrder, on_delete=models.CASCADE, related_name='specimens', help_text='Related lab order' ) # Specimen Information specimen_id = models.UUIDField( default=uuid.uuid4, unique=True, editable=False, help_text='Unique specimen identifier' ) specimen_number = models.CharField( max_length=20, unique=True, help_text='Specimen accession number' ) # Specimen Details specimen_type = models.CharField( max_length=30, choices=SpecimenType.choices, help_text='Specimen type' ) container_type = models.CharField( max_length=50, blank=True, null=True, help_text='Container type' ) volume = models.CharField( max_length=20, blank=True, null=True, help_text='Specimen volume' ) # Collection Information collected_datetime = models.DateTimeField( help_text='Date and time collected' ) collected_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='collected_specimens', help_text='Person who collected specimen' ) collection_site = models.CharField( max_length=100, blank=True, null=True, help_text='Collection site/location' ) collection_method = models.CharField( max_length=50, blank=True, null=True, help_text='Collection method' ) # Specimen Quality quality = models.CharField( max_length=20, choices=SpecimenQuality.choices, default=SpecimenQuality.ACCEPTABLE, help_text='Specimen quality' ) rejection_reason = models.CharField( max_length=100, blank=True, null=True, choices=RejectionReason.choices, help_text='Reason for rejection' ) quality_notes = models.TextField( blank=True, null=True, help_text='Quality assessment notes' ) # Processing Information received_datetime = models.DateTimeField( blank=True, null=True, help_text='Date and time received in lab' ) received_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='received_specimens', help_text='Lab staff who received specimen' ) # Storage Information storage_location = models.CharField( max_length=50, blank=True, null=True, help_text='Storage location' ) storage_temperature = models.CharField( max_length=30, choices=StorageTemperature.choices, default=StorageTemperature.ROOM_TEMP, help_text='Storage temperature' ) # Status status = models.CharField( max_length=20, choices=SpecimenStatus.choices, default=SpecimenStatus.COLLECTED, help_text='Specimen status' ) # Chain of Custody chain_of_custody = models.JSONField( default=list, help_text='Chain of custody tracking' ) # Metadata created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = 'laboratory_specimen' verbose_name = 'Specimen' verbose_name_plural = 'Specimens' ordering = ['-collected_datetime'] indexes = [ models.Index(fields=['order']), models.Index(fields=['specimen_number']), models.Index(fields=['specimen_type']), models.Index(fields=['status']), models.Index(fields=['collected_datetime']), models.Index(fields=['quality']), ] def __str__(self): return f"{self.specimen_number} - {self.specimen_type}" def save(self, *args, **kwargs): """ Generate specimen number if not provided. """ if not self.specimen_number: # Generate specimen number (simple implementation) today = timezone.now().date() last_specimen = Specimen.objects.filter( created_at__date=today ).order_by('-id').first() if last_specimen: last_number = int(last_specimen.specimen_number.split('-')[-1]) self.specimen_number = f"SPEC-{today.strftime('%Y%m%d')}-{last_number + 1:04d}" else: self.specimen_number = f"SPEC-{today.strftime('%Y%m%d')}-0001" super().save(*args, **kwargs) @property def patient(self): """ Get patient from order. """ return self.order.patient @property def is_rejected(self): """ Check if specimen is rejected. """ return self.quality == 'REJECTED' class LabResult(models.Model): """ Lab result model for test results and reporting. """ class ResultType(models.TextChoices): NUMERIC = 'NUMERIC', 'Numeric' TEXT = 'TEXT', 'Text' CODED = 'CODED', 'Coded' NARRATIVE = 'NARRATIVE', 'Narrative' class AbnormalFlag(models.TextChoices): NORMAL = 'N', 'Normal' HIGH = 'H', 'High' LOW = 'L', 'Low' CRITICAL_HIGH = 'HH', 'Critical High' CRITICAL_LOW = 'LL', 'Critical Low' ABNORMAL = 'A', 'Abnormal' class ResultStatus(models.TextChoices): PENDING = 'PENDING', 'Pending' IN_PROGRESS = 'IN_PROGRESS', 'In Progress' COMPLETED = 'COMPLETED', 'Completed' VERIFIED = 'VERIFIED', 'Verified' AMENDED = 'AMENDED', 'Amended' CANCELLED = 'CANCELLED', 'Cancelled' # Order and Test relationship order = models.ForeignKey( LabOrder, on_delete=models.CASCADE, related_name='results', help_text='Related lab order' ) test = models.ForeignKey( LabTest, on_delete=models.CASCADE, related_name='results', help_text='Lab test' ) specimen = models.ForeignKey( Specimen, on_delete=models.CASCADE, related_name='results', help_text='Specimen used for test' ) # Result Information result_id = models.UUIDField( default=uuid.uuid4, unique=True, editable=False, help_text='Unique result identifier' ) # Result Values result_value = models.TextField( blank=True, null=True, help_text='Test result value' ) result_unit = models.CharField( max_length=20, blank=True, null=True, help_text='Result unit of measure' ) result_type = models.CharField( max_length=20, choices=ResultType.choices, default=ResultType.NUMERIC, help_text='Type of result' ) # Reference Range reference_range = models.CharField( max_length=100, blank=True, null=True, help_text='Reference range' ) abnormal_flag = models.CharField( max_length=10, choices=AbnormalFlag.choices, blank=True, null=True, help_text='Abnormal flag' ) # Critical Values is_critical = models.BooleanField( default=False, help_text='Result is critical value' ) critical_called = models.BooleanField( default=False, help_text='Critical value was called to provider' ) critical_called_datetime = models.DateTimeField( blank=True, null=True, help_text='Date and time critical value was called' ) critical_called_to = models.CharField( max_length=100, blank=True, null=True, help_text='Person critical value was called to' ) # Processing Information analyzed_datetime = models.DateTimeField( blank=True, null=True, help_text='Date and time analyzed' ) analyzed_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='analyzed_results', help_text='Lab technician who analyzed' ) analyzer = models.CharField( max_length=100, blank=True, null=True, help_text='Analyzer/instrument used' ) # Verification verified = models.BooleanField( default=False, help_text='Result has been verified' ) verified_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='verified_results', help_text='Lab professional who verified result' ) verified_datetime = models.DateTimeField( blank=True, null=True, help_text='Date and time of verification' ) # Status status = models.CharField( max_length=20, choices=ResultStatus.choices, default=ResultStatus.PENDING, help_text='Result status' ) # Comments technician_comments = models.TextField( blank=True, null=True, help_text='Technician comments' ) pathologist_comments = models.TextField( blank=True, null=True, help_text='Pathologist comments' ) # Quality Control qc_passed = models.BooleanField( default=True, help_text='Quality control passed' ) qc_notes = models.TextField( blank=True, null=True, help_text='Quality control notes' ) # Reporting reported_datetime = models.DateTimeField( blank=True, null=True, help_text='Date and time result was reported' ) # Metadata created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = 'laboratory_lab_result' verbose_name = 'Lab Result' verbose_name_plural = 'Lab Results' ordering = ['-analyzed_datetime'] indexes = [ models.Index(fields=['order']), models.Index(fields=['test']), models.Index(fields=['specimen']), models.Index(fields=['status']), models.Index(fields=['is_critical']), models.Index(fields=['verified']), models.Index(fields=['analyzed_datetime']), ] unique_together = ['order', 'test', 'specimen'] def __str__(self): return f"{self.test.test_name} - {self.result_value} {self.result_unit or ''}" @property def patient(self): """ Get patient from order. """ return self.order.patient @property def is_abnormal(self): """ Check if result is abnormal. """ return self.abnormal_flag and self.abnormal_flag != 'N' @property def is_critical_high(self): """ Check if result is critically high. """ return self.abnormal_flag == 'HH' @property def is_critical_low(self): """ Check if result is critically low. """ return self.abnormal_flag == 'LL' class QualityControl(models.Model): """ Quality control model for lab quality management. """ class ControlLevel(models.TextChoices): NORMAL = 'NORMAL', 'Normal' LOW = 'LOW', 'Low' HIGH = 'HIGH', 'High' CRITICAL = 'CRITICAL', 'Critical' class QCStatus(models.TextChoices): PASSED = 'PASSED', 'Passed' FAILED = 'FAILED', 'Failed' WARNING = 'WARNING', 'Warning' PENDING = 'PENDING', 'Pending' # Tenant relationship tenant = models.ForeignKey( 'core.Tenant', on_delete=models.CASCADE, related_name='lab_quality_controls', help_text='Organization tenant' ) # Test relationship test = models.ForeignKey( LabTest, on_delete=models.CASCADE, related_name='quality_controls', help_text='Lab test' ) result = models.ForeignKey(LabResult, on_delete=models.CASCADE, related_name='quality_controls') # QC Information qc_id = models.UUIDField( default=uuid.uuid4, unique=True, editable=False, help_text='Unique QC identifier' ) # QC Material control_material = models.CharField( max_length=100, help_text='Control material name' ) control_lot = models.CharField( max_length=50, help_text='Control lot number' ) control_level = models.CharField( max_length=20, choices=ControlLevel.choices, help_text='Control level' ) # Expected Values target_value = models.DecimalField( max_digits=15, decimal_places=6, help_text='Target value' ) acceptable_range_low = models.DecimalField( max_digits=15, decimal_places=6, help_text='Acceptable range low' ) acceptable_range_high = models.DecimalField( max_digits=15, decimal_places=6, help_text='Acceptable range high' ) # QC Run Information run_datetime = models.DateTimeField( help_text='Date and time of QC run' ) observed_value = models.DecimalField( max_digits=15, decimal_places=6, help_text='Observed value' ) # QC Status status = models.CharField( max_length=20, choices=QCStatus.choices, help_text='QC status' ) # Staff performed_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='performed_qc', help_text='Staff who performed QC' ) reviewed_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='reviewed_qc', help_text='Staff who reviewed QC' ) # Analyzer analyzer = models.CharField( max_length=100, blank=True, null=True, help_text='Analyzer/instrument used' ) # Comments comments = models.TextField( blank=True, null=True, help_text='QC comments' ) corrective_action = models.TextField( blank=True, null=True, help_text='Corrective action taken' ) # Metadata created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = 'laboratory_quality_control' verbose_name = 'Quality Control' verbose_name_plural = 'Quality Controls' ordering = ['-run_datetime'] indexes = [ models.Index(fields=['tenant', 'test']), models.Index(fields=['test', 'run_datetime']), models.Index(fields=['status']), models.Index(fields=['control_level']), ] def __str__(self): return f"{self.test.test_name} - {self.control_level} - {self.run_datetime.date()}" def save(self, *args, **kwargs): """ Determine QC status based on observed value. """ if self.observed_value: if (self.acceptable_range_low <= self.observed_value <= self.acceptable_range_high): self.status = 'PASSED' else: self.status = 'FAILED' super().save(*args, **kwargs) @property def is_within_range(self): """ Check if observed value is within acceptable range. """ return (self.acceptable_range_low <= self.observed_value <= self.acceptable_range_high) @property def deviation_percentage(self): """ Calculate deviation percentage from target. """ if self.target_value and self.observed_value: return ((self.observed_value - self.target_value) / self.target_value) * 100 return None class ReferenceRange(models.Model): """ Reference range model for test normal values. """ class Gender(models.TextChoices): MALE = 'M', 'Male' FEMALE = 'F', 'Female' ALL = 'ALL', 'All' # Test relationship test = models.ForeignKey( LabTest, on_delete=models.CASCADE, related_name='reference_ranges', help_text='Lab test' ) # Range Information range_id = models.UUIDField( default=uuid.uuid4, unique=True, editable=False, help_text='Unique range identifier' ) # Demographics gender = models.CharField( max_length=10, choices=Gender.choices, default=Gender.ALL, help_text='Gender' ) age_min = models.PositiveIntegerField( blank=True, null=True, help_text='Minimum age in years' ) age_max = models.PositiveIntegerField( blank=True, null=True, help_text='Maximum age in years' ) # Range Values range_low = models.DecimalField( max_digits=15, decimal_places=6, blank=True, null=True, help_text='Range low value' ) range_high = models.DecimalField( max_digits=15, decimal_places=6, blank=True, null=True, help_text='Range high value' ) range_text = models.CharField( max_length=200, blank=True, null=True, help_text='Text description of range' ) # Critical Values critical_low = models.DecimalField( max_digits=15, decimal_places=6, blank=True, null=True, help_text='Critical low value' ) critical_high = models.DecimalField( max_digits=15, decimal_places=6, blank=True, null=True, help_text='Critical high value' ) # Units unit = models.CharField( max_length=20, help_text='Unit of measure' ) # Status is_active = models.BooleanField( default=True, help_text='Reference range is active' ) # 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_reference_ranges', help_text='User who created the reference range' ) class Meta: db_table = 'laboratory_reference_range' verbose_name = 'Reference Range' verbose_name_plural = 'Reference Ranges' ordering = ['test', 'gender', 'age_min'] indexes = [ models.Index(fields=['test', 'is_active']), models.Index(fields=['gender']), models.Index(fields=['age_min', 'age_max']), ] def __str__(self): age_range = "" if self.age_min is not None or self.age_max is not None: age_range = f" ({self.age_min or 0}-{self.age_max or '∞'} years)" return f"{self.test.test_name} - {self.gender}{age_range}" def is_applicable(self, patient_age, patient_gender): """ Check if reference range is applicable for patient. """ # Check gender if self.gender != 'ALL' and self.gender != patient_gender: return False # Check age if self.age_min is not None and patient_age < self.age_min: return False if self.age_max is not None and patient_age > self.age_max: return False return True @property def display_range(self): """ Get display string for range. """ if self.range_text: return self.range_text elif self.range_low is not None and self.range_high is not None: return f"{self.range_low} - {self.range_high} {self.unit}" elif self.range_low is not None: return f"> {self.range_low} {self.unit}" elif self.range_high is not None: return f"< {self.range_high} {self.unit}" else: return "Not defined"