2025-10-06 15:25:37 +03:00

2725 lines
78 KiB
Python

"""
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}%)"