""" Billing app views for hospital management system. Provides medical billing, insurance claims, and revenue cycle management. """ from django.shortcuts import render, get_object_or_404, redirect from django.contrib.auth.decorators import login_required from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin from django.views.generic import ( TemplateView, ListView, DetailView, CreateView, UpdateView, DeleteView ) from django.http import JsonResponse, HttpResponse from django.contrib import messages from django.db.models import Q, Count, Sum, Avg, Max, Min from django.utils import timezone from django.urls import reverse_lazy, reverse from django.core.paginator import Paginator from django.views.decorators.http import require_http_methods from datetime import datetime, timedelta, date from decimal import Decimal import json import csv from .models import ( MedicalBill, BillLineItem, InsuranceClaim, Payment, ClaimStatusUpdate, BillingConfiguration ) from .forms import ( MedicalBillForm, BillLineItemForm, InsuranceClaimForm, PaymentForm, BillingSearchForm, ClaimSearchForm, PaymentSearchForm ) from patients.models import PatientProfile, InsuranceInfo from accounts.models import User from emr.models import Encounter from inpatients.models import Admission from core.utils import AuditLogger class BillingDashboardView(LoginRequiredMixin, TemplateView): """ Billing dashboard view with comprehensive statistics and recent activity. """ template_name = 'billing/dashboard.html' def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) tenant = getattr(self.request, 'tenant', None) if tenant: today = timezone.now().date() thirty_days_ago = today - timedelta(days=30) # Basic statistics bills = MedicalBill.objects.filter(tenant=tenant) context['total_bills'] = bills.count() context['total_revenue'] = bills.aggregate( total=Sum('total_amount') )['total'] or Decimal('0.00') context['total_paid'] = bills.aggregate( total=Sum('paid_amount') )['total'] or Decimal('0.00') context['total_outstanding'] = context['total_revenue'] - context['total_paid'] # Recent activity context['recent_bills'] = bills.select_related( 'patient', 'attending_provider' ).order_by('-bill_date')[:10] # Status breakdown context['status_breakdown'] = bills.values('status').annotate( count=Count('id') ).order_by('status') # Monthly trends context['monthly_revenue'] = bills.filter( bill_date__gte=thirty_days_ago ).aggregate( total=Sum('total_amount') )['total'] or Decimal('0.00') # Overdue bills context['overdue_bills'] = bills.filter( due_date__lt=today, status__in=['draft', 'sent', 'partial_payment'] ) # Claims statistics claims = InsuranceClaim.objects.filter(medical_bill__tenant=tenant) context['total_claims'] = claims.count() context['pending_claims'] = claims.filter(status='submitted') context['denied_claims'] = claims.filter(status='denied') # Payments statistics payments = Payment.objects.filter(medical_bill__tenant=tenant) context['total_payments'] = payments.count() context['recent_payments'] = payments.select_related( 'medical_bill__patient' ).order_by('-payment_date')[:5] return context class MedicalBillListView(LoginRequiredMixin, ListView): """ List view for medical bills with filtering and search. """ model = MedicalBill template_name = 'billing/bills/bill_list.html' context_object_name = 'bills' paginate_by = 25 def get_queryset(self): tenant = getattr(self.request, 'tenant', None) if not tenant: return MedicalBill.objects.none() queryset = MedicalBill.objects.filter(tenant=tenant) # Apply filters bill_type = self.request.GET.get('bill_type') if bill_type: queryset = queryset.filter(bill_type=bill_type) status = self.request.GET.get('status') if status: queryset = queryset.filter(status=status) date_from = self.request.GET.get('date_from') if date_from: queryset = queryset.filter(bill_date__gte=date_from) date_to = self.request.GET.get('date_to') if date_to: queryset = queryset.filter(bill_date__lte=date_to) amount_min = self.request.GET.get('amount_min') if amount_min: queryset = queryset.filter(total_amount__gte=amount_min) amount_max = self.request.GET.get('amount_max') if amount_max: queryset = queryset.filter(total_amount__lte=amount_max) overdue_only = self.request.GET.get('overdue_only') if overdue_only: today = timezone.now().date() queryset = queryset.filter( due_date__lt=today, status__in=['draft', 'sent', 'partial_payment'] ) search = self.request.GET.get('search') if search: queryset = queryset.filter( Q(bill_number__icontains=search) | Q(patient__first_name__icontains=search) | Q(patient__last_name__icontains=search) | Q(patient__patient_id__icontains=search) | Q(attending_provider__first_name__icontains=search) | Q(attending_provider__last_name__icontains=search) ) return queryset.select_related( 'patient', 'attending_provider' ).order_by('-bill_date') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['search_form'] = BillingSearchForm( self.request.GET, user=self.request.user ) return context class MedicalBillDetailView(LoginRequiredMixin, DetailView): """ Detail view for medical bills. """ model = MedicalBill template_name = 'billing/bills/bill_detail.html' context_object_name = 'bill' slug_field = 'bill_id' slug_url_kwarg = 'bill_id' def get_queryset(self): tenant = getattr(self.request, 'tenant', None) if not tenant: return MedicalBill.objects.none() return MedicalBill.objects.filter(tenant=tenant) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) bill = self.get_object() # Get related data context['line_items'] = bill.line_items.all().order_by('line_number') context['claims'] = bill.insurance_claims.all().order_by('-submission_date') context['payments'] = bill.payments.all().order_by('-payment_date') return context class MedicalBillCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): """ Create view for medical bills. """ model = MedicalBill form_class = MedicalBillForm template_name = 'billing/bills/bill_form.html' permission_required = 'billing.add_medicalbill' def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user return kwargs def form_valid(self, form): # Set tenant and created_by form.instance.tenant = getattr(self.request, 'tenant', None) form.instance.created_by = self.request.user # Generate bill number if not form.instance.bill_number: form.instance.bill_number = form.instance.generate_bill_number() response = super().form_valid(form) messages.success( self.request, f'Medical bill {self.object.bill_number} created successfully.' ) return response def get_success_url(self): return reverse('billing:bill_detail', kwargs={'bill_id': self.object.bill_id}) class MedicalBillUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): """ Update view for medical bills. """ model = MedicalBill form_class = MedicalBillForm template_name = 'billing/bills/bill_form.html' permission_required = 'billing.change_medicalbill' slug_field = 'bill_id' slug_url_kwarg = 'bill_id' def get_queryset(self): tenant = getattr(self.request, 'tenant', None) if not tenant: return MedicalBill.objects.none() return MedicalBill.objects.filter(tenant=tenant) def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user return kwargs def form_valid(self, form): response = super().form_valid(form) messages.success( self.request, f'Medical bill {self.object.bill_number} updated successfully.' ) return response def get_success_url(self): return reverse('billing:bill_detail', kwargs={'bill_id': self.object.bill_id}) class MedicalBillDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView): """ Delete view for medical bills. """ model = MedicalBill template_name = 'billing/bills/bill_confirm_delete.html' permission_required = 'billing.delete_medicalbill' success_url = reverse_lazy('billing:bill_list') slug_field = 'bill_id' slug_url_kwarg = 'bill_id' def get_queryset(self): tenant = getattr(self.request, 'tenant', None) if not tenant: return MedicalBill.objects.none() return MedicalBill.objects.filter(tenant=tenant) def delete(self, request, *args, **kwargs): bill = self.get_object() bill_number = bill.bill_number response = super().delete(request, *args, **kwargs) messages.success( request, f'Medical bill {bill_number} deleted successfully.' ) return response class InsuranceClaimListView(LoginRequiredMixin, ListView): """ List view for insurance claims with filtering and search. """ model = InsuranceClaim template_name = 'billing/claims/claim_list.html' context_object_name = 'claims' paginate_by = 25 def get_queryset(self): tenant = getattr(self.request, 'tenant', None) if not tenant: return InsuranceClaim.objects.none() queryset = InsuranceClaim.objects.filter(medical_bill__tenant=tenant) # Apply filters claim_type = self.request.GET.get('claim_type') if claim_type: queryset = queryset.filter(claim_type=claim_type) status = self.request.GET.get('status') if status: queryset = queryset.filter(status=status) submission_date_from = self.request.GET.get('submission_date_from') if submission_date_from: queryset = queryset.filter(submission_date__gte=submission_date_from) submission_date_to = self.request.GET.get('submission_date_to') if submission_date_to: queryset = queryset.filter(submission_date__lte=submission_date_to) pending_only = self.request.GET.get('pending_only') if pending_only: queryset = queryset.filter(status='submitted') search = self.request.GET.get('search') if search: queryset = queryset.filter( Q(claim_number__icontains=search) | Q(medical_bill__bill_number__icontains=search) | Q(medical_bill__patient__first_name__icontains=search) | Q(medical_bill__patient__last_name__icontains=search) | Q(insurance_info__insurance_company__icontains=search) ) return queryset.select_related( 'medical_bill__patient', 'insurance_info' ).order_by('-submission_date') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['search_form'] = ClaimSearchForm(self.request.GET) return context class InsuranceClaimDetailView(LoginRequiredMixin, DetailView): """ Detail view for insurance claims. """ model = InsuranceClaim template_name = 'billing/claims/claim_detail.html' context_object_name = 'claim' slug_field = 'claim_id' slug_url_kwarg = 'claim_id' def get_queryset(self): tenant = getattr(self.request, 'tenant', None) if not tenant: return InsuranceClaim.objects.none() return InsuranceClaim.objects.filter(medical_bill__tenant=tenant) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) claim = self.get_object() # Get related data # context['status_updates'] = claim.claimstatusupdate_set.all().order_by('-update_date') return context class InsuranceClaimCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): """ Create view for insurance claims. """ model = InsuranceClaim form_class = InsuranceClaimForm template_name = 'billing/claims/claim_form.html' permission_required = 'billing.add_insuranceclaim' def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user # Get medical bill from URL parameter bill_id = self.kwargs.get('bill_id') if bill_id: try: medical_bill = MedicalBill.objects.get( bill_id=bill_id, tenant=getattr(self.request, 'tenant', None) ) kwargs['medical_bill'] = medical_bill except MedicalBill.DoesNotExist: pass return kwargs def form_valid(self, form): # Prefer URL bill_id; otherwise read from POST("medical_bill") bill_id = self.kwargs.get('bill_id') or self.request.POST.get('medical_bill') if bill_id: try: medical_bill = MedicalBill.objects.get( bill_id=bill_id, tenant=getattr(self.request, 'tenant', None) ) form.instance.medical_bill = medical_bill except MedicalBill.DoesNotExist: messages.error(self.request, 'Medical bill not found.') return redirect('billing:bill_list') else: messages.error(self.request, 'Please select a medical bill.') return redirect('billing:claim_create') form.instance.created_by = self.request.user if not form.instance.claim_number: form.instance.claim_number = form.instance.generate_claim_number() response = super().form_valid(form) messages.success(self.request, f'Insurance claim {self.object.claim_number} created successfully.') return response def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) tenant = getattr(self.request, 'tenant', None) ctx['available_bills'] = MedicalBill.objects.filter(tenant=tenant).select_related('patient') return ctx def get_success_url(self): return reverse('billing:claim_detail', kwargs={'claim_id': self.object.claim_id}) class InsuranceClaimUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): model = InsuranceClaim form_class = InsuranceClaimForm template_name = 'billing/claims/claim_form.html' permission_required = 'billing.add_insuranceclaim' def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user # Get medical bill from URL parameter bill_id = self.kwargs.get('bill_id') if bill_id: try: medical_bill = MedicalBill.objects.get( bill_id=bill_id, tenant=getattr(self.request, 'tenant', None) ) kwargs['medical_bill'] = medical_bill except MedicalBill.DoesNotExist: pass return kwargs def form_valid(self, form): # Prefer URL bill_id; otherwise read from POST("medical_bill") bill_id = self.kwargs.get('bill_id') or self.request.POST.get('medical_bill') if bill_id: try: medical_bill = MedicalBill.objects.get( bill_id=bill_id, tenant=getattr(self.request, 'tenant', None) ) form.instance.medical_bill = medical_bill except MedicalBill.DoesNotExist: messages.error(self.request, 'Medical bill not found.') return redirect('billing:bill_list') else: messages.error(self.request, 'Please select a medical bill.') return redirect('billing:claim_create') form.instance.created_by = self.request.user if not form.instance.claim_number: form.instance.claim_number = form.instance.generate_claim_number() response = super().form_valid(form) messages.success(self.request, f'Insurance claim {self.object.claim_number} updated successfully.') return response def get_context_data(self, **kwargs): ctx = super().get_context_data(**kwargs) tenant = getattr(self.request, 'tenant', None) ctx['available_bills'] = MedicalBill.objects.filter(tenant=tenant).select_related('patient') return ctx def get_success_url(self): return reverse('billing:claim_detail', kwargs={'claim_id': self.object.claim_id}) class PaymentListView(LoginRequiredMixin, ListView): """ List view for payments with filtering and search. """ model = Payment template_name = 'billing/payments/payment_list.html' context_object_name = 'payments' paginate_by = 25 def get_queryset(self): tenant = getattr(self.request, 'tenant', None) if not tenant: return Payment.objects.none() queryset = Payment.objects.filter(medical_bill__tenant=tenant) # Apply filters payment_method = self.request.GET.get('payment_method') if payment_method: queryset = queryset.filter(payment_method=payment_method) payment_source = self.request.GET.get('payment_source') if payment_source: queryset = queryset.filter(payment_source=payment_source) status = self.request.GET.get('status') if status: queryset = queryset.filter(status=status) date_from = self.request.GET.get('date_from') if date_from: queryset = queryset.filter(payment_date__gte=date_from) date_to = self.request.GET.get('date_to') if date_to: queryset = queryset.filter(payment_date__lte=date_to) search = self.request.GET.get('search') if search: queryset = queryset.filter( Q(payment_number__icontains=search) | Q(medical_bill__bill_number__icontains=search) | Q(medical_bill__patient__first_name__icontains=search) | Q(medical_bill__patient__last_name__icontains=search) | Q(check_number__icontains=search) | Q(authorization_code__icontains=search) ) return queryset.select_related( 'medical_bill__patient', 'received_by' ).order_by('-payment_date') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context['search_form'] = PaymentSearchForm(self.request.GET) return context class PaymentDetailView(LoginRequiredMixin, DetailView): """ Detail view for payments. """ model = Payment template_name = 'billing/payments/payment_detail.html' context_object_name = 'payment' slug_field = 'payment_id' slug_url_kwarg = 'payment_id' def get_queryset(self): tenant = getattr(self.request, 'tenant', None) if not tenant: return Payment.objects.none() return Payment.objects.filter(medical_bill__tenant=tenant) class PaymentCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): """ Create view for payments. """ model = Payment form_class = PaymentForm template_name = 'billing/payments/payment_form.html' permission_required = 'billing.add_payment' def get_form_kwargs(self): kwargs = super().get_form_kwargs() kwargs['user'] = self.request.user # Get medical bill from URL parameter bill_id = self.kwargs.get('bill_id') if bill_id: try: medical_bill = MedicalBill.objects.get( bill_id=bill_id, tenant=getattr(self.request, 'tenant', None) ) kwargs['medical_bill'] = medical_bill except MedicalBill.DoesNotExist: pass return kwargs def form_valid(self, form): # Set medical bill bill_id = self.kwargs.get('bill_id') if bill_id: try: medical_bill = MedicalBill.objects.get( bill_id=bill_id, tenant=getattr(self.request, 'tenant', None) ) form.instance.medical_bill = medical_bill except MedicalBill.DoesNotExist: messages.error(self.request, 'Medical bill not found.') return redirect('billing:bill_list') # Generate payment number if not form.instance.payment_number: form.instance.payment_number = form.instance.generate_payment_number() response = super().form_valid(form) messages.success( self.request, f'Payment {self.object.payment_number} recorded successfully.' ) return response def get_success_url(self): return reverse('billing:payment_detail', kwargs={'payment_id': self.object.payment_id}) @login_required def htmx_billing_stats(request): """ HTMX endpoint for billing statistics. """ tenant = getattr(request, 'tenant', None) if not tenant: return JsonResponse({'error': 'No tenant found'}) today = timezone.now().date() # Calculate statistics bills = MedicalBill.objects.filter(tenant=tenant) stats = { 'total_bills': bills.count(), 'total_revenue': float(bills.aggregate( total=Sum('total_amount') )['total'] or 0), 'total_paid': float(bills.aggregate( total=Sum('paid_amount') )['total'] or 0), 'overdue_bills': bills.filter( due_date__lt=today, status__in=['draft', 'sent', 'partial_payment'] ).count() } stats['total_outstanding'] = stats['total_revenue'] - stats['total_paid'] return JsonResponse(stats) @login_required def billing_stats(request): """ HTMX view for billing statistics. """ tenant = request.user.tenant today = timezone.now().date() stats = { 'total_revenue_today': MedicalBill.objects.filter( tenant=tenant, bill_date=today, status='PAID' ).aggregate(total=Sum('total_amount'))['total'] or Decimal('0.00'), 'outstanding_bills': MedicalBill.objects.filter( tenant=tenant, status__in=['PENDING', 'SUBMITTED', 'PARTIAL_PAID'] ).count(), 'outstanding_amount': MedicalBill.objects.filter( tenant=tenant, status__in=['PENDING', 'SUBMITTED', 'PARTIAL_PAID'] ).aggregate(total=Sum('balance_amount'))['total'] or Decimal('0.00'), 'overdue_bills': MedicalBill.objects.filter( tenant=tenant, status='OVERDUE' ).count(), 'overdue_amount': MedicalBill.objects.filter( tenant=tenant, status='OVERDUE' ).aggregate(total=Sum('balance_amount'))['total'] or Decimal('0.00'), 'pending_claims': InsuranceClaim.objects.filter( medical_bill__tenant=tenant, status__in=['SUBMITTED', 'PENDING', 'PROCESSING'] ).count(), 'denied_claims': InsuranceClaim.objects.filter( medical_bill__tenant=tenant, status='DENIED' ).count(), 'payments_today': Payment.objects.filter( medical_bill__tenant=tenant, payment_date=today ).aggregate(total=Sum('payment_amount'))['total'] or Decimal('0.00'), 'collections_amount': MedicalBill.objects.filter( tenant=tenant, collection_status__in=['FIRST_NOTICE', 'SECOND_NOTICE', 'FINAL_NOTICE', 'COLLECTIONS'] ).aggregate(total=Sum('balance_amount'))['total'] or Decimal('0.00'), } return render(request, 'billing/partials/billing_stats.html', {'stats': stats}) @login_required def bill_details_api(request, bill_id): tenant = getattr(request, 'tenant', None) if not tenant: return JsonResponse({'error': 'No tenant found'}, status=400) bill = get_object_or_404( MedicalBill.objects.select_related('patient', 'billing_provider'), bill_id=bill_id, tenant=tenant, ) data = { 'patient_name': bill.patient.get_full_name() if bill.patient else '', 'bill_number': bill.bill_number or '', 'bill_date': bill.bill_date.isoformat() if bill.bill_date else '', 'total_amount': str(bill.total_amount or 0), 'service_date_from': bill.service_date_from.isoformat() if bill.service_date_from else '', 'service_date_to': bill.service_date_to.isoformat() if bill.service_date_to else '', 'billing_provider': bill.billing_provider.get_full_name() if bill.billing_provider else '', } return JsonResponse(data) @login_required def bill_search(request): """ HTMX endpoint for bill search. """ search_term = request.GET.get('search', '').strip() tenant = getattr(request, 'tenant', None) if not tenant or not search_term: return render(request, 'billing/partials/bill_list.html', {'bills': []}) bills = MedicalBill.objects.filter( tenant=tenant ).filter( Q(bill_number__icontains=search_term) | Q(patient__first_name__icontains=search_term) | Q(patient__last_name__icontains=search_term) | Q(patient__patient_id__icontains=search_term) ).select_related('patient', 'attending_provider')[:10] return render(request, 'billing/partials/bill_list.html', {'bills': bills}) @login_required @require_http_methods(["POST"]) def submit_bill(request, bill_id): """ Submit a bill for processing. """ tenant = getattr(request, 'tenant', None) if not tenant: return JsonResponse({'success': False, 'error': 'No tenant found'}) try: bill = MedicalBill.objects.get(bill_id=bill_id, tenant=tenant) if bill.status == 'draft': bill.status = 'sent' bill.save() messages.success(request, f'Bill {bill.bill_number} submitted successfully.') return JsonResponse({'success': True}) else: return JsonResponse({'success': False, 'error': 'Bill is not in draft status'}) except MedicalBill.DoesNotExist: return JsonResponse({'success': False, 'error': 'Bill not found'}) @login_required def export_bills(request): """ Export medical bills to CSV. """ tenant = request.user.tenant # Create HTTP response with CSV content type response = HttpResponse(content_type='text/csv') response['Content-Disposition'] = 'attachment; filename="medical_bills.csv"' writer = csv.writer(response) # Write header row writer.writerow([ 'Bill Number', 'Patient Name', 'MRN', 'Bill Type', 'Bill Date', 'Due Date', 'Service Date From', 'Service Date To', 'Subtotal', 'Tax Amount', 'Total Amount', 'Paid Amount', 'Balance Amount', 'Status', 'Attending Provider', 'Created Date' ]) # Write data rows bills = MedicalBill.objects.filter( tenant=tenant ).select_related( 'patient', 'attending_provider' ).order_by('-bill_date') for bill in bills: writer.writerow([ bill.bill_number, bill.patient.get_full_name(), bill.patient.mrn, bill.get_bill_type_display(), bill.bill_date.strftime('%Y-%m-%d'), bill.due_date.strftime('%Y-%m-%d'), bill.service_date_from.strftime('%Y-%m-%d'), bill.service_date_to.strftime('%Y-%m-%d'), str(bill.subtotal), str(bill.tax_amount), str(bill.total_amount), str(bill.paid_amount), str(bill.balance_amount), bill.get_status_display(), bill.attending_provider.get_full_name() if bill.attending_provider else '', bill.created_at.strftime('%Y-%m-%d %H:%M:%S') ]) # Log audit event AuditLogger.log_event( request.user, 'BILLS_EXPORTED', 'MedicalBill', None, f"Exported {bills.count()} medical bills to CSV" ) return response @login_required def print_bills(request): """ Print bills view with print-friendly formatting. Supports both individual bill printing and bulk printing. """ tenant = getattr(request, 'tenant', None) if not tenant: return HttpResponse('No tenant found', status=400) # Get bill IDs from request bill_ids = request.GET.getlist('bill_ids') single_bill_id = request.GET.get('bill_id') if single_bill_id: bill_ids = [single_bill_id] if not bill_ids: messages.error(request, 'No bills selected for printing.') return redirect('billing:bill_list') # Get bills with related data bills = MedicalBill.objects.filter( tenant=tenant, bill_id__in=bill_ids ).select_related( 'patient', 'encounter', 'admission', 'created_by' ).prefetch_related( 'billlineitem_set', 'payment_set', 'insuranceclaim_set' ) if not bills.exists(): messages.error(request, 'No bills found for printing.') return redirect('billing:bill_list') # Calculate totals for each bill bills_data = [] for bill in bills: line_items = bill.billlineitem_set.all() payments = bill.payment_set.filter(status='completed') claims = bill.insuranceclaim_set.all() bills_data.append({ 'bill': bill, 'line_items': line_items, 'payments': payments, 'claims': claims, 'subtotal': sum(item.total_amount for item in line_items), 'total_payments': sum(payment.amount for payment in payments), 'total_claimed': sum(claim.claim_amount for claim in claims), }) context = { 'bills_data': bills_data, 'print_date': timezone.now(), 'tenant': tenant, 'is_bulk_print': len(bills_data) > 1, } return render(request, 'billing/print_bills.html', context) @login_required def print_bill_detail(request, bill_id): """ Print detailed view for a single bill. """ tenant = getattr(request, 'tenant', None) if not tenant: return HttpResponse('No tenant found', status=400) bill = get_object_or_404( MedicalBill.objects.select_related( 'patient', 'encounter', 'admission', 'created_by' ).prefetch_related( 'billlineitem_set', 'payment_set', 'insuranceclaim_set' ), tenant=tenant, bill_id=bill_id ) # Get related data line_items = bill.billlineitem_set.all().order_by('line_number') payments = bill.payment_set.filter(status='completed').order_by('-payment_date') claims = bill.insuranceclaim_set.all().order_by('-submission_date') # Calculate totals subtotal = sum(item.total_amount for item in line_items) total_payments = sum(payment.amount for payment in payments) total_claimed = sum(claim.claim_amount for claim in claims) context = { 'bill': bill, 'line_items': line_items, 'payments': payments, 'claims': claims, 'subtotal': subtotal, 'total_payments': total_payments, 'total_claimed': total_claimed, 'print_date': timezone.now(), 'tenant': tenant, } return render(request, 'billing/print_bill_detail.html', context) @login_required @require_http_methods(["POST"]) def bulk_submit_bills(request): """ Submit multiple bills for processing in bulk. Validates each bill and provides detailed feedback on success/failure. """ tenant = getattr(request, 'tenant', None) if not tenant: return JsonResponse({'success': False, 'error': 'No tenant found'}) # Get bill IDs from request bill_ids = request.POST.getlist('bill_ids') if not bill_ids: return JsonResponse({ 'success': False, 'error': 'No bills selected for submission' }) # Track results submitted_bills = [] failed_bills = [] already_submitted = [] not_found = [] # Process each bill for bill_id in bill_ids: try: bill = MedicalBill.objects.get(bill_id=bill_id, tenant=tenant) if bill.status == 'draft': # Validate bill before submission validation_errors = [] # Check if bill has line items if not bill.billlineitem_set.exists(): validation_errors.append('No line items') # Check if bill has valid amounts if bill.total_amount <= 0: validation_errors.append('Invalid total amount') # Check if patient information is complete if not bill.patient: validation_errors.append('No patient assigned') # Check if bill has required fields if not bill.bill_date: validation_errors.append('Missing bill date') if validation_errors: failed_bills.append({ 'bill_id': str(bill.bill_id), 'bill_number': bill.bill_number, 'errors': validation_errors }) else: # Submit the bill bill.status = 'submitted' # Note: submission_date and submitted_by fields don't exist in model # Using available fields instead bill.save() submitted_bills.append({ 'bill_id': str(bill.bill_id), 'bill_number': bill.bill_number, 'patient_name': bill.patient.get_full_name() if bill.patient else 'Unknown', 'total_amount': str(bill.total_amount) }) elif bill.status in ['submitted', 'paid', 'cancelled']: already_submitted.append({ 'bill_id': str(bill.bill_id), 'bill_number': bill.bill_number, 'status': bill.get_status_display() }) else: failed_bills.append({ 'bill_id': str(bill.bill_id), 'bill_number': bill.bill_number, 'errors': [f'Invalid status: {bill.get_status_display()}'] }) except MedicalBill.DoesNotExist: not_found.append({ 'bill_id': bill_id, 'error': 'Bill not found' }) except Exception as e: failed_bills.append({ 'bill_id': bill_id, 'bill_number': 'Unknown', 'errors': [f'Unexpected error: {str(e)}'] }) # Prepare response total_processed = len(bill_ids) total_submitted = len(submitted_bills) total_failed = len(failed_bills) + len(not_found) total_already_submitted = len(already_submitted) # Create success message messages_list = [] if total_submitted > 0: messages_list.append(f'{total_submitted} bill(s) submitted successfully') if total_already_submitted > 0: messages_list.append(f'{total_already_submitted} bill(s) were already submitted') if total_failed > 0: messages_list.append(f'{total_failed} bill(s) failed to submit') # Add Django messages if total_submitted > 0: messages.success(request, f'Successfully submitted {total_submitted} bill(s)') if total_failed > 0: messages.warning(request, f'{total_failed} bill(s) could not be submitted') if total_already_submitted > 0: messages.info(request, f'{total_already_submitted} bill(s) were already submitted') # Return detailed response response_data = { 'success': total_submitted > 0, 'summary': { 'total_processed': total_processed, 'total_submitted': total_submitted, 'total_failed': total_failed, 'total_already_submitted': total_already_submitted }, 'results': { 'submitted': submitted_bills, 'failed': failed_bills, 'already_submitted': already_submitted, 'not_found': not_found }, 'message': ' | '.join(messages_list) if messages_list else 'No bills processed' } return JsonResponse(response_data) @login_required def bulk_submit_bills_form(request): """ Display form for bulk bill submission with bill selection. """ tenant = getattr(request, 'tenant', None) if not tenant: messages.error(request, 'No tenant found') return redirect('billing:bill_list') # Get draft bills that can be submitted draft_bills = MedicalBill.objects.filter( tenant=tenant, status='draft' ).select_related('patient').order_by('-bill_date') # Add validation status for each bill bills_with_status = [] for bill in draft_bills: validation_errors = [] # Check validation criteria if not bill.billlineitem_set.exists(): validation_errors.append('No line items') if bill.total_amount <= 0: validation_errors.append('Invalid amount') if not bill.patient: validation_errors.append('No patient') if not bill.bill_date: validation_errors.append('No bill date') bills_with_status.append({ 'bill': bill, 'can_submit': len(validation_errors) == 0, 'validation_errors': validation_errors }) context = { 'bills_with_status': bills_with_status, 'total_draft_bills': len(bills_with_status), 'submittable_bills': len([b for b in bills_with_status if b['can_submit']]) } return render(request, 'billing/bulk_submit_bills.html', context) @login_required def payment_receipt(request, payment_id): """ Generate and display payment receipt. """ tenant = getattr(request, 'tenant', None) if not tenant: return HttpResponse('No tenant found', status=400) try: # Get payment with related objects payment = Payment.objects.select_related( 'medical_bill', 'medical_bill__patient', 'processed_by' ).get(payment_id=payment_id, medical_bill__tenant=tenant) # Calculate payment details payment_details = { 'payment': payment, 'bill': payment.medical_bill, 'patient': payment.medical_bill.patient, 'payment_date': payment.payment_date, 'amount_paid': payment.payment_amount, 'payment_method': payment.get_payment_method_display(), 'reference_number': payment.transaction_id, 'notes': payment.notes, 'processed_by': payment.processed_by, 'receipt_number': f"RCP-{payment.id:06d}", 'balance_after_payment': payment.medical_bill.balance_amount, } # Add hospital/tenant information hospital_info = { 'name': tenant.name, 'address': getattr(tenant, 'address', ''), 'phone': getattr(tenant, 'phone', ''), 'email': getattr(tenant, 'email', ''), 'website': getattr(tenant, 'website', ''), } context = { 'payment_details': payment_details, 'hospital_info': hospital_info, 'print_date': timezone.now(), } return render(request, 'billing/payment_receipt.html', context) except Payment.DoesNotExist: messages.error(request, 'Payment not found.') return redirect('billing:bill_list') except Exception as e: messages.error(request, f'Error generating receipt: {str(e)}') return redirect('billing:bill_list') @login_required def payment_email(request, payment_id): """ Send payment confirmation email to patient. """ tenant = getattr(request, 'tenant', None) if not tenant: return HttpResponse('No tenant found', status=400) try: # Get payment with related objects payment = Payment.objects.select_related( 'bill', 'bill__patient', 'created_by' ).get(id=payment_id, tenant=tenant) if request.method == 'POST': # Send email from django.core.mail import send_mail from django.template.loader import render_to_string from django.utils.html import strip_tags # Prepare email context email_context = { 'payment': payment, 'bill': payment.bill, 'patient': payment.bill.patient, 'hospital_info': { 'name': tenant.name, 'address': getattr(tenant, 'address', ''), 'phone': getattr(tenant, 'phone', ''), 'email': getattr(tenant, 'email', ''), 'website': getattr(tenant, 'website', ''), }, 'receipt_number': f"RCP-{payment.id:06d}", 'receipt_url': request.build_absolute_uri( reverse('billing:payment_receipt', args=[payment.id]) ), } # Render email template html_message = render_to_string('billing/payment_email.html', email_context) plain_message = strip_tags(html_message) # Email details subject = f'Payment Confirmation - Receipt #{email_context["receipt_number"]}' from_email = getattr(tenant, 'email', 'noreply@hospital.com') recipient_list = [payment.bill.patient.email] if payment.bill.patient.email else [] # Add additional recipients if specified additional_emails = request.POST.get('additional_emails', '').strip() if additional_emails: additional_list = [email.strip() for email in additional_emails.split(',') if email.strip()] recipient_list.extend(additional_list) if not recipient_list: messages.error(request, 'No email addresses found. Please provide at least one email address.') return redirect('billing:payment_email', payment_id=payment_id) try: # Send email send_mail( subject=subject, message=plain_message, from_email=from_email, recipient_list=recipient_list, html_message=html_message, fail_silently=False, ) # Log email sent messages.success(request, f'Payment confirmation email sent successfully to {", ".join(recipient_list)}.') # Redirect to payment detail return redirect('billing:payment_detail', payment_id=payment.id) except Exception as e: messages.error(request, f'Failed to send email: {str(e)}') return redirect('billing:payment_email', payment_id=payment_id) # GET request - show email form context = { 'payment': payment, 'bill': payment.bill, 'patient': payment.bill.patient, 'receipt_number': f"RCP-{payment.id:06d}", } return render(request, 'billing/payment_email_form.html', context) except Payment.DoesNotExist: messages.error(request, 'Payment not found.') return redirect('billing:payment_list') except Exception as e: messages.error(request, f'Error processing email: {str(e)}') return redirect('billing:payment_list') @login_required def payment_download(request, payment_id): """ Download payment receipt as PDF. """ tenant = getattr(request, 'tenant', None) if not tenant: return HttpResponse('No tenant found', status=400) try: # Get payment with related objects payment = Payment.objects.select_related( 'bill', 'bill__patient', 'created_by' ).get(id=payment_id, tenant=tenant) # Import PDF generation libraries from django.template.loader import render_to_string from weasyprint import HTML, CSS from django.conf import settings import os # Calculate payment details payment_details = { 'payment': payment, 'bill': payment.bill, 'patient': payment.bill.patient, 'payment_date': payment.payment_date, 'amount_paid': payment.amount, 'payment_method': payment.get_payment_method_display(), 'reference_number': payment.reference_number, 'notes': payment.notes, 'processed_by': payment.created_by, 'receipt_number': f"RCP-{payment.id:06d}", 'balance_after_payment': payment.bill.balance_amount, } # Add hospital/tenant information hospital_info = { 'name': tenant.name, 'address': getattr(tenant, 'address', ''), 'phone': getattr(tenant, 'phone', ''), 'email': getattr(tenant, 'email', ''), 'website': getattr(tenant, 'website', ''), } context = { 'payment_details': payment_details, 'hospital_info': hospital_info, 'print_date': timezone.now(), 'is_pdf': True, # Flag to indicate PDF generation } # Render HTML template for PDF html_string = render_to_string('billing/payment_receipt_pdf.html', context) # Generate PDF html = HTML(string=html_string) # Create CSS for PDF styling css_string = """ @page { size: A4; margin: 1cm; } body { font-family: Arial, sans-serif; font-size: 12px; line-height: 1.4; color: #000; } .receipt-header { text-align: center; border-bottom: 2px solid #000; padding-bottom: 20px; margin-bottom: 30px; } .hospital-name { font-size: 24px; font-weight: bold; margin-bottom: 10px; } .receipt-title { font-size: 18px; font-weight: bold; margin-top: 20px; } .section-title { font-weight: bold; border-bottom: 1px solid #000; padding-bottom: 5px; margin-bottom: 10px; margin-top: 20px; } .detail-table { width: 100%; border-collapse: collapse; margin-bottom: 20px; } .detail-table td { padding: 5px; border-bottom: 1px solid #ccc; } .detail-label { font-weight: bold; width: 40%; } .amount-paid { text-align: center; border: 2px solid #000; padding: 15px; margin: 20px 0; } .amount-value { font-size: 24px; font-weight: bold; } .footer { margin-top: 40px; border-top: 1px solid #000; padding-top: 20px; font-size: 10px; } .signature-line { border-bottom: 1px solid #000; width: 200px; margin: 20px auto 10px auto; } """ css = CSS(string=css_string) pdf = html.write_pdf(stylesheets=[css]) # Create response response = HttpResponse(pdf, content_type='application/pdf') response[ 'Content-Disposition'] = f'attachment; filename="payment_receipt_{payment_details["receipt_number"]}.pdf"' return response except Payment.DoesNotExist: messages.error(request, 'Payment not found.') return redirect('billing:payment_list') except ImportError: # Fallback if weasyprint is not available messages.error(request, 'PDF generation is not available. Please use the print function instead.') return redirect('billing:payment_receipt', payment_id=payment_id) except Exception as e: messages.error(request, f'Error generating PDF: {str(e)}') return redirect('billing:payment_receipt', payment_id=payment_id) @login_required def export_claims(request): """ Export insurance claims to CSV. Supports optional filtering by 'claims' GET param: ?claims=ID1,ID2,ID3 """ tenant = getattr(request, 'tenant', None) if not tenant: return HttpResponse('No tenant found', status=400) # Base queryset qs = InsuranceClaim.objects.filter(tenant=tenant).select_related( 'medical_bill__patient', 'insurance_info', ) # Optional selection filter (comma-separated claim_ids) selected = request.GET.get('claims') if selected: claim_ids = [c.strip() for c in selected.split(',') if c.strip()] if claim_ids: qs = qs.filter(claim_id__in=claim_ids) # Prepare CSV response response = HttpResponse(content_type='text/csv') response['Content-Disposition'] = 'attachment; filename="insurance_claims.csv"' writer = csv.writer(response) writer.writerow([ 'Claim Number', 'Bill Number', 'Patient Name', 'Insurance Company', 'Claim Type', 'Service From', 'Service To', 'Billed Amount', 'Status', ]) for claim in qs: bill = getattr(claim, 'medical_bill', None) patient = getattr(bill, 'patient', None) # Safely get nice display values insurance_company = getattr(getattr(claim, 'insurance_info', None), 'company', None) if not insurance_company: # Fallback to __str__ of insurance_info or empty insurance_company = str(getattr(claim, 'insurance_info', '')) or '' claim_type = getattr(claim, 'get_claim_type_display', None) if callable(claim_type): claim_type = claim.get_claim_type_display() else: claim_type = getattr(claim, 'claim_type', '') or '' status_val = '' get_status_display = getattr(claim, 'get_status_display', None) if callable(get_status_display): status_val = claim.get_status_display() else: # Fallback if no choices helper exists status_val = getattr(claim, 'status', '') or '' writer.writerow([ getattr(claim, 'claim_number', '') or '', getattr(bill, 'bill_number', '') if bill else '', patient.get_full_name() if patient else '', insurance_company, claim_type, claim.service_date_from.strftime('%Y-%m-%d') if getattr(claim, 'service_date_from', None) else '', claim.service_date_to.strftime('%Y-%m-%d') if getattr(claim, 'service_date_to', None) else '', str(getattr(claim, 'billed_amount', '')) or '0', status_val, ]) return response @login_required def bill_line_items_api(request, bill_id=None): """ Return line items for a medical bill as JSON. Supports: - /api/bills//line-items/ - /api/bills/line-items/?bill_id= """ tenant = getattr(request, 'tenant', None) if not tenant: return JsonResponse({'success': False, 'error': 'No tenant found'}, status=400) bill_id = bill_id or request.GET.get('bill_id') if not bill_id: return JsonResponse({'success': False, 'error': 'bill_id is required'}, status=400) bill = get_object_or_404( MedicalBill.objects.select_related('patient').prefetch_related('billlineitem_set'), bill_id=bill_id, tenant=tenant, ) # Prefer per-item service date if your model has it; otherwise fall back bill_service_date = ( bill.service_date_from.isoformat() if getattr(bill, 'service_date_from', None) else bill.bill_date.isoformat() if getattr(bill, 'bill_date', None) else '' ) items = [] for li in bill.billlineitem_set.all(): qty = getattr(li, 'quantity', 0) or 0 price = getattr(li, 'unit_price', Decimal('0')) or Decimal('0') # If your BillLineItem has service_date, use it; otherwise default li_service_date = getattr(li, 'service_date', None) if li_service_date: li_service_date = li_service_date.isoformat() else: li_service_date = bill_service_date items.append({ 'id': getattr(li, 'id', None), 'service_code': getattr(li, 'service_code', '') or '', 'description': getattr(li, 'description', '') or '', 'quantity': qty, 'unit_price': str(price), 'service_date': li_service_date, 'total': str(price * Decimal(qty)), }) return JsonResponse({ 'success': True, 'bill_id': str(bill.bill_id), 'patient_name': bill.patient.get_full_name() if bill.patient else '', 'line_items': items, }) @login_required def claim_appeal(request, claim_id): tenant = getattr(request, 'tenant', None) if not tenant: return HttpResponse('No tenant found', status=400) claim = get_object_or_404( InsuranceClaim, medical_bill__tenant=tenant, claim_id=claim_id ) if claim.status in ['DENIED', 'REJECTED']: claim.status = 'APPEALED' claim.save() messages.success(request, 'Claim has already been appealed.') return redirect('billing:claim_detail', claim_id=claim.claim_id) return JsonResponse({'success': False, 'error': 'check claim status'}, status=400) # # """ # Billing app views for hospital management system. # Provides medical billing, insurance claims, and revenue cycle management. # """ # # from django.shortcuts import render, get_object_or_404, redirect # from django.contrib.auth.decorators import login_required # from django.contrib import messages # from django.http import JsonResponse, HttpResponse # from django.db.models import Q, Count, Sum, Avg, Max, Min # from django.utils import timezone # from django.core.paginator import Paginator # from django.views.decorators.http import require_http_methods # from django.views.generic import ListView, DetailView, CreateView, UpdateView # from django.contrib.auth.mixins import LoginRequiredMixin # from django.urls import reverse # from datetime import datetime, timedelta, date # from decimal import Decimal # import json # import csv # # from .models import ( # MedicalBill, BillLineItem, InsuranceClaim, Payment, # ClaimStatusUpdate, BillingConfiguration # ) # from patients.models import PatientProfile, InsuranceInfo # from emr.models import Encounter # from inpatients.models import Admission # from core.utils import AuditLogger # # # class BillingDashboardView(LoginRequiredMixin, ListView): # """ # Billing dashboard view with comprehensive statistics and recent activity. # """ # template_name = 'billing/dashboard.html' # context_object_name = 'recent_bills' # # def get_queryset(self): # return MedicalBill.objects.filter( # tenant=self.request.user.tenant # ).select_related( # 'patient', 'encounter', 'admission' # ).order_by('-bill_date')[:10] # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # tenant = self.request.user.tenant # today = timezone.now().date() # # # Financial statistics # context['stats'] = { # 'total_revenue_today': MedicalBill.objects.filter( # tenant=tenant, # bill_date=today, # status='PAID' # ).aggregate(total=Sum('total_amount'))['total'] or Decimal('0.00'), # # 'outstanding_bills': MedicalBill.objects.filter( # tenant=tenant, # status__in=['PENDING', 'SUBMITTED', 'PARTIAL_PAID'] # ).count(), # # 'outstanding_amount': MedicalBill.objects.filter( # tenant=tenant, # status__in=['PENDING', 'SUBMITTED', 'PARTIAL_PAID'] # ).aggregate(total=Sum('balance_amount'))['total'] or Decimal('0.00'), # # 'overdue_bills': MedicalBill.objects.filter( # tenant=tenant, # status='OVERDUE' # ).count(), # # 'overdue_amount': MedicalBill.objects.filter( # tenant=tenant, # status='OVERDUE' # ).aggregate(total=Sum('balance_amount'))['total'] or Decimal('0.00'), # # 'pending_claims': InsuranceClaim.objects.filter( # medical_bill__tenant=tenant, # status__in=['SUBMITTED', 'PENDING', 'PROCESSING'] # ).count(), # # 'denied_claims': InsuranceClaim.objects.filter( # medical_bill__tenant=tenant, # status='DENIED' # ).count(), # # 'payments_today': Payment.objects.filter( # medical_bill__tenant=tenant, # payment_date=today # ).aggregate(total=Sum('payment_amount'))['total'] or Decimal('0.00'), # # 'collections_amount': MedicalBill.objects.filter( # tenant=tenant, # collection_status__in=['FIRST_NOTICE', 'SECOND_NOTICE', 'FINAL_NOTICE', 'COLLECTIONS'] # ).aggregate(total=Sum('balance_amount'))['total'] or Decimal('0.00'), # } # # # Recent payments # context['recent_payments'] = Payment.objects.filter( # medical_bill__tenant=tenant # ).select_related( # 'medical_bill', 'medical_bill__patient' # ).order_by('-payment_date')[:10] # # # Overdue bills # context['overdue_bills'] = MedicalBill.objects.filter( # tenant=tenant, # status='OVERDUE' # ).select_related( # 'patient' # ).order_by('-due_date')[:5] # # # Pending claims # context['pending_claims'] = InsuranceClaim.objects.filter( # medical_bill__tenant=tenant, # status__in=['SUBMITTED', 'PENDING', 'PROCESSING'] # ).select_related( # 'medical_bill__patient', 'insurance_info' # ).order_by('-submission_date')[:5] # # return context # # # class MedicalBillListView(LoginRequiredMixin, ListView): # """ # Medical bill list view with advanced filtering and search. # """ # model = MedicalBill # template_name = 'billing/bill_list.html' # context_object_name = 'bills' # paginate_by = 25 # # def get_queryset(self): # queryset = MedicalBill.objects.filter( # tenant=self.request.user.tenant # ).select_related( # 'patient', 'encounter', 'admission', 'attending_provider' # ) # # # Apply filters # status = self.request.GET.get('status') # bill_type = self.request.GET.get('bill_type') # patient_id = self.request.GET.get('patient_id') # provider_id = self.request.GET.get('provider_id') # date_from = self.request.GET.get('date_from') # date_to = self.request.GET.get('date_to') # search = self.request.GET.get('search') # # if status: # queryset = queryset.filter(status=status) # if bill_type: # queryset = queryset.filter(bill_type=bill_type) # if patient_id: # queryset = queryset.filter(patient_id=patient_id) # if provider_id: # queryset = queryset.filter(attending_provider_id=provider_id) # if date_from: # queryset = queryset.filter(bill_date__gte=date_from) # if date_to: # queryset = queryset.filter(bill_date__lte=date_to) # if search: # queryset = queryset.filter( # Q(bill_number__icontains=search) | # Q(patient__first_name__icontains=search) | # Q(patient__last_name__icontains=search) | # Q(patient__mrn__icontains=search) # ) # # return queryset.order_by('-bill_date') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # # # Add filter values to context # context.update({ # 'status': self.request.GET.get('status', ''), # 'bill_type': self.request.GET.get('bill_type', ''), # 'patient_id': self.request.GET.get('patient_id', ''), # 'provider_id': self.request.GET.get('provider_id', ''), # 'date_from': self.request.GET.get('date_from', ''), # 'date_to': self.request.GET.get('date_to', ''), # 'search': self.request.GET.get('search', ''), # }) # # # Statistics # queryset = self.get_queryset() # context['stats'] = { # 'total_bills': queryset.count(), # 'total_amount': queryset.aggregate(total=Sum('total_amount'))['total'] or Decimal('0.00'), # 'total_paid': queryset.aggregate(total=Sum('paid_amount'))['total'] or Decimal('0.00'), # 'total_balance': queryset.aggregate(total=Sum('balance_amount'))['total'] or Decimal('0.00'), # } # # return context # # # class MedicalBillDetailView(LoginRequiredMixin, DetailView): # """ # Medical bill detail view with line items, payments, and claims. # """ # model = MedicalBill # template_name = 'billing/bill_detail.html' # context_object_name = 'bill' # slug_field = 'bill_id' # slug_url_kwarg = 'bill_id' # # def get_queryset(self): # return MedicalBill.objects.filter( # tenant=self.request.user.tenant # ).select_related( # 'patient', 'encounter', 'admission', 'attending_provider', 'billing_provider' # ).prefetch_related( # 'line_items', 'payments', 'insurance_claims' # ) # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # bill = self.object # # # Line items # context['line_items'] = bill.line_items.all().order_by('line_number') # # # Payments # context['payments'] = bill.payments.all().order_by('-payment_date') # # # Insurance claims # context['insurance_claims'] = bill.insurance_claims.all().order_by('-submission_date') # # # Payment summary # context['payment_summary'] = { # 'total_payments': bill.payments.aggregate(total=Sum('payment_amount'))['total'] or Decimal('0.00'), # 'insurance_payments': bill.payments.filter(payment_source='INSURANCE').aggregate(total=Sum('payment_amount'))['total'] or Decimal('0.00'), # 'patient_payments': bill.payments.filter(payment_source='PATIENT').aggregate(total=Sum('payment_amount'))['total'] or Decimal('0.00'), # } # # return context # # # class MedicalBillCreateView(LoginRequiredMixin, CreateView): # """ # Medical bill creation view. # """ # model = MedicalBill # template_name = 'billing/bill_create.html' # fields = [ # 'patient', 'bill_type', 'service_date_from', 'service_date_to', # 'attending_provider', 'billing_provider', 'encounter', 'admission', # 'primary_insurance', 'secondary_insurance', 'payment_terms', 'notes' # ] # # def form_valid(self, form): # form.instance.tenant = self.request.user.tenant # form.instance.created_by = self.request.user # # # Set due date based on payment terms # if form.instance.payment_terms == 'NET_30': # form.instance.due_date = form.instance.bill_date + timedelta(days=30) # elif form.instance.payment_terms == 'NET_60': # form.instance.due_date = form.instance.bill_date + timedelta(days=60) # elif form.instance.payment_terms == 'NET_90': # form.instance.due_date = form.instance.bill_date + timedelta(days=90) # else: # form.instance.due_date = form.instance.bill_date + timedelta(days=30) # # response = super().form_valid(form) # # # Log audit event # AuditLogger.log_event( # self.request.user, # 'MEDICAL_BILL_CREATED', # 'MedicalBill', # str(self.object.bill_id), # f"Created medical bill {self.object.bill_number} for {self.object.patient.get_full_name()}" # ) # # messages.success(self.request, 'Medical bill created successfully') # return response # # def get_success_url(self): # return reverse('billing:bill_detail', kwargs={'bill_id': self.object.bill_id}) # # # class InsuranceClaimListView(LoginRequiredMixin, ListView): # """ # Insurance claim list view with filtering and search. # """ # model = InsuranceClaim # template_name = 'billing/claim_list.html' # context_object_name = 'claims' # paginate_by = 25 # # def get_queryset(self): # queryset = InsuranceClaim.objects.filter( # medical_bill__tenant=self.request.user.tenant # ).select_related( # 'medical_bill', 'medical_bill__patient', 'insurance_info' # ) # # # Apply filters # status = self.request.GET.get('status') # claim_type = self.request.GET.get('claim_type') # insurance_id = self.request.GET.get('insurance_id') # date_from = self.request.GET.get('date_from') # date_to = self.request.GET.get('date_to') # search = self.request.GET.get('search') # # if status: # queryset = queryset.filter(status=status) # if claim_type: # queryset = queryset.filter(claim_type=claim_type) # if insurance_id: # queryset = queryset.filter(insurance_info_id=insurance_id) # if date_from: # queryset = queryset.filter(submission_date__gte=date_from) # if date_to: # queryset = queryset.filter(submission_date__lte=date_to) # if search: # queryset = queryset.filter( # Q(claim_number__icontains=search) | # Q(medical_bill__bill_number__icontains=search) | # Q(medical_bill__patient__first_name__icontains=search) | # Q(medical_bill__patient__last_name__icontains=search) # ) # # return queryset.order_by('-submission_date') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # # # Add filter values to context # context.update({ # 'status': self.request.GET.get('status', ''), # 'claim_type': self.request.GET.get('claim_type', ''), # 'insurance_id': self.request.GET.get('insurance_id', ''), # 'date_from': self.request.GET.get('date_from', ''), # 'date_to': self.request.GET.get('date_to', ''), # 'search': self.request.GET.get('search', ''), # }) # # # Statistics # queryset = self.get_queryset() # context['stats'] = { # 'total_claims': queryset.count(), # 'total_billed': queryset.aggregate(total=Sum('billed_amount'))['total'] or Decimal('0.00'), # 'total_paid': queryset.aggregate(total=Sum('paid_amount'))['total'] or Decimal('0.00'), # 'total_patient_responsibility': queryset.aggregate(total=Sum('patient_responsibility'))['total'] or Decimal('0.00'), # } # # return context # # # class PaymentListView(LoginRequiredMixin, ListView): # """ # Payment list view with filtering and search. # """ # model = Payment # template_name = 'billing/payment_list.html' # context_object_name = 'payments' # paginate_by = 25 # # def get_queryset(self): # queryset = Payment.objects.filter( # medical_bill__tenant=self.request.user.tenant # ).select_related( # 'medical_bill', 'medical_bill__patient', 'insurance_claim' # ) # # # Apply filters # payment_method = self.request.GET.get('payment_method') # payment_source = self.request.GET.get('payment_source') # status = self.request.GET.get('status') # date_from = self.request.GET.get('date_from') # date_to = self.request.GET.get('date_to') # search = self.request.GET.get('search') # # if payment_method: # queryset = queryset.filter(payment_method=payment_method) # if payment_source: # queryset = queryset.filter(payment_source=payment_source) # if status: # queryset = queryset.filter(status=status) # if date_from: # queryset = queryset.filter(payment_date__gte=date_from) # if date_to: # queryset = queryset.filter(payment_date__lte=date_to) # if search: # queryset = queryset.filter( # Q(payment_number__icontains=search) | # Q(medical_bill__bill_number__icontains=search) | # Q(medical_bill__patient__first_name__icontains=search) | # Q(medical_bill__patient__last_name__icontains=search) # ) # # return queryset.order_by('-payment_date') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # # # Add filter values to context # context.update({ # 'payment_method': self.request.GET.get('payment_method', ''), # 'payment_source': self.request.GET.get('payment_source', ''), # 'status': self.request.GET.get('status', ''), # 'date_from': self.request.GET.get('date_from', ''), # 'date_to': self.request.GET.get('date_to', ''), # 'search': self.request.GET.get('search', ''), # }) # # # Statistics # queryset = self.get_queryset() # context['stats'] = { # 'total_payments': queryset.count(), # 'total_amount': queryset.aggregate(total=Sum('payment_amount'))['total'] or Decimal('0.00'), # 'total_refunds': queryset.aggregate(total=Sum('refund_amount'))['total'] or Decimal('0.00'), # 'net_amount': queryset.aggregate( # total=Sum('payment_amount') - Sum('refund_amount') # )['total'] or Decimal('0.00'), # } # # return context # # # # HTMX Views for real-time updates # @login_required # def htmx_billing_stats(request): # """ # HTMX view for billing statistics. # """ # tenant = request.user.tenant # today = timezone.now().date() # # stats = { # 'total_revenue_today': MedicalBill.objects.filter( # tenant=tenant, # bill_date=today, # status='PAID' # ).aggregate(total=Sum('total_amount'))['total'] or Decimal('0.00'), # # 'outstanding_bills': MedicalBill.objects.filter( # tenant=tenant, # status__in=['PENDING', 'SUBMITTED', 'PARTIAL_PAID'] # ).count(), # # 'outstanding_amount': MedicalBill.objects.filter( # tenant=tenant, # status__in=['PENDING', 'SUBMITTED', 'PARTIAL_PAID'] # ).aggregate(total=Sum('balance_amount'))['total'] or Decimal('0.00'), # # 'overdue_bills': MedicalBill.objects.filter( # tenant=tenant, # status='OVERDUE' # ).count(), # # 'overdue_amount': MedicalBill.objects.filter( # tenant=tenant, # status='OVERDUE' # ).aggregate(total=Sum('balance_amount'))['total'] or Decimal('0.00'), # # 'pending_claims': InsuranceClaim.objects.filter( # medical_bill__tenant=tenant, # status__in=['SUBMITTED', 'PENDING', 'PROCESSING'] # ).count(), # # 'denied_claims': InsuranceClaim.objects.filter( # medical_bill__tenant=tenant, # status='DENIED' # ).count(), # # 'payments_today': Payment.objects.filter( # medical_bill__tenant=tenant, # payment_date=today # ).aggregate(total=Sum('payment_amount'))['total'] or Decimal('0.00'), # # 'collections_amount': MedicalBill.objects.filter( # tenant=tenant, # collection_status__in=['FIRST_NOTICE', 'SECOND_NOTICE', 'FINAL_NOTICE', 'COLLECTIONS'] # ).aggregate(total=Sum('balance_amount'))['total'] or Decimal('0.00'), # } # # return render(request, 'billing/partials/billing_stats.html', {'stats': stats}) @login_required def htmx_bill_search(request): """ HTMX view for medical bill search. """ tenant = request.user.tenant search = request.GET.get('search', '') bills = MedicalBill.objects.filter(tenant=tenant) if search: bills = bills.filter( Q(bill_number__icontains=search) | Q(patient__first_name__icontains=search) | Q(patient__last_name__icontains=search) | Q(patient__mrn__icontains=search) ) bills = bills.select_related( 'patient', 'encounter', 'attending_provider' ).order_by('-bill_date')[:10] return render(request, 'billing/partials/bill_list.html', {'bills': bills}) @login_required def htmx_payment_search(request): """ HTMX view for payment search. """ tenant = request.user.tenant search = request.GET.get('search', '') payments = Payment.objects.filter(medical_bill__tenant=tenant) if search: payments = payments.filter( Q(payment_number__icontains=search) | Q(medical_bill__bill_number__icontains=search) | Q(medical_bill__patient__first_name__icontains=search) | Q(medical_bill__patient__last_name__icontains=search) ) payments = payments.select_related( 'medical_bill', 'medical_bill__patient' ).order_by('-payment_date')[:10] return render(request, 'billing/partials/payment_list.html', {'payments': payments}) @login_required def htmx_claim_search(request): """ HTMX view for insurance claim search. """ tenant = request.user.tenant search = request.GET.get('search', '') claims = InsuranceClaim.objects.filter(medical_bill__tenant=tenant) if search: claims = claims.filter( Q(claim_number__icontains=search) | Q(medical_bill__bill_number__icontains=search) | Q(medical_bill__patient__first_name__icontains=search) | Q(medical_bill__patient__last_name__icontains=search) ) claims = claims.select_related( 'medical_bill', 'medical_bill__patient', 'insurance_info' ).order_by('-submission_date')[:10] return render(request, 'billing/partials/claim_list.html', {'claims': claims}) # # Action Views # @login_required # @require_http_methods(["POST"]) # def submit_bill(request, bill_id): # """ # Submit medical bill for processing. # """ # bill = get_object_or_404( # MedicalBill, # bill_id=bill_id, # tenant=request.user.tenant # ) # # if bill.status == 'DRAFT': # bill.status = 'SUBMITTED' # bill.save() # # # Log audit event # AuditLogger.log_event( # request.user, # 'MEDICAL_BILL_SUBMITTED', # 'MedicalBill', # str(bill.bill_id), # f"Submitted medical bill {bill.bill_number} for {bill.patient.get_full_name()}" # ) # # messages.success(request, 'Medical bill submitted successfully') # # return redirect('billing:bill_detail', bill_id=bill.bill_id) @login_required @require_http_methods(["POST"]) def process_payment(request, bill_id): """ Process payment for medical bill. """ bill = get_object_or_404( MedicalBill, bill_id=bill_id, tenant=request.user.tenant ) payment_amount = Decimal(request.POST.get('payment_amount', '0.00')) payment_method = request.POST.get('payment_method', 'CASH') payment_source = request.POST.get('payment_source', 'PATIENT') if payment_amount > 0: # Create payment record payment = Payment.objects.create( medical_bill=bill, payment_amount=payment_amount, payment_method=payment_method, payment_source=payment_source, payment_date=timezone.now().date(), received_by=request.user, processed_by=request.user, status='PROCESSED' ) # Update bill paid amount and status bill.paid_amount += payment_amount bill.balance_amount = bill.total_amount - bill.paid_amount if bill.balance_amount <= 0: bill.status = 'PAID' elif bill.paid_amount > 0: bill.status = 'PARTIAL_PAID' bill.save() # Log audit event AuditLogger.log_event( request.user, 'PAYMENT_PROCESSED', 'Payment', str(payment.payment_id), f"Processed payment {payment.payment_number} for ${payment_amount} on bill {bill.bill_number}" ) messages.success(request, f'Payment of ${payment_amount} processed successfully') return redirect('billing:bill_detail', bill_id=bill.bill_id) # # @login_required @require_http_methods(["POST"]) def submit_insurance_claim(request, bill_id): """ Submit insurance claim for medical bill. """ bill = get_object_or_404( MedicalBill, bill_id=bill_id, tenant=request.user.tenant ) insurance_type = request.POST.get('insurance_type', 'PRIMARY') # Determine which insurance to use if insurance_type == 'PRIMARY' and bill.primary_insurance: insurance_info = bill.primary_insurance claim_type = 'PRIMARY' elif insurance_type == 'SECONDARY' and bill.secondary_insurance: insurance_info = bill.secondary_insurance claim_type = 'SECONDARY' else: messages.error(request, 'No insurance information available for claim submission') return redirect('billing:bill_detail', bill_id=bill.bill_id) # Create insurance claim claim = InsuranceClaim.objects.create( medical_bill=bill, insurance_info=insurance_info, claim_type=claim_type, submission_date=timezone.now().date(), service_date_from=bill.service_date_from, service_date_to=bill.service_date_to, billed_amount=bill.total_amount, status='SUBMITTED', created_by=request.user ) # Log audit event AuditLogger.log_event( request.user, 'INSURANCE_CLAIM_SUBMITTED', 'InsuranceClaim', str(claim.claim_id), f"Submitted {claim_type.lower()} insurance claim {claim.claim_number} for bill {bill.bill_number}" ) messages.success(request, f'{claim_type.title()} insurance claim submitted successfully') return redirect('billing:bill_detail', bill_id=bill.bill_id) # # # # Export Views # # # # Legacy view functions for backward compatibility dashboard = BillingDashboardView.as_view() bill_list = MedicalBillListView.as_view() bill_detail = MedicalBillDetailView.as_view() bill_create = MedicalBillCreateView.as_view() claim_list = InsuranceClaimListView.as_view() payment_list = PaymentListView.as_view() # # # # # from django.shortcuts import render, redirect, get_object_or_404 # from django.contrib.auth.decorators import login_required, permission_required # from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin # from django.contrib import messages # from django.views.generic import ( # CreateView, UpdateView, DeleteView, DetailView, ListView, FormView # ) # from django.urls import reverse_lazy, reverse # from django.http import JsonResponse, HttpResponse # from django.utils import timezone # from django.db import transaction # from django.core.mail import send_mail # from django.conf import settings # from django.db.models import Q, Sum, Count # from viewflow.views import CreateProcessView, UpdateProcessView # from decimal import Decimal # import json # # from .models import ( # Bill, BillItem, InsuranceClaim, Payment, PaymentMethod, # InsuranceProvider, ClaimDenial, PaymentPlan # ) # from .forms import ( # MedicalBillingForm, BillItemForm, InsuranceClaimForm, PaymentProcessingForm, # DenialManagementForm, PaymentPlanForm, CollectionsForm, # InsuranceVerificationForm, BulkBillingForm # ) # from .flows import MedicalBillingFlow, InsuranceClaimFlow, PaymentProcessingFlow, DenialManagementFlow, CollectionsFlow # from patients.models import Patient # # # class MedicalBillingView(LoginRequiredMixin, PermissionRequiredMixin, CreateProcessView): # """ # View for medical billing workflow # """ # model = Bill # form_class = MedicalBillingForm # template_name = 'billing/medical_billing.html' # permission_required = 'billing.can_create_bills' # flow_class = MedicalBillingFlow # # def get_form_kwargs(self): # kwargs = super().get_form_kwargs() # kwargs['tenant'] = self.request.user.tenant # return kwargs # # def form_valid(self, form): # with transaction.atomic(): # # Create bill # bill = form.save(commit=False) # bill.tenant = self.request.user.tenant # bill.created_by = self.request.user # bill.status = 'draft' # bill.save() # # # Start medical billing workflow # process = self.flow_class.start.run( # bill=bill, # insurance_verification=form.cleaned_data.get('insurance_verification', True), # auto_submit_primary=form.cleaned_data.get('auto_submit_primary', True), # auto_submit_secondary=form.cleaned_data.get('auto_submit_secondary', False), # generate_patient_statement=form.cleaned_data.get('generate_patient_statement', True), # created_by=self.request.user # ) # # messages.success( # self.request, # f'Medical bill created successfully for {bill.patient.get_full_name()}. ' # f'Billing workflow initiated.' # ) # # return redirect('billing:bill_detail', pk=bill.pk) # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context['title'] = 'Create Medical Bill' # context['breadcrumbs'] = [ # {'name': 'Home', 'url': reverse('core:dashboard')}, # {'name': 'Billing', 'url': reverse('billing:dashboard')}, # {'name': 'Bills', 'url': reverse('billing:bill_list')}, # {'name': 'Create Bill', 'url': ''} # ] # return context # # # class BillItemView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): # """ # View for bill item creation/editing # """ # model = BillItem # form_class = BillItemForm # template_name = 'billing/bill_item_form.html' # permission_required = 'billing.can_edit_bill_items' # # def get_success_url(self): # return reverse('billing:bill_detail', kwargs={'pk': self.kwargs['bill_id']}) # # def form_valid(self, form): # bill = get_object_or_404(Bill, pk=self.kwargs['bill_id']) # # with transaction.atomic(): # # Create bill item # item = form.save(commit=False) # item.bill = bill # item.tenant = bill.tenant # item.save() # # # Recalculate bill totals # self.recalculate_bill_totals(bill) # # messages.success( # self.request, # f'Bill item "{item.description}" added successfully.' # ) # # return super().form_valid(form) # # def recalculate_bill_totals(self, bill): # """Recalculate bill totals""" # items = bill.items.all() # subtotal = sum(item.total_amount for item in items) # # bill.subtotal = subtotal # bill.total_amount = subtotal + bill.tax_amount # bill.balance_due = bill.total_amount - bill.paid_amount # bill.save() # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context['bill'] = get_object_or_404(Bill, pk=self.kwargs['bill_id']) # context['title'] = 'Add Bill Item' # return context # # # class InsuranceClaimView(LoginRequiredMixin, PermissionRequiredMixin, CreateProcessView): # """ # View for insurance claim submission workflow # """ # model = InsuranceClaim # form_class = InsuranceClaimForm # template_name = 'billing/insurance_claim.html' # permission_required = 'billing.can_submit_claims' # flow_class = InsuranceClaimFlow # # def get_form_kwargs(self): # kwargs = super().get_form_kwargs() # kwargs['tenant'] = self.request.user.tenant # return kwargs # # def form_valid(self, form): # with transaction.atomic(): # # Create insurance claim # claim = form.save(commit=False) # claim.tenant = self.request.user.tenant # claim.submitted_by = self.request.user # claim.status = 'pending' # claim.save() # # # Start insurance claim workflow # process = self.flow_class.start.run( # claim=claim, # submit_electronically=form.cleaned_data.get('submit_electronically', True), # created_by=self.request.user # ) # # messages.success( # self.request, # f'Insurance claim submitted successfully. Claim ID: {claim.claim_number}' # ) # # return redirect('billing:claim_detail', pk=claim.pk) # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context['title'] = 'Submit Insurance Claim' # context['breadcrumbs'] = [ # {'name': 'Home', 'url': reverse('core:dashboard')}, # {'name': 'Billing', 'url': reverse('billing:dashboard')}, # {'name': 'Claims', 'url': reverse('billing:claim_list')}, # {'name': 'Submit Claim', 'url': ''} # ] # return context # # # class PaymentProcessingView(LoginRequiredMixin, PermissionRequiredMixin, CreateProcessView): # """ # View for payment processing workflow # """ # model = Payment # form_class = PaymentProcessingForm # template_name = 'billing/payment_processing.html' # permission_required = 'billing.can_process_payments' # flow_class = PaymentProcessingFlow # # def get_form_kwargs(self): # kwargs = super().get_form_kwargs() # kwargs['tenant'] = self.request.user.tenant # # # Pre-populate bill if provided # bill_id = self.kwargs.get('bill_id') # if bill_id: # kwargs['bill'] = get_object_or_404(Bill, pk=bill_id) # # return kwargs # # def form_valid(self, form): # with transaction.atomic(): # # Create payment # payment = form.save(commit=False) # payment.tenant = self.request.user.tenant # payment.processed_by = self.request.user # payment.status = 'pending' # payment.save() # # # Update bill balance # bill = payment.bill # bill.paid_amount += payment.payment_amount # bill.balance_due = bill.total_amount - bill.paid_amount # # if bill.balance_due <= 0: # bill.status = 'paid' # else: # bill.status = 'partial_payment' # # bill.save() # # # Start payment processing workflow # process = self.flow_class.start.run( # payment=payment, # send_receipt=form.cleaned_data.get('send_receipt', True), # created_by=self.request.user # ) # # messages.success( # self.request, # f'Payment of ${payment.payment_amount} processed successfully. ' # f'Remaining balance: ${bill.balance_due}' # ) # # return redirect('billing:payment_detail', pk=payment.pk) # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context['title'] = 'Process Payment' # # bill_id = self.kwargs.get('bill_id') # if bill_id: # context['bill'] = get_object_or_404(Bill, pk=bill_id) # # return context # # # class DenialManagementView(LoginRequiredMixin, PermissionRequiredMixin, CreateProcessView): # """ # View for denial management workflow # """ # model = ClaimDenial # form_class = DenialManagementForm # template_name = 'billing/denial_management.html' # permission_required = 'billing.can_manage_denials' # flow_class = DenialManagementFlow # # def get_form_kwargs(self): # kwargs = super().get_form_kwargs() # kwargs['tenant'] = self.request.user.tenant # return kwargs # # def form_valid(self, form): # with transaction.atomic(): # # Create denial record # denial = form.save(commit=False) # denial.tenant = self.request.user.tenant # denial.processed_by = self.request.user # denial.save() # # # Update claim status # claim = denial.claim # claim.status = 'denied' # claim.save() # # # Start denial management workflow # process = self.flow_class.start.run( # denial=denial, # resubmit_claim=form.cleaned_data.get('resubmit_claim', False), # file_appeal=form.cleaned_data.get('file_appeal', False), # created_by=self.request.user # ) # # messages.success( # self.request, # f'Denial processed for claim {claim.claim_number}. ' # f'Workflow initiated for corrective actions.' # ) # # return redirect('billing:denial_detail', pk=denial.pk) # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context['title'] = 'Manage Claim Denial' # return context # # # class BillListView(LoginRequiredMixin, PermissionRequiredMixin, ListView): # """ # View for listing bills # """ # model = Bill # template_name = 'billing/bill_list.html' # context_object_name = 'bills' # permission_required = 'billing.view_bill' # paginate_by = 25 # # def get_queryset(self): # queryset = Bill.objects.filter(tenant=self.request.user.tenant) # # # Apply filters # search = self.request.GET.get('search') # if search: # queryset = queryset.filter( # Q(patient__first_name__icontains=search) | # Q(patient__last_name__icontains=search) | # Q(bill_number__icontains=search) # ) # # status = self.request.GET.get('status') # if status: # queryset = queryset.filter(status=status) # # date_from = self.request.GET.get('date_from') # if date_from: # queryset = queryset.filter(service_date__gte=date_from) # # date_to = self.request.GET.get('date_to') # if date_to: # queryset = queryset.filter(service_date__lte=date_to) # # return queryset.order_by('-created_at') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context['title'] = 'Bills' # context['search'] = self.request.GET.get('search', '') # context['selected_status'] = self.request.GET.get('status', '') # context['date_from'] = self.request.GET.get('date_from', '') # context['date_to'] = self.request.GET.get('date_to', '') # context['billing_stats'] = self.get_billing_stats() # return context # # def get_billing_stats(self): # """Get billing statistics""" # bills = Bill.objects.filter(tenant=self.request.user.tenant) # return { # 'total_bills': bills.count(), # 'total_amount': bills.aggregate(Sum('total_amount'))['total_amount__sum'] or 0, # 'total_paid': bills.aggregate(Sum('paid_amount'))['paid_amount__sum'] or 0, # 'total_outstanding': bills.aggregate(Sum('balance_due'))['balance_due__sum'] or 0 # } # # # class BillDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView): # """ # View for bill details # """ # model = Bill # template_name = 'billing/bill_detail.html' # context_object_name = 'bill' # permission_required = 'billing.view_bill' # # def get_queryset(self): # return Bill.objects.filter(tenant=self.request.user.tenant) # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # bill = self.object # context['title'] = f'Bill {bill.bill_number}' # context['items'] = bill.items.all() # context['claims'] = bill.claims.all() # context['payments'] = bill.payments.all() # context['payment_plan'] = getattr(bill, 'payment_plan', None) # context['can_edit'] = self.request.user.has_perm('billing.change_bill') # context['can_process_payment'] = self.request.user.has_perm('billing.can_process_payments') # return context # # # # AJAX Views # @login_required # @permission_required('billing.view_patient') # def patient_billing_search_ajax(request): # """AJAX view for patient billing search""" # query = request.GET.get('q', '') # if len(query) < 2: # return JsonResponse({'patients': []}) # # patients = Patient.objects.filter( # tenant=request.user.tenant # ).filter( # Q(first_name__icontains=query) | # Q(last_name__icontains=query) | # Q(patient_id__icontains=query) | # Q(insurance_id__icontains=query) # )[:10] # # patient_data = [ # { # 'id': patient.id, # 'name': patient.get_full_name(), # 'patient_id': patient.patient_id, # 'insurance': patient.primary_insurance.name if patient.primary_insurance else 'No Insurance', # 'outstanding_balance': str(patient.get_outstanding_balance()) # } # for patient in patients # ] # # return JsonResponse({'patients': patient_data}) # # # @login_required # @permission_required('billing.can_calculate_totals') # def calculate_bill_totals_ajax(request): # """AJAX view to calculate bill totals""" # if request.method == 'POST': # try: # data = json.loads(request.body) # items = data.get('items', []) # # subtotal = Decimal('0.00') # for item in items: # quantity = Decimal(str(item.get('quantity', 1))) # unit_price = Decimal(str(item.get('unit_price', 0))) # discount = Decimal(str(item.get('discount', 0))) # # item_total = (quantity * unit_price) - discount # subtotal += item_total # # tax_rate = Decimal('0.08') # 8% tax rate (configurable) # tax_amount = subtotal * tax_rate # total_amount = subtotal + tax_amount # # return JsonResponse({ # 'success': True, # 'subtotal': str(subtotal), # 'tax_amount': str(tax_amount), # 'total_amount': str(total_amount) # }) # except Exception as e: # return JsonResponse({ # 'success': False, # 'error': str(e) # }) # # return JsonResponse({'success': False, 'message': 'Invalid request.'}) # # # @login_required # @permission_required('billing.can_verify_insurance') # def verify_insurance_ajax(request, patient_id): # """AJAX view to verify insurance""" # if request.method == 'POST': # try: # patient = Patient.objects.get( # id=patient_id, # tenant=request.user.tenant # ) # # # Perform insurance verification # verification_result = verify_patient_insurance(patient) # # return JsonResponse({ # 'success': verification_result['success'], # 'data': verification_result # }) # except Patient.DoesNotExist: # return JsonResponse({ # 'success': False, # 'message': 'Patient not found.' # }) # # return JsonResponse({'success': False, 'message': 'Invalid request.'}) # # # def verify_patient_insurance(patient): # """Verify patient insurance""" # try: # # This would implement actual insurance verification # return { # 'success': True, # 'status': 'active', # 'coverage': 'full', # 'copay': 25.00, # 'deductible': 500.00, # 'deductible_met': 150.00, # 'out_of_pocket_max': 2000.00, # 'out_of_pocket_met': 300.00 # } # except Exception as e: # return { # 'success': False, # 'error': str(e) # } #