agdar/finance/api_views.py
2025-11-02 14:35:35 +03:00

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)