388 lines
17 KiB
Python
388 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 hr.models import Employee
|
||
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
|
||
# Do NOT use both fields and exclude. Keep only fields.
|
||
fields = [
|
||
'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', '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)
|
||
self.user = kwargs.pop('user', None)
|
||
super().__init__(*args, **kwargs)
|
||
|
||
# If you had tenant-scoped lookups for fields that ARE in the form
|
||
# (e.g., device_used is FK and tenant-scoped), filter them here, e.g.:
|
||
# if self.tenant and 'device_used' in self.fields:
|
||
# self.fields['device_used'].queryset = Device.objects.filter(tenant=self.tenant)
|
||
|
||
# DO NOT touch encounter/patient/measured_by here because they are not in the form
|
||
|
||
def clean(self):
|
||
cleaned_data = super().clean()
|
||
|
||
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')
|
||
self.add_error('diastolic_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')
|
||
self.add_error('diastolic_bp', 'Systolic pressure must be greater than diastolic pressure')
|
||
|
||
oxygen_delivery = cleaned_data.get('oxygen_delivery')
|
||
oxygen_flow_rate = cleaned_data.get('oxygen_flow_rate')
|
||
|
||
# Ensure the constant matches your choices exactly
|
||
ROOM_AIR = 'ROOM_AIR' # change to 'room_air' if that’s what your choices use
|
||
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 = [
|
||
'note_type', 'title', 'content',
|
||
'template', 'structured_data', '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)
|
||
self.user = kwargs.pop('user', 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
|
||
|