""" Laboratory app forms for healthcare-focused CRUD operations. """ from django import forms from django.core.exceptions import ValidationError from django.utils import timezone from .models import ( LabTest, LabOrder, Specimen, LabResult, QualityControl, ReferenceRange ) from patients.models import PatientProfile from accounts.models import User class LabTestForm(forms.ModelForm): """ Form for creating and updating laboratory tests. """ def __init__(self, *args, tenant=None, **kwargs): """ Optionally accept a `tenant` kwarg if you plan on using it for something. Re-populates the `test_category` choices from the model’s CHOICES tuple. """ super().__init__(*args, **kwargs) # Pull the static choices from the model all_choices = LabTest.TEST_CATEGORY_CHOICES # If you need to adjust per-tenant, do it here. E.g.: # filtered = [c for c in all_choices if c[0] in tenant_allowed_codes] # self.fields['test_category'].choices = filtered self.fields['test_category'].choices = all_choices class Meta: model = LabTest fields = [ 'test_code', 'test_name', 'test_description', 'test_category', 'specimen_type', 'methodology', 'turnaround_time', 'department', 'fasting_required', 'is_active', 'collection_instructions' ] widgets = { 'test_category': forms.Select(attrs={'class': 'form-select'}), 'specimen_type': forms.Select(attrs={'class': 'form-select'}), 'department': forms.Select(attrs={'class': 'form-select'}), 'test_description': forms.Textarea(attrs={'rows': 3}), 'collection_instructions': forms.Textarea(attrs={'rows': 3}), 'turnaround_time': forms.NumberInput(attrs={'min': 1, 'max': 168}), } def clean_test_code(self): test_code = self.cleaned_data['test_code'] # Check for duplicate test codes within the same tenant if self.instance.pk: existing = LabTest.objects.filter( test_code=test_code, tenant=self.instance.tenant ).exclude(pk=self.instance.pk) else: # For new instances, tenant will be set in the view existing = LabTest.objects.filter(test_code=test_code) if existing.exists(): raise ValidationError('A test with this code already exists.') return test_code.upper() class ReferenceRangeForm(forms.ModelForm): """ Form for creating and updating reference ranges. """ class Meta: model = ReferenceRange fields = [ 'test', 'gender', 'age_min', 'age_max', 'range_low', 'range_high', 'unit', 'range_text' ] widgets = { 'range_text': forms.Textarea(attrs={'rows': 2}), 'age_min': forms.NumberInput(attrs={'min': 0, 'max': 120}), 'age_max': forms.NumberInput(attrs={'min': 0, 'max': 120}), } def __init__(self, *args, **kwargs): user = kwargs.pop('user', None) super().__init__(*args, **kwargs) if user and hasattr(user, 'tenant'): self.fields['test'].queryset = LabTest.objects.filter( tenant=user.tenant, is_active=True ).order_by('test_name') def clean(self): cleaned_data = super().clean() age_min = cleaned_data.get('age_min') age_max = cleaned_data.get('age_max') range_low = cleaned_data.get('range_low') range_high = cleaned_data.get('range_high') if age_min is not None and age_max is not None: if age_min > age_max: raise ValidationError('Minimum age cannot be greater than maximum age.') if range_low is not None and range_high is not None: if range_low >= range_high: raise ValidationError('Lower limit must be less than upper limit.') return cleaned_data class LabOrderForm(forms.ModelForm): """ Form for creating laboratory orders. """ class Meta: model = LabOrder fields = [ 'patient', 'ordering_provider', 'priority', 'clinical_indication', 'fasting_status', 'special_instructions' ] widgets = { 'clinical_indication': forms.Textarea(attrs={'rows': 3}), 'special_instructions': forms.Textarea(attrs={'rows': 2}), } def __init__(self, *args, **kwargs): user = kwargs.pop('user', None) super().__init__(*args, **kwargs) if user and hasattr(user, 'tenant'): self.fields['patient'].queryset = PatientProfile.objects.filter( tenant=user.tenant ).order_by('last_name', 'first_name') self.fields['test'].queryset = LabTest.objects.filter( tenant=user.tenant, is_active=True ).order_by('test_name') def clean(self): cleaned_data = super().clean() test = cleaned_data.get('test') fasting_status = cleaned_data.get('fasting_status') # Auto-set fasting requirement based on test if test and test.fasting_required and not fasting_status: cleaned_data['fasting_status'] = True self.add_error( 'fasting_status', f'This test ({test.test_name}) requires fasting.' ) return cleaned_data class SpecimenForm(forms.ModelForm): """ Form for creating specimen records. """ class Meta: model = Specimen fields = [ 'order', 'specimen_type', 'collected_datetime', 'collection_method', 'collection_site', 'volume', 'container_type', 'quality_notes', ] widgets = { 'collected_datetime': forms.DateTimeInput( attrs={'type': 'datetime-local'}, format='%Y-%m-%dT%H:%M' ), 'quality_notes': forms.Textarea(attrs={'rows': 2}), 'volume': forms.NumberInput(attrs={'min': 0.1, 'step': 0.1}), } def __init__(self, *args, **kwargs): user = kwargs.pop('user', None) super().__init__(*args, **kwargs) if user and hasattr(user, 'tenant'): self.fields['order'].queryset = LabOrder.objects.filter( tenant=user.tenant, # status__in=['PENDING', 'SCHEDULED'] ).select_related('patient', 'test').order_by('-order_datetime') # Set default collection time to now if not self.instance.pk: self.fields['collected_datetime'].initial = timezone.now() def clean_collected_datetime(self): collected_datetime = self.cleaned_data['collected_datetime'] if collected_datetime > timezone.now(): raise ValidationError('Collection time cannot be in the future.') # Check if collection time is too far in the past (more than 7 days) seven_days_ago = timezone.now() - timezone.timedelta(days=7) if collected_datetime < seven_days_ago: raise ValidationError('Collection time cannot be more than 7 days ago.') return collected_datetime class LabResultForm(forms.ModelForm): """ Form for entering laboratory results. """ class Meta: model = LabResult fields = [ 'order', 'test', 'result_value', 'result_unit', 'abnormal_flag', 'is_critical', 'pathologist_comments', 'technician_comments', 'analyzer' ] widgets = { 'pathologist_comments': forms.Textarea(attrs={'rows': 3}), 'technician_comments': forms.Textarea(attrs={'rows': 2}), 'result_value': forms.NumberInput(attrs={'step': 'any'}), } def __init__(self, *args, **kwargs): user = kwargs.pop('user', None) super().__init__(*args, **kwargs) if user and hasattr(user, 'tenant'): self.fields['order'].queryset = LabOrder.objects.filter( tenant=user.tenant, status='IN_PROGRESS' ).select_related('patient', 'test').order_by('-order_datetime') self.fields['test'].queryset = LabTest.objects.filter( tenant=user.tenant, is_active=True ).order_by('test_name') def clean(self): cleaned_data = super().clean() order = cleaned_data.get('order') test = cleaned_data.get('test') result_value = cleaned_data.get('result_value') result_text = cleaned_data.get('result_text') # Ensure order and test match if order and test and order.test != test: raise ValidationError('Selected test must match the order test.') # Ensure either numeric or text result is provided if not result_value and not result_text: raise ValidationError('Either numeric result value or text result must be provided.') # Auto-populate test from order if not provided if order and not test: cleaned_data['test'] = order.test return cleaned_data class QualityControlForm(forms.ModelForm): """ Form for quality control records. """ class Meta: model = QualityControl fields = [ 'test', 'control_level', 'target_value', 'observed_value', 'status', 'control_lot', 'analyzer', 'comments' ] widgets = { 'run_datetime': forms.DateTimeInput(attrs={'type': 'datetime-local'}), 'comments': forms.Textarea(attrs={'rows': 2}), 'target_value': forms.NumberInput(attrs={'step': 'any'}), 'observed_value': forms.NumberInput(attrs={'step': 'any'}), } def __init__(self, *args, **kwargs): user = kwargs.pop('user', None) super().__init__(*args, **kwargs) if user and hasattr(user, 'tenant'): self.fields['test'].queryset = LabTest.objects.filter( tenant=user.tenant, is_active=True ).order_by('test_name') def clean(self): cleaned_data = super().clean() target_value = cleaned_data.get('target_value') observed_value = cleaned_data.get('observed_value') status = cleaned_data.get('status') # Auto-determine status based on values if both are provided if target_value is not None and observed_value is not None: # Calculate percentage difference if target_value != 0: percent_diff = abs((observed_value - target_value) / target_value) * 100 # Auto-set status based on percentage difference if percent_diff <= 5: # Within 5% is acceptable if not status: cleaned_data['status'] = 'PASS' elif percent_diff <= 10: # 5-10% is warning if not status: cleaned_data['status'] = 'WARNING' else: # >10% is fail if not status: cleaned_data['status'] = 'FAIL' return cleaned_data # Additional forms for search and filtering class LabOrderSearchForm(forms.Form): """ Form for searching and filtering laboratory orders. """ search = forms.CharField( max_length=100, required=False, widget=forms.TextInput(attrs={ 'placeholder': 'Search by patient name, MRN, or test...', 'class': 'form-control' }) ) status = forms.ChoiceField( choices=[('', 'All Statuses')] + LabOrder._meta.get_field('status').choices, required=False, widget=forms.Select(attrs={'class': 'form-control'}) ) priority = forms.ChoiceField( choices=[('', 'All Priorities')] + LabOrder._meta.get_field('priority').choices, required=False, widget=forms.Select(attrs={'class': 'form-control'}) ) date_from = forms.DateField( required=False, widget=forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}) ) date_to = forms.DateField( required=False, widget=forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}) ) class LabResultSearchForm(forms.Form): """ Form for searching and filtering laboratory results. """ search = forms.CharField( max_length=100, required=False, widget=forms.TextInput(attrs={ 'placeholder': 'Search by patient name, MRN, or test...', 'class': 'form-control' }) ) status = forms.ChoiceField( choices=[('', 'All Statuses')] + LabResult._meta.get_field('status').choices, required=False, widget=forms.Select(attrs={'class': 'form-control'}) ) critical_only = forms.BooleanField( required=False, widget=forms.CheckboxInput(attrs={'class': 'form-check-input'}) ) date_from = forms.DateField( required=False, widget=forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}) ) date_to = forms.DateField( required=False, widget=forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}) )