428 lines
17 KiB
Python
428 lines
17 KiB
Python
from rest_framework import viewsets, permissions, status
|
|
from rest_framework.decorators import action
|
|
from rest_framework.response import Response
|
|
from django_filters.rest_framework import DjangoFilterBackend
|
|
from rest_framework import filters
|
|
from django.db.models import Q, Count, Sum, F
|
|
from django.utils import timezone
|
|
from datetime import timedelta
|
|
|
|
from ..models import (
|
|
Medication, Prescription, InventoryItem, DispenseRecord,
|
|
MedicationAdministration, DrugInteraction
|
|
)
|
|
from .serializers import (
|
|
MedicationSerializer, PrescriptionSerializer, InventoryItemSerializer,
|
|
DispenseRecordSerializer, MedicationAdministrationSerializer,
|
|
DrugInteractionSerializer, PharmacyStatsSerializer, PrescriptionFillSerializer
|
|
)
|
|
from core.utils import AuditLogger
|
|
|
|
|
|
class BaseViewSet(viewsets.ModelViewSet):
|
|
"""Base ViewSet with common functionality"""
|
|
permission_classes = [permissions.IsAuthenticated]
|
|
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
|
|
|
def get_queryset(self):
|
|
# Filter by tenant if user has one
|
|
if hasattr(self.request.user, 'tenant') and self.request.user.tenant:
|
|
return self.queryset.filter(tenant=self.request.user.tenant)
|
|
return self.queryset
|
|
|
|
def perform_create(self, serializer):
|
|
if hasattr(self.request.user, 'tenant'):
|
|
serializer.save(tenant=self.request.user.tenant)
|
|
else:
|
|
serializer.save()
|
|
|
|
|
|
class MedicationViewSet(BaseViewSet):
|
|
"""ViewSet for Medication model"""
|
|
queryset = Medication.objects.all()
|
|
serializer_class = MedicationSerializer
|
|
filterset_fields = [
|
|
'dosage_form', 'route', 'controlled_substance', 'therapeutic_class',
|
|
'is_active', 'black_box_warning'
|
|
]
|
|
search_fields = [
|
|
'name', 'generic_name', 'brand_names', 'ndc_number', 'rxcui',
|
|
'manufacturer', 'therapeutic_class'
|
|
]
|
|
ordering_fields = ['name', 'generic_name', 'created_at']
|
|
ordering = ['name']
|
|
|
|
@action(detail=False, methods=['get'])
|
|
def controlled_substances(self, request):
|
|
"""Get controlled substances"""
|
|
queryset = self.get_queryset().exclude(controlled_substance='NONE')
|
|
serializer = self.get_serializer(queryset, many=True)
|
|
return Response(serializer.data)
|
|
|
|
@action(detail=False, methods=['get'])
|
|
def search_by_ndc(self, request):
|
|
"""Search medications by NDC number"""
|
|
ndc = request.query_params.get('ndc')
|
|
if not ndc:
|
|
return Response({'error': 'NDC parameter required'}, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
queryset = self.get_queryset().filter(ndc_number__icontains=ndc)
|
|
serializer = self.get_serializer(queryset, many=True)
|
|
return Response(serializer.data)
|
|
|
|
@action(detail=True, methods=['get'])
|
|
def interactions(self, request, pk=None):
|
|
"""Get drug interactions for a medication"""
|
|
medication = self.get_object()
|
|
interactions = DrugInteraction.objects.filter(
|
|
Q(medication1=medication) | Q(medication2=medication),
|
|
is_active=True
|
|
)
|
|
serializer = DrugInteractionSerializer(interactions, many=True)
|
|
return Response(serializer.data)
|
|
|
|
|
|
class PrescriptionViewSet(BaseViewSet):
|
|
"""ViewSet for Prescription model"""
|
|
queryset = Prescription.objects.all()
|
|
serializer_class = PrescriptionSerializer
|
|
filterset_fields = [
|
|
'status', 'priority', 'patient', 'prescriber', 'medication',
|
|
'generic_substitution_allowed', 'prior_authorization_required'
|
|
]
|
|
search_fields = [
|
|
'prescription_number', 'patient__first_name', 'patient__last_name',
|
|
'patient__mrn', 'medication__name', 'prescriber__first_name',
|
|
'prescriber__last_name'
|
|
]
|
|
ordering_fields = ['date_prescribed', 'date_filled', 'priority', 'status']
|
|
ordering = ['-date_prescribed']
|
|
|
|
@action(detail=False, methods=['get'])
|
|
def pending(self, request):
|
|
"""Get pending prescriptions"""
|
|
queryset = self.get_queryset().filter(status='PENDING')
|
|
serializer = self.get_serializer(queryset, many=True)
|
|
return Response(serializer.data)
|
|
|
|
@action(detail=False, methods=['get'])
|
|
def ready_for_pickup(self, request):
|
|
"""Get prescriptions ready for pickup"""
|
|
queryset = self.get_queryset().filter(status='READY_FOR_PICKUP')
|
|
serializer = self.get_serializer(queryset, many=True)
|
|
return Response(serializer.data)
|
|
|
|
@action(detail=True, methods=['post'])
|
|
def fill(self, request, pk=None):
|
|
"""Fill a prescription"""
|
|
prescription = self.get_object()
|
|
serializer = PrescriptionFillSerializer(data=request.data)
|
|
|
|
if serializer.is_valid():
|
|
# Update prescription
|
|
prescription.quantity_dispensed = serializer.validated_data['quantity_dispensed']
|
|
prescription.status = 'FILLED'
|
|
prescription.date_filled = timezone.now()
|
|
prescription.save()
|
|
|
|
# Create dispense record
|
|
DispenseRecord.objects.create(
|
|
prescription=prescription,
|
|
quantity_dispensed=serializer.validated_data['quantity_dispensed'],
|
|
lot_number=serializer.validated_data['lot_number'],
|
|
expiration_date=serializer.validated_data['expiration_date'],
|
|
date_dispensed=timezone.now(),
|
|
dispensed_by=request.user,
|
|
pharmacy_location=serializer.validated_data['pharmacy_location'],
|
|
patient_counseled=serializer.validated_data['patient_counseled'],
|
|
counseled_by_id=serializer.validated_data.get('counseled_by'),
|
|
medication_guide_provided=serializer.validated_data['medication_guide_provided'],
|
|
notes=serializer.validated_data.get('notes', ''),
|
|
tenant=getattr(request.user, 'tenant', None)
|
|
)
|
|
|
|
# Log the action
|
|
AuditLogger.log_action(
|
|
user=request.user,
|
|
action='PRESCRIPTION_FILLED',
|
|
model='Prescription',
|
|
object_id=str(prescription.prescription_id),
|
|
details={
|
|
'prescription_number': prescription.prescription_number,
|
|
'quantity_dispensed': serializer.validated_data['quantity_dispensed']
|
|
}
|
|
)
|
|
|
|
return Response({'message': 'Prescription filled successfully'})
|
|
|
|
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
|
|
|
|
@action(detail=True, methods=['post'])
|
|
def cancel(self, request, pk=None):
|
|
"""Cancel a prescription"""
|
|
prescription = self.get_object()
|
|
reason = request.data.get('reason', '')
|
|
|
|
prescription.status = 'CANCELLED'
|
|
prescription.notes = f"Cancelled: {reason}"
|
|
prescription.save()
|
|
|
|
# Log the action
|
|
AuditLogger.log_action(
|
|
user=request.user,
|
|
action='PRESCRIPTION_CANCELLED',
|
|
model='Prescription',
|
|
object_id=str(prescription.prescription_id),
|
|
details={
|
|
'prescription_number': prescription.prescription_number,
|
|
'reason': reason
|
|
}
|
|
)
|
|
|
|
return Response({'message': 'Prescription cancelled successfully'})
|
|
|
|
|
|
class InventoryItemViewSet(BaseViewSet):
|
|
"""ViewSet for InventoryItem model"""
|
|
queryset = InventoryItem.objects.all()
|
|
serializer_class = InventoryItemSerializer
|
|
filterset_fields = ['medication', 'status', 'supplier', 'location']
|
|
search_fields = [
|
|
'medication__name', 'lot_number', 'supplier', 'location'
|
|
]
|
|
ordering_fields = ['expiration_date', 'quantity_on_hand', 'last_counted']
|
|
ordering = ['expiration_date']
|
|
|
|
@action(detail=False, methods=['get'])
|
|
def low_stock(self, request):
|
|
"""Get low stock items"""
|
|
queryset = self.get_queryset().filter(
|
|
quantity_on_hand__lte=F('reorder_level')
|
|
)
|
|
serializer = self.get_serializer(queryset, many=True)
|
|
return Response(serializer.data)
|
|
|
|
@action(detail=False, methods=['get'])
|
|
def expired(self, request):
|
|
"""Get expired items"""
|
|
queryset = self.get_queryset().filter(expiration_date__lt=timezone.now().date())
|
|
serializer = self.get_serializer(queryset, many=True)
|
|
return Response(serializer.data)
|
|
|
|
@action(detail=False, methods=['get'])
|
|
def expiring_soon(self, request):
|
|
"""Get items expiring within 30 days"""
|
|
thirty_days = timezone.now().date() + timedelta(days=30)
|
|
queryset = self.get_queryset().filter(
|
|
expiration_date__lte=thirty_days,
|
|
expiration_date__gte=timezone.now().date()
|
|
)
|
|
serializer = self.get_serializer(queryset, many=True)
|
|
return Response(serializer.data)
|
|
|
|
@action(detail=True, methods=['post'])
|
|
def adjust_quantity(self, request, pk=None):
|
|
"""Adjust inventory quantity"""
|
|
item = self.get_object()
|
|
adjustment = request.data.get('adjustment', 0)
|
|
reason = request.data.get('reason', '')
|
|
|
|
try:
|
|
adjustment = int(adjustment)
|
|
except (ValueError, TypeError):
|
|
return Response(
|
|
{'error': 'Invalid adjustment value'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
old_quantity = item.quantity_on_hand
|
|
item.quantity_on_hand += adjustment
|
|
item.save()
|
|
|
|
# Log the action
|
|
AuditLogger.log_action(
|
|
user=request.user,
|
|
action='INVENTORY_ADJUSTMENT',
|
|
model='InventoryItem',
|
|
object_id=str(item.item_id),
|
|
details={
|
|
'medication': item.medication.name,
|
|
'old_quantity': old_quantity,
|
|
'new_quantity': item.quantity_on_hand,
|
|
'adjustment': adjustment,
|
|
'reason': reason
|
|
}
|
|
)
|
|
|
|
return Response({'message': 'Inventory adjusted successfully'})
|
|
|
|
|
|
class DispenseRecordViewSet(viewsets.ReadOnlyModelViewSet):
|
|
"""ViewSet for DispenseRecord model (read-only)"""
|
|
queryset = DispenseRecord.objects.all()
|
|
serializer_class = DispenseRecordSerializer
|
|
permission_classes = [permissions.IsAuthenticated]
|
|
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
|
filterset_fields = ['prescription', 'dispensed_by', 'pharmacy_location']
|
|
search_fields = [
|
|
'prescription__prescription_number', 'prescription__patient__first_name',
|
|
'prescription__patient__last_name', 'prescription__medication__name'
|
|
]
|
|
ordering_fields = ['date_dispensed']
|
|
ordering = ['-date_dispensed']
|
|
|
|
def get_queryset(self):
|
|
if hasattr(self.request.user, 'tenant') and self.request.user.tenant:
|
|
return self.queryset.filter(tenant=self.request.user.tenant)
|
|
return self.queryset
|
|
|
|
|
|
class MedicationAdministrationViewSet(BaseViewSet):
|
|
"""ViewSet for MedicationAdministration model"""
|
|
queryset = MedicationAdministration.objects.all()
|
|
serializer_class = MedicationAdministrationSerializer
|
|
filterset_fields = ['patient', 'medication', 'status', 'administered_by']
|
|
search_fields = [
|
|
'patient__first_name', 'patient__last_name', 'patient__mrn',
|
|
'medication__name'
|
|
]
|
|
ordering_fields = ['scheduled_time', 'actual_time']
|
|
ordering = ['-scheduled_time']
|
|
|
|
@action(detail=False, methods=['get'])
|
|
def due_now(self, request):
|
|
"""Get medications due for administration now"""
|
|
now = timezone.now()
|
|
queryset = self.get_queryset().filter(
|
|
scheduled_time__lte=now,
|
|
status='SCHEDULED'
|
|
)
|
|
serializer = self.get_serializer(queryset, many=True)
|
|
return Response(serializer.data)
|
|
|
|
@action(detail=True, methods=['post'])
|
|
def administer(self, request, pk=None):
|
|
"""Mark medication as administered"""
|
|
administration = self.get_object()
|
|
|
|
administration.status = 'ADMINISTERED'
|
|
administration.actual_time = timezone.now()
|
|
administration.administered_by = request.user
|
|
administration.patient_response = request.data.get('patient_response', '')
|
|
administration.notes = request.data.get('notes', '')
|
|
administration.save()
|
|
|
|
# Log the action
|
|
AuditLogger.log_action(
|
|
user=request.user,
|
|
action='MEDICATION_ADMINISTERED',
|
|
model='MedicationAdministration',
|
|
object_id=str(administration.administration_id),
|
|
details={
|
|
'patient': administration.patient.get_full_name(),
|
|
'medication': administration.medication.name,
|
|
'dosage': administration.dosage_given
|
|
}
|
|
)
|
|
|
|
return Response({'message': 'Medication administered successfully'})
|
|
|
|
|
|
class DrugInteractionViewSet(viewsets.ReadOnlyModelViewSet):
|
|
"""ViewSet for DrugInteraction model (read-only)"""
|
|
queryset = DrugInteraction.objects.filter(is_active=True)
|
|
serializer_class = DrugInteractionSerializer
|
|
permission_classes = [permissions.IsAuthenticated]
|
|
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
|
filterset_fields = ['severity', 'interaction_type', 'medication1', 'medication2']
|
|
search_fields = [
|
|
'medication1__name', 'medication2__name', 'description'
|
|
]
|
|
ordering_fields = ['severity']
|
|
ordering = ['-severity']
|
|
|
|
@action(detail=False, methods=['post'])
|
|
def check_interactions(self, request):
|
|
"""Check for interactions between multiple medications"""
|
|
medication_ids = request.data.get('medication_ids', [])
|
|
|
|
if len(medication_ids) < 2:
|
|
return Response(
|
|
{'error': 'At least 2 medications required'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
interactions = []
|
|
for i, med1_id in enumerate(medication_ids):
|
|
for med2_id in medication_ids[i+1:]:
|
|
interaction = DrugInteraction.objects.filter(
|
|
Q(medication1_id=med1_id, medication2_id=med2_id) |
|
|
Q(medication1_id=med2_id, medication2_id=med1_id),
|
|
is_active=True
|
|
).first()
|
|
|
|
if interaction:
|
|
interactions.append(interaction)
|
|
|
|
serializer = self.get_serializer(interactions, many=True)
|
|
return Response(serializer.data)
|
|
|
|
|
|
class PharmacyStatsViewSet(viewsets.ViewSet):
|
|
"""ViewSet for pharmacy statistics"""
|
|
permission_classes = [permissions.IsAuthenticated]
|
|
|
|
@action(detail=False, methods=['get'])
|
|
def dashboard(self, request):
|
|
"""Get pharmacy dashboard statistics"""
|
|
tenant_filter = {}
|
|
if hasattr(request.user, 'tenant') and request.user.tenant:
|
|
tenant_filter['tenant'] = request.user.tenant
|
|
|
|
today = timezone.now().date()
|
|
|
|
# Prescription statistics
|
|
prescriptions = Prescription.objects.filter(**tenant_filter)
|
|
total_prescriptions = prescriptions.count()
|
|
pending_prescriptions = prescriptions.filter(status='PENDING').count()
|
|
filled_today = prescriptions.filter(date_filled__date=today).count()
|
|
|
|
# Inventory statistics
|
|
inventory = InventoryItem.objects.filter(**tenant_filter)
|
|
low_stock_items = inventory.filter(
|
|
quantity_on_hand__lte=F('reorder_level')
|
|
).count()
|
|
expired_items = inventory.filter(expiration_date__lt=today).count()
|
|
|
|
# Medication statistics
|
|
medications = Medication.objects.filter(**tenant_filter)
|
|
total_medications = medications.count()
|
|
controlled_substances = medications.exclude(controlled_substance='NONE').count()
|
|
|
|
# Inventory value
|
|
inventory_value = inventory.aggregate(
|
|
total=Sum('total_cost')
|
|
)['total'] or 0
|
|
|
|
# Prescription types breakdown
|
|
prescription_types = prescriptions.values('status').annotate(
|
|
count=Count('id')
|
|
).order_by('-count')
|
|
|
|
stats = {
|
|
'total_prescriptions': total_prescriptions,
|
|
'pending_prescriptions': pending_prescriptions,
|
|
'filled_today': filled_today,
|
|
'low_stock_items': low_stock_items,
|
|
'expired_items': expired_items,
|
|
'total_medications': total_medications,
|
|
'controlled_substances': controlled_substances,
|
|
'inventory_value': inventory_value,
|
|
'prescription_types': {item['status']: item['count'] for item in prescription_types},
|
|
'status_breakdown': dict(prescription_types.values_list('status', 'count'))
|
|
}
|
|
|
|
serializer = PharmacyStatsSerializer(stats)
|
|
return Response(serializer.data)
|
|
|