""" EMR app models for hospital management system. Provides electronic medical records, clinical documentation, and patient care management. """ 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, time import json from django.core.exceptions import ValidationError from django.db import models as django_models class EncounterManager(django_models.Manager): """ Custom manager for Encounter model with common queries. """ def active_encounters(self, tenant): """Get all active encounters for a tenant.""" return self.filter( tenant=tenant, status__in=['ARRIVED', 'TRIAGED', 'IN_PROGRESS', 'ON_HOLD'] ) def encounters_by_provider(self, provider, tenant, days_back=30): """Get encounters by provider within specified days.""" start_date = timezone.now().date() - timedelta(days=days_back) return self.filter( tenant=tenant, provider=provider, start_datetime__date__gte=start_date ).order_by('-start_datetime') def encounters_by_patient(self, patient, tenant): """Get all encounters for a specific patient.""" return self.filter( tenant=tenant, patient=patient ).order_by('-start_datetime') def encounters_by_type(self, encounter_type, tenant, days_back=30): """Get encounters by type within specified days.""" start_date = timezone.now().date() - timedelta(days=days_back) return self.filter( tenant=tenant, encounter_type=encounter_type, start_datetime__date__gte=start_date ).order_by('-start_datetime') def todays_encounters(self, tenant): """Get today's encounters for a tenant.""" today = timezone.now().date() return self.filter( tenant=tenant, start_datetime__date=today ).order_by('-start_datetime') def unsigned_encounters(self, tenant): """Get encounters that need signing off.""" return self.filter( tenant=tenant, status='FINISHED', signed_off=False ).order_by('-end_datetime') class Encounter(models.Model): """ Clinical encounter model for tracking patient visits and care episodes. """ objects = EncounterManager() class EncounterType(models.TextChoices): INPATIENT = 'INPATIENT', 'Inpatient' OUTPATIENT = 'OUTPATIENT', 'Outpatient' EMERGENCY = 'EMERGENCY', 'Emergency' URGENT_CARE = 'URGENT_CARE', 'Urgent Care' OBSERVATION = 'OBSERVATION', 'Observation' TELEMEDICINE = 'TELEMEDICINE', 'Telemedicine' HOME_VISIT = 'HOME_VISIT', 'Home Visit' CONSULTATION = 'CONSULTATION', 'Consultation' FOLLOW_UP = 'FOLLOW_UP', 'Follow-up' PROCEDURE = 'PROCEDURE', 'Procedure' SURGERY = 'SURGERY', 'Surgery' DIAGNOSTIC = 'DIAGNOSTIC', 'Diagnostic' PREVENTIVE = 'PREVENTIVE', 'Preventive Care' class EncounterClass(models.TextChoices): AMB = 'AMB', 'Ambulatory' EMER = 'EMER', 'Emergency' FLD = 'FLD', 'Field' HH = 'HH', 'Home Health' IMP = 'IMP', 'Inpatient' ACUTE = 'ACUTE', 'Inpatient Acute' NONAC = 'NONAC', 'Inpatient Non-Acute' OBSENC = 'OBSENC', 'Observation Encounter' PRENC = 'PRENC', 'Pre-Admission' SS = 'SS', 'Short Stay' VR = 'VR', 'Virtual' class EncounterStatus(models.TextChoices): PLANNED = 'PLANNED', 'Planned' ARRIVED = 'ARRIVED', 'Arrived' TRIAGED = 'TRIAGED', 'Triaged' IN_PROGRESS = 'IN_PROGRESS', 'In Progress' ON_HOLD = 'ON_HOLD', 'On Hold' COMPLETED = 'COMPLETED', 'Completed' CANCELLED = 'CANCELLED', 'Cancelled' ENTERED_IN_ERROR = 'ENTERED_IN_ERROR', 'Entered in Error' UNKNOWN = 'UNKNOWN', 'Unknown' class Priority(models.TextChoices): ROUTINE = 'ROUTINE', 'Routine' URGENT = 'URGENT', 'Urgent' STAT = 'STAT', 'STAT' EMERGENCY = 'EMERGENCY', 'Emergency' # Tenant relationship tenant = models.ForeignKey( 'core.Tenant', on_delete=models.CASCADE, related_name='encounters', help_text='Organization tenant' ) # Encounter Information encounter_id = models.UUIDField( default=uuid.uuid4, unique=True, editable=False, help_text='Unique encounter identifier' ) # Patient and Provider patient = models.ForeignKey( 'patients.PatientProfile', on_delete=models.CASCADE, related_name='encounters', help_text='Patient for this encounter' ) provider = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='encounters', help_text='Primary provider for this encounter' ) # Encounter Details encounter_type = models.CharField( max_length=30, choices=EncounterType.choices, help_text='Type of encounter' ) encounter_class = models.CharField( max_length=20, choices=EncounterClass.choices, help_text='Encounter class (HL7 standard)' ) # Timing start_datetime = models.DateTimeField( help_text='Encounter start date and time' ) end_datetime = models.DateTimeField( blank=True, null=True, help_text='Encounter end date and time' ) # Status status = models.CharField( max_length=20, choices=EncounterStatus.choices, default=EncounterStatus.PLANNED, help_text='Current encounter status' ) # Location Information location = models.CharField( max_length=100, blank=True, null=True, help_text='Encounter location' ) room_number = models.CharField( max_length=20, blank=True, null=True, help_text='Room number' ) # Related Records appointment = models.ForeignKey( 'appointments.AppointmentRequest', on_delete=models.SET_NULL, null=True, blank=True, related_name='encounters', help_text='Related appointment' ) admission = models.ForeignKey( 'inpatients.Admission', on_delete=models.SET_NULL, null=True, blank=True, related_name='encounters', help_text='Related admission' ) # Clinical Information chief_complaint = models.TextField( blank=True, null=True, help_text='Chief complaint' ) reason_for_visit = models.TextField( blank=True, null=True, help_text='Reason for visit' ) # Priority and Acuity priority = models.CharField( max_length=20, choices=Priority.choices, default=Priority.ROUTINE, help_text='Encounter priority' ) acuity_level = models.PositiveIntegerField( blank=True, null=True, validators=[MinValueValidator(1), MaxValueValidator(5)], help_text='Patient acuity level (1-5, 5 being highest)' ) # Documentation Status documentation_complete = models.BooleanField( default=False, help_text='Documentation is complete' ) signed_off = models.BooleanField( default=False, help_text='Encounter has been signed off' ) signed_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='signed_encounters', help_text='Provider who signed off' ) signed_datetime = models.DateTimeField( blank=True, null=True, help_text='Date and time of sign-off' ) # Billing Information billable = models.BooleanField( default=True, help_text='Encounter is billable' ) billing_codes = models.JSONField( default=list, blank=True, help_text='Associated billing codes' ) # Quality Measures quality_measures = models.JSONField( default=dict, blank=True, help_text='Quality measure data' ) # 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_encounters', help_text='User who created the encounter' ) class Meta: db_table = 'emr_encounter' verbose_name = 'Encounter' verbose_name_plural = 'Encounters' ordering = ['-start_datetime'] indexes = [ models.Index(fields=['tenant', 'status']), models.Index(fields=['patient', 'start_datetime']), models.Index(fields=['provider']), models.Index(fields=['encounter_type']), models.Index(fields=['start_datetime']), ] def __str__(self): return f"{self.patient.get_full_name()} - {self.start_datetime.strftime('%Y-%m-%d %H:%M')}" @property def duration(self): """ Calculate encounter duration. """ if self.end_datetime: return self.end_datetime - self.start_datetime elif self.status in ['IN_PROGRESS', 'ON_HOLD']: return timezone.now() - self.start_datetime return None @property def is_active(self): """ Check if encounter is currently active. """ return self.status in ['ARRIVED', 'TRIAGED', 'IN_PROGRESS', 'ON_HOLD'] def get_status_color(self): """ Get Bootstrap color class for status display. """ status_colors = { 'PLANNED': 'danger', 'ARRIVED': 'warning', 'TRIAGED': 'success', 'IN_PROGRESS': 'info', 'ON_HOLD': 'warning', 'COMPLETED': 'warning', 'CANCELLED': 'secondary', 'ENTERED_IN_ERROR': 'secondary', 'UNKNOWN': 'primary', } return status_colors.get(self.status, 'secondary') class VitalSigns(models.Model): """ Vital signs model for tracking patient vital signs measurements. """ class TemperatureMethod(models.TextChoices): ORAL = 'ORAL', 'Oral' RECTAL = 'RECTAL', 'Rectal' AXILLARY = 'AXILLARY', 'Axillary' TYMPANIC = 'TYMPANIC', 'Tympanic' TEMPORAL = 'TEMPORAL', 'Temporal' CORE = 'CORE', 'Core' class BpPosition(models.TextChoices): SITTING = 'SITTING', 'Sitting' STANDING = 'STANDING', 'Standing' LYING = 'LYING', 'Lying' SUPINE = 'SUPINE', 'Supine' class BpCuffSize(models.TextChoices): SMALL = 'SMALL', 'Small' REGULAR = 'REGULAR', 'Regular' LARGE = 'LARGE', 'Large' EXTRA_LARGE = 'EXTRA_LARGE', 'Extra Large' PEDIATRIC = 'PEDIATRIC', 'Pediatric' class HeartRhythm(models.TextChoices): REGULAR = 'REGULAR', 'Regular' REGULARLY_IRREGULAR = 'REGULARLY_IRREGULAR', 'Regularly irregular' IRREGULARLY_IRREGULAR = 'IRREGULARLY_IRREGULAR', 'Irregularly irregular' IRREGULAR_UNSPECIFIED = 'IRREGULAR_UNSPECIFIED', 'Irregular (unspecified)' class OxygenDelivery(models.TextChoices): ROOM_AIR = 'ROOM_AIR', 'Room Air' NASAL_CANNULA = 'NASAL_CANNULA', 'Nasal Cannula' SIMPLE_MASK = 'SIMPLE_MASK', 'Simple Mask' NON_REBREATHER = 'NON_REBREATHER', 'Non-Rebreather Mask' VENTURI_MASK = 'VENTURI_MASK', 'Venturi Mask' CPAP = 'CPAP', 'CPAP' BIPAP = 'BIPAP', 'BiPAP' MECHANICAL_VENTILATION = 'MECHANICAL_VENTILATION', 'Mechanical Ventilation' OTHER = 'OTHER', 'Other' # Encounter relationship encounter = models.ForeignKey( Encounter, on_delete=models.CASCADE, related_name='vital_signs', help_text='Associated encounter' ) # Patient relationship patient = models.ForeignKey( 'patients.PatientProfile', on_delete=models.CASCADE, related_name='vital_signs', help_text='Patient' ) # Measurement Information measurement_id = models.UUIDField( default=uuid.uuid4, unique=True, editable=False, help_text='Unique measurement identifier' ) # Timing measured_datetime = models.DateTimeField( default=timezone.now, help_text='Date and time of measurement' ) # Vital Signs Measurements temperature = models.DecimalField( max_digits=4, decimal_places=1, blank=True, null=True, help_text='Temperature in Celsius' ) temperature_method = models.CharField( max_length=20, choices=TemperatureMethod.choices, blank=True, null=True, help_text='Temperature measurement method' ) # Blood Pressure systolic_bp = models.PositiveIntegerField( blank=True, null=True, validators=[MinValueValidator(50), MaxValueValidator(300)], help_text='Systolic blood pressure (mmHg)' ) diastolic_bp = models.PositiveIntegerField( blank=True, null=True, validators=[MinValueValidator(30), MaxValueValidator(200)], help_text='Diastolic blood pressure (mmHg)' ) bp_position = models.CharField( max_length=20, choices=BpPosition.choices, blank=True, null=True, help_text='Patient position during BP measurement' ) bp_cuff_size = models.CharField( max_length=20, choices=BpCuffSize.choices, blank=True, null=True, help_text='Blood pressure cuff size' ) # Heart Rate heart_rate = models.PositiveIntegerField( blank=True, null=True, validators=[MinValueValidator(20), MaxValueValidator(300)], help_text='Heart rate (beats per minute)' ) heart_rhythm = models.CharField( max_length=25, choices=HeartRhythm.choices, blank=True, null=True, help_text='Heart rhythm' ) # Respiratory Rate respiratory_rate = models.PositiveIntegerField( blank=True, null=True, validators=[MinValueValidator(5), MaxValueValidator(60)], help_text='Respiratory rate (breaths per minute)' ) # Oxygen Saturation oxygen_saturation = models.PositiveIntegerField( blank=True, null=True, validators=[MinValueValidator(50), MaxValueValidator(100)], help_text='Oxygen saturation (%)' ) oxygen_delivery = models.CharField( max_length=30, choices=OxygenDelivery.choices, default=OxygenDelivery.ROOM_AIR, help_text='Oxygen delivery method' ) oxygen_flow_rate = models.DecimalField( max_digits=4, decimal_places=1, blank=True, null=True, help_text='Oxygen flow rate (L/min)' ) # Pain Scale pain_scale = models.PositiveIntegerField( blank=True, null=True, validators=[MinValueValidator(0), MaxValueValidator(10)], help_text='Pain scale (0-10)' ) pain_location = models.CharField( max_length=100, blank=True, null=True, help_text='Pain location' ) pain_quality = models.CharField( max_length=50, blank=True, null=True, help_text='Pain quality description' ) # Weight and Height weight = models.DecimalField( max_digits=5, decimal_places=1, blank=True, null=True, help_text='Weight in pounds' ) height = models.DecimalField( max_digits=5, decimal_places=1, blank=True, null=True, help_text='Height in inches' ) # BMI (calculated) bmi = models.DecimalField( max_digits=4, decimal_places=1, blank=True, null=True, help_text='Body Mass Index' ) # Additional Measurements head_circumference = models.DecimalField( max_digits=4, decimal_places=1, blank=True, null=True, help_text='Head circumference in cm (pediatric)' ) # Device Information device_used = models.CharField( max_length=100, blank=True, null=True, help_text='Device used for measurements' ) device_calibrated = models.BooleanField( default=True, help_text='Device was calibrated' ) # Staff Information measured_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='vital_signs_measurements', help_text='Staff member who took measurements' ) verified_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='verified_vital_signs', help_text='Staff member who verified measurements' ) # Quality Flags critical_values = models.JSONField( default=list, blank=True, help_text='Critical values identified' ) alerts_generated = models.JSONField( default=list, blank=True, help_text='Alerts generated from measurements' ) # Notes notes = models.TextField( blank=True, null=True, help_text='Additional notes about measurements' ) # Metadata created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = 'emr_vital_signs' verbose_name = 'Vital Signs' verbose_name_plural = 'Vital Signs' ordering = ['-measured_datetime'] indexes = [ models.Index(fields=['patient', 'measured_datetime']), models.Index(fields=['encounter']), models.Index(fields=['measured_datetime']), ] def __str__(self): return f"{self.patient.get_full_name()} - {self.measured_datetime.strftime('%Y-%m-%d %H:%M')}" def clean(self): """ Custom validation for VitalSigns model. """ # Validate blood pressure if (self.systolic_bp and not self.diastolic_bp) or (self.diastolic_bp and not self.systolic_bp): raise ValidationError('Both systolic and diastolic blood pressure must be provided together.') if self.systolic_bp and self.diastolic_bp and self.systolic_bp <= self.diastolic_bp: raise ValidationError('Systolic blood pressure must be greater than diastolic blood pressure.') # Validate oxygen saturation and delivery if self.oxygen_delivery and self.oxygen_delivery != 'ROOM_AIR' and not self.oxygen_flow_rate: raise ValidationError('Oxygen flow rate is required when oxygen delivery method is specified.') # Validate pain scale if self.pain_scale and (self.pain_scale < 0 or self.pain_scale > 10): raise ValidationError('Pain scale must be between 0 and 10.') # Validate temperature if self.temperature and (self.temperature < 30 or self.temperature > 45): raise ValidationError('Temperature must be between 30°C and 45°C.') # Validate heart rate if self.heart_rate and (self.heart_rate < 20 or self.heart_rate > 300): raise ValidationError('Heart rate must be between 20 and 300 bpm.') # Validate respiratory rate if self.respiratory_rate and (self.respiratory_rate < 5 or self.respiratory_rate > 60): raise ValidationError('Respiratory rate must be between 5 and 60 breaths per minute.') # Validate oxygen saturation if self.oxygen_saturation and (self.oxygen_saturation < 50 or self.oxygen_saturation > 100): raise ValidationError('Oxygen saturation must be between 50% and 100%.') def save(self, *args, **kwargs): """ Calculate BMI if weight and height are provided. """ self.full_clean() # Run validation before saving if self.weight and self.height: # BMI = (weight in pounds / (height in inches)^2) * 703 self.bmi = (self.weight / (self.height ** 2)) * 703 super().save(*args, **kwargs) @property def blood_pressure(self): """ Return formatted blood pressure. """ if self.systolic_bp and self.diastolic_bp: return f"{self.systolic_bp}/{self.diastolic_bp}" return None @property def has_critical_values(self): """ Check if any measurements have critical values. """ return len(self.critical_values) > 0 @property def is_normal_temperature(self): """ Check if temperature is within normal range (36.1°C - 37.2°C). """ return self.temperature and 36.1 <= self.temperature <= 37.2 @property def is_normal_blood_pressure(self): """ Check if blood pressure is within normal range (90/60 - 120/80). """ if not (self.systolic_bp and self.diastolic_bp): return None return 90 <= self.systolic_bp <= 140 and 60 <= self.diastolic_bp <= 90 @property def is_normal_oxygen_saturation(self): """ Check if oxygen saturation is normal (>= 95%). """ return self.oxygen_saturation and self.oxygen_saturation >= 95 def get_vital_signs_summary(self): """ Get a summary of vital signs for display. """ summary = [] if self.temperature: summary.append(f"T: {self.temperature}°C") if self.blood_pressure: summary.append(f"BP: {self.blood_pressure}") if self.heart_rate: summary.append(f"HR: {self.heart_rate}") if self.respiratory_rate: summary.append(f"RR: {self.respiratory_rate}") if self.oxygen_saturation: summary.append(f"SpO2: {self.oxygen_saturation}%") return " | ".join(summary) if summary else "No vital signs recorded" class ProblemList(models.Model): """ Problem list model for tracking patient problems and diagnoses. """ class CodingSystem(models.TextChoices): ICD10 = 'ICD10', 'ICD-10' ICD9 = 'ICD9', 'ICD-9' SNOMED = 'SNOMED', 'SNOMED CT' CPT = 'CPT', 'CPT' LOINC = 'LOINC', 'LOINC' OTHER = 'OTHER', 'Other' class ProblemType(models.TextChoices): DIAGNOSIS = 'DIAGNOSIS', 'Diagnosis' SYMPTOM = 'SYMPTOM', 'Symptom' FINDING = 'FINDING', 'Finding' COMPLAINT = 'COMPLAINT', 'Complaint' CONDITION = 'CONDITION', 'Condition' DISORDER = 'DISORDER', 'Disorder' SYNDROME = 'SYNDROME', 'Syndrome' INJURY = 'INJURY', 'Injury' ALLERGY = 'ALLERGY', 'Allergy' INTOLERANCE = 'INTOLERANCE', 'Intolerance' RISK_FACTOR = 'RISK_FACTOR', 'Risk Factor' OTHER = 'OTHER', 'Other' class Severity(models.TextChoices): MILD = 'MILD', 'Mild' MODERATE = 'MODERATE', 'Moderate' SEVERE = 'SEVERE', 'Severe' CRITICAL = 'CRITICAL', 'Critical' UNKNOWN = 'UNKNOWN', 'Unknown' class Priority(models.TextChoices): LOW = 'LOW', 'Low' MEDIUM = 'MEDIUM', 'Medium' HIGH = 'HIGH', 'High' URGENT = 'URGENT', 'Urgent' class ProblemStatus(models.TextChoices): ACTIVE = 'ACTIVE', 'Active' INACTIVE = 'INACTIVE', 'Inactive' RESOLVED = 'RESOLVED', 'Resolved' REMISSION = 'REMISSION', 'In Remission' RECURRENCE = 'RECURRENCE', 'Recurrence' RELAPSE = 'RELAPSE', 'Relapse' UNKNOWN = 'UNKNOWN', 'Unknown' OTHER = 'OTHER', 'Other' class Laterality(models.TextChoices): LEFT = 'LEFT', 'Left' RIGHT = 'RIGHT', 'Right' BILATERAL = 'BILATERAL', 'Bilateral' UNILATERAL = 'UNILATERAL', 'Unilateral' NOT_APPLICABLE = 'NOT_APPLICABLE', 'Not Applicable' # Tenant relationship tenant = models.ForeignKey( 'core.Tenant', on_delete=models.CASCADE, related_name='problem_lists', help_text='Organization tenant' ) # Patient relationship patient = models.ForeignKey( 'patients.PatientProfile', on_delete=models.CASCADE, related_name='problems', help_text='Patient' ) # Problem Information problem_id = models.UUIDField( default=uuid.uuid4, unique=True, editable=False, help_text='Unique problem identifier' ) # Problem Details problem_name = models.CharField( max_length=200, help_text='Problem name or description' ) problem_code = models.CharField( max_length=20, blank=True, null=True, help_text='ICD-10 or SNOMED code' ) coding_system = models.CharField( max_length=20, choices=CodingSystem.choices, blank=True, null=True, help_text='Coding system used' ) # Problem Classification problem_type = models.CharField( max_length=30, choices=ProblemType.choices, help_text='Type of problem' ) # Clinical Information onset_date = models.DateField( blank=True, null=True, help_text='Date of onset' ) onset_description = models.CharField( max_length=100, blank=True, null=True, help_text='Description of onset' ) # Severity and Priority severity = models.CharField( max_length=20, choices=Severity.choices, blank=True, null=True, help_text='Problem severity' ) priority = models.CharField( max_length=20, choices=Priority.choices, default=Priority.MEDIUM, help_text='Problem priority' ) # Status status = models.CharField( max_length=20, choices=ProblemStatus.choices, default=ProblemStatus.ACTIVE, help_text='Current problem status' ) # Resolution Information resolution_date = models.DateField( blank=True, null=True, help_text='Date problem was resolved' ) resolution_notes = models.TextField( blank=True, null=True, help_text='Notes about problem resolution' ) # Provider Information diagnosing_provider = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='diagnosed_problems', help_text='Provider who diagnosed the problem' ) managing_provider = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='managed_problems', help_text='Provider managing the problem' ) # Related Information related_encounter = models.ForeignKey( Encounter, on_delete=models.SET_NULL, null=True, blank=True, related_name='problems_identified', help_text='Encounter where problem was identified' ) # Clinical Context body_site = models.CharField( max_length=100, blank=True, null=True, help_text='Body site affected' ) laterality = models.CharField( max_length=20, choices=Laterality.choices, blank=True, null=True, help_text='Laterality' ) # Problem Notes clinical_notes = models.TextField( blank=True, null=True, help_text='Clinical notes about the problem' ) patient_concerns = models.TextField( blank=True, null=True, help_text='Patient concerns and comments' ) # Goals and Outcomes treatment_goals = models.JSONField( default=list, blank=True, help_text='Treatment goals for this problem' ) outcome_measures = models.JSONField( default=list, blank=True, help_text='Outcome measures being tracked' ) # Verification verified = models.BooleanField( default=False, help_text='Problem has been verified' ) verified_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='verified_problems', help_text='Provider who verified the problem' ) verified_date = models.DateTimeField( blank=True, null=True, help_text='Date problem was verified' ) # 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_problems', help_text='User who created the problem' ) class Meta: db_table = 'emr_problem_list' verbose_name = 'Problem' verbose_name_plural = 'Problem List' ordering = ['-created_at'] indexes = [ models.Index(fields=['tenant', 'status']), models.Index(fields=['patient', 'status']), models.Index(fields=['problem_type']), models.Index(fields=['priority']), models.Index(fields=['onset_date']), ] def __str__(self): return f"{self.patient.get_full_name()} - {self.problem_name}" @property def is_active(self): """ Check if problem is currently active. """ return self.status == 'ACTIVE' def clean(self): """ Custom validation for ProblemList model. """ # Validate resolution date is after onset date if self.onset_date and self.resolution_date and self.resolution_date < self.onset_date: raise ValidationError('Resolution date cannot be before onset date.') # Validate resolution date is not in the future for resolved problems if self.status in ['RESOLVED', 'REMISSION'] and self.resolution_date and self.resolution_date > timezone.now().date(): raise ValidationError('Resolution date cannot be in the future.') # Validate onset date is not in the future if self.onset_date and self.onset_date > timezone.now().date(): raise ValidationError('Onset date cannot be in the future.') def save(self, *args, **kwargs): """ Override save to perform custom validation. """ self.full_clean() super().save(*args, **kwargs) @property def duration(self): """ Calculate problem duration. """ if self.onset_date: end_date = self.resolution_date or timezone.now().date() return end_date - self.onset_date return None @property def duration_days(self): """ Get duration in days. """ duration = self.duration return duration.days if duration else None @property def is_chronic(self): """ Check if problem is considered chronic (duration > 90 days). """ duration = self.duration return duration and duration.days > 90 @property def can_be_resolved(self): """ Check if problem can still be resolved. """ return self.status in ['ACTIVE', 'RECURRENCE', 'RELAPSE'] def get_status_color(self): """ Get Bootstrap color class for status display. """ status_colors = { 'ACTIVE': 'danger', 'INACTIVE': 'warning', 'RESOLVED': 'success', 'REMISSION': 'info', 'RECURRENCE': 'warning', 'RELAPSE': 'warning', 'UNKNOWN': 'secondary', 'OTHER': 'secondary', } return status_colors.get(self.status, 'secondary') def get_priority_color(self): """ Get Bootstrap color class for priority display. """ priority_colors = { 'LOW': 'success', 'MEDIUM': 'info', 'HIGH': 'warning', 'URGENT': 'danger', } return priority_colors.get(self.priority, 'secondary') def get_severity_color(self): """ Get Bootstrap color class for priority display. """ severity_colors = { 'MILD': 'success', 'MODERATE': 'info', 'SEVERE': 'warning', 'CRITICAL': 'danger', } return severity_colors.get(self.severity, 'secondary') class CarePlan(models.Model): """ Care plan model for managing patient care coordination and goals. """ class PlanType(models.TextChoices): COMPREHENSIVE = 'COMPREHENSIVE', 'Comprehensive Care Plan' DISEASE_SPECIFIC = 'DISEASE_SPECIFIC', 'Disease-Specific Plan' PREVENTIVE = 'PREVENTIVE', 'Preventive Care Plan' CHRONIC_CARE = 'CHRONIC_CARE', 'Chronic Care Management' ACUTE_CARE = 'ACUTE_CARE', 'Acute Care Plan' DISCHARGE = 'DISCHARGE', 'Discharge Planning' REHABILITATION = 'REHABILITATION', 'Rehabilitation Plan' PALLIATIVE = 'PALLIATIVE', 'Palliative Care Plan' MENTAL_HEALTH = 'MENTAL_HEALTH', 'Mental Health Plan' MEDICATION = 'MEDICATION', 'Medication Management' NUTRITION = 'NUTRITION', 'Nutrition Plan' EXERCISE = 'EXERCISE', 'Exercise Plan' OTHER = 'OTHER', 'Other' class PlanCategory(models.TextChoices): ASSESSMENT = 'ASSESSMENT', 'Assessment and Monitoring' TREATMENT = 'TREATMENT', 'Treatment' EDUCATION = 'EDUCATION', 'Patient Education' COORDINATION = 'COORDINATION', 'Care Coordination' PREVENTION = 'PREVENTION', 'Prevention' LIFESTYLE = 'LIFESTYLE', 'Lifestyle Modification' MEDICATION = 'MEDICATION', 'Medication Management' FOLLOW_UP = 'FOLLOW_UP', 'Follow-up Care' EMERGENCY = 'EMERGENCY', 'Emergency Planning' SUPPORT = 'SUPPORT', 'Support Services' class PlanStatus(models.TextChoices): DRAFT = 'DRAFT', 'Draft' ACTIVE = 'ACTIVE', 'Active' ON_HOLD = 'ON_HOLD', 'On Hold' COMPLETED = 'COMPLETED', 'Completed' CANCELLED = 'CANCELLED', 'Cancelled' ERROR = 'ERROR', 'Entered in Error' UNKNOWN = 'UNKNOWN', 'Unknown' class PlanPriority(models.TextChoices): LOW = 'LOW', 'Low' ROUTINE = 'ROUTINE', 'Routine' URGENT = 'URGENT', 'Urgent' STAT = 'STAT', 'STAT' # Tenant relationship tenant = models.ForeignKey( 'core.Tenant', on_delete=models.CASCADE, related_name='care_plans', help_text='Organization tenant' ) # Patient relationship patient = models.ForeignKey( 'patients.PatientProfile', on_delete=models.CASCADE, related_name='care_plans', help_text='Patient' ) # Care Plan Information care_plan_id = models.UUIDField( default=uuid.uuid4, unique=True, editable=False, help_text='Unique care plan identifier' ) # Plan Details title = models.CharField( max_length=200, help_text='Care plan title' ) description = models.TextField( help_text='Care plan description' ) # Plan Type and Category plan_type = models.CharField( max_length=30, choices=PlanType.choices, help_text='Type of care plan' ) category = models.CharField( max_length=50, choices=PlanCategory.choices, help_text='Care plan category' ) # Timing start_date = models.DateField( help_text='Care plan start date' ) end_date = models.DateField( blank=True, null=True, help_text='Care plan end date' ) target_completion_date = models.DateField( blank=True, null=True, help_text='Target completion date' ) # Status status = models.CharField( max_length=20, choices=PlanStatus.choices, default=PlanStatus.DRAFT, help_text='Care plan status' ) # Priority priority = models.CharField( max_length=20, choices=PlanPriority.choices, default=PlanPriority.ROUTINE, help_text='Care plan priority' ) # Provider Information primary_provider = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='primary_care_plans', help_text='Primary provider responsible for care plan' ) care_team = models.ManyToManyField( settings.AUTH_USER_MODEL, related_name='care_team_plans', blank=True, help_text='Care team members' ) # Related Problems related_problems = models.ManyToManyField( ProblemList, related_name='care_plans', blank=True, help_text='Related problems addressed by this plan' ) # Goals and Objectives goals = models.JSONField( default=list, help_text='Care plan goals' ) objectives = models.JSONField( default=list, help_text='Specific objectives and targets' ) # Interventions and Activities interventions = models.JSONField( default=list, help_text='Planned interventions' ) activities = models.JSONField( default=list, help_text='Specific activities and tasks' ) # Monitoring and Evaluation monitoring_parameters = models.JSONField( default=list, help_text='Parameters to monitor' ) evaluation_criteria = models.JSONField( default=list, help_text='Criteria for evaluating progress' ) # Patient Involvement patient_goals = models.TextField( blank=True, null=True, help_text='Patient-identified goals' ) patient_preferences = models.TextField( blank=True, null=True, help_text='Patient preferences and concerns' ) patient_barriers = models.TextField( blank=True, null=True, help_text='Identified barriers to care' ) # Resources and Support resources_needed = models.JSONField( default=list, help_text='Resources needed for plan implementation' ) support_systems = models.JSONField( default=list, help_text='Available support systems' ) # Progress Tracking progress_notes = models.TextField( blank=True, null=True, help_text='Progress notes' ) last_reviewed = models.DateField( blank=True, null=True, help_text='Date of last review' ) next_review_date = models.DateField( blank=True, null=True, help_text='Next scheduled review date' ) # Outcomes outcomes_achieved = models.JSONField( default=list, help_text='Outcomes achieved' ) completion_percentage = models.PositiveIntegerField( default=0, validators=[MinValueValidator(0), MaxValueValidator(100)], help_text='Completion percentage' ) # Approval and Authorization approved = models.BooleanField( default=False, help_text='Care plan has been approved' ) approved_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='approved_care_plans', help_text='Provider who approved the plan' ) approved_date = models.DateTimeField( blank=True, null=True, help_text='Date of approval' ) # 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_care_plans', help_text='User who created the care plan' ) class Meta: db_table = 'emr_care_plan' verbose_name = 'Care Plan' verbose_name_plural = 'Care Plans' ordering = ['-created_at'] indexes = [ models.Index(fields=['tenant', 'status']), models.Index(fields=['patient', 'status']), models.Index(fields=['primary_provider']), models.Index(fields=['start_date', 'end_date']), models.Index(fields=['priority']), ] def __str__(self): return f"{self.patient.get_full_name()} - {self.title}" @property def is_active(self): """ Check if care plan is currently active. """ return self.status == 'ACTIVE' def clean(self): """ Custom validation for CarePlan model. """ # Validate date ranges if self.end_date and self.start_date and self.end_date < self.start_date: raise ValidationError('End date cannot be before start date.') if self.target_completion_date and self.start_date and self.target_completion_date < self.start_date: raise ValidationError('Target completion date cannot be before start date.') if self.next_review_date and self.start_date and self.next_review_date < self.start_date: raise ValidationError('Next review date cannot be before start date.') # Validate completion percentage if self.completion_percentage < 0 or self.completion_percentage > 100: raise ValidationError('Completion percentage must be between 0 and 100.') # Validate status-specific logic if self.status == 'COMPLETED' and self.completion_percentage != 100: raise ValidationError('Completed care plans must have 100% completion.') if self.status == 'COMPLETED' and not self.end_date: raise ValidationError('Completed care plans must have an end date.') def save(self, *args, **kwargs): """ Override save to perform custom validation and business logic. """ self.full_clean() # Auto-set end_date for completed plans if self.status == 'COMPLETED' and not self.end_date: self.end_date = timezone.now().date() # Auto-set completion percentage for completed plans if self.status == 'COMPLETED' and self.completion_percentage != 100: self.completion_percentage = 100 super().save(*args, **kwargs) @property def is_overdue(self): """ Check if care plan is overdue for review. """ if self.next_review_date: return timezone.now().date() > self.next_review_date return False @property def days_remaining(self): """ Calculate days remaining until target completion. """ if self.target_completion_date: return (self.target_completion_date - timezone.now().date()).days return None @property def duration_days(self): """ Calculate total planned duration in days. """ if self.start_date and self.end_date: return (self.end_date - self.start_date).days return None @property def progress_percentage(self): """ Calculate progress percentage based on time elapsed. """ if not self.start_date or not self.end_date: return None total_days = (self.end_date - self.start_date).days if total_days <= 0: return 100 elapsed_days = (timezone.now().date() - self.start_date).days if elapsed_days <= 0: return 0 return min(100, int((elapsed_days / total_days) * 100)) @property def can_be_completed(self): """ Check if care plan can be marked as completed. """ return self.status in ['ACTIVE', 'ON_HOLD'] def get_status_color(self): """ Get Bootstrap color class for status display. """ status_colors = { 'DRAFT': 'secondary', 'ACTIVE': 'success', 'ON_HOLD': 'warning', 'COMPLETED': 'primary', 'CANCELLED': 'danger', 'ERROR': 'danger', 'UNKNOWN': 'secondary', } return status_colors.get(self.status, 'secondary') def update_progress(self, new_percentage, notes=None): """ Update care plan progress. """ if 0 <= new_percentage <= 100: self.completion_percentage = new_percentage if notes: self.progress_notes = notes self.last_reviewed = timezone.now().date() self.save() return True return False class ClinicalNote(models.Model): """ Clinical note model for documenting patient care and observations. """ class NoteType(models.TextChoices): PROGRESS = 'PROGRESS', 'Progress' ADMISSION = 'ADMISSION', 'Admission Note' DISCHARGE = 'DISCHARGE', 'Discharge Note' CONSULTATION = 'CONSULTATION', 'Consultation Note' PROCEDURE = 'PROCEDURE', 'Procedure Note' OPERATIVE = 'OPERATIVE', 'Operative Note' NURSING = 'NURSING', 'Nursing Note' THERAPY = 'THERAPY', 'Therapy Note' SOCIAL_WORK = 'SOCIAL_WORK', 'Social Work Note' PSYCHOLOGY = 'PSYCHOLOGY', 'Psychology Note' NUTRITION = 'NUTRITION', 'Nutrition Note' PHARMACY = 'PHARMACY', 'Pharmacy Note' CASE_MANAGEMENT = 'CASE_MANAGEMENT', 'Case Management Note' EDUCATION = 'EDUCATION', 'Patient Education Note' TELEPHONE = 'TELEPHONE', 'Telephone Note' ADDENDUM = 'ADDENDUM', 'Addendum' CORRECTION = 'CORRECTION', 'Correction' OTHER = 'OTHER', 'Other' class NoteStatus(models.TextChoices): DRAFT = 'DRAFT', 'Draft' IN_PROGRESS = 'IN_PROGRESS', 'In Progress' COMPLETED = 'COMPLETED', 'Completed' SIGNED = 'SIGNED', 'Signed' AMENDED = 'AMENDED', 'Amended' CORRECTED = 'CORRECTED', 'Corrected' CANCELLED = 'CANCELLED', 'Cancelled' ERROR = 'ERROR', 'Entered in Error' UNKNOWN = 'UNKNOWN', 'Unknown' class NoteSignatureMethod(models.TextChoices): ELECTRONIC = 'ELECTRONIC', 'Electronic' DIGITAL = 'DIGITAL', 'Digital Signature' BIOMETRIC = 'BIOMETRIC', 'Biometric Signature' PASSWORD = 'PASSWORD', 'Password' TOKEN = 'TOKEN', 'Token Authentication' OTHER = 'OTHER', 'Other' # Encounter relationship encounter = models.ForeignKey( Encounter, on_delete=models.CASCADE, related_name='clinical_notes', help_text='Associated encounter' ) # Patient relationship patient = models.ForeignKey( 'patients.PatientProfile', on_delete=models.CASCADE, related_name='clinical_notes', help_text='Patient' ) # Note Information note_id = models.UUIDField( default=uuid.uuid4, unique=True, editable=False, help_text='Unique note identifier' ) # Note Details note_type = models.CharField( max_length=30, choices=NoteType.choices, help_text='Type of clinical note' ) title = models.CharField( max_length=200, help_text='Note title' ) # Content content = models.TextField( help_text='Note content' ) # Template Information template = models.ForeignKey( 'NoteTemplate', on_delete=models.SET_NULL, null=True, blank=True, related_name='notes', help_text='Template used for this note' ) structured_data = models.JSONField( default=dict, blank=True, help_text='Structured data from template' ) # Provider Information author = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.CASCADE, related_name='authored_notes', help_text='Note author' ) co_signers = models.ManyToManyField( settings.AUTH_USER_MODEL, related_name='co_signed_notes', blank=True, help_text='Co-signers for this note' ) # Status and Workflow status = models.CharField( max_length=20, choices=NoteStatus.choices, default=NoteStatus.DRAFT, help_text='Note status' ) # Signatures electronically_signed = models.BooleanField( default=False, help_text='Note has been electronically signed' ) signed_datetime = models.DateTimeField( blank=True, null=True, help_text='Date and time of signature' ) signature_method = models.CharField( max_length=20, choices=NoteSignatureMethod.choices, blank=True, null=True, help_text='Method of signature' ) # Amendment Information amended_note = models.ForeignKey( 'self', on_delete=models.SET_NULL, null=True, blank=True, related_name='amendments', help_text='Original note if this is an amendment' ) amendment_reason = models.TextField( blank=True, null=True, help_text='Reason for amendment' ) # Quality and Compliance quality_score = models.PositiveIntegerField( blank=True, null=True, validators=[MinValueValidator(0), MaxValueValidator(100)], help_text='Documentation quality score' ) compliance_flags = models.JSONField( default=list, blank=True, help_text='Compliance flags and issues' ) # Timing note_datetime = models.DateTimeField( default=timezone.now, help_text='Date and time note was written' ) # Privacy and Access confidential = models.BooleanField( default=False, help_text='Note contains confidential information' ) restricted_access = models.BooleanField( default=False, help_text='Access to note is restricted' ) access_restrictions = models.JSONField( default=list, blank=True, help_text='Specific access restrictions' ) # Related Information related_problems = models.ManyToManyField( ProblemList, related_name='related_clinical_notes', blank=True, help_text='Related problems' ) related_care_plans = models.ManyToManyField( CarePlan, related_name='clinical_notes', blank=True, help_text='Related care plans' ) # Metadata created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = 'emr_clinical_note' verbose_name = 'Clinical Note' verbose_name_plural = 'Clinical Notes' ordering = ['-note_datetime'] indexes = [ models.Index(fields=['patient', 'note_datetime']), models.Index(fields=['encounter']), models.Index(fields=['author']), models.Index(fields=['note_type']), models.Index(fields=['status']), ] def __str__(self): return f"{self.patient.get_full_name()} - {self.title} ({self.note_datetime.strftime('%Y-%m-%d')})" @property def is_signed(self): """ Check if note has been signed. """ return self.status == 'SIGNED' and self.electronically_signed @property def word_count(self): """ Calculate word count of note content. """ return len(self.content.split()) class NoteTemplate(models.Model): """ Note template model for standardizing clinical documentation. """ class NoteType(models.TextChoices): PROGRESS = 'PROGRESS', 'Progress Note' ADMISSION = 'ADMISSION', 'Admission Note' DISCHARGE = 'DISCHARGE', 'Discharge Note' CONSULTATION = 'CONSULTATION', 'Consultation Note' PROCEDURE = 'PROCEDURE', 'Procedure Note' OPERATIVE = 'OPERATIVE', 'Operative Note' NURSING = 'NURSING', 'Nursing Note' THERAPY = 'THERAPY', 'Therapy Note' SOCIAL_WORK = 'SOCIAL_WORK', 'Social Work Note' PSYCHOLOGY = 'PSYCHOLOGY', 'Psychology Note' NUTRITION = 'NUTRITION', 'Nutrition Note' PHARMACY = 'PHARMACY', 'Pharmacy Note' CASE_MANAGEMENT = 'CASE_MANAGEMENT', 'Case Management Note' EDUCATION = 'EDUCATION', 'Patient Education Note' TELEPHONE = 'TELEPHONE', 'Telephone Note' OTHER = 'OTHER', 'Other' class NoteSpecialty(models.TextChoices): GENERAL_MEDICINE = 'GENERAL_MEDICINE', 'General Medicine' SURGERY = 'SURGERY', 'Surgery' CARDIOLOGY = 'CARDIOLOGY', 'Cardiology' NEUROLOGY = 'NEUROLOGY', 'Neurology' ONCOLOGY = 'ONCOLOGY', 'Oncology' PEDIATRICS = 'PEDIATRICS', 'Pediatrics' OBSTETRICS = 'OBSTETRICS', 'Obstetrics' GYNECOLOGY = 'GYNECOLOGY', 'Gynecology' ORTHOPEDICS = 'ORTHOPEDICS', 'Orthopedics' PSYCHIATRY = 'PSYCHIATRY', 'Psychiatry' EMERGENCY = 'EMERGENCY', 'Emergency Medicine' CRITICAL_CARE = 'CRITICAL_CARE', 'Critical Care' REHABILITATION = 'REHABILITATION', 'Rehabilitation' NURSING = 'NURSING', 'Nursing' THERAPY = 'THERAPY', 'Therapy' SOCIAL_WORK = 'SOCIAL_WORK', 'Social Work' NUTRITION = 'NUTRITION', 'Nutrition' PHARMACY = 'PHARMACY', 'Pharmacy' OTHER = 'OTHER', 'Other' # Tenant relationship tenant = models.ForeignKey( 'core.Tenant', on_delete=models.CASCADE, related_name='note_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=200, help_text='Template name' ) description = models.TextField( blank=True, null=True, help_text='Template description' ) # Template Type note_type = models.CharField( max_length=30, choices=NoteType.choices, help_text='Type of note this template is for' ) specialty = models.CharField( max_length=100, choices=NoteSpecialty.choices, blank=True, null=True, help_text='Medical specialty' ) # Template Content template_content = models.TextField( help_text='Template content with placeholders' ) structured_fields = models.JSONField( default=list, help_text='Structured fields definition' ) # Usage Information is_active = models.BooleanField( default=True, help_text='Template is active and available for use' ) is_default = models.BooleanField( default=False, help_text='Default template for this note type' ) usage_count = models.PositiveIntegerField( default=0, help_text='Number of times template has been used' ) # Version Control version = models.CharField( max_length=20, default='1.0', help_text='Template version' ) previous_version = models.ForeignKey( 'self', on_delete=models.SET_NULL, null=True, blank=True, related_name='newer_versions', help_text='Previous version of this template' ) # Quality and Compliance quality_indicators = models.JSONField( default=list, help_text='Quality indicators to track' ) compliance_requirements = models.JSONField( default=list, help_text='Compliance requirements' ) # 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_note_templates', help_text='User who created the template' ) class Meta: db_table = 'emr_note_template' verbose_name = 'Note Template' verbose_name_plural = 'Note Templates' ordering = ['note_type', 'name'] indexes = [ models.Index(fields=['tenant', 'is_active']), models.Index(fields=['note_type', 'specialty']), models.Index(fields=['is_default']), ] unique_together = ['tenant', 'note_type', 'specialty', 'is_default'] def __str__(self): return f"{self.name} ({self.get_note_type_display()})" def save(self, *args, **kwargs): """ Ensure only one default template per note type and specialty. """ if self.is_default: # Remove default flag from other templates NoteTemplate.objects.filter( tenant=self.tenant, note_type=self.note_type, specialty=self.specialty, is_default=True ).exclude(id=self.id).update(is_default=False) super().save(*args, **kwargs) class Icd10(models.Model): """ ICD-10-CM tabular code entry. Handles chapters/sections/diagnoses (with parent-child hierarchy). """ # Tenant relationship # tenant = models.ForeignKey( # 'core.Tenant', # on_delete=models.CASCADE, # related_name='icd10_codes', # help_text='Organization tenant' # ) code = models.CharField(max_length=10, unique=True, db_index=True) description = models.TextField(blank=True, null=True) chapter_name = models.CharField(max_length=255, blank=True, null=True) section_name = models.CharField(max_length=255, blank=True, null=True) parent = models.ForeignKey( 'self', on_delete=models.CASCADE, blank=True, null=True, related_name='children' ) is_header = models.BooleanField(default=False) created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = 'emr_icd10' verbose_name = 'ICD-10 Code' verbose_name_plural = 'ICD-10 Codes' ordering = ['code'] def __str__(self): return f"{self.code} — {self.description[:80] if self.description else ''}" class ClinicalRecommendation(models.Model): """ Clinical recommendation model for AI-powered clinical decision support. """ class RecommendationCategory(models.TextChoices): PREVENTIVE = 'PREVENTIVE', 'Preventive Care' DIAGNOSTIC = 'DIAGNOSTIC', 'Diagnostic' TREATMENT = 'TREATMENT', 'Treatment' MONITORING = 'MONITORING', 'Monitoring' LIFESTYLE = 'LIFESTYLE', 'Lifestyle' MEDICATION = 'MEDICATION', 'Medication' FOLLOW_UP = 'FOLLOW_UP', 'Follow-up' REFERRAL = 'REFERRAL', 'Referral' EDUCATION = 'EDUCATION', 'Patient Education' OTHER = 'OTHER', 'Other' class RecommendationPriority(models.TextChoices): LOW = 'LOW', 'Low' MEDIUM = 'MEDIUM', 'Medium' HIGH = 'HIGH', 'High' URGENT = 'URGENT', 'Urgent' CRITICAL = 'CRITICAL', 'Critical' class RecommendationStatus(models.TextChoices): PENDING = 'PENDING', 'Pending' ACTIVE = 'ACTIVE', 'Active' ACCEPTED = 'ACCEPTED', 'Accepted' DEFERRED = 'DEFERRED', 'Deferred' DISMISSED = 'DISMISSED', 'Dismissed' COMPLETED = 'COMPLETED', 'Completed' EXPIRED = 'EXPIRED', 'Expired' # Tenant relationship tenant = models.ForeignKey( 'core.Tenant', on_delete=models.CASCADE, related_name='clinical_recommendations', help_text='Organization tenant' ) # Patient relationship patient = models.ForeignKey( 'patients.PatientProfile', on_delete=models.CASCADE, related_name='clinical_recommendations', help_text='Patient' ) # Recommendation details recommendation_id = models.UUIDField( default=uuid.uuid4, unique=True, editable=False, help_text='Unique recommendation identifier' ) title = models.CharField( max_length=200, help_text='Recommendation title' ) description = models.TextField( help_text='Detailed recommendation description' ) # Classification category = models.CharField( max_length=20, choices=RecommendationCategory.choices, help_text='Recommendation category' ) priority = models.CharField( max_length=20, choices=RecommendationPriority.choices, default=RecommendationPriority.MEDIUM, help_text='Recommendation priority' ) # Clinical details evidence_level = models.CharField( max_length=10, help_text='Level of evidence (1A, 1B, 2A, etc.)' ) source = models.CharField( max_length=100, help_text='Source of recommendation (guideline, study, etc.)' ) rationale = models.TextField( blank=True, null=True, help_text='Clinical rationale for recommendation' ) # Status and actions status = models.CharField( max_length=20, choices=RecommendationStatus.choices, default=RecommendationStatus.PENDING, help_text='Current recommendation status' ) # Action tracking accepted_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='accepted_recommendations', help_text='Provider who accepted the recommendation' ) accepted_at = models.DateTimeField( blank=True, null=True, help_text='Date and time recommendation was accepted' ) deferred_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='deferred_recommendations', help_text='Provider who deferred the recommendation' ) deferred_at = models.DateTimeField( blank=True, null=True, help_text='Date and time recommendation was deferred' ) dismissed_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='dismissed_recommendations', help_text='Provider who dismissed the recommendation' ) dismissed_at = models.DateTimeField( blank=True, null=True, help_text='Date and time recommendation was dismissed' ) # Timing created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) expires_at = models.DateTimeField( blank=True, null=True, help_text='Recommendation expiration date' ) # Related data related_problems = models.ManyToManyField( ProblemList, related_name='recommendations', blank=True, help_text='Related problems' ) related_encounter = models.ForeignKey( Encounter, on_delete=models.SET_NULL, null=True, blank=True, related_name='recommendations', help_text='Related encounter' ) # Metadata created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='created_recommendations', help_text='User who created the recommendation' ) class Meta: db_table = 'emr_clinical_recommendation' verbose_name = 'Clinical Recommendation' verbose_name_plural = 'Clinical Recommendations' ordering = ['-created_at'] indexes = [ models.Index(fields=['tenant', 'status']), models.Index(fields=['patient', 'status']), models.Index(fields=['category']), models.Index(fields=['priority']), models.Index(fields=['created_at']), ] def __str__(self): return f"{self.patient.get_full_name()} - {self.title}" @property def is_expired(self): """ Check if recommendation has expired. """ if self.expires_at: return timezone.now() > self.expires_at return False @property def is_active(self): """ Check if recommendation is currently active. """ return self.status in ['PENDING', 'ACTIVE'] and not self.is_expired class AllergyAlert(models.Model): """ Allergy alert model for tracking patient allergies and reactions. """ class AlertSeverity(models.TextChoices): MILD = 'MILD', 'Mild' MODERATE = 'MODERATE', 'Moderate' SEVERE = 'SEVERE', 'Severe' LIFE_THREATENING = 'LIFE_THREATENING', 'Life-threatening' # Tenant relationship tenant = models.ForeignKey( 'core.Tenant', on_delete=models.CASCADE, related_name='allergy_alerts', help_text='Organization tenant' ) # Patient relationship patient = models.ForeignKey( 'patients.PatientProfile', on_delete=models.CASCADE, related_name='allergy_alerts', help_text='Patient' ) # Alert details alert_id = models.UUIDField( default=uuid.uuid4, unique=True, editable=False, help_text='Unique alert identifier' ) allergen = models.CharField( max_length=100, help_text='Allergen name' ) reaction_type = models.CharField( max_length=100, help_text='Type of allergic reaction' ) severity = models.CharField( max_length=20, choices=AlertSeverity.choices, help_text='Alert severity' ) # Clinical details symptoms = models.TextField( blank=True, null=True, help_text='Allergic reaction symptoms' ) onset = models.CharField( max_length=50, blank=True, null=True, help_text='Reaction onset timing' ) # Status resolved = models.BooleanField( default=False, help_text='Alert has been resolved' ) resolved_at = models.DateTimeField( blank=True, null=True, help_text='Date alert was resolved' ) resolved_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='resolved_allergy_alerts', help_text='Provider who resolved the alert' ) # Metadata created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) detected_at = models.DateTimeField( default=timezone.now, help_text='When alert was detected' ) class Meta: db_table = 'emr_allergy_alert' verbose_name = 'Allergy Alert' verbose_name_plural = 'Allergy Alerts' ordering = ['-detected_at'] indexes = [ models.Index(fields=['tenant', 'resolved']), models.Index(fields=['patient', 'resolved']), models.Index(fields=['severity']), models.Index(fields=['detected_at']), ] def __str__(self): return f"{self.patient.get_full_name()} - {self.allergen}" class TreatmentProtocol(models.Model): """ Treatment protocol model for standardized treatment approaches. """ # Tenant relationship tenant = models.ForeignKey( 'core.Tenant', on_delete=models.CASCADE, related_name='treatment_protocols', help_text='Organization tenant' ) # Protocol details protocol_id = models.UUIDField( default=uuid.uuid4, unique=True, editable=False, help_text='Unique protocol identifier' ) name = models.CharField( max_length=200, help_text='Protocol name' ) description = models.TextField( help_text='Protocol description' ) # Clinical details indication = models.TextField( help_text='Clinical indications for use' ) goals = models.JSONField( default=list, help_text='Treatment goals' ) interventions = models.JSONField( default=list, help_text='Required interventions' ) monitoring_parameters = models.JSONField( default=list, help_text='Parameters to monitor' ) # Effectiveness success_rate = models.DecimalField( max_digits=5, decimal_places=2, help_text='Protocol success rate (%)' ) average_duration = models.PositiveIntegerField( help_text='Average treatment duration in days' ) # Status is_active = models.BooleanField( default=True, help_text='Protocol is active and available' ) # Usage tracking usage_count = models.PositiveIntegerField( default=0, help_text='Number of times protocol 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_protocols', help_text='User who created the protocol' ) class Meta: db_table = 'emr_treatment_protocol' verbose_name = 'Treatment Protocol' verbose_name_plural = 'Treatment Protocols' ordering = ['-success_rate', 'name'] indexes = [ models.Index(fields=['tenant', 'is_active']), models.Index(fields=['success_rate']), ] def __str__(self): return f"{self.name} ({self.success_rate}% success)" class ClinicalGuideline(models.Model): """ Clinical guideline model for referencing medical guidelines. """ # Tenant relationship tenant = models.ForeignKey( 'core.Tenant', on_delete=models.CASCADE, related_name='clinical_guidelines', help_text='Organization tenant' ) # Guideline details guideline_id = models.UUIDField( default=uuid.uuid4, unique=True, editable=False, help_text='Unique guideline identifier' ) title = models.CharField( max_length=300, help_text='Guideline title' ) organization = models.CharField( max_length=100, help_text='Publishing organization' ) summary = models.TextField( help_text='Guideline summary' ) url = models.URLField( blank=True, null=True, help_text='Link to full guideline' ) # Publication details publication_date = models.DateField( help_text='Guideline publication date' ) last_updated = models.DateField( auto_now=True, help_text='Last updated date' ) version = models.CharField( max_length=20, blank=True, null=True, help_text='Guideline version' ) # Status is_active = models.BooleanField( default=True, help_text='Guideline is current and active' ) # Relevance keywords = models.JSONField( default=list, help_text='Keywords for searching' ) specialties = models.JSONField( default=list, help_text='Relevant medical specialties' ) # Metadata created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) class Meta: db_table = 'emr_clinical_guideline' verbose_name = 'Clinical Guideline' verbose_name_plural = 'Clinical Guidelines' ordering = ['-last_updated', 'title'] indexes = [ models.Index(fields=['tenant', 'is_active']), models.Index(fields=['organization']), models.Index(fields=['publication_date']), ] def __str__(self): return f"{self.title} - {self.organization}" class CriticalAlert(models.Model): """ Critical alert model for high-priority clinical alerts. """ class AlertPriority(models.TextChoices): HIGH = 'HIGH', 'High' URGENT = 'URGENT', 'Urgent' CRITICAL = 'CRITICAL', 'Critical' # Tenant relationship tenant = models.ForeignKey( 'core.Tenant', on_delete=models.CASCADE, related_name='critical_alerts', help_text='Organization tenant' ) # Patient relationship patient = models.ForeignKey( 'patients.PatientProfile', on_delete=models.CASCADE, related_name='critical_alerts', help_text='Patient' ) # Alert details alert_id = models.UUIDField( default=uuid.uuid4, unique=True, editable=False, help_text='Unique alert identifier' ) title = models.CharField( max_length=200, help_text='Alert title' ) description = models.TextField( help_text='Alert description' ) priority = models.CharField( max_length=20, choices=AlertPriority.choices, default=AlertPriority.HIGH, help_text='Alert priority level' ) # Clinical details recommendation = models.TextField( blank=True, null=True, help_text='Recommended actions' ) # Status acknowledged = models.BooleanField( default=False, help_text='Alert has been acknowledged' ) acknowledged_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='emr_acknowledged_alerts', help_text='Provider who acknowledged the alert' ) acknowledged_at = models.DateTimeField( blank=True, null=True, help_text='Date and time alert was acknowledged' ) # Timing created_at = models.DateTimeField(auto_now_add=True) updated_at = models.DateTimeField(auto_now=True) expires_at = models.DateTimeField( blank=True, null=True, help_text='Alert expiration date' ) # Related data related_encounter = models.ForeignKey( Encounter, on_delete=models.SET_NULL, null=True, blank=True, related_name='critical_alerts', help_text='Related encounter' ) # Metadata created_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='created_alerts', help_text='User who created the alert' ) class Meta: db_table = 'emr_critical_alert' verbose_name = 'Critical Alert' verbose_name_plural = 'Critical Alerts' ordering = ['-created_at'] indexes = [ models.Index(fields=['tenant', 'acknowledged']), models.Index(fields=['patient', 'acknowledged']), models.Index(fields=['priority']), models.Index(fields=['created_at']), ] def __str__(self): return f"{self.patient.get_full_name()} - {self.title}" @property def is_expired(self): """ Check if alert has expired. """ if self.expires_at: return timezone.now() > self.expires_at return False @property def is_active(self): """ Check if alert is currently active. """ return not self.acknowledged and not self.is_expired class DiagnosticSuggestion(models.Model): """ Diagnostic suggestion model for AI-suggested diagnostic tests. """ class Status(models.TextChoices): PENDING = 'PENDING', 'Pending' ORDERED = 'ORDERED', 'Ordered' COMPLETED = 'COMPLETED', 'Completed' CANCELLED = 'CANCELLED', 'Cancelled' # Tenant relationship tenant = models.ForeignKey( 'core.Tenant', on_delete=models.CASCADE, related_name='diagnostic_suggestions', help_text='Organization tenant' ) # Patient relationship patient = models.ForeignKey( 'patients.PatientProfile', on_delete=models.CASCADE, related_name='diagnostic_suggestions', help_text='Patient' ) # Suggestion details suggestion_id = models.UUIDField( default=uuid.uuid4, unique=True, editable=False, help_text='Unique suggestion identifier' ) test_name = models.CharField( max_length=200, help_text='Suggested test name' ) test_code = models.CharField( max_length=20, help_text='Test code or identifier' ) indication = models.TextField( help_text='Clinical indication for the test' ) confidence = models.DecimalField( max_digits=5, decimal_places=2, validators=[MinValueValidator(0), MaxValueValidator(100)], help_text='AI confidence score (%)' ) # Status status = models.CharField( max_length=20, choices=Status.choices, default=Status.ORDERED, help_text='Suggestion status' ) # Ordering information ordered_by = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.SET_NULL, null=True, blank=True, related_name='ordered_suggestions', help_text='Provider who ordered the test' ) ordered_at = models.DateTimeField( blank=True, null=True, help_text='Date and time test was ordered' ) # 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_suggestions', help_text='User who created the suggestion' ) class Meta: db_table = 'emr_diagnostic_suggestion' verbose_name = 'Diagnostic Suggestion' verbose_name_plural = 'Diagnostic Suggestions' ordering = ['-confidence', '-created_at'] indexes = [ models.Index(fields=['tenant', 'status']), models.Index(fields=['patient', 'status']), models.Index(fields=['confidence']), models.Index(fields=['created_at']), ] def __str__(self): return f"{self.patient.get_full_name()} - {self.test_name} ({self.confidence}%)"