Marwan Alwali ab2c4a36c5 update
2025-10-02 10:13:03 +03:00

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