2162 lines
74 KiB
Python
2162 lines
74 KiB
Python
"""
|
|
Billing app views for hospital management system.
|
|
Provides medical billing, insurance claims, and revenue cycle management.
|
|
"""
|
|
|
|
from django.shortcuts import render, get_object_or_404, redirect
|
|
from django.contrib.auth.decorators import login_required
|
|
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
|
|
from django.views.generic import (
|
|
TemplateView, ListView, DetailView, CreateView, UpdateView, DeleteView
|
|
)
|
|
from django.http import JsonResponse, HttpResponse
|
|
from django.contrib import messages
|
|
from django.db.models import Q, Count, Sum, Avg, Max, Min
|
|
from django.utils import timezone
|
|
from django.urls import reverse_lazy, reverse
|
|
from django.core.paginator import Paginator
|
|
from django.views.decorators.http import require_http_methods
|
|
from datetime import datetime, timedelta, date
|
|
from decimal import Decimal
|
|
import json
|
|
import csv
|
|
|
|
from .models import (
|
|
MedicalBill, BillLineItem, InsuranceClaim, Payment,
|
|
ClaimStatusUpdate, BillingConfiguration
|
|
)
|
|
from .forms import (
|
|
MedicalBillForm, BillLineItemForm, InsuranceClaimForm, PaymentForm,
|
|
BillingSearchForm, ClaimSearchForm, PaymentSearchForm
|
|
)
|
|
from patients.models import PatientProfile, InsuranceInfo
|
|
from accounts.models import User
|
|
from emr.models import Encounter
|
|
from inpatients.models import Admission
|
|
|
|
|
|
# ============================================================================
|
|
# DASHBOARD VIEW
|
|
# ============================================================================
|
|
|
|
class BillingDashboardView(LoginRequiredMixin, TemplateView):
|
|
"""
|
|
Billing dashboard view with comprehensive statistics and recent activity.
|
|
"""
|
|
template_name = 'billing/dashboard.html'
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
tenant = getattr(self.request, 'tenant', None)
|
|
|
|
if tenant:
|
|
today = timezone.now().date()
|
|
thirty_days_ago = today - timedelta(days=30)
|
|
|
|
# Basic statistics
|
|
bills = MedicalBill.objects.filter(tenant=tenant)
|
|
context['total_bills'] = bills.count()
|
|
context['total_revenue'] = bills.aggregate(
|
|
total=Sum('total_amount')
|
|
)['total'] or Decimal('0.00')
|
|
context['total_paid'] = bills.aggregate(
|
|
total=Sum('paid_amount')
|
|
)['total'] or Decimal('0.00')
|
|
context['total_outstanding'] = context['total_revenue'] - context['total_paid']
|
|
|
|
# Recent activity
|
|
context['recent_bills'] = bills.select_related(
|
|
'patient', 'attending_provider'
|
|
).order_by('-bill_date')[:10]
|
|
|
|
# Status breakdown
|
|
context['status_breakdown'] = bills.values('status').annotate(
|
|
count=Count('id')
|
|
).order_by('status')
|
|
|
|
# Monthly trends
|
|
context['monthly_revenue'] = bills.filter(
|
|
bill_date__gte=thirty_days_ago
|
|
).aggregate(
|
|
total=Sum('total_amount')
|
|
)['total'] or Decimal('0.00')
|
|
|
|
# Overdue bills
|
|
context['overdue_bills'] = bills.filter(
|
|
due_date__lt=today,
|
|
status__in=['draft', 'sent', 'partial_payment']
|
|
)
|
|
|
|
# Claims statistics
|
|
claims = InsuranceClaim.objects.filter(medical_bill__tenant=tenant)
|
|
context['total_claims'] = claims.count()
|
|
context['pending_claims'] = claims.filter(status='submitted')
|
|
context['denied_claims'] = claims.filter(status='denied')
|
|
|
|
# Payments statistics
|
|
payments = Payment.objects.filter(medical_bill__tenant=tenant)
|
|
context['total_payments'] = payments.count()
|
|
context['recent_payments'] = payments.select_related(
|
|
'medical_bill__patient'
|
|
).order_by('-payment_date')[:5]
|
|
|
|
return context
|
|
|
|
|
|
# ============================================================================
|
|
# MEDICAL BILL VIEWS
|
|
# ============================================================================
|
|
|
|
class MedicalBillListView(LoginRequiredMixin, ListView):
|
|
"""
|
|
List view for medical bills with filtering and search.
|
|
"""
|
|
model = MedicalBill
|
|
template_name = 'billing/bills/bill_list.html'
|
|
context_object_name = 'bills'
|
|
paginate_by = 25
|
|
|
|
def get_queryset(self):
|
|
tenant = getattr(self.request, 'tenant', None)
|
|
if not tenant:
|
|
return MedicalBill.objects.none()
|
|
|
|
queryset = MedicalBill.objects.filter(tenant=tenant)
|
|
|
|
# Apply filters
|
|
bill_type = self.request.GET.get('bill_type')
|
|
if bill_type:
|
|
queryset = queryset.filter(bill_type=bill_type)
|
|
|
|
status = self.request.GET.get('status')
|
|
if status:
|
|
queryset = queryset.filter(status=status)
|
|
|
|
date_from = self.request.GET.get('date_from')
|
|
if date_from:
|
|
queryset = queryset.filter(bill_date__gte=date_from)
|
|
|
|
date_to = self.request.GET.get('date_to')
|
|
if date_to:
|
|
queryset = queryset.filter(bill_date__lte=date_to)
|
|
|
|
amount_min = self.request.GET.get('amount_min')
|
|
if amount_min:
|
|
queryset = queryset.filter(total_amount__gte=amount_min)
|
|
|
|
amount_max = self.request.GET.get('amount_max')
|
|
if amount_max:
|
|
queryset = queryset.filter(total_amount__lte=amount_max)
|
|
|
|
overdue_only = self.request.GET.get('overdue_only')
|
|
if overdue_only:
|
|
today = timezone.now().date()
|
|
queryset = queryset.filter(
|
|
due_date__lt=today,
|
|
status__in=['draft', 'sent', 'partial_payment']
|
|
)
|
|
|
|
search = self.request.GET.get('search')
|
|
if search:
|
|
queryset = queryset.filter(
|
|
Q(bill_number__icontains=search) |
|
|
Q(patient__first_name__icontains=search) |
|
|
Q(patient__last_name__icontains=search) |
|
|
Q(patient__patient_id__icontains=search) |
|
|
Q(attending_provider__first_name__icontains=search) |
|
|
Q(attending_provider__last_name__icontains=search)
|
|
)
|
|
|
|
return queryset.select_related(
|
|
'patient', 'attending_provider'
|
|
).order_by('-bill_date')
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['search_form'] = BillingSearchForm(
|
|
self.request.GET,
|
|
user=self.request.user
|
|
)
|
|
return context
|
|
|
|
|
|
class MedicalBillDetailView(LoginRequiredMixin, DetailView):
|
|
"""
|
|
Detail view for medical bills.
|
|
"""
|
|
model = MedicalBill
|
|
template_name = 'billing/bills/bill_detail.html'
|
|
context_object_name = 'bill'
|
|
slug_field = 'bill_id'
|
|
slug_url_kwarg = 'bill_id'
|
|
|
|
def get_queryset(self):
|
|
tenant = getattr(self.request, 'tenant', None)
|
|
if not tenant:
|
|
return MedicalBill.objects.none()
|
|
return MedicalBill.objects.filter(tenant=tenant)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
bill = self.get_object()
|
|
|
|
# Get related data
|
|
context['line_items'] = bill.line_items.all().order_by('line_number')
|
|
context['claims'] = bill.insurance_claims.all().order_by('-submission_date')
|
|
context['payments'] = bill.payments.all().order_by('-payment_date')
|
|
|
|
return context
|
|
|
|
|
|
class MedicalBillCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
|
"""
|
|
Create view for medical bills.
|
|
"""
|
|
model = MedicalBill
|
|
form_class = MedicalBillForm
|
|
template_name = 'billing/bills/bill_form.html'
|
|
permission_required = 'billing.add_medicalbill'
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs['user'] = self.request.user
|
|
return kwargs
|
|
|
|
def form_valid(self, form):
|
|
# Set tenant and created_by
|
|
form.instance.tenant = getattr(self.request, 'tenant', None)
|
|
form.instance.created_by = self.request.user
|
|
|
|
# Generate bill number
|
|
if not form.instance.bill_number:
|
|
form.instance.bill_number = form.instance.generate_bill_number()
|
|
|
|
response = super().form_valid(form)
|
|
|
|
messages.success(
|
|
self.request,
|
|
f'Medical bill {self.object.bill_number} created successfully.'
|
|
)
|
|
|
|
return response
|
|
|
|
def get_success_url(self):
|
|
return reverse('billing:bill_detail', kwargs={'bill_id': self.object.bill_id})
|
|
|
|
|
|
class MedicalBillUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView):
|
|
"""
|
|
Update view for medical bills.
|
|
"""
|
|
model = MedicalBill
|
|
form_class = MedicalBillForm
|
|
template_name = 'billing/bills/bill_form.html'
|
|
permission_required = 'billing.change_medicalbill'
|
|
slug_field = 'bill_id'
|
|
slug_url_kwarg = 'bill_id'
|
|
|
|
def get_queryset(self):
|
|
tenant = getattr(self.request, 'tenant', None)
|
|
if not tenant:
|
|
return MedicalBill.objects.none()
|
|
return MedicalBill.objects.filter(tenant=tenant)
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs['user'] = self.request.user
|
|
return kwargs
|
|
|
|
def form_valid(self, form):
|
|
response = super().form_valid(form)
|
|
|
|
messages.success(
|
|
self.request,
|
|
f'Medical bill {self.object.bill_number} updated successfully.'
|
|
)
|
|
|
|
return response
|
|
|
|
def get_success_url(self):
|
|
return reverse('billing:bill_detail', kwargs={'bill_id': self.object.bill_id})
|
|
|
|
|
|
class MedicalBillDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView):
|
|
"""
|
|
Delete view for medical bills.
|
|
"""
|
|
model = MedicalBill
|
|
template_name = 'billing/bills/bill_confirm_delete.html'
|
|
permission_required = 'billing.delete_medicalbill'
|
|
success_url = reverse_lazy('billing:bill_list')
|
|
slug_field = 'bill_id'
|
|
slug_url_kwarg = 'bill_id'
|
|
|
|
def get_queryset(self):
|
|
tenant = getattr(self.request, 'tenant', None)
|
|
if not tenant:
|
|
return MedicalBill.objects.none()
|
|
return MedicalBill.objects.filter(tenant=tenant)
|
|
|
|
def delete(self, request, *args, **kwargs):
|
|
bill = self.get_object()
|
|
bill_number = bill.bill_number
|
|
|
|
response = super().delete(request, *args, **kwargs)
|
|
|
|
messages.success(
|
|
request,
|
|
f'Medical bill {bill_number} deleted successfully.'
|
|
)
|
|
|
|
return response
|
|
|
|
|
|
# ============================================================================
|
|
# INSURANCE CLAIM VIEWS
|
|
# ============================================================================
|
|
|
|
class InsuranceClaimListView(LoginRequiredMixin, ListView):
|
|
"""
|
|
List view for insurance claims with filtering and search.
|
|
"""
|
|
model = InsuranceClaim
|
|
template_name = 'billing/claims/claim_list.html'
|
|
context_object_name = 'claims'
|
|
paginate_by = 25
|
|
|
|
def get_queryset(self):
|
|
tenant = getattr(self.request, 'tenant', None)
|
|
if not tenant:
|
|
return InsuranceClaim.objects.none()
|
|
|
|
queryset = InsuranceClaim.objects.filter(medical_bill__tenant=tenant)
|
|
|
|
# Apply filters
|
|
claim_type = self.request.GET.get('claim_type')
|
|
if claim_type:
|
|
queryset = queryset.filter(claim_type=claim_type)
|
|
|
|
status = self.request.GET.get('status')
|
|
if status:
|
|
queryset = queryset.filter(status=status)
|
|
|
|
submission_date_from = self.request.GET.get('submission_date_from')
|
|
if submission_date_from:
|
|
queryset = queryset.filter(submission_date__gte=submission_date_from)
|
|
|
|
submission_date_to = self.request.GET.get('submission_date_to')
|
|
if submission_date_to:
|
|
queryset = queryset.filter(submission_date__lte=submission_date_to)
|
|
|
|
pending_only = self.request.GET.get('pending_only')
|
|
if pending_only:
|
|
queryset = queryset.filter(status='submitted')
|
|
|
|
search = self.request.GET.get('search')
|
|
if search:
|
|
queryset = queryset.filter(
|
|
Q(claim_number__icontains=search) |
|
|
Q(medical_bill__bill_number__icontains=search) |
|
|
Q(medical_bill__patient__first_name__icontains=search) |
|
|
Q(medical_bill__patient__last_name__icontains=search) |
|
|
Q(insurance_info__insurance_company__icontains=search)
|
|
)
|
|
|
|
return queryset.select_related(
|
|
'medical_bill__patient', 'insurance_info'
|
|
).order_by('-submission_date')
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['search_form'] = ClaimSearchForm(self.request.GET)
|
|
return context
|
|
|
|
|
|
class InsuranceClaimDetailView(LoginRequiredMixin, DetailView):
|
|
"""
|
|
Detail view for insurance claims.
|
|
"""
|
|
model = InsuranceClaim
|
|
template_name = 'billing/claims/claim_detail.html'
|
|
context_object_name = 'claim'
|
|
slug_field = 'claim_id'
|
|
slug_url_kwarg = 'claim_id'
|
|
|
|
def get_queryset(self):
|
|
tenant = getattr(self.request, 'tenant', None)
|
|
if not tenant:
|
|
return InsuranceClaim.objects.none()
|
|
return InsuranceClaim.objects.filter(medical_bill__tenant=tenant)
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
claim = self.get_object()
|
|
|
|
# Get related data
|
|
context['status_updates'] = claim.claimstatusupdate_set.all().order_by('-update_date')
|
|
|
|
return context
|
|
|
|
|
|
class InsuranceClaimCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
|
"""
|
|
Create view for insurance claims.
|
|
"""
|
|
model = InsuranceClaim
|
|
form_class = InsuranceClaimForm
|
|
template_name = 'billing/claims/claim_form.html'
|
|
permission_required = 'billing.add_insuranceclaim'
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs['user'] = self.request.user
|
|
|
|
# Get medical bill from URL parameter
|
|
bill_id = self.kwargs.get('bill_id')
|
|
if bill_id:
|
|
try:
|
|
medical_bill = MedicalBill.objects.get(
|
|
bill_id=bill_id,
|
|
tenant=getattr(self.request, 'tenant', None)
|
|
)
|
|
kwargs['medical_bill'] = medical_bill
|
|
except MedicalBill.DoesNotExist:
|
|
pass
|
|
|
|
return kwargs
|
|
|
|
def form_valid(self, form):
|
|
# Set medical bill and created_by
|
|
bill_id = self.kwargs.get('bill_id')
|
|
if bill_id:
|
|
try:
|
|
medical_bill = MedicalBill.objects.get(
|
|
bill_id=bill_id,
|
|
tenant=getattr(self.request, 'tenant', None)
|
|
)
|
|
form.instance.medical_bill = medical_bill
|
|
except MedicalBill.DoesNotExist:
|
|
messages.error(self.request, 'Medical bill not found.')
|
|
return redirect('billing:bill_list')
|
|
|
|
form.instance.created_by = self.request.user
|
|
|
|
# Generate claim number
|
|
if not form.instance.claim_number:
|
|
form.instance.claim_number = form.instance.generate_claim_number()
|
|
|
|
response = super().form_valid(form)
|
|
|
|
messages.success(
|
|
self.request,
|
|
f'Insurance claim {self.object.claim_number} created successfully.'
|
|
)
|
|
|
|
return response
|
|
|
|
def get_success_url(self):
|
|
return reverse('billing:claim_detail', kwargs={'claim_id': self.object.claim_id})
|
|
|
|
|
|
# ============================================================================
|
|
# PAYMENT VIEWS
|
|
# ============================================================================
|
|
|
|
class PaymentListView(LoginRequiredMixin, ListView):
|
|
"""
|
|
List view for payments with filtering and search.
|
|
"""
|
|
model = Payment
|
|
template_name = 'billing/payments/payment_list.html'
|
|
context_object_name = 'payments'
|
|
paginate_by = 25
|
|
|
|
def get_queryset(self):
|
|
tenant = getattr(self.request, 'tenant', None)
|
|
if not tenant:
|
|
return Payment.objects.none()
|
|
|
|
queryset = Payment.objects.filter(medical_bill__tenant=tenant)
|
|
|
|
# Apply filters
|
|
payment_method = self.request.GET.get('payment_method')
|
|
if payment_method:
|
|
queryset = queryset.filter(payment_method=payment_method)
|
|
|
|
payment_source = self.request.GET.get('payment_source')
|
|
if payment_source:
|
|
queryset = queryset.filter(payment_source=payment_source)
|
|
|
|
status = self.request.GET.get('status')
|
|
if status:
|
|
queryset = queryset.filter(status=status)
|
|
|
|
date_from = self.request.GET.get('date_from')
|
|
if date_from:
|
|
queryset = queryset.filter(payment_date__gte=date_from)
|
|
|
|
date_to = self.request.GET.get('date_to')
|
|
if date_to:
|
|
queryset = queryset.filter(payment_date__lte=date_to)
|
|
|
|
search = self.request.GET.get('search')
|
|
if search:
|
|
queryset = queryset.filter(
|
|
Q(payment_number__icontains=search) |
|
|
Q(medical_bill__bill_number__icontains=search) |
|
|
Q(medical_bill__patient__first_name__icontains=search) |
|
|
Q(medical_bill__patient__last_name__icontains=search) |
|
|
Q(check_number__icontains=search) |
|
|
Q(authorization_code__icontains=search)
|
|
)
|
|
|
|
return queryset.select_related(
|
|
'medical_bill__patient', 'received_by'
|
|
).order_by('-payment_date')
|
|
|
|
def get_context_data(self, **kwargs):
|
|
context = super().get_context_data(**kwargs)
|
|
context['search_form'] = PaymentSearchForm(self.request.GET)
|
|
return context
|
|
|
|
|
|
class PaymentDetailView(LoginRequiredMixin, DetailView):
|
|
"""
|
|
Detail view for payments.
|
|
"""
|
|
model = Payment
|
|
template_name = 'billing/payments/payment_detail.html'
|
|
context_object_name = 'payment'
|
|
slug_field = 'payment_id'
|
|
slug_url_kwarg = 'payment_id'
|
|
|
|
def get_queryset(self):
|
|
tenant = getattr(self.request, 'tenant', None)
|
|
if not tenant:
|
|
return Payment.objects.none()
|
|
return Payment.objects.filter(medical_bill__tenant=tenant)
|
|
|
|
|
|
class PaymentCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView):
|
|
"""
|
|
Create view for payments.
|
|
"""
|
|
model = Payment
|
|
form_class = PaymentForm
|
|
template_name = 'billing/payments/payment_form.html'
|
|
permission_required = 'billing.add_payment'
|
|
|
|
def get_form_kwargs(self):
|
|
kwargs = super().get_form_kwargs()
|
|
kwargs['user'] = self.request.user
|
|
|
|
# Get medical bill from URL parameter
|
|
bill_id = self.kwargs.get('bill_id')
|
|
if bill_id:
|
|
try:
|
|
medical_bill = MedicalBill.objects.get(
|
|
bill_id=bill_id,
|
|
tenant=getattr(self.request, 'tenant', None)
|
|
)
|
|
kwargs['medical_bill'] = medical_bill
|
|
except MedicalBill.DoesNotExist:
|
|
pass
|
|
|
|
return kwargs
|
|
|
|
def form_valid(self, form):
|
|
# Set medical bill
|
|
bill_id = self.kwargs.get('bill_id')
|
|
if bill_id:
|
|
try:
|
|
medical_bill = MedicalBill.objects.get(
|
|
bill_id=bill_id,
|
|
tenant=getattr(self.request, 'tenant', None)
|
|
)
|
|
form.instance.medical_bill = medical_bill
|
|
except MedicalBill.DoesNotExist:
|
|
messages.error(self.request, 'Medical bill not found.')
|
|
return redirect('billing:bill_list')
|
|
|
|
# Generate payment number
|
|
if not form.instance.payment_number:
|
|
form.instance.payment_number = form.instance.generate_payment_number()
|
|
|
|
response = super().form_valid(form)
|
|
|
|
messages.success(
|
|
self.request,
|
|
f'Payment {self.object.payment_number} recorded successfully.'
|
|
)
|
|
|
|
return response
|
|
|
|
def get_success_url(self):
|
|
return reverse('billing:payment_detail', kwargs={'payment_id': self.object.payment_id})
|
|
|
|
|
|
# ============================================================================
|
|
# HTMX VIEWS
|
|
# ============================================================================
|
|
|
|
# @login_required
|
|
# def htmx_billing_stats(request):
|
|
# """
|
|
# HTMX endpoint for billing statistics.
|
|
# """
|
|
# tenant = getattr(request, 'tenant', None)
|
|
# if not tenant:
|
|
# return JsonResponse({'error': 'No tenant found'})
|
|
#
|
|
# today = timezone.now().date()
|
|
#
|
|
# # Calculate statistics
|
|
# bills = MedicalBill.objects.filter(tenant=tenant)
|
|
# stats = {
|
|
# 'total_bills': bills.count(),
|
|
# 'total_revenue': float(bills.aggregate(
|
|
# total=Sum('total_amount')
|
|
# )['total'] or 0),
|
|
# 'total_paid': float(bills.aggregate(
|
|
# total=Sum('paid_amount')
|
|
# )['total'] or 0),
|
|
# 'overdue_bills': bills.filter(
|
|
# due_date__lt=today,
|
|
# status__in=['draft', 'sent', 'partial_payment']
|
|
# ).count()
|
|
# }
|
|
#
|
|
# stats['total_outstanding'] = stats['total_revenue'] - stats['total_paid']
|
|
#
|
|
# return JsonResponse(stats)
|
|
|
|
@login_required
|
|
def htmx_billing_stats(request):
|
|
"""
|
|
HTMX view for billing statistics.
|
|
"""
|
|
tenant = request.user.tenant
|
|
today = timezone.now().date()
|
|
|
|
stats = {
|
|
'total_revenue_today': MedicalBill.objects.filter(
|
|
tenant=tenant,
|
|
bill_date=today,
|
|
status='PAID'
|
|
).aggregate(total=Sum('total_amount'))['total'] or Decimal('0.00'),
|
|
|
|
'outstanding_bills': MedicalBill.objects.filter(
|
|
tenant=tenant,
|
|
status__in=['PENDING', 'SUBMITTED', 'PARTIAL_PAID']
|
|
).count(),
|
|
|
|
'outstanding_amount': MedicalBill.objects.filter(
|
|
tenant=tenant,
|
|
status__in=['PENDING', 'SUBMITTED', 'PARTIAL_PAID']
|
|
).aggregate(total=Sum('balance_amount'))['total'] or Decimal('0.00'),
|
|
|
|
'overdue_bills': MedicalBill.objects.filter(
|
|
tenant=tenant,
|
|
status='OVERDUE'
|
|
).count(),
|
|
|
|
'overdue_amount': MedicalBill.objects.filter(
|
|
tenant=tenant,
|
|
status='OVERDUE'
|
|
).aggregate(total=Sum('balance_amount'))['total'] or Decimal('0.00'),
|
|
|
|
'pending_claims': InsuranceClaim.objects.filter(
|
|
medical_bill__tenant=tenant,
|
|
status__in=['SUBMITTED', 'PENDING', 'PROCESSING']
|
|
).count(),
|
|
|
|
'denied_claims': InsuranceClaim.objects.filter(
|
|
medical_bill__tenant=tenant,
|
|
status='DENIED'
|
|
).count(),
|
|
|
|
'payments_today': Payment.objects.filter(
|
|
medical_bill__tenant=tenant,
|
|
payment_date=today
|
|
).aggregate(total=Sum('payment_amount'))['total'] or Decimal('0.00'),
|
|
|
|
'collections_amount': MedicalBill.objects.filter(
|
|
tenant=tenant,
|
|
collection_status__in=['FIRST_NOTICE', 'SECOND_NOTICE', 'FINAL_NOTICE', 'COLLECTIONS']
|
|
).aggregate(total=Sum('balance_amount'))['total'] or Decimal('0.00'),
|
|
}
|
|
|
|
return render(request, 'billing/partials/billing_stats.html', {'stats': stats})
|
|
|
|
|
|
@login_required
|
|
def htmx_bill_search(request):
|
|
"""
|
|
HTMX endpoint for bill search.
|
|
"""
|
|
search_term = request.GET.get('search', '').strip()
|
|
tenant = getattr(request, 'tenant', None)
|
|
|
|
if not tenant or not search_term:
|
|
return render(request, 'billing/partials/bill_list.html', {'bills': []})
|
|
|
|
bills = MedicalBill.objects.filter(
|
|
tenant=tenant
|
|
).filter(
|
|
Q(bill_number__icontains=search_term) |
|
|
Q(patient__first_name__icontains=search_term) |
|
|
Q(patient__last_name__icontains=search_term) |
|
|
Q(patient__patient_id__icontains=search_term)
|
|
).select_related('patient', 'attending_provider')[:10]
|
|
|
|
return render(request, 'billing/partials/bill_list.html', {'bills': bills})
|
|
|
|
|
|
# ============================================================================
|
|
# ACTION VIEWS
|
|
# ============================================================================
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def submit_bill(request, bill_id):
|
|
"""
|
|
Submit a bill for processing.
|
|
"""
|
|
tenant = getattr(request, 'tenant', None)
|
|
if not tenant:
|
|
return JsonResponse({'success': False, 'error': 'No tenant found'})
|
|
|
|
try:
|
|
bill = MedicalBill.objects.get(bill_id=bill_id, tenant=tenant)
|
|
|
|
if bill.status == 'draft':
|
|
bill.status = 'sent'
|
|
bill.save()
|
|
|
|
messages.success(request, f'Bill {bill.bill_number} submitted successfully.')
|
|
return JsonResponse({'success': True})
|
|
else:
|
|
return JsonResponse({'success': False, 'error': 'Bill is not in draft status'})
|
|
|
|
except MedicalBill.DoesNotExist:
|
|
return JsonResponse({'success': False, 'error': 'Bill not found'})
|
|
|
|
|
|
# ============================================================================
|
|
# EXPORT VIEWS
|
|
# ============================================================================
|
|
|
|
@login_required
|
|
def export_bills(request):
|
|
"""
|
|
Export bills to CSV.
|
|
"""
|
|
tenant = getattr(request, 'tenant', None)
|
|
if not tenant:
|
|
return HttpResponse('No tenant found', status=400)
|
|
|
|
# Create CSV response
|
|
response = HttpResponse(content_type='text/csv')
|
|
response['Content-Disposition'] = 'attachment; filename="medical_bills.csv"'
|
|
|
|
writer = csv.writer(response)
|
|
writer.writerow([
|
|
'Bill Number', 'Patient Name', 'Bill Date', 'Due Date',
|
|
'Total Amount', 'Paid Amount', 'Balance', 'Status'
|
|
])
|
|
|
|
bills = MedicalBill.objects.filter(tenant=tenant).select_related('patient')
|
|
|
|
for bill in bills:
|
|
writer.writerow([
|
|
bill.bill_number,
|
|
bill.patient.get_full_name(),
|
|
bill.bill_date.strftime('%Y-%m-%d'),
|
|
bill.due_date.strftime('%Y-%m-%d') if bill.due_date else '',
|
|
str(bill.total_amount),
|
|
str(bill.paid_amount),
|
|
str(bill.balance_amount),
|
|
bill.get_status_display()
|
|
])
|
|
|
|
return response
|
|
|
|
|
|
# ============================================================================
|
|
# PRINT VIEWS
|
|
# ============================================================================
|
|
|
|
@login_required
|
|
def print_bills(request):
|
|
"""
|
|
Print bills view with print-friendly formatting.
|
|
Supports both individual bill printing and bulk printing.
|
|
"""
|
|
tenant = getattr(request, 'tenant', None)
|
|
if not tenant:
|
|
return HttpResponse('No tenant found', status=400)
|
|
|
|
# Get bill IDs from request
|
|
bill_ids = request.GET.getlist('bill_ids')
|
|
single_bill_id = request.GET.get('bill_id')
|
|
|
|
if single_bill_id:
|
|
bill_ids = [single_bill_id]
|
|
|
|
if not bill_ids:
|
|
messages.error(request, 'No bills selected for printing.')
|
|
return redirect('billing:bill_list')
|
|
|
|
# Get bills with related data
|
|
bills = MedicalBill.objects.filter(
|
|
tenant=tenant,
|
|
bill_id__in=bill_ids
|
|
).select_related(
|
|
'patient',
|
|
'encounter',
|
|
'admission',
|
|
'created_by'
|
|
).prefetch_related(
|
|
'billlineitem_set',
|
|
'payment_set',
|
|
'insuranceclaim_set'
|
|
)
|
|
|
|
if not bills.exists():
|
|
messages.error(request, 'No bills found for printing.')
|
|
return redirect('billing:bill_list')
|
|
|
|
# Calculate totals for each bill
|
|
bills_data = []
|
|
for bill in bills:
|
|
line_items = bill.billlineitem_set.all()
|
|
payments = bill.payment_set.filter(status='completed')
|
|
claims = bill.insuranceclaim_set.all()
|
|
|
|
bills_data.append({
|
|
'bill': bill,
|
|
'line_items': line_items,
|
|
'payments': payments,
|
|
'claims': claims,
|
|
'subtotal': sum(item.total_amount for item in line_items),
|
|
'total_payments': sum(payment.amount for payment in payments),
|
|
'total_claimed': sum(claim.claim_amount for claim in claims),
|
|
})
|
|
|
|
context = {
|
|
'bills_data': bills_data,
|
|
'print_date': timezone.now(),
|
|
'tenant': tenant,
|
|
'is_bulk_print': len(bills_data) > 1,
|
|
}
|
|
|
|
return render(request, 'billing/print_bills.html', context)
|
|
|
|
|
|
@login_required
|
|
def print_bill_detail(request, bill_id):
|
|
"""
|
|
Print detailed view for a single bill.
|
|
"""
|
|
tenant = getattr(request, 'tenant', None)
|
|
if not tenant:
|
|
return HttpResponse('No tenant found', status=400)
|
|
|
|
bill = get_object_or_404(
|
|
MedicalBill.objects.select_related(
|
|
'patient',
|
|
'encounter',
|
|
'admission',
|
|
'created_by'
|
|
).prefetch_related(
|
|
'billlineitem_set',
|
|
'payment_set',
|
|
'insuranceclaim_set'
|
|
),
|
|
tenant=tenant,
|
|
bill_id=bill_id
|
|
)
|
|
|
|
# Get related data
|
|
line_items = bill.billlineitem_set.all().order_by('line_number')
|
|
payments = bill.payment_set.filter(status='completed').order_by('-payment_date')
|
|
claims = bill.insuranceclaim_set.all().order_by('-submission_date')
|
|
|
|
# Calculate totals
|
|
subtotal = sum(item.total_amount for item in line_items)
|
|
total_payments = sum(payment.amount for payment in payments)
|
|
total_claimed = sum(claim.claim_amount for claim in claims)
|
|
|
|
context = {
|
|
'bill': bill,
|
|
'line_items': line_items,
|
|
'payments': payments,
|
|
'claims': claims,
|
|
'subtotal': subtotal,
|
|
'total_payments': total_payments,
|
|
'total_claimed': total_claimed,
|
|
'print_date': timezone.now(),
|
|
'tenant': tenant,
|
|
}
|
|
|
|
return render(request, 'billing/print_bill_detail.html', context)
|
|
|
|
|
|
@login_required
|
|
@require_http_methods(["POST"])
|
|
def bulk_submit_bills(request):
|
|
"""
|
|
Submit multiple bills for processing in bulk.
|
|
Validates each bill and provides detailed feedback on success/failure.
|
|
"""
|
|
tenant = getattr(request, 'tenant', None)
|
|
if not tenant:
|
|
return JsonResponse({'success': False, 'error': 'No tenant found'})
|
|
|
|
# Get bill IDs from request
|
|
bill_ids = request.POST.getlist('bill_ids')
|
|
|
|
if not bill_ids:
|
|
return JsonResponse({
|
|
'success': False,
|
|
'error': 'No bills selected for submission'
|
|
})
|
|
|
|
# Track results
|
|
submitted_bills = []
|
|
failed_bills = []
|
|
already_submitted = []
|
|
not_found = []
|
|
|
|
# Process each bill
|
|
for bill_id in bill_ids:
|
|
try:
|
|
bill = MedicalBill.objects.get(bill_id=bill_id, tenant=tenant)
|
|
|
|
if bill.status == 'draft':
|
|
# Validate bill before submission
|
|
validation_errors = []
|
|
|
|
# Check if bill has line items
|
|
if not bill.billlineitem_set.exists():
|
|
validation_errors.append('No line items')
|
|
|
|
# Check if bill has valid amounts
|
|
if bill.total_amount <= 0:
|
|
validation_errors.append('Invalid total amount')
|
|
|
|
# Check if patient information is complete
|
|
if not bill.patient:
|
|
validation_errors.append('No patient assigned')
|
|
|
|
# Check if bill has required fields
|
|
if not bill.bill_date:
|
|
validation_errors.append('Missing bill date')
|
|
|
|
if validation_errors:
|
|
failed_bills.append({
|
|
'bill_id': str(bill.bill_id),
|
|
'bill_number': bill.bill_number,
|
|
'errors': validation_errors
|
|
})
|
|
else:
|
|
# Submit the bill
|
|
bill.status = 'submitted'
|
|
# Note: submission_date and submitted_by fields don't exist in model
|
|
# Using available fields instead
|
|
bill.save()
|
|
|
|
submitted_bills.append({
|
|
'bill_id': str(bill.bill_id),
|
|
'bill_number': bill.bill_number,
|
|
'patient_name': bill.patient.get_full_name() if bill.patient else 'Unknown',
|
|
'total_amount': str(bill.total_amount)
|
|
})
|
|
|
|
elif bill.status in ['submitted', 'paid', 'cancelled']:
|
|
already_submitted.append({
|
|
'bill_id': str(bill.bill_id),
|
|
'bill_number': bill.bill_number,
|
|
'status': bill.get_status_display()
|
|
})
|
|
else:
|
|
failed_bills.append({
|
|
'bill_id': str(bill.bill_id),
|
|
'bill_number': bill.bill_number,
|
|
'errors': [f'Invalid status: {bill.get_status_display()}']
|
|
})
|
|
|
|
except MedicalBill.DoesNotExist:
|
|
not_found.append({
|
|
'bill_id': bill_id,
|
|
'error': 'Bill not found'
|
|
})
|
|
except Exception as e:
|
|
failed_bills.append({
|
|
'bill_id': bill_id,
|
|
'bill_number': 'Unknown',
|
|
'errors': [f'Unexpected error: {str(e)}']
|
|
})
|
|
|
|
# Prepare response
|
|
total_processed = len(bill_ids)
|
|
total_submitted = len(submitted_bills)
|
|
total_failed = len(failed_bills) + len(not_found)
|
|
total_already_submitted = len(already_submitted)
|
|
|
|
# Create success message
|
|
messages_list = []
|
|
if total_submitted > 0:
|
|
messages_list.append(f'{total_submitted} bill(s) submitted successfully')
|
|
if total_already_submitted > 0:
|
|
messages_list.append(f'{total_already_submitted} bill(s) were already submitted')
|
|
if total_failed > 0:
|
|
messages_list.append(f'{total_failed} bill(s) failed to submit')
|
|
|
|
# Add Django messages
|
|
if total_submitted > 0:
|
|
messages.success(request, f'Successfully submitted {total_submitted} bill(s)')
|
|
if total_failed > 0:
|
|
messages.warning(request, f'{total_failed} bill(s) could not be submitted')
|
|
if total_already_submitted > 0:
|
|
messages.info(request, f'{total_already_submitted} bill(s) were already submitted')
|
|
|
|
# Return detailed response
|
|
response_data = {
|
|
'success': total_submitted > 0,
|
|
'summary': {
|
|
'total_processed': total_processed,
|
|
'total_submitted': total_submitted,
|
|
'total_failed': total_failed,
|
|
'total_already_submitted': total_already_submitted
|
|
},
|
|
'results': {
|
|
'submitted': submitted_bills,
|
|
'failed': failed_bills,
|
|
'already_submitted': already_submitted,
|
|
'not_found': not_found
|
|
},
|
|
'message': ' | '.join(messages_list) if messages_list else 'No bills processed'
|
|
}
|
|
|
|
return JsonResponse(response_data)
|
|
|
|
|
|
@login_required
|
|
def bulk_submit_bills_form(request):
|
|
"""
|
|
Display form for bulk bill submission with bill selection.
|
|
"""
|
|
tenant = getattr(request, 'tenant', None)
|
|
if not tenant:
|
|
messages.error(request, 'No tenant found')
|
|
return redirect('billing:bill_list')
|
|
|
|
# Get draft bills that can be submitted
|
|
draft_bills = MedicalBill.objects.filter(
|
|
tenant=tenant,
|
|
status='draft'
|
|
).select_related('patient').order_by('-bill_date')
|
|
|
|
# Add validation status for each bill
|
|
bills_with_status = []
|
|
for bill in draft_bills:
|
|
validation_errors = []
|
|
|
|
# Check validation criteria
|
|
if not bill.billlineitem_set.exists():
|
|
validation_errors.append('No line items')
|
|
if bill.total_amount <= 0:
|
|
validation_errors.append('Invalid amount')
|
|
if not bill.patient:
|
|
validation_errors.append('No patient')
|
|
if not bill.bill_date:
|
|
validation_errors.append('No bill date')
|
|
|
|
bills_with_status.append({
|
|
'bill': bill,
|
|
'can_submit': len(validation_errors) == 0,
|
|
'validation_errors': validation_errors
|
|
})
|
|
|
|
context = {
|
|
'bills_with_status': bills_with_status,
|
|
'total_draft_bills': len(bills_with_status),
|
|
'submittable_bills': len([b for b in bills_with_status if b['can_submit']])
|
|
}
|
|
|
|
return render(request, 'billing/bulk_submit_bills.html', context)
|
|
|
|
|
|
@login_required
|
|
def payment_receipt(request, payment_id):
|
|
"""
|
|
Generate and display payment receipt.
|
|
"""
|
|
tenant = getattr(request, 'tenant', None)
|
|
if not tenant:
|
|
return HttpResponse('No tenant found', status=400)
|
|
|
|
try:
|
|
# Get payment with related objects
|
|
payment = Payment.objects.select_related(
|
|
'medical_bill', 'medical_bill__patient', 'processed_by'
|
|
).get(id=payment_id, medical_bill__tenant=tenant)
|
|
|
|
# Calculate payment details
|
|
payment_details = {
|
|
'payment': payment,
|
|
'bill': payment.medical_bill,
|
|
'patient': payment.medical_bill.patient,
|
|
'payment_date': payment.payment_date,
|
|
'amount_paid': payment.payment_amount,
|
|
'payment_method': payment.get_payment_method_display(),
|
|
'reference_number': payment.transaction_id,
|
|
'notes': payment.notes,
|
|
'processed_by': payment.processed_by,
|
|
'receipt_number': f"RCP-{payment.id:06d}",
|
|
'balance_after_payment': payment.medical_bill.balance_amount,
|
|
}
|
|
|
|
# Add hospital/tenant information
|
|
hospital_info = {
|
|
'name': tenant.name,
|
|
'address': getattr(tenant, 'address', ''),
|
|
'phone': getattr(tenant, 'phone', ''),
|
|
'email': getattr(tenant, 'email', ''),
|
|
'website': getattr(tenant, 'website', ''),
|
|
}
|
|
|
|
context = {
|
|
'payment_details': payment_details,
|
|
'hospital_info': hospital_info,
|
|
'print_date': timezone.now(),
|
|
}
|
|
|
|
return render(request, 'billing/payment_receipt.html', context)
|
|
|
|
except Payment.DoesNotExist:
|
|
messages.error(request, 'Payment not found.')
|
|
return redirect('billing:bill_list')
|
|
except Exception as e:
|
|
messages.error(request, f'Error generating receipt: {str(e)}')
|
|
return redirect('billing:bill_list')
|
|
|
|
|
|
@login_required
|
|
def payment_email(request, payment_id):
|
|
"""
|
|
Send payment confirmation email to patient.
|
|
"""
|
|
tenant = getattr(request, 'tenant', None)
|
|
if not tenant:
|
|
return HttpResponse('No tenant found', status=400)
|
|
|
|
try:
|
|
# Get payment with related objects
|
|
payment = Payment.objects.select_related(
|
|
'bill', 'bill__patient', 'created_by'
|
|
).get(id=payment_id, tenant=tenant)
|
|
|
|
if request.method == 'POST':
|
|
# Send email
|
|
from django.core.mail import send_mail
|
|
from django.template.loader import render_to_string
|
|
from django.utils.html import strip_tags
|
|
|
|
# Prepare email context
|
|
email_context = {
|
|
'payment': payment,
|
|
'bill': payment.bill,
|
|
'patient': payment.bill.patient,
|
|
'hospital_info': {
|
|
'name': tenant.name,
|
|
'address': getattr(tenant, 'address', ''),
|
|
'phone': getattr(tenant, 'phone', ''),
|
|
'email': getattr(tenant, 'email', ''),
|
|
'website': getattr(tenant, 'website', ''),
|
|
},
|
|
'receipt_number': f"RCP-{payment.id:06d}",
|
|
'receipt_url': request.build_absolute_uri(
|
|
reverse('billing:payment_receipt', args=[payment.id])
|
|
),
|
|
}
|
|
|
|
# Render email template
|
|
html_message = render_to_string('billing/payment_email.html', email_context)
|
|
plain_message = strip_tags(html_message)
|
|
|
|
# Email details
|
|
subject = f'Payment Confirmation - Receipt #{email_context["receipt_number"]}'
|
|
from_email = getattr(tenant, 'email', 'noreply@hospital.com')
|
|
recipient_list = [payment.bill.patient.email] if payment.bill.patient.email else []
|
|
|
|
# Add additional recipients if specified
|
|
additional_emails = request.POST.get('additional_emails', '').strip()
|
|
if additional_emails:
|
|
additional_list = [email.strip() for email in additional_emails.split(',') if email.strip()]
|
|
recipient_list.extend(additional_list)
|
|
|
|
if not recipient_list:
|
|
messages.error(request, 'No email addresses found. Please provide at least one email address.')
|
|
return redirect('billing:payment_email', payment_id=payment_id)
|
|
|
|
try:
|
|
# Send email
|
|
send_mail(
|
|
subject=subject,
|
|
message=plain_message,
|
|
from_email=from_email,
|
|
recipient_list=recipient_list,
|
|
html_message=html_message,
|
|
fail_silently=False,
|
|
)
|
|
|
|
# Log email sent
|
|
messages.success(request,
|
|
f'Payment confirmation email sent successfully to {", ".join(recipient_list)}.')
|
|
|
|
# Redirect to payment detail
|
|
return redirect('billing:payment_detail', payment_id=payment.id)
|
|
|
|
except Exception as e:
|
|
messages.error(request, f'Failed to send email: {str(e)}')
|
|
return redirect('billing:payment_email', payment_id=payment_id)
|
|
|
|
# GET request - show email form
|
|
context = {
|
|
'payment': payment,
|
|
'bill': payment.bill,
|
|
'patient': payment.bill.patient,
|
|
'receipt_number': f"RCP-{payment.id:06d}",
|
|
}
|
|
|
|
return render(request, 'billing/payment_email_form.html', context)
|
|
|
|
except Payment.DoesNotExist:
|
|
messages.error(request, 'Payment not found.')
|
|
return redirect('billing:payment_list')
|
|
except Exception as e:
|
|
messages.error(request, f'Error processing email: {str(e)}')
|
|
return redirect('billing:payment_list')
|
|
|
|
|
|
@login_required
|
|
def payment_download(request, payment_id):
|
|
"""
|
|
Download payment receipt as PDF.
|
|
"""
|
|
tenant = getattr(request, 'tenant', None)
|
|
if not tenant:
|
|
return HttpResponse('No tenant found', status=400)
|
|
|
|
try:
|
|
# Get payment with related objects
|
|
payment = Payment.objects.select_related(
|
|
'bill', 'bill__patient', 'created_by'
|
|
).get(id=payment_id, tenant=tenant)
|
|
|
|
# Import PDF generation libraries
|
|
from django.template.loader import render_to_string
|
|
from weasyprint import HTML, CSS
|
|
from django.conf import settings
|
|
import os
|
|
|
|
# Calculate payment details
|
|
payment_details = {
|
|
'payment': payment,
|
|
'bill': payment.bill,
|
|
'patient': payment.bill.patient,
|
|
'payment_date': payment.payment_date,
|
|
'amount_paid': payment.amount,
|
|
'payment_method': payment.get_payment_method_display(),
|
|
'reference_number': payment.reference_number,
|
|
'notes': payment.notes,
|
|
'processed_by': payment.created_by,
|
|
'receipt_number': f"RCP-{payment.id:06d}",
|
|
'balance_after_payment': payment.bill.balance_amount,
|
|
}
|
|
|
|
# Add hospital/tenant information
|
|
hospital_info = {
|
|
'name': tenant.name,
|
|
'address': getattr(tenant, 'address', ''),
|
|
'phone': getattr(tenant, 'phone', ''),
|
|
'email': getattr(tenant, 'email', ''),
|
|
'website': getattr(tenant, 'website', ''),
|
|
}
|
|
|
|
context = {
|
|
'payment_details': payment_details,
|
|
'hospital_info': hospital_info,
|
|
'print_date': timezone.now(),
|
|
'is_pdf': True, # Flag to indicate PDF generation
|
|
}
|
|
|
|
# Render HTML template for PDF
|
|
html_string = render_to_string('billing/payment_receipt_pdf.html', context)
|
|
|
|
# Generate PDF
|
|
html = HTML(string=html_string)
|
|
|
|
# Create CSS for PDF styling
|
|
css_string = """
|
|
@page {
|
|
size: A4;
|
|
margin: 1cm;
|
|
}
|
|
body {
|
|
font-family: Arial, sans-serif;
|
|
font-size: 12px;
|
|
line-height: 1.4;
|
|
color: #000;
|
|
}
|
|
.receipt-header {
|
|
text-align: center;
|
|
border-bottom: 2px solid #000;
|
|
padding-bottom: 20px;
|
|
margin-bottom: 30px;
|
|
}
|
|
.hospital-name {
|
|
font-size: 24px;
|
|
font-weight: bold;
|
|
margin-bottom: 10px;
|
|
}
|
|
.receipt-title {
|
|
font-size: 18px;
|
|
font-weight: bold;
|
|
margin-top: 20px;
|
|
}
|
|
.section-title {
|
|
font-weight: bold;
|
|
border-bottom: 1px solid #000;
|
|
padding-bottom: 5px;
|
|
margin-bottom: 10px;
|
|
margin-top: 20px;
|
|
}
|
|
.detail-table {
|
|
width: 100%;
|
|
border-collapse: collapse;
|
|
margin-bottom: 20px;
|
|
}
|
|
.detail-table td {
|
|
padding: 5px;
|
|
border-bottom: 1px solid #ccc;
|
|
}
|
|
.detail-label {
|
|
font-weight: bold;
|
|
width: 40%;
|
|
}
|
|
.amount-paid {
|
|
text-align: center;
|
|
border: 2px solid #000;
|
|
padding: 15px;
|
|
margin: 20px 0;
|
|
}
|
|
.amount-value {
|
|
font-size: 24px;
|
|
font-weight: bold;
|
|
}
|
|
.footer {
|
|
margin-top: 40px;
|
|
border-top: 1px solid #000;
|
|
padding-top: 20px;
|
|
font-size: 10px;
|
|
}
|
|
.signature-line {
|
|
border-bottom: 1px solid #000;
|
|
width: 200px;
|
|
margin: 20px auto 10px auto;
|
|
}
|
|
"""
|
|
|
|
css = CSS(string=css_string)
|
|
pdf = html.write_pdf(stylesheets=[css])
|
|
|
|
# Create response
|
|
response = HttpResponse(pdf, content_type='application/pdf')
|
|
response[
|
|
'Content-Disposition'] = f'attachment; filename="payment_receipt_{payment_details["receipt_number"]}.pdf"'
|
|
|
|
return response
|
|
|
|
except Payment.DoesNotExist:
|
|
messages.error(request, 'Payment not found.')
|
|
return redirect('billing:payment_list')
|
|
except ImportError:
|
|
# Fallback if weasyprint is not available
|
|
messages.error(request, 'PDF generation is not available. Please use the print function instead.')
|
|
return redirect('billing:payment_receipt', payment_id=payment_id)
|
|
except Exception as e:
|
|
messages.error(request, f'Error generating PDF: {str(e)}')
|
|
return redirect('billing:payment_receipt', payment_id=payment_id)
|
|
|
|
#
|
|
#
|
|
# """
|
|
# Billing app views for hospital management system.
|
|
# Provides medical billing, insurance claims, and revenue cycle management.
|
|
# """
|
|
#
|
|
# from django.shortcuts import render, get_object_or_404, redirect
|
|
# from django.contrib.auth.decorators import login_required
|
|
# from django.contrib import messages
|
|
# from django.http import JsonResponse, HttpResponse
|
|
# from django.db.models import Q, Count, Sum, Avg, Max, Min
|
|
# from django.utils import timezone
|
|
# from django.core.paginator import Paginator
|
|
# from django.views.decorators.http import require_http_methods
|
|
# from django.views.generic import ListView, DetailView, CreateView, UpdateView
|
|
# from django.contrib.auth.mixins import LoginRequiredMixin
|
|
# from django.urls import reverse
|
|
# from datetime import datetime, timedelta, date
|
|
# from decimal import Decimal
|
|
# import json
|
|
# import csv
|
|
#
|
|
# from .models import (
|
|
# MedicalBill, BillLineItem, InsuranceClaim, Payment,
|
|
# ClaimStatusUpdate, BillingConfiguration
|
|
# )
|
|
# from patients.models import PatientProfile, InsuranceInfo
|
|
# from emr.models import Encounter
|
|
# from inpatients.models import Admission
|
|
# from core.utils import AuditLogger
|
|
#
|
|
#
|
|
# class BillingDashboardView(LoginRequiredMixin, ListView):
|
|
# """
|
|
# Billing dashboard view with comprehensive statistics and recent activity.
|
|
# """
|
|
# template_name = 'billing/dashboard.html'
|
|
# context_object_name = 'recent_bills'
|
|
#
|
|
# def get_queryset(self):
|
|
# return MedicalBill.objects.filter(
|
|
# tenant=self.request.user.tenant
|
|
# ).select_related(
|
|
# 'patient', 'encounter', 'admission'
|
|
# ).order_by('-bill_date')[:10]
|
|
#
|
|
# def get_context_data(self, **kwargs):
|
|
# context = super().get_context_data(**kwargs)
|
|
# tenant = self.request.user.tenant
|
|
# today = timezone.now().date()
|
|
#
|
|
# # Financial statistics
|
|
# context['stats'] = {
|
|
# 'total_revenue_today': MedicalBill.objects.filter(
|
|
# tenant=tenant,
|
|
# bill_date=today,
|
|
# status='PAID'
|
|
# ).aggregate(total=Sum('total_amount'))['total'] or Decimal('0.00'),
|
|
#
|
|
# 'outstanding_bills': MedicalBill.objects.filter(
|
|
# tenant=tenant,
|
|
# status__in=['PENDING', 'SUBMITTED', 'PARTIAL_PAID']
|
|
# ).count(),
|
|
#
|
|
# 'outstanding_amount': MedicalBill.objects.filter(
|
|
# tenant=tenant,
|
|
# status__in=['PENDING', 'SUBMITTED', 'PARTIAL_PAID']
|
|
# ).aggregate(total=Sum('balance_amount'))['total'] or Decimal('0.00'),
|
|
#
|
|
# 'overdue_bills': MedicalBill.objects.filter(
|
|
# tenant=tenant,
|
|
# status='OVERDUE'
|
|
# ).count(),
|
|
#
|
|
# 'overdue_amount': MedicalBill.objects.filter(
|
|
# tenant=tenant,
|
|
# status='OVERDUE'
|
|
# ).aggregate(total=Sum('balance_amount'))['total'] or Decimal('0.00'),
|
|
#
|
|
# 'pending_claims': InsuranceClaim.objects.filter(
|
|
# medical_bill__tenant=tenant,
|
|
# status__in=['SUBMITTED', 'PENDING', 'PROCESSING']
|
|
# ).count(),
|
|
#
|
|
# 'denied_claims': InsuranceClaim.objects.filter(
|
|
# medical_bill__tenant=tenant,
|
|
# status='DENIED'
|
|
# ).count(),
|
|
#
|
|
# 'payments_today': Payment.objects.filter(
|
|
# medical_bill__tenant=tenant,
|
|
# payment_date=today
|
|
# ).aggregate(total=Sum('payment_amount'))['total'] or Decimal('0.00'),
|
|
#
|
|
# 'collections_amount': MedicalBill.objects.filter(
|
|
# tenant=tenant,
|
|
# collection_status__in=['FIRST_NOTICE', 'SECOND_NOTICE', 'FINAL_NOTICE', 'COLLECTIONS']
|
|
# ).aggregate(total=Sum('balance_amount'))['total'] or Decimal('0.00'),
|
|
# }
|
|
#
|
|
# # Recent payments
|
|
# context['recent_payments'] = Payment.objects.filter(
|
|
# medical_bill__tenant=tenant
|
|
# ).select_related(
|
|
# 'medical_bill', 'medical_bill__patient'
|
|
# ).order_by('-payment_date')[:10]
|
|
#
|
|
# # Overdue bills
|
|
# context['overdue_bills'] = MedicalBill.objects.filter(
|
|
# tenant=tenant,
|
|
# status='OVERDUE'
|
|
# ).select_related(
|
|
# 'patient'
|
|
# ).order_by('-due_date')[:5]
|
|
#
|
|
# # Pending claims
|
|
# context['pending_claims'] = InsuranceClaim.objects.filter(
|
|
# medical_bill__tenant=tenant,
|
|
# status__in=['SUBMITTED', 'PENDING', 'PROCESSING']
|
|
# ).select_related(
|
|
# 'medical_bill__patient', 'insurance_info'
|
|
# ).order_by('-submission_date')[:5]
|
|
#
|
|
# return context
|
|
#
|
|
#
|
|
# class MedicalBillListView(LoginRequiredMixin, ListView):
|
|
# """
|
|
# Medical bill list view with advanced filtering and search.
|
|
# """
|
|
# model = MedicalBill
|
|
# template_name = 'billing/bill_list.html'
|
|
# context_object_name = 'bills'
|
|
# paginate_by = 25
|
|
#
|
|
# def get_queryset(self):
|
|
# queryset = MedicalBill.objects.filter(
|
|
# tenant=self.request.user.tenant
|
|
# ).select_related(
|
|
# 'patient', 'encounter', 'admission', 'attending_provider'
|
|
# )
|
|
#
|
|
# # Apply filters
|
|
# status = self.request.GET.get('status')
|
|
# bill_type = self.request.GET.get('bill_type')
|
|
# patient_id = self.request.GET.get('patient_id')
|
|
# provider_id = self.request.GET.get('provider_id')
|
|
# date_from = self.request.GET.get('date_from')
|
|
# date_to = self.request.GET.get('date_to')
|
|
# search = self.request.GET.get('search')
|
|
#
|
|
# if status:
|
|
# queryset = queryset.filter(status=status)
|
|
# if bill_type:
|
|
# queryset = queryset.filter(bill_type=bill_type)
|
|
# if patient_id:
|
|
# queryset = queryset.filter(patient_id=patient_id)
|
|
# if provider_id:
|
|
# queryset = queryset.filter(attending_provider_id=provider_id)
|
|
# if date_from:
|
|
# queryset = queryset.filter(bill_date__gte=date_from)
|
|
# if date_to:
|
|
# queryset = queryset.filter(bill_date__lte=date_to)
|
|
# if search:
|
|
# queryset = queryset.filter(
|
|
# Q(bill_number__icontains=search) |
|
|
# Q(patient__first_name__icontains=search) |
|
|
# Q(patient__last_name__icontains=search) |
|
|
# Q(patient__mrn__icontains=search)
|
|
# )
|
|
#
|
|
# return queryset.order_by('-bill_date')
|
|
#
|
|
# def get_context_data(self, **kwargs):
|
|
# context = super().get_context_data(**kwargs)
|
|
#
|
|
# # Add filter values to context
|
|
# context.update({
|
|
# 'status': self.request.GET.get('status', ''),
|
|
# 'bill_type': self.request.GET.get('bill_type', ''),
|
|
# 'patient_id': self.request.GET.get('patient_id', ''),
|
|
# 'provider_id': self.request.GET.get('provider_id', ''),
|
|
# 'date_from': self.request.GET.get('date_from', ''),
|
|
# 'date_to': self.request.GET.get('date_to', ''),
|
|
# 'search': self.request.GET.get('search', ''),
|
|
# })
|
|
#
|
|
# # Statistics
|
|
# queryset = self.get_queryset()
|
|
# context['stats'] = {
|
|
# 'total_bills': queryset.count(),
|
|
# 'total_amount': queryset.aggregate(total=Sum('total_amount'))['total'] or Decimal('0.00'),
|
|
# 'total_paid': queryset.aggregate(total=Sum('paid_amount'))['total'] or Decimal('0.00'),
|
|
# 'total_balance': queryset.aggregate(total=Sum('balance_amount'))['total'] or Decimal('0.00'),
|
|
# }
|
|
#
|
|
# return context
|
|
#
|
|
#
|
|
# class MedicalBillDetailView(LoginRequiredMixin, DetailView):
|
|
# """
|
|
# Medical bill detail view with line items, payments, and claims.
|
|
# """
|
|
# model = MedicalBill
|
|
# template_name = 'billing/bill_detail.html'
|
|
# context_object_name = 'bill'
|
|
# slug_field = 'bill_id'
|
|
# slug_url_kwarg = 'bill_id'
|
|
#
|
|
# def get_queryset(self):
|
|
# return MedicalBill.objects.filter(
|
|
# tenant=self.request.user.tenant
|
|
# ).select_related(
|
|
# 'patient', 'encounter', 'admission', 'attending_provider', 'billing_provider'
|
|
# ).prefetch_related(
|
|
# 'line_items', 'payments', 'insurance_claims'
|
|
# )
|
|
#
|
|
# def get_context_data(self, **kwargs):
|
|
# context = super().get_context_data(**kwargs)
|
|
# bill = self.object
|
|
#
|
|
# # Line items
|
|
# context['line_items'] = bill.line_items.all().order_by('line_number')
|
|
#
|
|
# # Payments
|
|
# context['payments'] = bill.payments.all().order_by('-payment_date')
|
|
#
|
|
# # Insurance claims
|
|
# context['insurance_claims'] = bill.insurance_claims.all().order_by('-submission_date')
|
|
#
|
|
# # Payment summary
|
|
# context['payment_summary'] = {
|
|
# 'total_payments': bill.payments.aggregate(total=Sum('payment_amount'))['total'] or Decimal('0.00'),
|
|
# 'insurance_payments': bill.payments.filter(payment_source='INSURANCE').aggregate(total=Sum('payment_amount'))['total'] or Decimal('0.00'),
|
|
# 'patient_payments': bill.payments.filter(payment_source='PATIENT').aggregate(total=Sum('payment_amount'))['total'] or Decimal('0.00'),
|
|
# }
|
|
#
|
|
# return context
|
|
#
|
|
#
|
|
# class MedicalBillCreateView(LoginRequiredMixin, CreateView):
|
|
# """
|
|
# Medical bill creation view.
|
|
# """
|
|
# model = MedicalBill
|
|
# template_name = 'billing/bill_create.html'
|
|
# fields = [
|
|
# 'patient', 'bill_type', 'service_date_from', 'service_date_to',
|
|
# 'attending_provider', 'billing_provider', 'encounter', 'admission',
|
|
# 'primary_insurance', 'secondary_insurance', 'payment_terms', 'notes'
|
|
# ]
|
|
#
|
|
# def form_valid(self, form):
|
|
# form.instance.tenant = self.request.user.tenant
|
|
# form.instance.created_by = self.request.user
|
|
#
|
|
# # Set due date based on payment terms
|
|
# if form.instance.payment_terms == 'NET_30':
|
|
# form.instance.due_date = form.instance.bill_date + timedelta(days=30)
|
|
# elif form.instance.payment_terms == 'NET_60':
|
|
# form.instance.due_date = form.instance.bill_date + timedelta(days=60)
|
|
# elif form.instance.payment_terms == 'NET_90':
|
|
# form.instance.due_date = form.instance.bill_date + timedelta(days=90)
|
|
# else:
|
|
# form.instance.due_date = form.instance.bill_date + timedelta(days=30)
|
|
#
|
|
# response = super().form_valid(form)
|
|
#
|
|
# # Log audit event
|
|
# AuditLogger.log_event(
|
|
# self.request.user,
|
|
# 'MEDICAL_BILL_CREATED',
|
|
# 'MedicalBill',
|
|
# str(self.object.bill_id),
|
|
# f"Created medical bill {self.object.bill_number} for {self.object.patient.get_full_name()}"
|
|
# )
|
|
#
|
|
# messages.success(self.request, 'Medical bill created successfully')
|
|
# return response
|
|
#
|
|
# def get_success_url(self):
|
|
# return reverse('billing:bill_detail', kwargs={'bill_id': self.object.bill_id})
|
|
#
|
|
#
|
|
# class InsuranceClaimListView(LoginRequiredMixin, ListView):
|
|
# """
|
|
# Insurance claim list view with filtering and search.
|
|
# """
|
|
# model = InsuranceClaim
|
|
# template_name = 'billing/claim_list.html'
|
|
# context_object_name = 'claims'
|
|
# paginate_by = 25
|
|
#
|
|
# def get_queryset(self):
|
|
# queryset = InsuranceClaim.objects.filter(
|
|
# medical_bill__tenant=self.request.user.tenant
|
|
# ).select_related(
|
|
# 'medical_bill', 'medical_bill__patient', 'insurance_info'
|
|
# )
|
|
#
|
|
# # Apply filters
|
|
# status = self.request.GET.get('status')
|
|
# claim_type = self.request.GET.get('claim_type')
|
|
# insurance_id = self.request.GET.get('insurance_id')
|
|
# date_from = self.request.GET.get('date_from')
|
|
# date_to = self.request.GET.get('date_to')
|
|
# search = self.request.GET.get('search')
|
|
#
|
|
# if status:
|
|
# queryset = queryset.filter(status=status)
|
|
# if claim_type:
|
|
# queryset = queryset.filter(claim_type=claim_type)
|
|
# if insurance_id:
|
|
# queryset = queryset.filter(insurance_info_id=insurance_id)
|
|
# if date_from:
|
|
# queryset = queryset.filter(submission_date__gte=date_from)
|
|
# if date_to:
|
|
# queryset = queryset.filter(submission_date__lte=date_to)
|
|
# if search:
|
|
# queryset = queryset.filter(
|
|
# Q(claim_number__icontains=search) |
|
|
# Q(medical_bill__bill_number__icontains=search) |
|
|
# Q(medical_bill__patient__first_name__icontains=search) |
|
|
# Q(medical_bill__patient__last_name__icontains=search)
|
|
# )
|
|
#
|
|
# return queryset.order_by('-submission_date')
|
|
#
|
|
# def get_context_data(self, **kwargs):
|
|
# context = super().get_context_data(**kwargs)
|
|
#
|
|
# # Add filter values to context
|
|
# context.update({
|
|
# 'status': self.request.GET.get('status', ''),
|
|
# 'claim_type': self.request.GET.get('claim_type', ''),
|
|
# 'insurance_id': self.request.GET.get('insurance_id', ''),
|
|
# 'date_from': self.request.GET.get('date_from', ''),
|
|
# 'date_to': self.request.GET.get('date_to', ''),
|
|
# 'search': self.request.GET.get('search', ''),
|
|
# })
|
|
#
|
|
# # Statistics
|
|
# queryset = self.get_queryset()
|
|
# context['stats'] = {
|
|
# 'total_claims': queryset.count(),
|
|
# 'total_billed': queryset.aggregate(total=Sum('billed_amount'))['total'] or Decimal('0.00'),
|
|
# 'total_paid': queryset.aggregate(total=Sum('paid_amount'))['total'] or Decimal('0.00'),
|
|
# 'total_patient_responsibility': queryset.aggregate(total=Sum('patient_responsibility'))['total'] or Decimal('0.00'),
|
|
# }
|
|
#
|
|
# return context
|
|
#
|
|
#
|
|
# class PaymentListView(LoginRequiredMixin, ListView):
|
|
# """
|
|
# Payment list view with filtering and search.
|
|
# """
|
|
# model = Payment
|
|
# template_name = 'billing/payment_list.html'
|
|
# context_object_name = 'payments'
|
|
# paginate_by = 25
|
|
#
|
|
# def get_queryset(self):
|
|
# queryset = Payment.objects.filter(
|
|
# medical_bill__tenant=self.request.user.tenant
|
|
# ).select_related(
|
|
# 'medical_bill', 'medical_bill__patient', 'insurance_claim'
|
|
# )
|
|
#
|
|
# # Apply filters
|
|
# payment_method = self.request.GET.get('payment_method')
|
|
# payment_source = self.request.GET.get('payment_source')
|
|
# status = self.request.GET.get('status')
|
|
# date_from = self.request.GET.get('date_from')
|
|
# date_to = self.request.GET.get('date_to')
|
|
# search = self.request.GET.get('search')
|
|
#
|
|
# if payment_method:
|
|
# queryset = queryset.filter(payment_method=payment_method)
|
|
# if payment_source:
|
|
# queryset = queryset.filter(payment_source=payment_source)
|
|
# if status:
|
|
# queryset = queryset.filter(status=status)
|
|
# if date_from:
|
|
# queryset = queryset.filter(payment_date__gte=date_from)
|
|
# if date_to:
|
|
# queryset = queryset.filter(payment_date__lte=date_to)
|
|
# if search:
|
|
# queryset = queryset.filter(
|
|
# Q(payment_number__icontains=search) |
|
|
# Q(medical_bill__bill_number__icontains=search) |
|
|
# Q(medical_bill__patient__first_name__icontains=search) |
|
|
# Q(medical_bill__patient__last_name__icontains=search)
|
|
# )
|
|
#
|
|
# return queryset.order_by('-payment_date')
|
|
#
|
|
# def get_context_data(self, **kwargs):
|
|
# context = super().get_context_data(**kwargs)
|
|
#
|
|
# # Add filter values to context
|
|
# context.update({
|
|
# 'payment_method': self.request.GET.get('payment_method', ''),
|
|
# 'payment_source': self.request.GET.get('payment_source', ''),
|
|
# 'status': self.request.GET.get('status', ''),
|
|
# 'date_from': self.request.GET.get('date_from', ''),
|
|
# 'date_to': self.request.GET.get('date_to', ''),
|
|
# 'search': self.request.GET.get('search', ''),
|
|
# })
|
|
#
|
|
# # Statistics
|
|
# queryset = self.get_queryset()
|
|
# context['stats'] = {
|
|
# 'total_payments': queryset.count(),
|
|
# 'total_amount': queryset.aggregate(total=Sum('payment_amount'))['total'] or Decimal('0.00'),
|
|
# 'total_refunds': queryset.aggregate(total=Sum('refund_amount'))['total'] or Decimal('0.00'),
|
|
# 'net_amount': queryset.aggregate(
|
|
# total=Sum('payment_amount') - Sum('refund_amount')
|
|
# )['total'] or Decimal('0.00'),
|
|
# }
|
|
#
|
|
# return context
|
|
#
|
|
#
|
|
# # HTMX Views for real-time updates
|
|
# @login_required
|
|
# def htmx_billing_stats(request):
|
|
# """
|
|
# HTMX view for billing statistics.
|
|
# """
|
|
# tenant = request.user.tenant
|
|
# today = timezone.now().date()
|
|
#
|
|
# stats = {
|
|
# 'total_revenue_today': MedicalBill.objects.filter(
|
|
# tenant=tenant,
|
|
# bill_date=today,
|
|
# status='PAID'
|
|
# ).aggregate(total=Sum('total_amount'))['total'] or Decimal('0.00'),
|
|
#
|
|
# 'outstanding_bills': MedicalBill.objects.filter(
|
|
# tenant=tenant,
|
|
# status__in=['PENDING', 'SUBMITTED', 'PARTIAL_PAID']
|
|
# ).count(),
|
|
#
|
|
# 'outstanding_amount': MedicalBill.objects.filter(
|
|
# tenant=tenant,
|
|
# status__in=['PENDING', 'SUBMITTED', 'PARTIAL_PAID']
|
|
# ).aggregate(total=Sum('balance_amount'))['total'] or Decimal('0.00'),
|
|
#
|
|
# 'overdue_bills': MedicalBill.objects.filter(
|
|
# tenant=tenant,
|
|
# status='OVERDUE'
|
|
# ).count(),
|
|
#
|
|
# 'overdue_amount': MedicalBill.objects.filter(
|
|
# tenant=tenant,
|
|
# status='OVERDUE'
|
|
# ).aggregate(total=Sum('balance_amount'))['total'] or Decimal('0.00'),
|
|
#
|
|
# 'pending_claims': InsuranceClaim.objects.filter(
|
|
# medical_bill__tenant=tenant,
|
|
# status__in=['SUBMITTED', 'PENDING', 'PROCESSING']
|
|
# ).count(),
|
|
#
|
|
# 'denied_claims': InsuranceClaim.objects.filter(
|
|
# medical_bill__tenant=tenant,
|
|
# status='DENIED'
|
|
# ).count(),
|
|
#
|
|
# 'payments_today': Payment.objects.filter(
|
|
# medical_bill__tenant=tenant,
|
|
# payment_date=today
|
|
# ).aggregate(total=Sum('payment_amount'))['total'] or Decimal('0.00'),
|
|
#
|
|
# 'collections_amount': MedicalBill.objects.filter(
|
|
# tenant=tenant,
|
|
# collection_status__in=['FIRST_NOTICE', 'SECOND_NOTICE', 'FINAL_NOTICE', 'COLLECTIONS']
|
|
# ).aggregate(total=Sum('balance_amount'))['total'] or Decimal('0.00'),
|
|
# }
|
|
#
|
|
# return render(request, 'billing/partials/billing_stats.html', {'stats': stats})
|
|
#
|
|
#
|
|
# @login_required
|
|
# def htmx_bill_search(request):
|
|
# """
|
|
# HTMX view for medical bill search.
|
|
# """
|
|
# tenant = request.user.tenant
|
|
# search = request.GET.get('search', '')
|
|
#
|
|
# bills = MedicalBill.objects.filter(tenant=tenant)
|
|
#
|
|
# if search:
|
|
# bills = bills.filter(
|
|
# Q(bill_number__icontains=search) |
|
|
# Q(patient__first_name__icontains=search) |
|
|
# Q(patient__last_name__icontains=search) |
|
|
# Q(patient__mrn__icontains=search)
|
|
# )
|
|
#
|
|
# bills = bills.select_related(
|
|
# 'patient', 'encounter', 'attending_provider'
|
|
# ).order_by('-bill_date')[:10]
|
|
#
|
|
# return render(request, 'billing/partials/bill_list.html', {'bills': bills})
|
|
#
|
|
#
|
|
# @login_required
|
|
# def htmx_payment_search(request):
|
|
# """
|
|
# HTMX view for payment search.
|
|
# """
|
|
# tenant = request.user.tenant
|
|
# search = request.GET.get('search', '')
|
|
#
|
|
# payments = Payment.objects.filter(medical_bill__tenant=tenant)
|
|
#
|
|
# if search:
|
|
# payments = payments.filter(
|
|
# Q(payment_number__icontains=search) |
|
|
# Q(medical_bill__bill_number__icontains=search) |
|
|
# Q(medical_bill__patient__first_name__icontains=search) |
|
|
# Q(medical_bill__patient__last_name__icontains=search)
|
|
# )
|
|
#
|
|
# payments = payments.select_related(
|
|
# 'medical_bill', 'medical_bill__patient'
|
|
# ).order_by('-payment_date')[:10]
|
|
#
|
|
# return render(request, 'billing/partials/payment_list.html', {'payments': payments})
|
|
#
|
|
#
|
|
# @login_required
|
|
# def htmx_claim_search(request):
|
|
# """
|
|
# HTMX view for insurance claim search.
|
|
# """
|
|
# tenant = request.user.tenant
|
|
# search = request.GET.get('search', '')
|
|
#
|
|
# claims = InsuranceClaim.objects.filter(medical_bill__tenant=tenant)
|
|
#
|
|
# if search:
|
|
# claims = claims.filter(
|
|
# Q(claim_number__icontains=search) |
|
|
# Q(medical_bill__bill_number__icontains=search) |
|
|
# Q(medical_bill__patient__first_name__icontains=search) |
|
|
# Q(medical_bill__patient__last_name__icontains=search)
|
|
# )
|
|
#
|
|
# claims = claims.select_related(
|
|
# 'medical_bill', 'medical_bill__patient', 'insurance_info'
|
|
# ).order_by('-submission_date')[:10]
|
|
#
|
|
# return render(request, 'billing/partials/claim_list.html', {'claims': claims})
|
|
#
|
|
#
|
|
# # Action Views
|
|
# @login_required
|
|
# @require_http_methods(["POST"])
|
|
# def submit_bill(request, bill_id):
|
|
# """
|
|
# Submit medical bill for processing.
|
|
# """
|
|
# bill = get_object_or_404(
|
|
# MedicalBill,
|
|
# bill_id=bill_id,
|
|
# tenant=request.user.tenant
|
|
# )
|
|
#
|
|
# if bill.status == 'DRAFT':
|
|
# bill.status = 'SUBMITTED'
|
|
# bill.save()
|
|
#
|
|
# # Log audit event
|
|
# AuditLogger.log_event(
|
|
# request.user,
|
|
# 'MEDICAL_BILL_SUBMITTED',
|
|
# 'MedicalBill',
|
|
# str(bill.bill_id),
|
|
# f"Submitted medical bill {bill.bill_number} for {bill.patient.get_full_name()}"
|
|
# )
|
|
#
|
|
# messages.success(request, 'Medical bill submitted successfully')
|
|
#
|
|
# return redirect('billing:bill_detail', bill_id=bill.bill_id)
|
|
#
|
|
#
|
|
# @login_required
|
|
# @require_http_methods(["POST"])
|
|
# def process_payment(request, bill_id):
|
|
# """
|
|
# Process payment for medical bill.
|
|
# """
|
|
# bill = get_object_or_404(
|
|
# MedicalBill,
|
|
# bill_id=bill_id,
|
|
# tenant=request.user.tenant
|
|
# )
|
|
#
|
|
# payment_amount = Decimal(request.POST.get('payment_amount', '0.00'))
|
|
# payment_method = request.POST.get('payment_method', 'CASH')
|
|
# payment_source = request.POST.get('payment_source', 'PATIENT')
|
|
#
|
|
# if payment_amount > 0:
|
|
# # Create payment record
|
|
# payment = Payment.objects.create(
|
|
# medical_bill=bill,
|
|
# payment_amount=payment_amount,
|
|
# payment_method=payment_method,
|
|
# payment_source=payment_source,
|
|
# payment_date=timezone.now().date(),
|
|
# received_by=request.user,
|
|
# processed_by=request.user,
|
|
# status='PROCESSED'
|
|
# )
|
|
#
|
|
# # Update bill paid amount and status
|
|
# bill.paid_amount += payment_amount
|
|
# bill.balance_amount = bill.total_amount - bill.paid_amount
|
|
#
|
|
# if bill.balance_amount <= 0:
|
|
# bill.status = 'PAID'
|
|
# elif bill.paid_amount > 0:
|
|
# bill.status = 'PARTIAL_PAID'
|
|
#
|
|
# bill.save()
|
|
#
|
|
# # Log audit event
|
|
# AuditLogger.log_event(
|
|
# request.user,
|
|
# 'PAYMENT_PROCESSED',
|
|
# 'Payment',
|
|
# str(payment.payment_id),
|
|
# f"Processed payment {payment.payment_number} for ${payment_amount} on bill {bill.bill_number}"
|
|
# )
|
|
#
|
|
# messages.success(request, f'Payment of ${payment_amount} processed successfully')
|
|
#
|
|
# return redirect('billing:bill_detail', bill_id=bill.bill_id)
|
|
#
|
|
#
|
|
# @login_required
|
|
# @require_http_methods(["POST"])
|
|
# def submit_insurance_claim(request, bill_id):
|
|
# """
|
|
# Submit insurance claim for medical bill.
|
|
# """
|
|
# bill = get_object_or_404(
|
|
# MedicalBill,
|
|
# bill_id=bill_id,
|
|
# tenant=request.user.tenant
|
|
# )
|
|
#
|
|
# insurance_type = request.POST.get('insurance_type', 'PRIMARY')
|
|
#
|
|
# # Determine which insurance to use
|
|
# if insurance_type == 'PRIMARY' and bill.primary_insurance:
|
|
# insurance_info = bill.primary_insurance
|
|
# claim_type = 'PRIMARY'
|
|
# elif insurance_type == 'SECONDARY' and bill.secondary_insurance:
|
|
# insurance_info = bill.secondary_insurance
|
|
# claim_type = 'SECONDARY'
|
|
# else:
|
|
# messages.error(request, 'No insurance information available for claim submission')
|
|
# return redirect('billing:bill_detail', bill_id=bill.bill_id)
|
|
#
|
|
# # Create insurance claim
|
|
# claim = InsuranceClaim.objects.create(
|
|
# medical_bill=bill,
|
|
# insurance_info=insurance_info,
|
|
# claim_type=claim_type,
|
|
# submission_date=timezone.now().date(),
|
|
# service_date_from=bill.service_date_from,
|
|
# service_date_to=bill.service_date_to,
|
|
# billed_amount=bill.total_amount,
|
|
# status='SUBMITTED',
|
|
# created_by=request.user
|
|
# )
|
|
#
|
|
# # Log audit event
|
|
# AuditLogger.log_event(
|
|
# request.user,
|
|
# 'INSURANCE_CLAIM_SUBMITTED',
|
|
# 'InsuranceClaim',
|
|
# str(claim.claim_id),
|
|
# f"Submitted {claim_type.lower()} insurance claim {claim.claim_number} for bill {bill.bill_number}"
|
|
# )
|
|
#
|
|
# messages.success(request, f'{claim_type.title()} insurance claim submitted successfully')
|
|
# return redirect('billing:bill_detail', bill_id=bill.bill_id)
|
|
#
|
|
#
|
|
# # Export Views
|
|
# @login_required
|
|
# def export_bills(request):
|
|
# """
|
|
# Export medical bills to CSV.
|
|
# """
|
|
# tenant = request.user.tenant
|
|
#
|
|
# # Create HTTP response with CSV content type
|
|
# response = HttpResponse(content_type='text/csv')
|
|
# response['Content-Disposition'] = 'attachment; filename="medical_bills.csv"'
|
|
#
|
|
# writer = csv.writer(response)
|
|
#
|
|
# # Write header row
|
|
# writer.writerow([
|
|
# 'Bill Number', 'Patient Name', 'MRN', 'Bill Type', 'Bill Date', 'Due Date',
|
|
# 'Service Date From', 'Service Date To', 'Subtotal', 'Tax Amount', 'Total Amount',
|
|
# 'Paid Amount', 'Balance Amount', 'Status', 'Attending Provider', 'Created Date'
|
|
# ])
|
|
#
|
|
# # Write data rows
|
|
# bills = MedicalBill.objects.filter(
|
|
# tenant=tenant
|
|
# ).select_related(
|
|
# 'patient', 'attending_provider'
|
|
# ).order_by('-bill_date')
|
|
#
|
|
# for bill in bills:
|
|
# writer.writerow([
|
|
# bill.bill_number,
|
|
# bill.patient.get_full_name(),
|
|
# bill.patient.mrn,
|
|
# bill.get_bill_type_display(),
|
|
# bill.bill_date.strftime('%Y-%m-%d'),
|
|
# bill.due_date.strftime('%Y-%m-%d'),
|
|
# bill.service_date_from.strftime('%Y-%m-%d'),
|
|
# bill.service_date_to.strftime('%Y-%m-%d'),
|
|
# str(bill.subtotal),
|
|
# str(bill.tax_amount),
|
|
# str(bill.total_amount),
|
|
# str(bill.paid_amount),
|
|
# str(bill.balance_amount),
|
|
# bill.get_status_display(),
|
|
# bill.attending_provider.get_full_name() if bill.attending_provider else '',
|
|
# bill.created_at.strftime('%Y-%m-%d %H:%M:%S')
|
|
# ])
|
|
#
|
|
# # Log audit event
|
|
# AuditLogger.log_event(
|
|
# request.user,
|
|
# 'BILLS_EXPORTED',
|
|
# 'MedicalBill',
|
|
# None,
|
|
# f"Exported {bills.count()} medical bills to CSV"
|
|
# )
|
|
#
|
|
# return response
|
|
#
|
|
#
|
|
# # Legacy view functions for backward compatibility
|
|
# dashboard = BillingDashboardView.as_view()
|
|
# bill_list = MedicalBillListView.as_view()
|
|
# bill_detail = MedicalBillDetailView.as_view()
|
|
# bill_create = MedicalBillCreateView.as_view()
|
|
# claim_list = InsuranceClaimListView.as_view()
|
|
# payment_list = PaymentListView.as_view()
|
|
#
|
|
#
|
|
#
|
|
#
|