2025-10-03 20:11:25 +03:00

1389 lines
39 KiB
Python

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