""" 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 # ============================================================================ # DASHBOARD VIEW # ============================================================================ 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 # ============================================================================ # MEDICAL BILL VIEWS # ============================================================================ 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 # ============================================================================ # INSURANCE CLAIM VIEWS # ============================================================================ 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): # Set medical bill and created_by 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') form.instance.created_by = self.request.user # Generate claim number 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_success_url(self): return reverse('billing:claim_detail', kwargs={'claim_id': self.object.claim_id}) # ============================================================================ # PAYMENT VIEWS # ============================================================================ 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}) # ============================================================================ # HTMX VIEWS # ============================================================================ # @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 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 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}) # ============================================================================ # ACTION VIEWS # ============================================================================ @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'}) # ============================================================================ # EXPORT VIEWS # ============================================================================ @login_required def export_bills(request): """ Export bills to CSV. """ tenant = getattr(request, 'tenant', None) if not tenant: return HttpResponse('No tenant found', status=400) # Create CSV response response = HttpResponse(content_type='text/csv') response['Content-Disposition'] = 'attachment; filename="medical_bills.csv"' writer = csv.writer(response) writer.writerow([ 'Bill Number', 'Patient Name', 'Bill Date', 'Due Date', 'Total Amount', 'Paid Amount', 'Balance', 'Status' ]) bills = MedicalBill.objects.filter(tenant=tenant).select_related('patient') for bill in bills: writer.writerow([ bill.bill_number, bill.patient.get_full_name(), bill.bill_date.strftime('%Y-%m-%d'), bill.due_date.strftime('%Y-%m-%d') if bill.due_date else '', str(bill.total_amount), str(bill.paid_amount), str(bill.balance_amount), bill.get_status_display() ]) return response # ============================================================================ # PRINT VIEWS # ============================================================================ @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(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) # # # """ # 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 # @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 # # # # 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() # # # #