2840 lines
98 KiB
Python
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)
|
|
# }
|
|
#
|