1546 lines
43 KiB
Python
1546 lines
43 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.
|
|
"""
|
|
|
|
# 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=[
|
|
('INPATIENT', 'Inpatient'),
|
|
('OUTPATIENT', 'Outpatient'),
|
|
('EMERGENCY', 'Emergency'),
|
|
('URGENT_CARE', 'Urgent Care'),
|
|
('OBSERVATION', 'Observation'),
|
|
('TELEMEDICINE', 'Telemedicine'),
|
|
('HOME_VISIT', 'Home Visit'),
|
|
('CONSULTATION', 'Consultation'),
|
|
('FOLLOW_UP', 'Follow-up'),
|
|
('PROCEDURE', 'Procedure'),
|
|
('SURGERY', 'Surgery'),
|
|
('DIAGNOSTIC', 'Diagnostic'),
|
|
('PREVENTIVE', 'Preventive Care'),
|
|
('OTHER', 'Other'),
|
|
],
|
|
help_text='Type of encounter'
|
|
)
|
|
|
|
encounter_class = models.CharField(
|
|
max_length=20,
|
|
choices=[
|
|
('AMB', 'Ambulatory'),
|
|
('EMER', 'Emergency'),
|
|
('FLD', 'Field'),
|
|
('HH', 'Home Health'),
|
|
('IMP', 'Inpatient'),
|
|
('ACUTE', 'Inpatient Acute'),
|
|
('NONAC', 'Inpatient Non-Acute'),
|
|
('OBSENC', 'Observation Encounter'),
|
|
('PRENC', 'Pre-Admission'),
|
|
('SS', 'Short Stay'),
|
|
('VR', 'Virtual'),
|
|
],
|
|
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=[
|
|
('PLANNED', 'Planned'),
|
|
('ARRIVED', 'Arrived'),
|
|
('TRIAGED', 'Triaged'),
|
|
('IN_PROGRESS', 'In Progress'),
|
|
('ON_HOLD', 'On Hold'),
|
|
('FINISHED', 'Finished'),
|
|
('CANCELLED', 'Cancelled'),
|
|
('ENTERED_IN_ERROR', 'Entered in Error'),
|
|
('UNKNOWN', 'Unknown'),
|
|
],
|
|
default='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=[
|
|
('ROUTINE', 'Routine'),
|
|
('URGENT', 'Urgent'),
|
|
('ASAP', 'ASAP'),
|
|
('STAT', 'STAT'),
|
|
('EMERGENCY', 'Emergency'),
|
|
],
|
|
default='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.
|
|
"""
|
|
|
|
# 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=[
|
|
('ORAL', 'Oral'),
|
|
('RECTAL', 'Rectal'),
|
|
('AXILLARY', 'Axillary'),
|
|
('TYMPANIC', 'Tympanic'),
|
|
('TEMPORAL', 'Temporal'),
|
|
('CORE', 'Core'),
|
|
],
|
|
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=[
|
|
('SITTING', 'Sitting'),
|
|
('STANDING', 'Standing'),
|
|
('LYING', 'Lying'),
|
|
('SUPINE', 'Supine'),
|
|
],
|
|
blank=True,
|
|
null=True,
|
|
help_text='Patient position during BP measurement'
|
|
)
|
|
bp_cuff_size = models.CharField(
|
|
max_length=20,
|
|
choices=[
|
|
('SMALL', 'Small'),
|
|
('REGULAR', 'Regular'),
|
|
('LARGE', 'Large'),
|
|
('EXTRA_LARGE', 'Extra Large'),
|
|
('PEDIATRIC', 'Pediatric'),
|
|
],
|
|
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=[
|
|
('REGULAR', 'Regular'),
|
|
('IRREGULAR', 'Irregular'),
|
|
('REGULARLY_IRREGULAR', 'Regularly Irregular'),
|
|
('IRREGULARLY_IRREGULAR', 'Irregularly Irregular'),
|
|
],
|
|
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=[
|
|
('ROOM_AIR', 'Room Air'),
|
|
('NASAL_CANNULA', 'Nasal Cannula'),
|
|
('SIMPLE_MASK', 'Simple Mask'),
|
|
('NON_REBREATHER', 'Non-Rebreather Mask'),
|
|
('VENTURI_MASK', 'Venturi Mask'),
|
|
('CPAP', 'CPAP'),
|
|
('BIPAP', 'BiPAP'),
|
|
('MECHANICAL_VENTILATION', 'Mechanical Ventilation'),
|
|
('OTHER', 'Other'),
|
|
],
|
|
default='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.
|
|
"""
|
|
|
|
# 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=[
|
|
('ICD10', 'ICD-10'),
|
|
('ICD9', 'ICD-9'),
|
|
('SNOMED', 'SNOMED CT'),
|
|
('CPT', 'CPT'),
|
|
('LOINC', 'LOINC'),
|
|
('OTHER', 'Other'),
|
|
],
|
|
blank=True,
|
|
null=True,
|
|
help_text='Coding system used'
|
|
)
|
|
|
|
# Problem Classification
|
|
problem_type = models.CharField(
|
|
max_length=30,
|
|
choices=[
|
|
('DIAGNOSIS', 'Diagnosis'),
|
|
('SYMPTOM', 'Symptom'),
|
|
('FINDING', 'Finding'),
|
|
('COMPLAINT', 'Complaint'),
|
|
('CONDITION', 'Condition'),
|
|
('DISORDER', 'Disorder'),
|
|
('SYNDROME', 'Syndrome'),
|
|
('INJURY', 'Injury'),
|
|
('ALLERGY', 'Allergy'),
|
|
('INTOLERANCE', 'Intolerance'),
|
|
('RISK_FACTOR', 'Risk Factor'),
|
|
('OTHER', 'Other'),
|
|
],
|
|
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=[
|
|
('MILD', 'Mild'),
|
|
('MODERATE', 'Moderate'),
|
|
('SEVERE', 'Severe'),
|
|
('CRITICAL', 'Critical'),
|
|
('UNKNOWN', 'Unknown'),
|
|
],
|
|
blank=True,
|
|
null=True,
|
|
help_text='Problem severity'
|
|
)
|
|
priority = models.CharField(
|
|
max_length=20,
|
|
choices=[
|
|
('LOW', 'Low'),
|
|
('MEDIUM', 'Medium'),
|
|
('HIGH', 'High'),
|
|
('URGENT', 'Urgent'),
|
|
],
|
|
default='MEDIUM',
|
|
help_text='Problem priority'
|
|
)
|
|
|
|
# Status
|
|
status = models.CharField(
|
|
max_length=20,
|
|
choices=[
|
|
('ACTIVE', 'Active'),
|
|
('INACTIVE', 'Inactive'),
|
|
('RESOLVED', 'Resolved'),
|
|
('REMISSION', 'In Remission'),
|
|
('RECURRENCE', 'Recurrence'),
|
|
('RELAPSE', 'Relapse'),
|
|
('UNKNOWN', 'Unknown'),
|
|
],
|
|
default='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=[
|
|
('LEFT', 'Left'),
|
|
('RIGHT', 'Right'),
|
|
('BILATERAL', 'Bilateral'),
|
|
('UNILATERAL', 'Unilateral'),
|
|
('NOT_APPLICABLE', 'Not Applicable'),
|
|
],
|
|
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.
|
|
"""
|
|
|
|
# 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=[
|
|
('COMPREHENSIVE', 'Comprehensive Care Plan'),
|
|
('DISEASE_SPECIFIC', 'Disease-Specific Plan'),
|
|
('PREVENTIVE', 'Preventive Care Plan'),
|
|
('CHRONIC_CARE', 'Chronic Care Management'),
|
|
('ACUTE_CARE', 'Acute Care Plan'),
|
|
('DISCHARGE', 'Discharge Planning'),
|
|
('REHABILITATION', 'Rehabilitation Plan'),
|
|
('PALLIATIVE', 'Palliative Care Plan'),
|
|
('MENTAL_HEALTH', 'Mental Health Plan'),
|
|
('MEDICATION', 'Medication Management'),
|
|
('NUTRITION', 'Nutrition Plan'),
|
|
('EXERCISE', 'Exercise Plan'),
|
|
('OTHER', 'Other'),
|
|
],
|
|
help_text='Type of care plan'
|
|
)
|
|
|
|
category = models.CharField(
|
|
max_length=50,
|
|
choices=[
|
|
('ASSESSMENT', 'Assessment and Monitoring'),
|
|
('TREATMENT', 'Treatment'),
|
|
('EDUCATION', 'Patient Education'),
|
|
('COORDINATION', 'Care Coordination'),
|
|
('PREVENTION', 'Prevention'),
|
|
('LIFESTYLE', 'Lifestyle Modification'),
|
|
('MEDICATION', 'Medication Management'),
|
|
('FOLLOW_UP', 'Follow-up Care'),
|
|
('EMERGENCY', 'Emergency Planning'),
|
|
('SUPPORT', 'Support Services'),
|
|
],
|
|
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=[
|
|
('DRAFT', 'Draft'),
|
|
('ACTIVE', 'Active'),
|
|
('ON_HOLD', 'On Hold'),
|
|
('COMPLETED', 'Completed'),
|
|
('CANCELLED', 'Cancelled'),
|
|
('ENTERED_IN_ERROR', 'Entered in Error'),
|
|
('UNKNOWN', 'Unknown'),
|
|
],
|
|
default='DRAFT',
|
|
help_text='Care plan status'
|
|
)
|
|
|
|
# Priority
|
|
priority = models.CharField(
|
|
max_length=20,
|
|
choices=[
|
|
('LOW', 'Low'),
|
|
('ROUTINE', 'Routine'),
|
|
('URGENT', 'Urgent'),
|
|
('ASAP', 'ASAP'),
|
|
('STAT', 'STAT'),
|
|
],
|
|
default='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.
|
|
"""
|
|
|
|
# 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=[
|
|
('PROGRESS', 'Progress Note'),
|
|
('ADMISSION', 'Admission Note'),
|
|
('DISCHARGE', 'Discharge Note'),
|
|
('CONSULTATION', 'Consultation Note'),
|
|
('PROCEDURE', 'Procedure Note'),
|
|
('OPERATIVE', 'Operative Note'),
|
|
('NURSING', 'Nursing Note'),
|
|
('THERAPY', 'Therapy Note'),
|
|
('SOCIAL_WORK', 'Social Work Note'),
|
|
('PSYCHOLOGY', 'Psychology Note'),
|
|
('NUTRITION', 'Nutrition Note'),
|
|
('PHARMACY', 'Pharmacy Note'),
|
|
('CASE_MANAGEMENT', 'Case Management Note'),
|
|
('EDUCATION', 'Patient Education Note'),
|
|
('TELEPHONE', 'Telephone Note'),
|
|
('ADDENDUM', 'Addendum'),
|
|
('CORRECTION', 'Correction'),
|
|
('OTHER', 'Other'),
|
|
],
|
|
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=[
|
|
('DRAFT', 'Draft'),
|
|
('IN_PROGRESS', 'In Progress'),
|
|
('COMPLETED', 'Completed'),
|
|
('SIGNED', 'Signed'),
|
|
('AMENDED', 'Amended'),
|
|
('CORRECTED', 'Corrected'),
|
|
('ENTERED_IN_ERROR', 'Entered in Error'),
|
|
],
|
|
default='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=[
|
|
('ELECTRONIC', 'Electronic Signature'),
|
|
('DIGITAL', 'Digital Signature'),
|
|
('BIOMETRIC', 'Biometric Signature'),
|
|
('PASSWORD', 'Password Authentication'),
|
|
('TOKEN', 'Token Authentication'),
|
|
],
|
|
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.
|
|
"""
|
|
|
|
# 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=[
|
|
('PROGRESS', 'Progress Note'),
|
|
('ADMISSION', 'Admission Note'),
|
|
('DISCHARGE', 'Discharge Note'),
|
|
('CONSULTATION', 'Consultation Note'),
|
|
('PROCEDURE', 'Procedure Note'),
|
|
('OPERATIVE', 'Operative Note'),
|
|
('NURSING', 'Nursing Note'),
|
|
('THERAPY', 'Therapy Note'),
|
|
('SOCIAL_WORK', 'Social Work Note'),
|
|
('PSYCHOLOGY', 'Psychology Note'),
|
|
('NUTRITION', 'Nutrition Note'),
|
|
('PHARMACY', 'Pharmacy Note'),
|
|
('CASE_MANAGEMENT', 'Case Management Note'),
|
|
('EDUCATION', 'Patient Education Note'),
|
|
('TELEPHONE', 'Telephone Note'),
|
|
('OTHER', 'Other'),
|
|
],
|
|
help_text='Type of note this template is for'
|
|
)
|
|
|
|
specialty = models.CharField(
|
|
max_length=100,
|
|
choices=[
|
|
('GENERAL_MEDICINE', 'General Medicine'),
|
|
('SURGERY', 'Surgery'),
|
|
('CARDIOLOGY', 'Cardiology'),
|
|
('NEUROLOGY', 'Neurology'),
|
|
('ONCOLOGY', 'Oncology'),
|
|
('PEDIATRICS', 'Pediatrics'),
|
|
('OBSTETRICS', 'Obstetrics'),
|
|
('GYNECOLOGY', 'Gynecology'),
|
|
('ORTHOPEDICS', 'Orthopedics'),
|
|
('PSYCHIATRY', 'Psychiatry'),
|
|
('EMERGENCY', 'Emergency Medicine'),
|
|
('CRITICAL_CARE', 'Critical Care'),
|
|
('REHABILITATION', 'Rehabilitation'),
|
|
('NURSING', 'Nursing'),
|
|
('THERAPY', 'Therapy'),
|
|
('SOCIAL_WORK', 'Social Work'),
|
|
('NUTRITION', 'Nutrition'),
|
|
('PHARMACY', 'Pharmacy'),
|
|
('OTHER', 'Other'),
|
|
],
|
|
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)
|
|
|