2025-08-12 13:33:25 +03:00

789 lines
26 KiB
Python

"""
Billing app forms for hospital management system.
Provides forms for medical billing, insurance claims, and revenue cycle management.
"""
from django import forms
from django.core.exceptions import ValidationError
from django.utils import timezone
from decimal import Decimal
from datetime import date, datetime
from .models import (
MedicalBill, BillLineItem, InsuranceClaim, Payment,
ClaimStatusUpdate, BillingConfiguration
)
from patients.models import PatientProfile, InsuranceInfo
from accounts.models import User
from emr.models import Encounter
from inpatients.models import Admission
class MedicalBillForm(forms.ModelForm):
"""
Form for creating and editing medical bills.
"""
class Meta:
model = MedicalBill
fields = [
'patient', 'bill_type', 'service_date_from', 'service_date_to',
'bill_date', 'due_date', 'attending_provider', 'billing_provider',
'primary_insurance', 'secondary_insurance', 'encounter', 'admission',
'subtotal', 'tax_amount', 'discount_amount', 'adjustment_amount',
'payment_terms', 'notes'
]
widgets = {
'patient': forms.Select(attrs={
'class': 'form-select',
'required': True
}),
'bill_type': forms.Select(attrs={
'class': 'form-select',
'required': True
}),
'service_date_from': forms.DateInput(attrs={
'class': 'form-control',
'type': 'date',
'required': True
}),
'service_date_to': forms.DateInput(attrs={
'class': 'form-control',
'type': 'date',
'required': True
}),
'bill_date': forms.DateInput(attrs={
'class': 'form-control',
'type': 'date',
'required': True
}),
'due_date': forms.DateInput(attrs={
'class': 'form-control',
'type': 'date'
}),
'attending_provider': forms.Select(attrs={
'class': 'form-select'
}),
'billing_provider': forms.Select(attrs={
'class': 'form-select'
}),
'primary_insurance': forms.Select(attrs={
'class': 'form-select'
}),
'secondary_insurance': forms.Select(attrs={
'class': 'form-select'
}),
'encounter': forms.Select(attrs={
'class': 'form-select'
}),
'admission': forms.Select(attrs={
'class': 'form-select'
}),
'subtotal': forms.NumberInput(attrs={
'class': 'form-control',
'step': '0.01',
'min': '0'
}),
'tax_amount': forms.NumberInput(attrs={
'class': 'form-control',
'step': '0.01',
'min': '0'
}),
'discount_amount': forms.NumberInput(attrs={
'class': 'form-control',
'step': '0.01',
'min': '0'
}),
'adjustment_amount': forms.NumberInput(attrs={
'class': 'form-control',
'step': '0.01'
}),
'payment_terms': forms.Select(attrs={
'class': 'form-select'
}),
'notes': forms.Textarea(attrs={
'class': 'form-control',
'rows': 4,
'placeholder': 'Additional notes about this bill...'
})
}
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
if self.user and hasattr(self.user, 'tenant'):
# Filter choices by tenant
self.fields['patient'].queryset = PatientProfile.objects.filter(
tenant=self.user.tenant
)
self.fields['attending_provider'].queryset = User.objects.filter(
tenant=self.user.tenant,
is_active=True
)
self.fields['billing_provider'].queryset = User.objects.filter(
tenant=self.user.tenant,
is_active=True
)
self.fields['encounter'].queryset = Encounter.objects.filter(
tenant=self.user.tenant
)
self.fields['admission'].queryset = Admission.objects.filter(
tenant=self.user.tenant
)
def clean(self):
cleaned_data = super().clean()
service_date_from = cleaned_data.get('service_date_from')
service_date_to = cleaned_data.get('service_date_to')
bill_date = cleaned_data.get('bill_date')
due_date = cleaned_data.get('due_date')
# Validate service date range
if service_date_from and service_date_to:
if service_date_from > service_date_to:
raise ValidationError({
'service_date_to': 'Service end date must be after start date.'
})
# Validate bill date
if bill_date and service_date_to:
if bill_date < service_date_to:
raise ValidationError({
'bill_date': 'Bill date cannot be before service end date.'
})
# Validate due date
if due_date and bill_date:
if due_date < bill_date:
raise ValidationError({
'due_date': 'Due date cannot be before bill date.'
})
return cleaned_data
class BillLineItemForm(forms.ModelForm):
"""
Form for creating and editing bill line items.
"""
class Meta:
model = BillLineItem
fields = [
'line_number', 'service_date', 'service_code', 'service_description',
'service_category', 'quantity', 'unit_of_measure', 'unit_price',
'modifier_1', 'modifier_2', 'modifier_3', 'modifier_4',
'primary_diagnosis', 'secondary_diagnoses', 'rendering_provider',
'supervising_provider', 'place_of_service', 'revenue_code',
'ndc_code', 'drug_quantity', 'drug_unit', 'notes'
]
widgets = {
'line_number': forms.NumberInput(attrs={
'class': 'form-control',
'min': '1'
}),
'service_date': forms.DateInput(attrs={
'class': 'form-control',
'type': 'date',
'required': True
}),
'service_code': forms.TextInput(attrs={
'class': 'form-control',
'required': True,
'placeholder': 'CPT/HCPCS code'
}),
'service_description': forms.TextInput(attrs={
'class': 'form-control',
'required': True,
'placeholder': 'Service description'
}),
'service_category': forms.Select(attrs={
'class': 'form-select'
}),
'quantity': forms.NumberInput(attrs={
'class': 'form-control',
'step': '0.01',
'min': '0.01',
'required': True
}),
'unit_of_measure': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'UN (Units)'
}),
'unit_price': forms.NumberInput(attrs={
'class': 'form-control',
'step': '0.01',
'min': '0',
'required': True
}),
'modifier_1': forms.TextInput(attrs={
'class': 'form-control',
'maxlength': '2'
}),
'modifier_2': forms.TextInput(attrs={
'class': 'form-control',
'maxlength': '2'
}),
'modifier_3': forms.TextInput(attrs={
'class': 'form-control',
'maxlength': '2'
}),
'modifier_4': forms.TextInput(attrs={
'class': 'form-control',
'maxlength': '2'
}),
'primary_diagnosis': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'ICD-10 code'
}),
'secondary_diagnoses': forms.Textarea(attrs={
'class': 'form-control',
'rows': 2,
'placeholder': 'Additional ICD-10 codes (comma-separated)'
}),
'rendering_provider': forms.Select(attrs={
'class': 'form-select'
}),
'supervising_provider': forms.Select(attrs={
'class': 'form-select'
}),
'place_of_service': forms.TextInput(attrs={
'class': 'form-control',
'maxlength': '2'
}),
'revenue_code': forms.TextInput(attrs={
'class': 'form-control',
'maxlength': '4'
}),
'ndc_code': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'NDC code for drugs'
}),
'drug_quantity': forms.NumberInput(attrs={
'class': 'form-control',
'step': '0.01',
'min': '0'
}),
'drug_unit': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'ML, GM, etc.'
}),
'notes': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Line item notes...'
})
}
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
if self.user and hasattr(self.user, 'tenant'):
# Filter provider choices by tenant
self.fields['rendering_provider'].queryset = User.objects.filter(
tenant=self.user.tenant,
is_active=True
)
self.fields['supervising_provider'].queryset = User.objects.filter(
tenant=self.user.tenant,
is_active=True
)
def clean(self):
cleaned_data = super().clean()
quantity = cleaned_data.get('quantity')
unit_price = cleaned_data.get('unit_price')
# Validate quantity and unit price
if quantity and quantity <= 0:
raise ValidationError({
'quantity': 'Quantity must be greater than zero.'
})
if unit_price and unit_price < 0:
raise ValidationError({
'unit_price': 'Unit price cannot be negative.'
})
return cleaned_data
class InsuranceClaimForm(forms.ModelForm):
"""
Form for creating and editing insurance claims.
"""
class Meta:
model = InsuranceClaim
fields = [
'insurance_info', 'claim_type', 'service_date_from', 'service_date_to',
'billed_amount', 'clearinghouse', 'batch_number', 'prior_auth_number',
'notes'
]
widgets = {
'insurance_info': forms.Select(attrs={
'class': 'form-select',
'required': True
}),
'claim_type': forms.Select(attrs={
'class': 'form-select',
'required': True
}),
'service_date_from': forms.DateInput(attrs={
'class': 'form-control',
'type': 'date',
'required': True
}),
'service_date_to': forms.DateInput(attrs={
'class': 'form-control',
'type': 'date',
'required': True
}),
'billed_amount': forms.NumberInput(attrs={
'class': 'form-control',
'step': '0.01',
'min': '0',
'required': True
}),
'clearinghouse': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Clearinghouse name'
}),
'batch_number': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Batch number'
}),
'prior_auth_number': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Prior authorization number'
}),
'notes': forms.Textarea(attrs={
'class': 'form-control',
'rows': 4,
'placeholder': 'Claim notes...'
})
}
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user', None)
self.medical_bill = kwargs.pop('medical_bill', None)
super().__init__(*args, **kwargs)
if self.medical_bill:
# Filter insurance info by patient
self.fields['insurance_info'].queryset = InsuranceInfo.objects.filter(
patient=self.medical_bill.patient,
is_active=True
)
# Set default values from medical bill
self.fields['service_date_from'].initial = self.medical_bill.service_date_from
self.fields['service_date_to'].initial = self.medical_bill.service_date_to
self.fields['billed_amount'].initial = self.medical_bill.total_amount
def clean(self):
cleaned_data = super().clean()
service_date_from = cleaned_data.get('service_date_from')
service_date_to = cleaned_data.get('service_date_to')
billed_amount = cleaned_data.get('billed_amount')
# Validate service date range
if service_date_from and service_date_to:
if service_date_from > service_date_to:
raise ValidationError({
'service_date_to': 'Service end date must be after start date.'
})
# Validate billed amount
if billed_amount and billed_amount <= 0:
raise ValidationError({
'billed_amount': 'Billed amount must be greater than zero.'
})
return cleaned_data
class PaymentForm(forms.ModelForm):
"""
Form for creating and editing payments.
"""
class Meta:
model = Payment
fields = [
'payment_date', 'payment_amount', 'payment_method', 'payment_source',
'check_number', 'bank_name', 'routing_number', 'card_type',
'card_last_four', 'authorization_code', 'transaction_id',
'insurance_claim', 'eob_number', 'received_by', 'processed_by',
'notes'
]
widgets = {
'payment_date': forms.DateInput(attrs={
'class': 'form-control',
'type': 'date',
'required': True
}),
'payment_amount': forms.NumberInput(attrs={
'class': 'form-control',
'step': '0.01',
'min': '0.01',
'required': True
}),
'payment_method': forms.Select(attrs={
'class': 'form-select',
'required': True
}),
'payment_source': forms.Select(attrs={
'class': 'form-select',
'required': True
}),
'check_number': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Check number'
}),
'bank_name': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Bank name'
}),
'routing_number': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Routing number'
}),
'card_type': forms.Select(attrs={
'class': 'form-select'
}),
'card_last_four': forms.TextInput(attrs={
'class': 'form-control',
'maxlength': '4',
'placeholder': 'Last 4 digits'
}),
'authorization_code': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Authorization code'
}),
'transaction_id': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Transaction ID'
}),
'insurance_claim': forms.Select(attrs={
'class': 'form-select'
}),
'eob_number': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'EOB number'
}),
'received_by': forms.Select(attrs={
'class': 'form-select'
}),
'processed_by': forms.Select(attrs={
'class': 'form-select'
}),
'notes': forms.Textarea(attrs={
'class': 'form-control',
'rows': 4,
'placeholder': 'Payment notes...'
})
}
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user', None)
self.medical_bill = kwargs.pop('medical_bill', None)
super().__init__(*args, **kwargs)
if self.user and hasattr(self.user, 'tenant'):
# Filter choices by tenant
self.fields['received_by'].queryset = User.objects.filter(
tenant=self.user.tenant,
is_active=True
)
self.fields['processed_by'].queryset = User.objects.filter(
tenant=self.user.tenant,
is_active=True
)
if self.medical_bill:
# Filter insurance claims by medical bill
self.fields['insurance_claim'].queryset = InsuranceClaim.objects.filter(
medical_bill=self.medical_bill
)
def clean(self):
cleaned_data = super().clean()
payment_amount = cleaned_data.get('payment_amount')
payment_method = cleaned_data.get('payment_method')
payment_date = cleaned_data.get('payment_date')
# Validate payment amount
if payment_amount and payment_amount <= 0:
raise ValidationError({
'payment_amount': 'Payment amount must be greater than zero.'
})
# Validate payment date
if payment_date and payment_date > timezone.now().date():
raise ValidationError({
'payment_date': 'Payment date cannot be in the future.'
})
# Method-specific validations
if payment_method == 'check':
check_number = cleaned_data.get('check_number')
if not check_number:
raise ValidationError({
'check_number': 'Check number is required for check payments.'
})
elif payment_method == 'credit_card':
card_last_four = cleaned_data.get('card_last_four')
authorization_code = cleaned_data.get('authorization_code')
if not card_last_four:
raise ValidationError({
'card_last_four': 'Last 4 digits are required for credit card payments.'
})
if not authorization_code:
raise ValidationError({
'authorization_code': 'Authorization code is required for credit card payments.'
})
return cleaned_data
class BillingSearchForm(forms.Form):
"""
Form for searching and filtering billing records.
"""
search = forms.CharField(
required=False,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Search bills, patients, providers...'
})
)
bill_type = forms.ChoiceField(
required=False,
choices=[('', 'All Types')] + [
('INPATIENT', 'Inpatient'),
('OUTPATIENT', 'Outpatient'),
('EMERGENCY', 'Emergency'),
('SURGERY', 'Surgery'),
('LABORATORY', 'Laboratory'),
('RADIOLOGY', 'Radiology'),
('PHARMACY', 'Pharmacy'),
('PROFESSIONAL', 'Professional Services'),
],
widget=forms.Select(attrs={
'class': 'form-select'
})
)
status = forms.ChoiceField(
required=False,
choices=[('', 'All Statuses')] + [
('DRAFT', 'Draft'),
('PENDING', 'Pending'),
('SUBMITTED', 'Submitted'),
('PARTIAL_PAID', 'Partially Paid'),
('PAID', 'Paid'),
('OVERDUE', 'Overdue'),
('COLLECTIONS', 'Collections'),
('WRITTEN_OFF', 'Written Off'),
],
widget=forms.Select(attrs={
'class': 'form-select'
})
)
date_from = forms.DateField(
required=False,
widget=forms.DateInput(attrs={
'class': 'form-control',
'type': 'date'
})
)
date_to = forms.DateField(
required=False,
widget=forms.DateInput(attrs={
'class': 'form-control',
'type': 'date'
})
)
amount_min = forms.DecimalField(
required=False,
widget=forms.NumberInput(attrs={
'class': 'form-control',
'step': '0.01',
'min': '0'
})
)
amount_max = forms.DecimalField(
required=False,
widget=forms.NumberInput(attrs={
'class': 'form-control',
'step': '0.01',
'min': '0'
})
)
overdue_only = forms.BooleanField(
required=False,
widget=forms.CheckboxInput(attrs={
'class': 'form-check-input'
})
)
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
class ClaimSearchForm(forms.Form):
"""
Form for searching and filtering insurance claims.
"""
search = forms.CharField(
required=False,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Search claims, patients, insurance...'
})
)
claim_type = forms.ChoiceField(
required=False,
choices=[('', 'All Types')] + [
('PRIMARY', 'Primary Claim'),
('SECONDARY', 'Secondary Claim'),
('TERTIARY', 'Tertiary Claim'),
('CORRECTED', 'Corrected Claim'),
('VOID', 'Void Claim'),
('REPLACEMENT', 'Replacement Claim'),
],
widget=forms.Select(attrs={
'class': 'form-select'
})
)
status = forms.ChoiceField(
required=False,
choices=[('', 'All Statuses')] + [
('DRAFT', 'Draft'),
('SUBMITTED', 'Submitted'),
('PENDING', 'Pending'),
('PROCESSING', 'Processing'),
('PAID', 'Paid'),
('DENIED', 'Denied'),
('REJECTED', 'Rejected'),
('APPEALED', 'Appealed'),
],
widget=forms.Select(attrs={
'class': 'form-select'
})
)
submission_date_from = forms.DateField(
required=False,
widget=forms.DateInput(attrs={
'class': 'form-control',
'type': 'date'
})
)
submission_date_to = forms.DateField(
required=False,
widget=forms.DateInput(attrs={
'class': 'form-control',
'type': 'date'
})
)
pending_only = forms.BooleanField(
required=False,
widget=forms.CheckboxInput(attrs={
'class': 'form-check-input'
})
)
class PaymentSearchForm(forms.Form):
"""
Form for searching and filtering payments.
"""
search = forms.CharField(
required=False,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Search payments, patients, check numbers...'
})
)
payment_method = forms.ChoiceField(
required=False,
choices=[('', 'All Methods')] + [
('CASH', 'Cash'),
('CHECK', 'Check'),
('CREDIT_CARD', 'Credit Card'),
('DEBIT_CARD', 'Debit Card'),
('BANK_TRANSFER', 'Bank Transfer'),
('ACH', 'ACH Transfer'),
('WIRE', 'Wire Transfer'),
('MONEY_ORDER', 'Money Order'),
],
widget=forms.Select(attrs={
'class': 'form-select'
})
)
payment_source = forms.ChoiceField(
required=False,
choices=[('', 'All Sources')] + [
('PATIENT', 'Patient'),
('INSURANCE', 'Insurance'),
('GUARANTOR', 'Guarantor'),
('GOVERNMENT', 'Government'),
('CHARITY', 'Charity'),
('OTHER', 'Other'),
],
widget=forms.Select(attrs={
'class': 'form-select'
})
)
status = forms.ChoiceField(
required=False,
choices=[('', 'All Statuses')] + [
('PENDING', 'Pending'),
('PROCESSED', 'Processed'),
('CLEARED', 'Cleared'),
('BOUNCED', 'Bounced'),
('REVERSED', 'Reversed'),
('REFUNDED', 'Refunded'),
],
widget=forms.Select(attrs={
'class': 'form-select'
})
)
date_from = forms.DateField(
required=False,
widget=forms.DateInput(attrs={
'class': 'form-control',
'type': 'date'
})
)
date_to = forms.DateField(
required=False,
widget=forms.DateInput(attrs={
'class': 'form-control',
'type': 'date'
})
)