Marwan Alwali be70e47e22 update
2025-08-30 09:45:26 +03:00

2162 lines
74 KiB
Python

"""
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()
#
#
#
#