382 lines
17 KiB
Python
382 lines
17 KiB
Python
"""
|
|
Forms for EMR app.
|
|
"""
|
|
|
|
from django import forms
|
|
from django.utils import timezone
|
|
from django.core.exceptions import ValidationError
|
|
from django.contrib.auth import get_user_model
|
|
|
|
from .models import (
|
|
Encounter, VitalSigns, ProblemList, CarePlan,
|
|
ClinicalNote, NoteTemplate
|
|
)
|
|
|
|
User = get_user_model()
|
|
|
|
|
|
class EncounterForm(forms.ModelForm):
|
|
"""
|
|
Form for creating and updating encounters.
|
|
"""
|
|
class Meta:
|
|
model = Encounter
|
|
fields = [
|
|
'patient', 'provider', 'encounter_type', 'encounter_class',
|
|
'start_datetime', 'end_datetime', 'status', 'location', 'room_number',
|
|
'appointment', 'admission', 'chief_complaint', 'reason_for_visit',
|
|
'priority', 'acuity_level', 'billable'
|
|
]
|
|
widgets = {
|
|
'start_datetime': forms.DateTimeInput(attrs={'type': 'datetime-local'}),
|
|
'end_datetime': forms.DateTimeInput(attrs={'type': 'datetime-local'}),
|
|
'chief_complaint': forms.Textarea(attrs={'rows': 3}),
|
|
'reason_for_visit': forms.Textarea(attrs={'rows': 3}),
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self.tenant = kwargs.pop('tenant', None)
|
|
super().__init__(*args, **kwargs)
|
|
|
|
# Filter related objects by tenant
|
|
if self.tenant:
|
|
self.fields['patient'].queryset = self.fields['patient'].queryset.filter(tenant=self.tenant)
|
|
self.fields['provider'].queryset = User.objects.filter(tenant=self.tenant)
|
|
self.fields['appointment'].queryset = self.fields['appointment'].queryset.filter(tenant=self.tenant)
|
|
self.fields['admission'].queryset = self.fields['admission'].queryset.filter(tenant=self.tenant)
|
|
|
|
def clean(self):
|
|
cleaned_data = super().clean()
|
|
start_datetime = cleaned_data.get('start_datetime')
|
|
end_datetime = cleaned_data.get('end_datetime')
|
|
status = cleaned_data.get('status')
|
|
|
|
# Validate end_datetime is after start_datetime
|
|
if start_datetime and end_datetime and end_datetime < start_datetime:
|
|
self.add_error('end_datetime', 'End time must be after start time')
|
|
|
|
# Validate status and end_datetime consistency
|
|
if status == 'FINISHED' and not end_datetime:
|
|
cleaned_data['end_datetime'] = timezone.now()
|
|
|
|
return cleaned_data
|
|
|
|
|
|
class VitalSignsForm(forms.ModelForm):
|
|
"""
|
|
Form for recording vital signs.
|
|
"""
|
|
class Meta:
|
|
model = VitalSigns
|
|
fields = [
|
|
'encounter', 'patient', 'measured_datetime',
|
|
'temperature', 'temperature_method',
|
|
'systolic_bp', 'diastolic_bp', 'bp_position', 'bp_cuff_size',
|
|
'heart_rate', 'heart_rhythm',
|
|
'respiratory_rate',
|
|
'oxygen_saturation', 'oxygen_delivery', 'oxygen_flow_rate',
|
|
'pain_scale', 'pain_location', 'pain_quality',
|
|
'weight', 'height',
|
|
'head_circumference',
|
|
'device_used', 'device_calibrated',
|
|
'measured_by', 'notes'
|
|
]
|
|
widgets = {
|
|
'measured_datetime': forms.DateTimeInput(attrs={'type': 'datetime-local'}),
|
|
'notes': forms.Textarea(attrs={'rows': 3}),
|
|
'pain_location': forms.TextInput(),
|
|
'pain_quality': forms.TextInput(),
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self.tenant = kwargs.pop('tenant', None)
|
|
super().__init__(*args, **kwargs)
|
|
|
|
# Filter related objects by tenant
|
|
if self.tenant:
|
|
self.fields['encounter'].queryset = Encounter.objects.filter(tenant=self.tenant)
|
|
self.fields['patient'].queryset = self.fields['patient'].queryset.filter(tenant=self.tenant)
|
|
self.fields['measured_by'].queryset = User.objects.filter(tenant=self.tenant)
|
|
|
|
def clean(self):
|
|
cleaned_data = super().clean()
|
|
|
|
# Validate blood pressure values
|
|
systolic_bp = cleaned_data.get('systolic_bp')
|
|
diastolic_bp = cleaned_data.get('diastolic_bp')
|
|
|
|
if (systolic_bp and not diastolic_bp) or (diastolic_bp and not systolic_bp):
|
|
self.add_error('systolic_bp', 'Both systolic and diastolic values must be provided')
|
|
|
|
if systolic_bp and diastolic_bp and systolic_bp <= diastolic_bp:
|
|
self.add_error('systolic_bp', 'Systolic pressure must be greater than diastolic pressure')
|
|
|
|
# Validate oxygen flow rate is provided when delivery method is not room air
|
|
oxygen_delivery = cleaned_data.get('oxygen_delivery')
|
|
oxygen_flow_rate = cleaned_data.get('oxygen_flow_rate')
|
|
|
|
if oxygen_delivery and oxygen_delivery != 'ROOM_AIR' and not oxygen_flow_rate:
|
|
self.add_error('oxygen_flow_rate', 'Flow rate is required for this oxygen delivery method')
|
|
|
|
return cleaned_data
|
|
|
|
|
|
class ProblemListForm(forms.ModelForm):
|
|
"""
|
|
Form for creating and updating problems.
|
|
"""
|
|
class Meta:
|
|
model = ProblemList
|
|
fields = [
|
|
'patient', 'problem_name', 'problem_code', 'coding_system',
|
|
'problem_type', 'onset_date', 'onset_description',
|
|
'severity', 'priority', 'status',
|
|
'resolution_date', 'resolution_notes',
|
|
'diagnosing_provider', 'managing_provider',
|
|
'related_encounter', 'body_site', 'laterality',
|
|
'clinical_notes', 'patient_concerns',
|
|
'treatment_goals', 'outcome_measures',
|
|
'verified', 'verified_by'
|
|
]
|
|
widgets = {
|
|
'onset_date': forms.DateInput(attrs={'type': 'date'}),
|
|
'resolution_date': forms.DateInput(attrs={'type': 'date'}),
|
|
'clinical_notes': forms.Textarea(attrs={'rows': 3}),
|
|
'patient_concerns': forms.Textarea(attrs={'rows': 3}),
|
|
'resolution_notes': forms.Textarea(attrs={'rows': 3}),
|
|
'treatment_goals': forms.Textarea(attrs={'rows': 3}),
|
|
'outcome_measures': forms.Textarea(attrs={'rows': 3}),
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self.tenant = kwargs.pop('tenant', None)
|
|
super().__init__(*args, **kwargs)
|
|
|
|
# Filter related objects by tenant
|
|
if self.tenant:
|
|
self.fields['patient'].queryset = self.fields['patient'].queryset.filter(tenant=self.tenant)
|
|
self.fields['diagnosing_provider'].queryset = User.objects.filter(tenant=self.tenant)
|
|
self.fields['managing_provider'].queryset = User.objects.filter(tenant=self.tenant)
|
|
self.fields['verified_by'].queryset = User.objects.filter(tenant=self.tenant)
|
|
self.fields['related_encounter'].queryset = Encounter.objects.filter(tenant=self.tenant)
|
|
|
|
def clean(self):
|
|
cleaned_data = super().clean()
|
|
status = cleaned_data.get('status')
|
|
resolution_date = cleaned_data.get('resolution_date')
|
|
|
|
# Validate resolution date is provided for resolved problems
|
|
if status in ['RESOLVED', 'REMISSION'] and not resolution_date:
|
|
self.add_error('resolution_date', 'Resolution date is required for resolved problems')
|
|
|
|
# Validate resolution date is not in the future
|
|
if resolution_date and resolution_date > timezone.now().date():
|
|
self.add_error('resolution_date', 'Resolution date cannot be in the future')
|
|
|
|
# Validate onset date is not in the future
|
|
onset_date = cleaned_data.get('onset_date')
|
|
if onset_date and onset_date > timezone.now().date():
|
|
self.add_error('onset_date', 'Onset date cannot be in the future')
|
|
|
|
# Validate resolution date is after onset date
|
|
if onset_date and resolution_date and resolution_date < onset_date:
|
|
self.add_error('resolution_date', 'Resolution date must be after onset date')
|
|
|
|
return cleaned_data
|
|
|
|
|
|
class CarePlanForm(forms.ModelForm):
|
|
"""
|
|
Form for creating and updating care plans.
|
|
"""
|
|
class Meta:
|
|
model = CarePlan
|
|
fields = [
|
|
'patient', 'title', 'description', 'plan_type', 'category',
|
|
'start_date', 'end_date', 'target_completion_date',
|
|
'status', 'priority', 'primary_provider', 'care_team',
|
|
'related_problems', 'goals', 'objectives',
|
|
'interventions', 'activities',
|
|
'monitoring_parameters', 'evaluation_criteria',
|
|
'patient_goals', 'patient_preferences', 'patient_barriers',
|
|
'resources_needed', 'support_systems',
|
|
'progress_notes', 'last_reviewed', 'next_review_date',
|
|
'outcomes_achieved', 'completion_percentage',
|
|
'approved', 'approved_by'
|
|
]
|
|
widgets = {
|
|
'start_date': forms.DateInput(attrs={'type': 'date'}),
|
|
'end_date': forms.DateInput(attrs={'type': 'date'}),
|
|
'target_completion_date': forms.DateInput(attrs={'type': 'date'}),
|
|
'last_reviewed': forms.DateInput(attrs={'type': 'date'}),
|
|
'next_review_date': forms.DateInput(attrs={'type': 'date'}),
|
|
'description': forms.Textarea(attrs={'rows': 3}),
|
|
'patient_goals': forms.Textarea(attrs={'rows': 3}),
|
|
'patient_preferences': forms.Textarea(attrs={'rows': 3}),
|
|
'patient_barriers': forms.Textarea(attrs={'rows': 3}),
|
|
'progress_notes': forms.Textarea(attrs={'rows': 3}),
|
|
'goals': forms.Textarea(attrs={'rows': 3}),
|
|
'objectives': forms.Textarea(attrs={'rows': 3}),
|
|
'interventions': forms.Textarea(attrs={'rows': 3}),
|
|
'activities': forms.Textarea(attrs={'rows': 3}),
|
|
'monitoring_parameters': forms.Textarea(attrs={'rows': 3}),
|
|
'evaluation_criteria': forms.Textarea(attrs={'rows': 3}),
|
|
'resources_needed': forms.Textarea(attrs={'rows': 3}),
|
|
'support_systems': forms.Textarea(attrs={'rows': 3}),
|
|
'outcomes_achieved': forms.Textarea(attrs={'rows': 3}),
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self.tenant = kwargs.pop('tenant', None)
|
|
super().__init__(*args, **kwargs)
|
|
|
|
# Filter related objects by tenant
|
|
if self.tenant:
|
|
self.fields['patient'].queryset = self.fields['patient'].queryset.filter(tenant=self.tenant)
|
|
self.fields['primary_provider'].queryset = User.objects.filter(tenant=self.tenant)
|
|
self.fields['care_team'].queryset = User.objects.filter(tenant=self.tenant)
|
|
self.fields['approved_by'].queryset = User.objects.filter(tenant=self.tenant)
|
|
self.fields['related_problems'].queryset = ProblemList.objects.filter(tenant=self.tenant)
|
|
|
|
def clean(self):
|
|
cleaned_data = super().clean()
|
|
start_date = cleaned_data.get('start_date')
|
|
end_date = cleaned_data.get('end_date')
|
|
target_completion_date = cleaned_data.get('target_completion_date')
|
|
|
|
# Validate end_date is after start_date
|
|
if start_date and end_date and end_date < start_date:
|
|
self.add_error('end_date', 'End date must be after start date')
|
|
|
|
# Validate target_completion_date is after start_date
|
|
if start_date and target_completion_date and target_completion_date < start_date:
|
|
self.add_error('target_completion_date', 'Target completion date must be after start date')
|
|
|
|
# Validate completion percentage
|
|
completion_percentage = cleaned_data.get('completion_percentage')
|
|
status = cleaned_data.get('status')
|
|
|
|
if status == 'COMPLETED' and completion_percentage < 100:
|
|
self.add_error('completion_percentage', 'Completion percentage must be 100% for completed care plans')
|
|
|
|
if status == 'DRAFT' and completion_percentage > 0:
|
|
self.add_error('completion_percentage', 'Completion percentage must be 0% for draft care plans')
|
|
|
|
return cleaned_data
|
|
|
|
|
|
class ClinicalNoteForm(forms.ModelForm):
|
|
"""
|
|
Form for creating and updating clinical notes.
|
|
"""
|
|
class Meta:
|
|
model = ClinicalNote
|
|
fields = [
|
|
'encounter', 'patient', 'note_type', 'title', 'content',
|
|
'template', 'structured_data', 'author', 'co_signers',
|
|
'status', 'note_datetime',
|
|
'electronically_signed', 'signed_datetime', 'signature_method',
|
|
'amended_note', 'amendment_reason',
|
|
'confidential', 'restricted_access', 'access_restrictions',
|
|
'related_problems', 'related_care_plans'
|
|
]
|
|
widgets = {
|
|
'note_datetime': forms.DateTimeInput(attrs={'type': 'datetime-local'}),
|
|
'signed_datetime': forms.DateTimeInput(attrs={'type': 'datetime-local'}),
|
|
'content': forms.Textarea(attrs={'rows': 10, 'class': 'note-editor'}),
|
|
'amendment_reason': forms.Textarea(attrs={'rows': 3}),
|
|
'structured_data': forms.Textarea(attrs={'rows': 3}),
|
|
'access_restrictions': forms.Textarea(attrs={'rows': 3}),
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self.tenant = kwargs.pop('tenant', None)
|
|
super().__init__(*args, **kwargs)
|
|
|
|
# Filter related objects by tenant
|
|
if self.tenant:
|
|
self.fields['encounter'].queryset = Encounter.objects.filter(tenant=self.tenant)
|
|
self.fields['patient'].queryset = self.fields['patient'].queryset.filter(tenant=self.tenant)
|
|
self.fields['author'].queryset = User.objects.filter(tenant=self.tenant)
|
|
self.fields['co_signers'].queryset = User.objects.filter(tenant=self.tenant)
|
|
self.fields['template'].queryset = NoteTemplate.objects.filter(tenant=self.tenant, is_active=True)
|
|
self.fields['amended_note'].queryset = ClinicalNote.objects.filter(
|
|
patient__tenant=self.tenant
|
|
).exclude(pk=self.instance.pk if self.instance.pk else None)
|
|
self.fields['related_problems'].queryset = ProblemList.objects.filter(tenant=self.tenant)
|
|
self.fields['related_care_plans'].queryset = CarePlan.objects.filter(tenant=self.tenant)
|
|
|
|
def clean(self):
|
|
cleaned_data = super().clean()
|
|
status = cleaned_data.get('status')
|
|
electronically_signed = cleaned_data.get('electronically_signed')
|
|
signed_datetime = cleaned_data.get('signed_datetime')
|
|
|
|
# Validate signature information
|
|
if status == 'SIGNED' and not electronically_signed:
|
|
self.add_error('electronically_signed', 'Note must be electronically signed if status is Signed')
|
|
|
|
if electronically_signed and not signed_datetime:
|
|
cleaned_data['signed_datetime'] = timezone.now()
|
|
|
|
# Validate amendment information
|
|
amended_note = cleaned_data.get('amended_note')
|
|
amendment_reason = cleaned_data.get('amendment_reason')
|
|
|
|
if amended_note and not amendment_reason:
|
|
self.add_error('amendment_reason', 'Amendment reason is required when amending a note')
|
|
|
|
return cleaned_data
|
|
|
|
|
|
class NoteTemplateForm(forms.ModelForm):
|
|
"""
|
|
Form for creating and updating note templates.
|
|
"""
|
|
class Meta:
|
|
model = NoteTemplate
|
|
fields = [
|
|
'name', 'description', 'note_type', 'specialty',
|
|
'template_content', 'structured_fields',
|
|
'is_active', 'is_default', 'version',
|
|
'previous_version', 'quality_indicators', 'compliance_requirements'
|
|
]
|
|
widgets = {
|
|
'description': forms.Textarea(attrs={'rows': 3}),
|
|
'template_content': forms.Textarea(attrs={'rows': 10, 'class': 'template-editor'}),
|
|
'structured_fields': forms.Textarea(attrs={'rows': 5}),
|
|
'quality_indicators': forms.Textarea(attrs={'rows': 3}),
|
|
'compliance_requirements': forms.Textarea(attrs={'rows': 3}),
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
self.tenant = kwargs.pop('tenant', None)
|
|
super().__init__(*args, **kwargs)
|
|
|
|
# Filter related objects by tenant
|
|
if self.tenant:
|
|
self.fields['previous_version'].queryset = NoteTemplate.objects.filter(
|
|
tenant=self.tenant
|
|
).exclude(pk=self.instance.pk if self.instance.pk else None)
|
|
|
|
def clean(self):
|
|
cleaned_data = super().clean()
|
|
is_default = cleaned_data.get('is_default')
|
|
note_type = cleaned_data.get('note_type')
|
|
specialty = cleaned_data.get('specialty')
|
|
|
|
# Validate template content
|
|
template_content = cleaned_data.get('template_content')
|
|
if not template_content or len(template_content.strip()) < 10:
|
|
self.add_error('template_content', 'Template content is too short')
|
|
|
|
# Validate version format
|
|
version = cleaned_data.get('version')
|
|
if version:
|
|
import re
|
|
if not re.match(r'^\d+\.\d+$', version):
|
|
self.add_error('version', 'Version must be in format X.Y (e.g., 1.0)')
|
|
|
|
return cleaned_data
|
|
|