Marwan Alwali 4d06ca4b5e update
2025-09-20 14:26:19 +03:00

1603 lines
46 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
class Encounter(models.Model):
"""
Clinical encounter model for tracking patient visits and care episodes.
"""
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'
FINISHED = 'FINISHED', 'Finished'
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']
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 save(self, *args, **kwargs):
"""
Calculate BMI if weight and height are provided.
"""
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
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'
@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
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'
ENTERED_IN_ERROR = 'ENTERED_IN_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'
@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
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).
"""
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 ''}"