356 lines
16 KiB
Python
356 lines
16 KiB
Python
"""
|
|
Forms for Pharmacy app CRUD operations (corrected to match models).
|
|
"""
|
|
|
|
from django import forms
|
|
from django.core.exceptions import ValidationError
|
|
from django.utils import timezone
|
|
from datetime import date
|
|
|
|
from .models import (
|
|
Medication, Prescription, MedicationInventoryItem,
|
|
DispenseRecord, MedicationAdministration, DrugInteraction
|
|
)
|
|
from inventory.models import InventoryStock
|
|
from patients.models import PatientProfile
|
|
from accounts.models import User
|
|
|
|
|
|
class MedicationForm(forms.ModelForm):
|
|
"""
|
|
Form for medication management.
|
|
"""
|
|
class Meta:
|
|
model = Medication
|
|
fields = [
|
|
'generic_name', 'brand_name', 'dosage_form', 'strength',
|
|
'unit_of_measure', 'ndc_number', 'manufacturer', 'drug_class',
|
|
'controlled_substance_schedule', 'indications', 'contraindications',
|
|
'side_effects', 'warnings', 'is_active', 'is_available'
|
|
]
|
|
widgets = {
|
|
'generic_name': forms.TextInput(attrs={'class': 'form-control'}),
|
|
'brand_name': forms.TextInput(attrs={'class': 'form-control'}),
|
|
'dosage_form': forms.Select(attrs={'class': 'form-select'}),
|
|
'strength': forms.TextInput(attrs={'class': 'form-control'}),
|
|
'unit_of_measure': forms.Select(attrs={'class': 'form-select'}),
|
|
'ndc_number': forms.TextInput(attrs={'class': 'form-control'}),
|
|
'manufacturer': forms.TextInput(attrs={'class': 'form-control'}),
|
|
'drug_class': forms.TextInput(attrs={'class': 'form-control'}),
|
|
'controlled_substance_schedule': forms.Select(attrs={'class': 'form-select'}),
|
|
'indications': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
|
'contraindications': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
|
'side_effects': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
|
'warnings': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
|
'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
|
'is_available': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
|
}
|
|
|
|
def clean_ndc_number(self):
|
|
ndc_number = self.cleaned_data.get('ndc_number')
|
|
if ndc_number:
|
|
digits = ''.join(filter(str.isdigit, ndc_number))
|
|
if len(digits) != 11:
|
|
raise ValidationError('NDC number must be 11 digits.')
|
|
return ndc_number
|
|
return ndc_number
|
|
|
|
|
|
class PrescriptionForm(forms.ModelForm):
|
|
"""
|
|
Form for prescription management.
|
|
"""
|
|
class Meta:
|
|
model = Prescription
|
|
fields = [
|
|
'patient', 'prescriber', 'medication',
|
|
'quantity_prescribed', 'quantity_unit', 'dosage_instructions',
|
|
'frequency', 'duration', 'refills_authorized',
|
|
'refills_remaining', 'date_prescribed', 'date_written',
|
|
'expiration_date', 'status', 'indication', 'diagnosis_code',
|
|
'pharmacy_notes', 'patient_instructions', 'prior_authorization_required',
|
|
'prior_authorization_number',
|
|
]
|
|
widgets = {
|
|
'patient': forms.Select(attrs={'class': 'form-select'}),
|
|
'prescriber': forms.Select(attrs={'class': 'form-select'}),
|
|
'medication': forms.Select(attrs={'class': 'form-select'}),
|
|
'quantity_prescribed': forms.NumberInput(attrs={'class': 'form-control', 'min': 1}),
|
|
'quantity_unit': forms.Select(attrs={'class': 'form-select'}),
|
|
'dosage_instructions': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
|
'frequency': forms.TextInput(attrs={'class': 'form-control'}),
|
|
'duration': forms.TextInput(attrs={'class': 'form-control'}),
|
|
'refills_authorized': forms.NumberInput(attrs={'class': 'form-control', 'min': 0}),
|
|
'refills_remaining': forms.NumberInput(attrs={'class': 'form-control', 'min': 0}),
|
|
'date_prescribed': forms.DateTimeInput(attrs={'class': 'form-control', 'type': 'datetime-local'}),
|
|
'date_written': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
|
|
'expiration_date': forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
|
|
'status': forms.Select(attrs={'class': 'form-select'}),
|
|
'indication': forms.TextInput(attrs={'class': 'form-control'}),
|
|
'diagnosis_code': forms.TextInput(attrs={'class': 'form-control'}),
|
|
'pharmacy_notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 2}),
|
|
'patient_instructions': forms.Textarea(attrs={'class': 'form-control', 'rows': 2}),
|
|
'prior_authorization_required': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
|
'prior_authorization_number': forms.TextInput(attrs={'class': 'form-control'}),
|
|
# 'priority': forms.Select(attrs={'class': 'form-select'}),
|
|
}
|
|
|
|
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, is_active=True
|
|
).order_by('last_name')
|
|
self.fields['medication'].queryset = Medication.objects.filter(
|
|
tenant=user.tenant, is_active=True
|
|
).order_by('generic_name')
|
|
self.fields['prescriber'].queryset = User.objects.filter(
|
|
tenant=user.tenant, is_active=True,
|
|
employee_profile__role__in=['DOCTOR', 'SPECIALIST', 'PHYSICIAN', 'PHYSICIAN_ASSISTANT', 'SURGEON']
|
|
).order_by('last_name')
|
|
|
|
def clean(self):
|
|
data = super().clean()
|
|
dpres = data.get('date_prescribed')
|
|
dwritten = data.get('date_written')
|
|
dexp = data.get('expiration_date')
|
|
if dpres and dpres > timezone.now():
|
|
self.add_error('date_prescribed', 'Cannot be in the future.')
|
|
if dexp and dwritten and dexp <= dwritten:
|
|
self.add_error('expiration_date', 'Must be after written date.')
|
|
auth = data.get('refills_authorized')
|
|
rem = data.get('refills_remaining')
|
|
if rem is not None and auth is not None and rem > auth:
|
|
self.add_error('refills_remaining', 'Cannot exceed authorized refills.')
|
|
return data
|
|
|
|
|
|
class MedicationInventoryItemForm(forms.ModelForm):
|
|
"""
|
|
Form for medication inventory item management.
|
|
"""
|
|
class Meta:
|
|
model = MedicationInventoryItem
|
|
fields = [
|
|
'medication', 'inventory_item', 'formulary_tier',
|
|
'therapeutic_equivalent', 'auto_substitution_allowed',
|
|
'max_dispense_quantity', 'requires_counseling',
|
|
'requires_id_verification', 'pharmacy_notes'
|
|
]
|
|
widgets = {
|
|
'medication': forms.Select(attrs={'class': 'form-select'}),
|
|
'inventory_item': forms.Select(attrs={'class': 'form-select'}),
|
|
'formulary_tier': forms.TextInput(attrs={'class': 'form-control'}),
|
|
'therapeutic_equivalent': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
|
'auto_substitution_allowed': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
|
'max_dispense_quantity': forms.NumberInput(attrs={'class': 'form-control', 'min': 1}),
|
|
'requires_counseling': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
|
'requires_id_verification': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
|
'pharmacy_notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
|
}
|
|
|
|
# Legacy alias for backward compatibility
|
|
InventoryItemForm = MedicationInventoryItemForm
|
|
|
|
|
|
class DispenseRecordForm(forms.ModelForm):
|
|
"""
|
|
Form for dispense record management.
|
|
"""
|
|
class Meta:
|
|
model = DispenseRecord
|
|
fields = [
|
|
'prescription', 'inventory_stock', 'quantity_dispensed',
|
|
'dispensed_by', 'date_dispensed', 'patient_counseled', 'counseling_notes'
|
|
]
|
|
widgets = {
|
|
'prescription': forms.Select(attrs={'class': 'form-select'}),
|
|
'inventory_stock': forms.Select(attrs={'class': 'form-select'}),
|
|
'quantity_dispensed': forms.NumberInput(attrs={'class': 'form-control', 'min': 1}),
|
|
'dispensed_by': forms.Select(attrs={'class': 'form-select'}),
|
|
'date_dispensed': forms.DateTimeInput(attrs={'class': 'form-control', 'type': 'datetime-local'}),
|
|
'patient_counseled': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
|
'counseling_notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 2}),
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
user = kwargs.pop('user', None)
|
|
super().__init__(*args, **kwargs)
|
|
if user and hasattr(user, 'tenant'):
|
|
self.fields['prescription'].queryset = Prescription.objects.filter(
|
|
tenant=user.tenant, status__in=['ACTIVE', 'PENDING']
|
|
).order_by('-date_prescribed')
|
|
# Get inventory stocks for pharmacy medications with available quantity
|
|
self.fields['inventory_stock'].queryset = InventoryStock.objects.filter(
|
|
inventory_item__tenant=user.tenant,
|
|
inventory_item__category='PHARMACY_MEDICATIONS',
|
|
quantity_available__gt=0,
|
|
quality_status='GOOD'
|
|
).order_by('inventory_item__item_name')
|
|
self.fields['dispensed_by'].queryset = User.objects.filter(
|
|
tenant=user.tenant, role__in=['PHARMACIST', 'PHARMACY_TECH']
|
|
).order_by('last_name')
|
|
|
|
def clean_date_dispensed(self):
|
|
dd = self.cleaned_data.get('date_dispensed')
|
|
if dd and dd > timezone.now():
|
|
raise ValidationError('Cannot be in future.')
|
|
return dd
|
|
|
|
def clean(self):
|
|
data = super().clean()
|
|
pres = data.get('prescription')
|
|
inv_stock = data.get('inventory_stock')
|
|
qty = data.get('quantity_dispensed')
|
|
|
|
if pres and inv_stock:
|
|
# Check if the inventory stock is for the prescribed medication
|
|
med_inventory = inv_stock.inventory_item.medication_inventory_items.filter(
|
|
medication=pres.medication
|
|
).first()
|
|
if not med_inventory:
|
|
self.add_error('inventory_stock', 'Selected stock does not match prescribed medication.')
|
|
|
|
if inv_stock and qty and qty > inv_stock.quantity_available:
|
|
self.add_error('quantity_dispensed', 'Exceeds available quantity.')
|
|
|
|
return data
|
|
|
|
|
|
class MedicationAdministrationForm(forms.ModelForm):
|
|
"""
|
|
Form for medication administration management.
|
|
"""
|
|
class Meta:
|
|
model = MedicationAdministration
|
|
fields = [
|
|
'prescription', 'scheduled_datetime', 'actual_datetime',
|
|
'dose_given', 'route_given', 'status', 'administered_by',
|
|
'witnessed_by', 'reason_not_given', 'reason_notes',
|
|
'patient_response', 'side_effects_observed'
|
|
]
|
|
widgets = {
|
|
'prescription': forms.Select(attrs={'class': 'form-select'}),
|
|
'scheduled_datetime': forms.DateTimeInput(attrs={'class': 'form-control', 'type': 'datetime-local'}),
|
|
'actual_datetime': forms.DateTimeInput(attrs={'class': 'form-control', 'type': 'datetime-local'}),
|
|
'dose_given': forms.TextInput(attrs={'class': 'form-control'}),
|
|
'route_given': forms.Select(attrs={'class': 'form-select'}),
|
|
'status': forms.Select(attrs={'class': 'form-select'}),
|
|
'administered_by': forms.Select(attrs={'class': 'form-select'}),
|
|
'witnessed_by': forms.Select(attrs={'class': 'form-select'}),
|
|
'reason_not_given': forms.Textarea(attrs={'class': 'form-control', 'rows': 2}),
|
|
'reason_notes': forms.Textarea(attrs={'class': 'form-control', 'rows': 2}),
|
|
'patient_response': forms.Textarea(attrs={'class': 'form-control', 'rows': 2}),
|
|
'side_effects_observed': forms.Textarea(attrs={'class': 'form-control', 'rows': 2}),
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
user = kwargs.pop('user', None)
|
|
super().__init__(*args, **kwargs)
|
|
if user and hasattr(user, 'tenant'):
|
|
self.fields['prescription'].queryset = Prescription.objects.filter(
|
|
tenant=user.tenant
|
|
)
|
|
self.fields['administered_by'].queryset = User.objects.filter(
|
|
tenant=user.tenant, role__in=['NURSE', 'PHARMACIST']
|
|
)
|
|
self.fields['witnessed_by'].queryset = User.objects.filter(
|
|
tenant=user.tenant, role='PHARMACIST'
|
|
)
|
|
|
|
def clean(self):
|
|
data = super().clean()
|
|
status = data.get('status')
|
|
reason = data.get('reason_not_given')
|
|
dose = data.get('dose_given')
|
|
if status == 'NOT_GIVEN' and not reason:
|
|
self.add_error('reason_not_given', 'Provide a reason.')
|
|
if status == 'GIVEN' and not dose:
|
|
self.add_error('dose_given', 'Dose is required.')
|
|
return data
|
|
|
|
|
|
class DrugInteractionForm(forms.ModelForm):
|
|
"""
|
|
Form for drug interaction management.
|
|
"""
|
|
class Meta:
|
|
model = DrugInteraction
|
|
fields = [
|
|
'medication_1', 'medication_2', 'interaction_type',
|
|
'severity', 'mechanism', 'clinical_effect',
|
|
'management_recommendations', 'monitoring_parameters', 'is_active'
|
|
]
|
|
widgets = {
|
|
'medication_1': forms.Select(attrs={'class': 'form-select'}),
|
|
'medication_2': forms.Select(attrs={'class': 'form-select'}),
|
|
'interaction_type': forms.Select(attrs={'class': 'form-select'}),
|
|
'severity': forms.Select(attrs={'class': 'form-select'}),
|
|
'mechanism': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
|
'clinical_effect': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
|
'management_recommendations': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
|
|
'monitoring_parameters': forms.Textarea(attrs={'class': 'form-control', 'rows': 2}),
|
|
'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
|
|
}
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
user = kwargs.pop('user', None)
|
|
super().__init__(*args, **kwargs)
|
|
if user and hasattr(user, 'tenant'):
|
|
meds = Medication.objects.filter(tenant=user.tenant, is_active=True)
|
|
self.fields['medication_1'].queryset = meds
|
|
self.fields['medication_2'].queryset = meds
|
|
|
|
def clean(self):
|
|
data = super().clean()
|
|
if data.get('medication_1') == data.get('medication_2'):
|
|
raise ValidationError('Medications must differ.')
|
|
return data
|
|
|
|
|
|
class PharmacySearchForm(forms.Form):
|
|
"""
|
|
Form for searching pharmacy data.
|
|
"""
|
|
search = forms.CharField(required=False, max_length=255,
|
|
widget=forms.TextInput(attrs={'class':'form-control','placeholder':'Search...'})
|
|
)
|
|
patient = forms.ModelChoiceField(
|
|
queryset=PatientProfile.objects.none(), required=False,
|
|
widget=forms.Select(attrs={'class':'form-select'})
|
|
)
|
|
medication = forms.ModelChoiceField(
|
|
queryset=Medication.objects.none(), required=False,
|
|
widget=forms.Select(attrs={'class':'form-select'})
|
|
)
|
|
prescriber = forms.ModelChoiceField(
|
|
queryset=User.objects.none(), required=False,
|
|
widget=forms.Select(attrs={'class':'form-select'})
|
|
)
|
|
status = forms.ChoiceField(
|
|
choices=[('', 'All')] + list(Prescription._meta.get_field('status').choices),
|
|
required=False, widget=forms.Select(attrs={'class':'form-select'})
|
|
)
|
|
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'})
|
|
)
|
|
|
|
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, is_active=True
|
|
).order_by('last_name')
|
|
self.fields['medication'].queryset = Medication.objects.filter(
|
|
tenant=user.tenant, is_active=True
|
|
).order_by('generic_name')
|
|
self.fields['prescriber'].queryset = User.objects.filter(
|
|
tenant=user.tenant, is_active=True,
|
|
role__in=['DOCTOR','SPECIALIST']
|
|
).order_by('last_name')
|