""" 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