1457 lines
40 KiB
Python
1457 lines
40 KiB
Python
"""
|
||
Billing app models for hospital management system.
|
||
Provides medical billing, insurance claims, and revenue cycle management.
|
||
"""
|
||
|
||
import uuid
|
||
from django.db import models
|
||
from django.core.validators import RegexValidator, MinValueValidator, MaxValueValidator
|
||
from django.utils import timezone
|
||
from django.conf import settings
|
||
from datetime import timedelta, datetime, date
|
||
from decimal import Decimal
|
||
import json
|
||
from .utils import default_aging_buckets
|
||
|
||
|
||
class MedicalBill(models.Model):
|
||
"""
|
||
Medical bill model for patient billing and revenue cycle management.
|
||
"""
|
||
|
||
# Tenant relationship
|
||
tenant = models.ForeignKey(
|
||
'core.Tenant',
|
||
on_delete=models.CASCADE,
|
||
related_name='medical_bills',
|
||
help_text='Organization tenant'
|
||
)
|
||
|
||
# Bill Information
|
||
bill_id = models.UUIDField(
|
||
default=uuid.uuid4,
|
||
unique=True,
|
||
editable=False,
|
||
help_text='Unique bill identifier'
|
||
)
|
||
bill_number = models.CharField(
|
||
max_length=20,
|
||
unique=True,
|
||
help_text='Medical bill number'
|
||
)
|
||
|
||
# Patient Information
|
||
patient = models.ForeignKey(
|
||
'patients.PatientProfile',
|
||
on_delete=models.CASCADE,
|
||
related_name='medical_bills',
|
||
help_text='Patient'
|
||
)
|
||
|
||
# Bill Type and Category
|
||
bill_type = models.CharField(
|
||
max_length=20,
|
||
choices=[
|
||
('INPATIENT', 'Inpatient'),
|
||
('OUTPATIENT', 'Outpatient'),
|
||
('EMERGENCY', 'Emergency'),
|
||
('SURGERY', 'Surgery'),
|
||
('LABORATORY', 'Laboratory'),
|
||
('RADIOLOGY', 'Radiology'),
|
||
('PHARMACY', 'Pharmacy'),
|
||
('PROFESSIONAL', 'Professional Services'),
|
||
('FACILITY', 'Facility Charges'),
|
||
('ANCILLARY', 'Ancillary Services'),
|
||
],
|
||
help_text='Bill type'
|
||
)
|
||
|
||
# Bill Dates
|
||
service_date_from = models.DateField(
|
||
help_text='Service date from'
|
||
)
|
||
service_date_to = models.DateField(
|
||
help_text='Service date to'
|
||
)
|
||
bill_date = models.DateField(
|
||
default=timezone.now,
|
||
help_text='Bill date'
|
||
)
|
||
due_date = models.DateField(
|
||
help_text='Payment due date'
|
||
)
|
||
|
||
# Financial Information
|
||
subtotal = models.DecimalField(
|
||
max_digits=12,
|
||
decimal_places=2,
|
||
default=Decimal('0.00'),
|
||
help_text='Subtotal amount'
|
||
)
|
||
tax_amount = models.DecimalField(
|
||
max_digits=12,
|
||
decimal_places=2,
|
||
default=Decimal('0.00'),
|
||
help_text='Tax amount'
|
||
)
|
||
discount_amount = models.DecimalField(
|
||
max_digits=12,
|
||
decimal_places=2,
|
||
default=Decimal('0.00'),
|
||
help_text='Discount amount'
|
||
)
|
||
adjustment_amount = models.DecimalField(
|
||
max_digits=12,
|
||
decimal_places=2,
|
||
default=Decimal('0.00'),
|
||
help_text='Adjustment amount'
|
||
)
|
||
total_amount = models.DecimalField(
|
||
max_digits=12,
|
||
decimal_places=2,
|
||
default=Decimal('0.00'),
|
||
help_text='Total bill amount'
|
||
)
|
||
paid_amount = models.DecimalField(
|
||
max_digits=12,
|
||
decimal_places=2,
|
||
default=Decimal('0.00'),
|
||
help_text='Amount paid'
|
||
)
|
||
balance_amount = models.DecimalField(
|
||
max_digits=12,
|
||
decimal_places=2,
|
||
default=Decimal('0.00'),
|
||
help_text='Balance amount'
|
||
)
|
||
|
||
# Insurance Information
|
||
primary_insurance = models.ForeignKey(
|
||
'patients.InsuranceInfo',
|
||
on_delete=models.SET_NULL,
|
||
null=True,
|
||
blank=True,
|
||
related_name='primary_bills',
|
||
help_text='Primary insurance'
|
||
)
|
||
secondary_insurance = models.ForeignKey(
|
||
'patients.InsuranceInfo',
|
||
on_delete=models.SET_NULL,
|
||
null=True,
|
||
blank=True,
|
||
related_name='secondary_bills',
|
||
help_text='Secondary insurance'
|
||
)
|
||
|
||
# Bill Status
|
||
status = models.CharField(
|
||
max_length=20,
|
||
choices=[
|
||
('DRAFT', 'Draft'),
|
||
('PENDING', 'Pending'),
|
||
('SUBMITTED', 'Submitted'),
|
||
('PARTIAL_PAID', 'Partially Paid'),
|
||
('PAID', 'Paid'),
|
||
('OVERDUE', 'Overdue'),
|
||
('COLLECTIONS', 'Collections'),
|
||
('WRITTEN_OFF', 'Written Off'),
|
||
('CANCELLED', 'Cancelled'),
|
||
],
|
||
default='DRAFT',
|
||
help_text='Bill status'
|
||
)
|
||
|
||
# Provider Information
|
||
attending_provider = models.ForeignKey(
|
||
settings.AUTH_USER_MODEL,
|
||
on_delete=models.SET_NULL,
|
||
null=True,
|
||
blank=True,
|
||
related_name='attending_bills',
|
||
help_text='Attending provider'
|
||
)
|
||
billing_provider = models.ForeignKey(
|
||
settings.AUTH_USER_MODEL,
|
||
on_delete=models.SET_NULL,
|
||
null=True,
|
||
blank=True,
|
||
related_name='billing_provider_bills',
|
||
help_text='Billing provider'
|
||
)
|
||
|
||
# Related Information
|
||
encounter = models.ForeignKey(
|
||
'emr.Encounter',
|
||
on_delete=models.SET_NULL,
|
||
null=True,
|
||
blank=True,
|
||
related_name='medical_bills',
|
||
help_text='Related encounter'
|
||
)
|
||
admission = models.ForeignKey(
|
||
'inpatients.Admission',
|
||
on_delete=models.SET_NULL,
|
||
null=True,
|
||
blank=True,
|
||
related_name='medical_bills',
|
||
help_text='Related admission'
|
||
)
|
||
|
||
# Billing Notes
|
||
notes = models.TextField(
|
||
blank=True,
|
||
null=True,
|
||
help_text='Billing notes and comments'
|
||
)
|
||
|
||
# Payment Terms
|
||
payment_terms = models.CharField(
|
||
max_length=20,
|
||
choices=[
|
||
('NET_30', 'Net 30 Days'),
|
||
('NET_60', 'Net 60 Days'),
|
||
('NET_90', 'Net 90 Days'),
|
||
('IMMEDIATE', 'Immediate'),
|
||
('CUSTOM', 'Custom Terms'),
|
||
],
|
||
default='NET_30',
|
||
help_text='Payment terms'
|
||
)
|
||
|
||
# Collection Information
|
||
collection_status = models.CharField(
|
||
max_length=20,
|
||
choices=[
|
||
('NONE', 'None'),
|
||
('FIRST_NOTICE', 'First Notice'),
|
||
('SECOND_NOTICE', 'Second Notice'),
|
||
('FINAL_NOTICE', 'Final Notice'),
|
||
('COLLECTIONS', 'Collections'),
|
||
('LEGAL', 'Legal Action'),
|
||
],
|
||
default='NONE',
|
||
help_text='Collection status'
|
||
)
|
||
last_statement_date = models.DateField(
|
||
blank=True,
|
||
null=True,
|
||
help_text='Last statement date'
|
||
)
|
||
|
||
# Metadata
|
||
created_at = models.DateTimeField(auto_now_add=True)
|
||
updated_at = models.DateTimeField(auto_now=True)
|
||
created_by = models.ForeignKey(
|
||
settings.AUTH_USER_MODEL,
|
||
on_delete=models.SET_NULL,
|
||
null=True,
|
||
blank=True,
|
||
related_name='created_medical_bills',
|
||
help_text='User who created the bill'
|
||
)
|
||
|
||
class Meta:
|
||
db_table = 'billing_medical_bill'
|
||
verbose_name = 'Medical Bill'
|
||
verbose_name_plural = 'Medical Bills'
|
||
ordering = ['-bill_date']
|
||
indexes = [
|
||
models.Index(fields=['tenant', 'status']),
|
||
models.Index(fields=['patient', 'bill_date']),
|
||
models.Index(fields=['bill_number']),
|
||
models.Index(fields=['status', 'due_date']),
|
||
models.Index(fields=['collection_status']),
|
||
]
|
||
|
||
def __str__(self):
|
||
return f"{self.bill_number} - {self.patient.get_full_name()}"
|
||
|
||
def save(self, *args, **kwargs):
|
||
"""
|
||
Generate bill number and calculate totals.
|
||
"""
|
||
if not self.bill_number:
|
||
# Generate bill number (simple implementation)
|
||
today = timezone.now().date()
|
||
last_bill = MedicalBill.objects.filter(
|
||
tenant=self.tenant,
|
||
created_at__date=today
|
||
).order_by('-id').first()
|
||
|
||
if last_bill:
|
||
last_number = int(last_bill.bill_number.split('-')[-1])
|
||
self.bill_number = f"BILL-{today.strftime('%Y%m%d')}-{last_number + 1:04d}"
|
||
else:
|
||
self.bill_number = f"BILL-{today.strftime('%Y%m%d')}-0001"
|
||
|
||
# Calculate totals
|
||
self.total_amount = self.subtotal + self.tax_amount - self.discount_amount + self.adjustment_amount
|
||
self.balance_amount = self.total_amount - self.paid_amount
|
||
|
||
super().save(*args, **kwargs)
|
||
|
||
@property
|
||
def is_overdue(self):
|
||
"""
|
||
Check if bill is overdue.
|
||
"""
|
||
return self.due_date < timezone.now().date() and self.balance_amount > 0
|
||
|
||
@property
|
||
def days_outstanding(self):
|
||
"""
|
||
Calculate days outstanding.
|
||
"""
|
||
return (timezone.now().date() - self.bill_date).days
|
||
|
||
@property
|
||
def payment_percentage(self):
|
||
"""
|
||
Calculate payment percentage.
|
||
"""
|
||
if self.total_amount > 0:
|
||
return round((self.paid_amount / self.total_amount) * 100, 1)
|
||
return 0
|
||
|
||
|
||
class BillLineItem(models.Model):
|
||
"""
|
||
Bill line item model for detailed billing charges.
|
||
"""
|
||
SERVICE_CATEGORY_CHOICES = [
|
||
('EVALUATION', 'Evaluation & Management'),
|
||
('SURGERY', 'Surgery'),
|
||
('RADIOLOGY', 'Radiology'),
|
||
('PATHOLOGY', 'Pathology & Laboratory'),
|
||
('MEDICINE', 'Medicine'),
|
||
('ANESTHESIA', 'Anesthesia'),
|
||
('SUPPLIES', 'Medical Supplies'),
|
||
('PHARMACY', 'Pharmacy'),
|
||
('ROOM_BOARD', 'Room & Board'),
|
||
('NURSING', 'Nursing Services'),
|
||
('THERAPY', 'Therapy Services'),
|
||
('EMERGENCY', 'Emergency Services'),
|
||
('AMBULANCE', 'Ambulance Services'),
|
||
('DME', 'Durable Medical Equipment'),
|
||
('OTHER', 'Other Services'),
|
||
]
|
||
UNIT_OF_MEASURE_CHOICES = [
|
||
('EACH', 'Each'),
|
||
('UNIT', 'Unit'),
|
||
('HOUR', 'Hour'),
|
||
('DAY', 'Day'),
|
||
('VISIT', 'Visit'),
|
||
('PROCEDURE', 'Procedure'),
|
||
('DOSE', 'Dose'),
|
||
('MILE', 'Mile'),
|
||
('MINUTE', 'Minute'),
|
||
]
|
||
PLACE_OF_SERVICE_CHOICES = [
|
||
('11', 'Office'),
|
||
('12', 'Home'),
|
||
('21', 'Inpatient Hospital'),
|
||
('22', 'Outpatient Hospital'),
|
||
('23', 'Emergency Room'),
|
||
('24', 'Ambulatory Surgical Center'),
|
||
('25', 'Birthing Center'),
|
||
('26', 'Military Treatment Facility'),
|
||
('31', 'Skilled Nursing Facility'),
|
||
('32', 'Nursing Facility'),
|
||
('33', 'Custodial Care Facility'),
|
||
('34', 'Hospice'),
|
||
('41', 'Ambulance - Land'),
|
||
('42', 'Ambulance - Air or Water'),
|
||
('49', 'Independent Clinic'),
|
||
('50', 'Federally Qualified Health Center'),
|
||
('51', 'Inpatient Psychiatric Facility'),
|
||
('52', 'Psychiatric Facility-Partial Hospitalization'),
|
||
('53', 'Community Mental Health Center'),
|
||
('54', 'Intermediate Care Facility/Mentally Retarded'),
|
||
('55', 'Residential Substance Abuse Treatment Facility'),
|
||
('56', 'Psychiatric Residential Treatment Center'),
|
||
('57', 'Non-residential Substance Abuse Treatment Facility'),
|
||
('60', 'Mass Immunization Center'),
|
||
('61', 'Comprehensive Inpatient Rehabilitation Facility'),
|
||
('62', 'Comprehensive Outpatient Rehabilitation Facility'),
|
||
('65', 'End-Stage Renal Disease Treatment Facility'),
|
||
('71', 'Public Health Clinic'),
|
||
('72', 'Rural Health Clinic'),
|
||
('81', 'Independent Laboratory'),
|
||
('99', 'Other Place of Service'),
|
||
]
|
||
STATUS_CHOICES = [
|
||
('ACTIVE', 'Active'),
|
||
('DENIED', 'Denied'),
|
||
('ADJUSTED', 'Adjusted'),
|
||
('VOIDED', 'Voided'),
|
||
]
|
||
# Medical Bill relationship
|
||
medical_bill = models.ForeignKey(
|
||
MedicalBill,
|
||
on_delete=models.CASCADE,
|
||
related_name='line_items',
|
||
help_text='Medical bill'
|
||
)
|
||
|
||
# Line Item Information
|
||
line_item_id = models.UUIDField(
|
||
default=uuid.uuid4,
|
||
unique=True,
|
||
editable=False,
|
||
help_text='Unique line item identifier'
|
||
)
|
||
line_number = models.PositiveIntegerField(
|
||
help_text='Line item number'
|
||
)
|
||
|
||
# Service Information
|
||
service_date = models.DateField(
|
||
help_text='Service date'
|
||
)
|
||
service_code = models.CharField(
|
||
max_length=20,
|
||
help_text='Service code (CPT, HCPCS, etc.)'
|
||
)
|
||
service_description = models.CharField(
|
||
max_length=200,
|
||
help_text='Service description'
|
||
)
|
||
|
||
# Service Category
|
||
service_category = models.CharField(
|
||
max_length=30,
|
||
choices=SERVICE_CATEGORY_CHOICES,
|
||
help_text='Service category'
|
||
)
|
||
|
||
# Quantity and Units
|
||
quantity = models.DecimalField(
|
||
max_digits=10,
|
||
decimal_places=2,
|
||
default=Decimal('1.00'),
|
||
help_text='Quantity of service'
|
||
)
|
||
unit_of_measure = models.CharField(
|
||
max_length=20,
|
||
choices=UNIT_OF_MEASURE_CHOICES,
|
||
default='EACH',
|
||
help_text='Unit of measure'
|
||
)
|
||
|
||
# Pricing Information
|
||
unit_price = models.DecimalField(
|
||
max_digits=10,
|
||
decimal_places=2,
|
||
help_text='Unit price'
|
||
)
|
||
total_price = models.DecimalField(
|
||
max_digits=12,
|
||
decimal_places=2,
|
||
help_text='Total price (quantity × unit price)'
|
||
)
|
||
|
||
# Modifiers
|
||
modifier_1 = models.CharField(
|
||
max_length=5,
|
||
blank=True,
|
||
null=True,
|
||
help_text='First modifier'
|
||
)
|
||
modifier_2 = models.CharField(
|
||
max_length=5,
|
||
blank=True,
|
||
null=True,
|
||
help_text='Second modifier'
|
||
)
|
||
modifier_3 = models.CharField(
|
||
max_length=5,
|
||
blank=True,
|
||
null=True,
|
||
help_text='Third modifier'
|
||
)
|
||
modifier_4 = models.CharField(
|
||
max_length=5,
|
||
blank=True,
|
||
null=True,
|
||
help_text='Fourth modifier'
|
||
)
|
||
|
||
# Diagnosis Information
|
||
primary_diagnosis = models.CharField(
|
||
max_length=20,
|
||
blank=True,
|
||
null=True,
|
||
help_text='Primary diagnosis code'
|
||
)
|
||
secondary_diagnoses = models.JSONField(
|
||
default=list,
|
||
help_text='Secondary diagnosis codes'
|
||
)
|
||
|
||
# Provider Information
|
||
rendering_provider = models.ForeignKey(
|
||
settings.AUTH_USER_MODEL,
|
||
on_delete=models.SET_NULL,
|
||
null=True,
|
||
blank=True,
|
||
related_name='rendered_line_items',
|
||
help_text='Rendering provider'
|
||
)
|
||
supervising_provider = models.ForeignKey(
|
||
settings.AUTH_USER_MODEL,
|
||
on_delete=models.SET_NULL,
|
||
null=True,
|
||
blank=True,
|
||
related_name='supervised_line_items',
|
||
help_text='Supervising provider'
|
||
)
|
||
|
||
# Place of Service
|
||
place_of_service = models.CharField(
|
||
max_length=5,
|
||
choices=PLACE_OF_SERVICE_CHOICES,
|
||
default='22',
|
||
help_text='Place of service code'
|
||
)
|
||
|
||
# Revenue Code (for facility billing)
|
||
revenue_code = models.CharField(
|
||
max_length=4,
|
||
blank=True,
|
||
null=True,
|
||
help_text='Revenue code for facility billing'
|
||
)
|
||
|
||
# NDC Information (for drugs)
|
||
ndc_code = models.CharField(
|
||
max_length=20,
|
||
blank=True,
|
||
null=True,
|
||
help_text='National Drug Code'
|
||
)
|
||
drug_quantity = models.DecimalField(
|
||
max_digits=10,
|
||
decimal_places=3,
|
||
blank=True,
|
||
null=True,
|
||
help_text='Drug quantity'
|
||
)
|
||
drug_unit = models.CharField(
|
||
max_length=10,
|
||
blank=True,
|
||
null=True,
|
||
help_text='Drug unit of measure'
|
||
)
|
||
|
||
# Line Item Status
|
||
status = models.CharField(
|
||
max_length=20,
|
||
choices=STATUS_CHOICES,
|
||
default='ACTIVE',
|
||
help_text='Line item status'
|
||
)
|
||
|
||
# Notes
|
||
notes = models.TextField(
|
||
blank=True,
|
||
null=True,
|
||
help_text='Line item notes'
|
||
)
|
||
|
||
# Metadata
|
||
created_at = models.DateTimeField(auto_now_add=True)
|
||
updated_at = models.DateTimeField(auto_now=True)
|
||
|
||
class Meta:
|
||
db_table = 'billing_bill_line_item'
|
||
verbose_name = 'Bill Line Item'
|
||
verbose_name_plural = 'Bill Line Items'
|
||
ordering = ['line_number']
|
||
indexes = [
|
||
models.Index(fields=['medical_bill', 'line_number']),
|
||
models.Index(fields=['service_code']),
|
||
models.Index(fields=['service_date']),
|
||
models.Index(fields=['rendering_provider']),
|
||
]
|
||
unique_together = ['medical_bill', 'line_number']
|
||
|
||
def __str__(self):
|
||
return f"{self.medical_bill.bill_number} - Line {self.line_number}"
|
||
|
||
def save(self, *args, **kwargs):
|
||
"""
|
||
Calculate total price from quantity and unit price.
|
||
"""
|
||
self.total_price = self.quantity * self.unit_price
|
||
super().save(*args, **kwargs)
|
||
|
||
@property
|
||
def patient(self):
|
||
"""
|
||
Get patient from medical bill.
|
||
"""
|
||
return self.medical_bill.patient
|
||
|
||
@property
|
||
def tenant(self):
|
||
"""
|
||
Get tenant from medical bill.
|
||
"""
|
||
return self.medical_bill.tenant
|
||
|
||
|
||
class InsuranceClaim(models.Model):
|
||
"""
|
||
Insurance claim model for claims processing and management.
|
||
"""
|
||
CLAIM_TYPE_CHOICES = [
|
||
('PRIMARY', 'Primary Claim'),
|
||
('SECONDARY', 'Secondary Claim'),
|
||
('TERTIARY', 'Tertiary Claim'),
|
||
('CORRECTED', 'Corrected Claim'),
|
||
('VOID', 'Void Claim'),
|
||
('REPLACEMENT', 'Replacement Claim'),
|
||
]
|
||
STATUS_CHOICES = [
|
||
('DRAFT', 'Draft'),
|
||
('SUBMITTED', 'Submitted'),
|
||
('PENDING', 'Pending'),
|
||
('PROCESSING', 'Processing'),
|
||
('PAID', 'Paid'),
|
||
('DENIED', 'Denied'),
|
||
('REJECTED', 'Rejected'),
|
||
('APPEALED', 'Appealed'),
|
||
('VOIDED', 'Voided'),
|
||
]
|
||
|
||
# Medical Bill relationship
|
||
medical_bill = models.ForeignKey(
|
||
MedicalBill,
|
||
on_delete=models.CASCADE,
|
||
related_name='insurance_claims',
|
||
help_text='Related medical bill'
|
||
)
|
||
|
||
# Claim Information
|
||
claim_id = models.UUIDField(
|
||
default=uuid.uuid4,
|
||
unique=True,
|
||
editable=False,
|
||
help_text='Unique claim identifier'
|
||
)
|
||
claim_number = models.CharField(
|
||
max_length=30,
|
||
unique=True,
|
||
help_text='Insurance claim number'
|
||
)
|
||
|
||
# Insurance Information
|
||
insurance_info = models.ForeignKey(
|
||
'patients.InsuranceInfo',
|
||
on_delete=models.CASCADE,
|
||
related_name='insurance_claims',
|
||
help_text='Insurance information'
|
||
)
|
||
|
||
# Claim Type
|
||
claim_type = models.CharField(
|
||
max_length=20,
|
||
choices=CLAIM_TYPE_CHOICES,
|
||
default='PRIMARY',
|
||
help_text='Claim type'
|
||
)
|
||
|
||
# Claim Dates
|
||
submission_date = models.DateField(
|
||
help_text='Claim submission date'
|
||
)
|
||
service_date_from = models.DateField(
|
||
help_text='Service date from'
|
||
)
|
||
service_date_to = models.DateField(
|
||
help_text='Service date to'
|
||
)
|
||
|
||
# Financial Information
|
||
billed_amount = models.DecimalField(
|
||
max_digits=12,
|
||
decimal_places=2,
|
||
help_text='Total billed amount'
|
||
)
|
||
allowed_amount = models.DecimalField(
|
||
max_digits=12,
|
||
decimal_places=2,
|
||
default=Decimal('0.00'),
|
||
help_text='Insurance allowed amount'
|
||
)
|
||
paid_amount = models.DecimalField(
|
||
max_digits=12,
|
||
decimal_places=2,
|
||
default=Decimal('0.00'),
|
||
help_text='Insurance paid amount'
|
||
)
|
||
patient_responsibility = models.DecimalField(
|
||
max_digits=12,
|
||
decimal_places=2,
|
||
default=Decimal('0.00'),
|
||
help_text='Patient responsibility amount'
|
||
)
|
||
deductible_amount = models.DecimalField(
|
||
max_digits=12,
|
||
decimal_places=2,
|
||
default=Decimal('0.00'),
|
||
help_text='Deductible amount'
|
||
)
|
||
coinsurance_amount = models.DecimalField(
|
||
max_digits=12,
|
||
decimal_places=2,
|
||
default=Decimal('0.00'),
|
||
help_text='Coinsurance amount'
|
||
)
|
||
copay_amount = models.DecimalField(
|
||
max_digits=12,
|
||
decimal_places=2,
|
||
default=Decimal('0.00'),
|
||
help_text='Copay amount'
|
||
)
|
||
|
||
# Claim Status
|
||
status = models.CharField(
|
||
max_length=20,
|
||
choices=STATUS_CHOICES,
|
||
default='DRAFT',
|
||
help_text='Claim status'
|
||
)
|
||
|
||
# Processing Information
|
||
clearinghouse = models.CharField(
|
||
max_length=100,
|
||
blank=True,
|
||
null=True,
|
||
help_text='Clearinghouse used for submission'
|
||
)
|
||
batch_number = models.CharField(
|
||
max_length=50,
|
||
blank=True,
|
||
null=True,
|
||
help_text='Batch number'
|
||
)
|
||
|
||
# Response Information
|
||
response_date = models.DateField(
|
||
blank=True,
|
||
null=True,
|
||
help_text='Insurance response date'
|
||
)
|
||
check_number = models.CharField(
|
||
max_length=50,
|
||
blank=True,
|
||
null=True,
|
||
help_text='Insurance check number'
|
||
)
|
||
check_date = models.DateField(
|
||
blank=True,
|
||
null=True,
|
||
help_text='Insurance check date'
|
||
)
|
||
|
||
# Denial Information
|
||
denial_reason = models.CharField(
|
||
max_length=200,
|
||
blank=True,
|
||
null=True,
|
||
help_text='Denial reason'
|
||
)
|
||
denial_code = models.CharField(
|
||
max_length=20,
|
||
blank=True,
|
||
null=True,
|
||
help_text='Denial code'
|
||
)
|
||
|
||
# Prior Authorization
|
||
prior_auth_number = models.CharField(
|
||
max_length=50,
|
||
blank=True,
|
||
null=True,
|
||
help_text='Prior authorization number'
|
||
)
|
||
|
||
# Claim Notes
|
||
notes = models.TextField(
|
||
blank=True,
|
||
null=True,
|
||
help_text='Claim notes and comments'
|
||
)
|
||
|
||
# Resubmission Information
|
||
original_claim = models.ForeignKey(
|
||
'self',
|
||
on_delete=models.SET_NULL,
|
||
null=True,
|
||
blank=True,
|
||
related_name='resubmissions',
|
||
help_text='Original claim if this is a resubmission'
|
||
)
|
||
resubmission_count = models.PositiveIntegerField(
|
||
default=0,
|
||
help_text='Number of resubmissions'
|
||
)
|
||
|
||
# Metadata
|
||
created_at = models.DateTimeField(auto_now_add=True)
|
||
updated_at = models.DateTimeField(auto_now=True)
|
||
created_by = models.ForeignKey(
|
||
settings.AUTH_USER_MODEL,
|
||
on_delete=models.SET_NULL,
|
||
null=True,
|
||
blank=True,
|
||
related_name='created_insurance_claims',
|
||
help_text='User who created the claim'
|
||
)
|
||
|
||
class Meta:
|
||
db_table = 'billing_insurance_claim'
|
||
verbose_name = 'Insurance Claim'
|
||
verbose_name_plural = 'Insurance Claims'
|
||
ordering = ['-submission_date']
|
||
indexes = [
|
||
models.Index(fields=['medical_bill']),
|
||
models.Index(fields=['insurance_info']),
|
||
models.Index(fields=['claim_number']),
|
||
models.Index(fields=['status', 'submission_date']),
|
||
models.Index(fields=['response_date']),
|
||
]
|
||
|
||
def __str__(self):
|
||
return f"{self.claim_number} - {self.insurance_info.insurance_company}"
|
||
|
||
def save(self, *args, **kwargs):
|
||
"""
|
||
Generate claim number if not provided.
|
||
"""
|
||
if not self.claim_number:
|
||
# Generate claim number (simple implementation)
|
||
today = timezone.now().date()
|
||
last_claim = InsuranceClaim.objects.filter(
|
||
medical_bill__tenant=self.medical_bill.tenant,
|
||
created_at__date=today
|
||
).order_by('-id').first()
|
||
|
||
if last_claim:
|
||
last_number = int(last_claim.claim_number.split('-')[-1])
|
||
self.claim_number = f"CLM-{today.strftime('%Y%m%d')}-{last_number + 1:04d}"
|
||
else:
|
||
self.claim_number = f"CLM-{today.strftime('%Y%m%d')}-0001"
|
||
|
||
super().save(*args, **kwargs)
|
||
|
||
@property
|
||
def patient(self):
|
||
"""
|
||
Get patient from medical bill.
|
||
"""
|
||
return self.medical_bill.patient
|
||
|
||
@property
|
||
def tenant(self):
|
||
"""
|
||
Get tenant from medical bill.
|
||
"""
|
||
return self.medical_bill.tenant
|
||
|
||
@property
|
||
def days_pending(self):
|
||
"""
|
||
Calculate days pending since submission.
|
||
"""
|
||
return (timezone.now().date() - self.submission_date).days
|
||
|
||
@property
|
||
def payment_percentage(self):
|
||
"""
|
||
Calculate payment percentage.
|
||
"""
|
||
if self.billed_amount > 0:
|
||
return round((self.paid_amount / self.billed_amount) * 100, 1)
|
||
return 0
|
||
|
||
|
||
class Payment(models.Model):
|
||
"""
|
||
Payment model for tracking payments and receipts.
|
||
"""
|
||
PAYMENT_METHOD_CHOICES = [
|
||
('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'),
|
||
('INSURANCE', 'Insurance Payment'),
|
||
('ADJUSTMENT', 'Adjustment'),
|
||
('WRITE_OFF', 'Write Off'),
|
||
('OTHER', 'Other'),
|
||
]
|
||
PAYMENT_SOURCE_CHOICES = [
|
||
('PATIENT', 'Patient'),
|
||
('INSURANCE', 'Insurance'),
|
||
('GUARANTOR', 'Guarantor'),
|
||
('GOVERNMENT', 'Government'),
|
||
('CHARITY', 'Charity'),
|
||
('OTHER', 'Other'),
|
||
]
|
||
STATUS_CHOICES = [
|
||
('PENDING', 'Pending'),
|
||
('PROCESSED', 'Processed'),
|
||
('CLEARED', 'Cleared'),
|
||
('BOUNCED', 'Bounced'),
|
||
('REVERSED', 'Reversed'),
|
||
('REFUNDED', 'Refunded'),
|
||
]
|
||
CARD_TYPE_CHOICES = [
|
||
('VISA', 'Visa'),
|
||
('MASTERCARD', 'MasterCard'),
|
||
('AMEX', 'American Express'),
|
||
('DISCOVER', 'Discover'),
|
||
('OTHER', 'Other'),
|
||
]
|
||
# Medical Bill relationship
|
||
medical_bill = models.ForeignKey(
|
||
MedicalBill,
|
||
on_delete=models.CASCADE,
|
||
related_name='payments',
|
||
help_text='Related medical bill'
|
||
)
|
||
|
||
# Payment Information
|
||
payment_id = models.UUIDField(
|
||
default=uuid.uuid4,
|
||
unique=True,
|
||
editable=False,
|
||
help_text='Unique payment identifier'
|
||
)
|
||
payment_number = models.CharField(
|
||
max_length=20,
|
||
unique=True,
|
||
help_text='Payment number'
|
||
)
|
||
|
||
# Payment Details
|
||
payment_date = models.DateField(
|
||
help_text='Payment date'
|
||
)
|
||
payment_amount = models.DecimalField(
|
||
max_digits=12,
|
||
decimal_places=2,
|
||
help_text='Payment amount'
|
||
)
|
||
|
||
# Payment Method
|
||
payment_method = models.CharField(
|
||
max_length=20,
|
||
choices=PAYMENT_METHOD_CHOICES,
|
||
help_text='Payment method'
|
||
)
|
||
|
||
# Payment Source
|
||
payment_source = models.CharField(
|
||
max_length=20,
|
||
choices=PAYMENT_SOURCE_CHOICES,
|
||
help_text='Payment source'
|
||
)
|
||
|
||
# Check Information
|
||
check_number = models.CharField(
|
||
max_length=50,
|
||
blank=True,
|
||
null=True,
|
||
help_text='Check number'
|
||
)
|
||
bank_name = models.CharField(
|
||
max_length=100,
|
||
blank=True,
|
||
null=True,
|
||
help_text='Bank name'
|
||
)
|
||
routing_number = models.CharField(
|
||
max_length=20,
|
||
blank=True,
|
||
null=True,
|
||
help_text='Bank routing number'
|
||
)
|
||
|
||
# Credit Card Information (encrypted/tokenized)
|
||
card_type = models.CharField(
|
||
max_length=20,
|
||
choices=CARD_TYPE_CHOICES,
|
||
blank=True,
|
||
null=True,
|
||
help_text='Credit card type'
|
||
)
|
||
card_last_four = models.CharField(
|
||
max_length=4,
|
||
blank=True,
|
||
null=True,
|
||
help_text='Last four digits of card'
|
||
)
|
||
authorization_code = models.CharField(
|
||
max_length=20,
|
||
blank=True,
|
||
null=True,
|
||
help_text='Authorization code'
|
||
)
|
||
transaction_id = models.CharField(
|
||
max_length=50,
|
||
blank=True,
|
||
null=True,
|
||
help_text='Transaction ID'
|
||
)
|
||
|
||
# Insurance Payment Information
|
||
insurance_claim = models.ForeignKey(
|
||
InsuranceClaim,
|
||
on_delete=models.SET_NULL,
|
||
null=True,
|
||
blank=True,
|
||
related_name='payments',
|
||
help_text='Related insurance claim'
|
||
)
|
||
eob_number = models.CharField(
|
||
max_length=50,
|
||
blank=True,
|
||
null=True,
|
||
help_text='Explanation of Benefits number'
|
||
)
|
||
|
||
# Payment Status
|
||
status = models.CharField(
|
||
max_length=20,
|
||
choices=STATUS_CHOICES,
|
||
default='PENDING',
|
||
help_text='Payment status'
|
||
)
|
||
|
||
# Deposit Information
|
||
deposit_date = models.DateField(
|
||
blank=True,
|
||
null=True,
|
||
help_text='Deposit date'
|
||
)
|
||
deposit_slip = models.CharField(
|
||
max_length=50,
|
||
blank=True,
|
||
null=True,
|
||
help_text='Deposit slip number'
|
||
)
|
||
|
||
# Payment Notes
|
||
notes = models.TextField(
|
||
blank=True,
|
||
null=True,
|
||
help_text='Payment notes and comments'
|
||
)
|
||
|
||
# Refund Information
|
||
refund_amount = models.DecimalField(
|
||
max_digits=12,
|
||
decimal_places=2,
|
||
default=Decimal('0.00'),
|
||
help_text='Refund amount'
|
||
)
|
||
refund_date = models.DateField(
|
||
blank=True,
|
||
null=True,
|
||
help_text='Refund date'
|
||
)
|
||
refund_reason = models.CharField(
|
||
max_length=200,
|
||
blank=True,
|
||
null=True,
|
||
help_text='Refund reason'
|
||
)
|
||
|
||
# Staff Information
|
||
received_by = models.ForeignKey(
|
||
settings.AUTH_USER_MODEL,
|
||
on_delete=models.SET_NULL,
|
||
null=True,
|
||
blank=True,
|
||
related_name='received_payments',
|
||
help_text='Staff member who received payment'
|
||
)
|
||
processed_by = models.ForeignKey(
|
||
settings.AUTH_USER_MODEL,
|
||
on_delete=models.SET_NULL,
|
||
null=True,
|
||
blank=True,
|
||
related_name='processed_payments',
|
||
help_text='Staff member who processed payment'
|
||
)
|
||
|
||
# Metadata
|
||
created_at = models.DateTimeField(auto_now_add=True)
|
||
updated_at = models.DateTimeField(auto_now=True)
|
||
|
||
class Meta:
|
||
db_table = 'billing_payment'
|
||
verbose_name = 'Payment'
|
||
verbose_name_plural = 'Payments'
|
||
ordering = ['-payment_date']
|
||
indexes = [
|
||
models.Index(fields=['medical_bill']),
|
||
models.Index(fields=['payment_number']),
|
||
models.Index(fields=['payment_date']),
|
||
models.Index(fields=['payment_method']),
|
||
models.Index(fields=['status']),
|
||
]
|
||
|
||
def __str__(self):
|
||
return f"{self.payment_number} - ${self.payment_amount}"
|
||
|
||
def save(self, *args, **kwargs):
|
||
"""
|
||
Generate payment number if not provided.
|
||
"""
|
||
if not self.payment_number:
|
||
# Generate payment number (simple implementation)
|
||
today = timezone.now().date()
|
||
last_payment = Payment.objects.filter(
|
||
medical_bill__tenant=self.medical_bill.tenant,
|
||
created_at__date=today
|
||
).order_by('-id').first()
|
||
|
||
if last_payment:
|
||
last_number = int(last_payment.payment_number.split('-')[-1])
|
||
self.payment_number = f"PAY-{today.strftime('%Y%m%d')}-{last_number + 1:04d}"
|
||
else:
|
||
self.payment_number = f"PAY-{today.strftime('%Y%m%d')}-0001"
|
||
|
||
super().save(*args, **kwargs)
|
||
|
||
@property
|
||
def patient(self):
|
||
"""
|
||
Get patient from medical bill.
|
||
"""
|
||
return self.medical_bill.patient
|
||
|
||
@property
|
||
def tenant(self):
|
||
"""
|
||
Get tenant from medical bill.
|
||
"""
|
||
return self.medical_bill.tenant
|
||
|
||
@property
|
||
def net_payment(self):
|
||
"""
|
||
Calculate net payment after refunds.
|
||
"""
|
||
return self.payment_amount - self.refund_amount
|
||
|
||
|
||
class ClaimStatusUpdate(models.Model):
|
||
"""
|
||
Claim status update model for tracking claim processing history.
|
||
"""
|
||
UPDATE_SOURCE_CHOICES = [
|
||
('MANUAL', 'Manual Update'),
|
||
('EDI', 'EDI Response'),
|
||
('PHONE', 'Phone Call'),
|
||
('PORTAL', 'Insurance Portal'),
|
||
('EMAIL', 'Email'),
|
||
('FAX', 'Fax'),
|
||
('MAIL', 'Mail'),
|
||
('SYSTEM', 'System Generated'),
|
||
]
|
||
# Insurance Claim relationship
|
||
insurance_claim = models.ForeignKey(
|
||
InsuranceClaim,
|
||
on_delete=models.CASCADE,
|
||
related_name='status_updates',
|
||
help_text='Related insurance claim'
|
||
)
|
||
|
||
# Update Information
|
||
update_id = models.UUIDField(
|
||
default=uuid.uuid4,
|
||
unique=True,
|
||
editable=False,
|
||
help_text='Unique update identifier'
|
||
)
|
||
|
||
# Status Information
|
||
previous_status = models.CharField(
|
||
max_length=20,
|
||
help_text='Previous claim status'
|
||
)
|
||
new_status = models.CharField(
|
||
max_length=20,
|
||
help_text='New claim status'
|
||
)
|
||
status_date = models.DateTimeField(
|
||
default=timezone.now,
|
||
help_text='Status change date and time'
|
||
)
|
||
|
||
# Update Details
|
||
update_source = models.CharField(
|
||
max_length=20,
|
||
choices=UPDATE_SOURCE_CHOICES,
|
||
help_text='Update source'
|
||
)
|
||
|
||
# Response Information
|
||
response_code = models.CharField(
|
||
max_length=20,
|
||
blank=True,
|
||
null=True,
|
||
help_text='Insurance response code'
|
||
)
|
||
response_message = models.TextField(
|
||
blank=True,
|
||
null=True,
|
||
help_text='Insurance response message'
|
||
)
|
||
|
||
# Financial Updates
|
||
allowed_amount = models.DecimalField(
|
||
max_digits=12,
|
||
decimal_places=2,
|
||
blank=True,
|
||
null=True,
|
||
help_text='Updated allowed amount'
|
||
)
|
||
paid_amount = models.DecimalField(
|
||
max_digits=12,
|
||
decimal_places=2,
|
||
blank=True,
|
||
null=True,
|
||
help_text='Updated paid amount'
|
||
)
|
||
patient_responsibility = models.DecimalField(
|
||
max_digits=12,
|
||
decimal_places=2,
|
||
blank=True,
|
||
null=True,
|
||
help_text='Updated patient responsibility'
|
||
)
|
||
|
||
# Update Notes
|
||
notes = models.TextField(
|
||
blank=True,
|
||
null=True,
|
||
help_text='Update notes and comments'
|
||
)
|
||
|
||
# Staff Information
|
||
updated_by = models.ForeignKey(
|
||
settings.AUTH_USER_MODEL,
|
||
on_delete=models.SET_NULL,
|
||
null=True,
|
||
blank=True,
|
||
related_name='claim_status_updates',
|
||
help_text='Staff member who made the update'
|
||
)
|
||
|
||
# Metadata
|
||
created_at = models.DateTimeField(auto_now_add=True)
|
||
|
||
class Meta:
|
||
db_table = 'billing_claim_status_update'
|
||
verbose_name = 'Claim Status Update'
|
||
verbose_name_plural = 'Claim Status Updates'
|
||
ordering = ['-status_date']
|
||
indexes = [
|
||
models.Index(fields=['insurance_claim']),
|
||
models.Index(fields=['status_date']),
|
||
models.Index(fields=['new_status']),
|
||
]
|
||
|
||
def __str__(self):
|
||
return f"{self.insurance_claim.claim_number} - {self.previous_status} → {self.new_status}"
|
||
|
||
@property
|
||
def patient(self):
|
||
"""
|
||
Get patient from insurance claim.
|
||
"""
|
||
return self.insurance_claim.patient
|
||
|
||
@property
|
||
def tenant(self):
|
||
"""
|
||
Get tenant from insurance claim.
|
||
"""
|
||
return self.insurance_claim.tenant
|
||
|
||
|
||
class BillingConfiguration(models.Model):
|
||
"""
|
||
Billing configuration model for tenant-specific billing settings.
|
||
"""
|
||
DEFAULT_PAYMENT_TERMS_CHOICES = [
|
||
('NET_30', 'Net 30 Days'),
|
||
('NET_60', 'Net 60 Days'),
|
||
('NET_90', 'Net 90 Days'),
|
||
('IMMEDIATE', 'Immediate'),
|
||
]
|
||
STATEMENT_FREQUENCY_CHOICES = [
|
||
('MONTHLY', 'Monthly'),
|
||
('QUARTERLY', 'Quarterly'),
|
||
('ON_DEMAND', 'On Demand'),
|
||
]
|
||
CLAIM_SUBMISSION_FREQUENCY_CHOICES = [
|
||
('DAILY', 'Daily'),
|
||
('WEEKLY', 'Weekly'),
|
||
('MANUAL', 'Manual'),
|
||
]
|
||
# Tenant relationship
|
||
tenant = models.OneToOneField(
|
||
'core.Tenant',
|
||
on_delete=models.CASCADE,
|
||
related_name='billing_configuration',
|
||
help_text='Organization tenant'
|
||
)
|
||
|
||
# Configuration Information
|
||
config_id = models.UUIDField(
|
||
default=uuid.uuid4,
|
||
unique=True,
|
||
editable=False,
|
||
help_text='Unique configuration identifier'
|
||
)
|
||
|
||
# Billing Settings
|
||
default_payment_terms = models.CharField(
|
||
max_length=20,
|
||
choices=DEFAULT_PAYMENT_TERMS_CHOICES,
|
||
default='NET_30',
|
||
help_text='Default payment terms'
|
||
)
|
||
|
||
# Tax Settings
|
||
tax_rate = models.DecimalField(
|
||
max_digits=5,
|
||
decimal_places=4,
|
||
default=Decimal('0.0000'),
|
||
help_text='Default tax rate (as decimal, e.g., 0.0825 for 8.25%)'
|
||
)
|
||
tax_exempt = models.BooleanField(
|
||
default=True,
|
||
help_text='Organization is tax exempt'
|
||
)
|
||
|
||
# Statement Settings
|
||
statement_frequency = models.CharField(
|
||
max_length=20,
|
||
choices=STATEMENT_FREQUENCY_CHOICES,
|
||
default='MONTHLY',
|
||
help_text='Statement frequency'
|
||
)
|
||
statement_message = models.TextField(
|
||
blank=True,
|
||
null=True,
|
||
help_text='Default statement message'
|
||
)
|
||
|
||
# Collection Settings
|
||
first_notice_days = models.PositiveIntegerField(
|
||
default=30,
|
||
help_text='Days after due date for first notice'
|
||
)
|
||
second_notice_days = models.PositiveIntegerField(
|
||
default=60,
|
||
help_text='Days after due date for second notice'
|
||
)
|
||
final_notice_days = models.PositiveIntegerField(
|
||
default=90,
|
||
help_text='Days after due date for final notice'
|
||
)
|
||
collections_days = models.PositiveIntegerField(
|
||
default=120,
|
||
help_text='Days after due date to send to collections'
|
||
)
|
||
|
||
# Interest Settings
|
||
apply_interest = models.BooleanField(
|
||
default=False,
|
||
help_text='Apply interest to overdue accounts'
|
||
)
|
||
interest_rate = models.DecimalField(
|
||
max_digits=5,
|
||
decimal_places=4,
|
||
default=Decimal('0.0000'),
|
||
help_text='Monthly interest rate (as decimal)'
|
||
)
|
||
|
||
# Payment Settings
|
||
accept_credit_cards = models.BooleanField(
|
||
default=True,
|
||
help_text='Accept credit card payments'
|
||
)
|
||
accept_ach = models.BooleanField(
|
||
default=True,
|
||
help_text='Accept ACH payments'
|
||
)
|
||
payment_portal_enabled = models.BooleanField(
|
||
default=True,
|
||
help_text='Enable online payment portal'
|
||
)
|
||
|
||
# Claim Settings
|
||
auto_submit_claims = models.BooleanField(
|
||
default=False,
|
||
help_text='Automatically submit claims'
|
||
)
|
||
claim_submission_frequency = models.CharField(
|
||
max_length=20,
|
||
choices=CLAIM_SUBMISSION_FREQUENCY_CHOICES,
|
||
default='DAILY',
|
||
help_text='Claim submission frequency'
|
||
)
|
||
|
||
# Clearinghouse Settings
|
||
primary_clearinghouse = models.CharField(
|
||
max_length=100,
|
||
blank=True,
|
||
null=True,
|
||
help_text='Primary clearinghouse'
|
||
)
|
||
secondary_clearinghouse = models.CharField(
|
||
max_length=100,
|
||
blank=True,
|
||
null=True,
|
||
help_text='Secondary clearinghouse'
|
||
)
|
||
|
||
# Reporting Settings
|
||
aging_buckets = models.JSONField(
|
||
default=default_aging_buckets,
|
||
help_text='Aging report buckets in days'
|
||
)
|
||
|
||
# Metadata
|
||
created_at = models.DateTimeField(auto_now_add=True)
|
||
updated_at = models.DateTimeField(auto_now=True)
|
||
created_by = models.ForeignKey(
|
||
settings.AUTH_USER_MODEL,
|
||
on_delete=models.SET_NULL,
|
||
null=True,
|
||
blank=True,
|
||
related_name='created_billing_configurations',
|
||
help_text='User who created the configuration'
|
||
)
|
||
|
||
class Meta:
|
||
db_table = 'billing_configuration'
|
||
verbose_name = 'Billing Configuration'
|
||
verbose_name_plural = 'Billing Configurations'
|
||
|
||
def __str__(self):
|
||
return f"Billing Config - {self.tenant.name}"
|
||
|