373 lines
12 KiB
Python
373 lines
12 KiB
Python
"""
|
|
DRF API ViewSets for Finance app.
|
|
"""
|
|
|
|
from rest_framework import viewsets, filters, status
|
|
from rest_framework.decorators import action
|
|
from rest_framework.response import Response
|
|
from rest_framework.permissions import IsAuthenticated
|
|
from django_filters.rest_framework import DjangoFilterBackend
|
|
from django.utils import timezone
|
|
from django.db.models import Sum, Q
|
|
|
|
from .models import Invoice, Payment, Service, Package, PackagePurchase, Payer
|
|
from .serializers import (
|
|
InvoiceListSerializer, InvoiceDetailSerializer, InvoiceCreateSerializer,
|
|
PaymentSerializer, ServiceSerializer, PackageSerializer,
|
|
PackagePurchaseSerializer, PayerSerializer
|
|
)
|
|
|
|
|
|
class InvoiceViewSet(viewsets.ModelViewSet):
|
|
"""
|
|
API endpoint for Invoice CRUD operations.
|
|
|
|
list: Get list of invoices
|
|
retrieve: Get invoice details
|
|
create: Create new invoice
|
|
update: Update invoice
|
|
destroy: Delete invoice
|
|
|
|
Custom actions:
|
|
- issue: Issue draft invoice
|
|
- payments: Get invoice payments
|
|
- mark_paid: Mark invoice as paid
|
|
"""
|
|
|
|
permission_classes = [IsAuthenticated]
|
|
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
|
filterset_fields = ['patient', 'status', 'issue_date']
|
|
search_fields = ['invoice_number', 'patient__mrn', 'patient__first_name_en']
|
|
ordering_fields = ['issue_date', 'due_date', 'total']
|
|
ordering = ['-issue_date']
|
|
|
|
def get_queryset(self):
|
|
"""Filter by tenant."""
|
|
return Invoice.objects.filter(
|
|
tenant=self.request.user.tenant
|
|
).select_related('patient', 'appointment').prefetch_related('line_items', 'payments')
|
|
|
|
def get_serializer_class(self):
|
|
"""Return appropriate serializer based on action."""
|
|
if self.action == 'list':
|
|
return InvoiceListSerializer
|
|
elif self.action == 'create':
|
|
return InvoiceCreateSerializer
|
|
return InvoiceDetailSerializer
|
|
|
|
def perform_create(self, serializer):
|
|
"""Set tenant on create."""
|
|
serializer.save(tenant=self.request.user.tenant)
|
|
|
|
@action(detail=True, methods=['post'])
|
|
def issue(self, request, pk=None):
|
|
"""Issue a draft invoice."""
|
|
invoice = self.get_object()
|
|
|
|
if invoice.status != 'DRAFT':
|
|
return Response(
|
|
{'error': 'Only draft invoices can be issued'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
invoice.status = 'ISSUED'
|
|
invoice.save()
|
|
|
|
serializer = InvoiceDetailSerializer(invoice)
|
|
return Response(serializer.data)
|
|
|
|
@action(detail=True, methods=['get'])
|
|
def payments(self, request, pk=None):
|
|
"""Get invoice payments."""
|
|
invoice = self.get_object()
|
|
payments = invoice.payments.all()
|
|
serializer = PaymentSerializer(payments, many=True)
|
|
return Response(serializer.data)
|
|
|
|
@action(detail=True, methods=['post'])
|
|
def mark_paid(self, request, pk=None):
|
|
"""Mark invoice as fully paid."""
|
|
invoice = self.get_object()
|
|
|
|
# Calculate total paid
|
|
total_paid = invoice.payments.filter(
|
|
status='COMPLETED'
|
|
).aggregate(Sum('amount'))['amount__sum'] or 0
|
|
|
|
if total_paid >= invoice.total:
|
|
invoice.status = 'PAID'
|
|
invoice.save()
|
|
serializer = InvoiceDetailSerializer(invoice)
|
|
return Response(serializer.data)
|
|
else:
|
|
return Response(
|
|
{
|
|
'error': 'Invoice not fully paid',
|
|
'total': float(invoice.total),
|
|
'paid': float(total_paid),
|
|
'due': float(invoice.total - total_paid)
|
|
},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
@action(detail=False, methods=['get'])
|
|
def overdue(self, request):
|
|
"""Get overdue invoices."""
|
|
today = timezone.now().date()
|
|
invoices = self.get_queryset().filter(
|
|
due_date__lt=today,
|
|
status__in=['ISSUED', 'PARTIALLY_PAID']
|
|
)
|
|
serializer = InvoiceListSerializer(invoices, many=True)
|
|
return Response(serializer.data)
|
|
|
|
@action(detail=False, methods=['get'])
|
|
def statistics(self, request):
|
|
"""Get invoice statistics."""
|
|
queryset = self.get_queryset()
|
|
|
|
stats = {
|
|
'total_invoices': queryset.count(),
|
|
'total_amount': queryset.aggregate(Sum('total'))['total__sum'] or 0,
|
|
'by_status': dict(queryset.values('status').annotate(
|
|
count=Count('id'),
|
|
amount=Sum('total')
|
|
).values_list('status', 'amount')),
|
|
'overdue_count': queryset.filter(
|
|
due_date__lt=timezone.now().date(),
|
|
status__in=['ISSUED', 'PARTIALLY_PAID']
|
|
).count(),
|
|
}
|
|
|
|
return Response(stats)
|
|
|
|
|
|
class PaymentViewSet(viewsets.ModelViewSet):
|
|
"""
|
|
API endpoint for Payment CRUD operations.
|
|
|
|
list: Get list of payments
|
|
retrieve: Get payment details
|
|
create: Record new payment
|
|
update: Update payment
|
|
destroy: Delete payment
|
|
|
|
Custom actions:
|
|
- process: Process pending payment
|
|
- refund: Refund completed payment
|
|
"""
|
|
|
|
permission_classes = [IsAuthenticated]
|
|
serializer_class = PaymentSerializer
|
|
filter_backends = [DjangoFilterBackend, filters.SearchFilter, filters.OrderingFilter]
|
|
filterset_fields = ['invoice', 'method', 'status', 'payment_date']
|
|
search_fields = ['transaction_id', 'reference', 'invoice__invoice_number']
|
|
ordering_fields = ['payment_date', 'amount']
|
|
ordering = ['-payment_date']
|
|
|
|
def get_queryset(self):
|
|
"""Filter by tenant."""
|
|
return Payment.objects.filter(
|
|
tenant=self.request.user.tenant
|
|
).select_related('invoice__patient', 'processed_by')
|
|
|
|
def perform_create(self, serializer):
|
|
"""Set tenant and processed_by on create."""
|
|
payment = serializer.save(
|
|
tenant=self.request.user.tenant,
|
|
processed_by=self.request.user,
|
|
status='PENDING'
|
|
)
|
|
|
|
# Update invoice status if fully paid
|
|
invoice = payment.invoice
|
|
total_paid = invoice.payments.filter(
|
|
status='COMPLETED'
|
|
).aggregate(Sum('amount'))['amount__sum'] or 0
|
|
|
|
if total_paid >= invoice.total:
|
|
invoice.status = 'PAID'
|
|
elif total_paid > 0:
|
|
invoice.status = 'PARTIALLY_PAID'
|
|
invoice.save()
|
|
|
|
@action(detail=True, methods=['post'])
|
|
def process(self, request, pk=None):
|
|
"""Process a pending payment."""
|
|
payment = self.get_object()
|
|
|
|
if payment.status != 'PENDING':
|
|
return Response(
|
|
{'error': 'Only pending payments can be processed'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
payment.status = 'COMPLETED'
|
|
payment.save()
|
|
|
|
# Update invoice status
|
|
invoice = payment.invoice
|
|
total_paid = invoice.payments.filter(
|
|
status='COMPLETED'
|
|
).aggregate(Sum('amount'))['amount__sum'] or 0
|
|
|
|
if total_paid >= invoice.total:
|
|
invoice.status = 'PAID'
|
|
else:
|
|
invoice.status = 'PARTIALLY_PAID'
|
|
invoice.save()
|
|
|
|
serializer = PaymentSerializer(payment)
|
|
return Response(serializer.data)
|
|
|
|
@action(detail=True, methods=['post'])
|
|
def refund(self, request, pk=None):
|
|
"""Refund a completed payment."""
|
|
payment = self.get_object()
|
|
|
|
if payment.status != 'COMPLETED':
|
|
return Response(
|
|
{'error': 'Only completed payments can be refunded'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
payment.status = 'REFUNDED'
|
|
payment.save()
|
|
|
|
# Update invoice status
|
|
invoice = payment.invoice
|
|
total_paid = invoice.payments.filter(
|
|
status='COMPLETED'
|
|
).aggregate(Sum('amount'))['amount__sum'] or 0
|
|
|
|
if total_paid == 0:
|
|
invoice.status = 'ISSUED'
|
|
elif total_paid < invoice.total:
|
|
invoice.status = 'PARTIALLY_PAID'
|
|
invoice.save()
|
|
|
|
serializer = PaymentSerializer(payment)
|
|
return Response(serializer.data)
|
|
|
|
|
|
class ServiceViewSet(viewsets.ReadOnlyModelViewSet):
|
|
"""
|
|
API endpoint for Service (read-only).
|
|
|
|
list: Get list of services
|
|
retrieve: Get service details
|
|
"""
|
|
|
|
permission_classes = [IsAuthenticated]
|
|
serializer_class = ServiceSerializer
|
|
filter_backends = [DjangoFilterBackend, filters.SearchFilter]
|
|
filterset_fields = ['clinic', 'is_active']
|
|
search_fields = ['code', 'name_en', 'name_ar']
|
|
|
|
def get_queryset(self):
|
|
"""Filter by tenant and active services."""
|
|
return Service.objects.filter(
|
|
tenant=self.request.user.tenant,
|
|
is_active=True
|
|
).select_related('clinic')
|
|
|
|
|
|
class PackageViewSet(viewsets.ReadOnlyModelViewSet):
|
|
"""
|
|
API endpoint for Package (read-only).
|
|
|
|
list: Get list of packages
|
|
retrieve: Get package details
|
|
"""
|
|
|
|
permission_classes = [IsAuthenticated]
|
|
serializer_class = PackageSerializer
|
|
filter_backends = [DjangoFilterBackend, filters.SearchFilter]
|
|
filterset_fields = ['is_active']
|
|
search_fields = ['name_en', 'name_ar']
|
|
|
|
def get_queryset(self):
|
|
"""Filter by tenant and active packages."""
|
|
return Package.objects.filter(
|
|
tenant=self.request.user.tenant,
|
|
is_active=True
|
|
).prefetch_related('services')
|
|
|
|
|
|
class PackagePurchaseViewSet(viewsets.ModelViewSet):
|
|
"""
|
|
API endpoint for PackagePurchase CRUD operations.
|
|
|
|
list: Get list of package purchases
|
|
retrieve: Get package purchase details
|
|
create: Create new package purchase
|
|
update: Update package purchase
|
|
"""
|
|
|
|
permission_classes = [IsAuthenticated]
|
|
serializer_class = PackagePurchaseSerializer
|
|
filter_backends = [DjangoFilterBackend, filters.SearchFilter]
|
|
filterset_fields = ['patient', 'package', 'is_active']
|
|
search_fields = ['patient__mrn', 'patient__first_name_en']
|
|
|
|
def get_queryset(self):
|
|
"""Filter by tenant."""
|
|
return PackagePurchase.objects.filter(
|
|
tenant=self.request.user.tenant
|
|
).select_related('patient', 'package', 'invoice')
|
|
|
|
def perform_create(self, serializer):
|
|
"""Set tenant on create."""
|
|
serializer.save(tenant=self.request.user.tenant)
|
|
|
|
@action(detail=True, methods=['post'])
|
|
def use_session(self, request, pk=None):
|
|
"""Use one session from the package."""
|
|
purchase = self.get_object()
|
|
|
|
if purchase.remaining_sessions <= 0:
|
|
return Response(
|
|
{'error': 'No sessions remaining'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
if not purchase.is_active:
|
|
return Response(
|
|
{'error': 'Package is not active'},
|
|
status=status.HTTP_400_BAD_REQUEST
|
|
)
|
|
|
|
purchase.used_sessions += 1
|
|
purchase.save()
|
|
|
|
serializer = PackagePurchaseSerializer(purchase)
|
|
return Response(serializer.data)
|
|
|
|
|
|
class PayerViewSet(viewsets.ModelViewSet):
|
|
"""
|
|
API endpoint for Payer CRUD operations.
|
|
|
|
list: Get list of payers
|
|
retrieve: Get payer details
|
|
create: Create new payer
|
|
update: Update payer
|
|
destroy: Delete payer
|
|
"""
|
|
|
|
permission_classes = [IsAuthenticated]
|
|
serializer_class = PayerSerializer
|
|
filter_backends = [DjangoFilterBackend, filters.SearchFilter]
|
|
filterset_fields = ['patient', 'type', 'is_active']
|
|
search_fields = ['name', 'policy_number']
|
|
|
|
def get_queryset(self):
|
|
"""Filter by tenant."""
|
|
return Payer.objects.filter(
|
|
tenant=self.request.user.tenant
|
|
).select_related('patient')
|
|
|
|
def perform_create(self, serializer):
|
|
"""Set tenant on create."""
|
|
serializer.save(tenant=self.request.user.tenant)
|