1463 lines
42 KiB
Python
1463 lines
42 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.
|
||
"""
|
||
|
||
class BillType(models.TextChoices):
|
||
INPATIENT = 'INPATIENT', 'Inpatient'
|
||
OUTPATIENT = 'OUTPATIENT', 'Outpatient'
|
||
EMERGENCY = 'EMERGENCY', 'Emergency'
|
||
SURGERY = 'SURGERY', 'Surgery'
|
||
LABORATORY = 'LABORATORY', 'Laboratory'
|
||
RADIOLOGY = 'RADIOLOGY', 'Radiology'
|
||
PHARMACY = 'PHARMACY', 'Pharmacy'
|
||
PROFESSIONAL = 'PROFESSIONAL', 'Professional Services'
|
||
FACILITY = 'FACILITY', 'Facility Charges'
|
||
ANCILLARY = 'ANCILLARY', 'Ancillary Services'
|
||
|
||
class BillStatus(models.TextChoices):
|
||
DRAFT = 'DRAFT', 'Draft'
|
||
PENDING = 'PENDING', 'Pending'
|
||
SUBMITTED = 'SUBMITTED', 'Submitted'
|
||
PARTIAL_PAID = 'PARTIAL_PAID', 'Partially Paid' # kept as provided
|
||
PAID = 'PAID', 'Paid'
|
||
OVERDUE = 'OVERDUE', 'Overdue'
|
||
COLLECTIONS = 'COLLECTIONS', 'Collections'
|
||
WRITTEN_OFF = 'WRITTEN_OFF', 'Written Off'
|
||
CANCELLED = 'CANCELLED', 'Cancelled'
|
||
|
||
class PaymentTerms(models.TextChoices):
|
||
NET_30 = 'NET_30', 'Net 30 Days'
|
||
NET_60 = 'NET_60', 'Net 60 Days'
|
||
NET_90 = 'NET_90', 'Net 90 Days'
|
||
IMMEDIATE = 'IMMEDIATE', 'Immediate'
|
||
CUSTOM = 'CUSTOM', 'Custom Terms'
|
||
|
||
class CollectionStatus(models.TextChoices):
|
||
NONE = 'NONE', 'None'
|
||
FIRST_NOTICE = 'FIRST_NOTICE', 'First Notice'
|
||
SECOND_NOTICE = 'SECOND_NOTICE', 'Second Notice'
|
||
FINAL_NOTICE = 'FINAL_NOTICE', 'Final Notice'
|
||
COLLECTIONS = 'COLLECTIONS', 'Collections'
|
||
LEGAL = 'LEGAL', 'Legal Action'
|
||
|
||
# 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=BillType.choices,
|
||
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=BillStatus.choices,
|
||
default=BillStatus.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=PaymentTerms.choices,
|
||
default=PaymentTerms.NET_30,
|
||
help_text='Payment terms'
|
||
)
|
||
|
||
# Collection Information
|
||
collection_status = models.CharField(
|
||
max_length=20,
|
||
choices=CollectionStatus.choices,
|
||
default=CollectionStatus.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.
|
||
"""
|
||
|
||
class ServiceCategory(models.TextChoices):
|
||
EVALUATION = 'EVALUATION', 'Evaluation & Management'
|
||
SURGERY = 'SURGERY', 'Surgery'
|
||
RADIOLOGY = 'RADIOLOGY', 'Radiology'
|
||
PATHOLOGY = 'PATHOLOGY', 'Pathology & Laboratory'
|
||
MEDICINE = 'MEDICINE', 'Medicine'
|
||
ANESTHESIA = 'ANESTHESIA', 'Anesthesia'
|
||
SUPPLIES = 'SUPPLIES', 'Medical Supplies'
|
||
PHARMACY = 'PHARMACY', 'Pharmacy'
|
||
ROOM_BOARD = 'ROOM_BOARD', 'Room & Board'
|
||
NURSING = 'NURSING', 'Nursing Services'
|
||
THERAPY = 'THERAPY', 'Therapy Services'
|
||
EMERGENCY = 'EMERGENCY', 'Emergency Services'
|
||
AMBULANCE = 'AMBULANCE', 'Ambulance Services'
|
||
DME = 'DME', 'Durable Medical Equipment'
|
||
OTHER = 'OTHER', 'Other Services'
|
||
|
||
class ServiceUnitOfMeasure(models.TextChoices):
|
||
EACH = 'EACH', 'Each'
|
||
UNIT = 'UNIT', 'Unit'
|
||
HOUR = 'HOUR', 'Hour'
|
||
DAY = 'DAY', 'Day'
|
||
VISIT = 'VISIT', 'Visit'
|
||
PROCEDURE = 'PROCEDURE', 'Procedure'
|
||
DOSE = 'DOSE', 'Dose'
|
||
MILE = 'MILE', 'Mile'
|
||
MINUTE = 'MINUTE', 'Minute'
|
||
|
||
class PlaceOfService(models.IntegerChoices):
|
||
OFFICE = 11, 'Office'
|
||
HOME = 12, 'Home'
|
||
INPATIENT_HOSPITAL = 21, 'Inpatient Hospital'
|
||
OUTPATIENT_HOSPITAL = 22, 'Outpatient Hospital'
|
||
EMERGENCY_ROOM = 23, 'Emergency Room'
|
||
AMBULATORY_SURGICAL_CENTER = 24, 'Ambulatory Surgical Center'
|
||
BIRTHING_CENTER = 25, 'Birthing Center'
|
||
MILITARY_TREATMENT_FACILITY = 26, 'Military Treatment Facility'
|
||
SKILLED_NURSING_FACILITY = 31, 'Skilled Nursing Facility'
|
||
NURSING_FACILITY = 32, 'Nursing Facility'
|
||
CUSTODIAL_CARE_FACILITY = 33, 'Custodial Care Facility'
|
||
HOSPICE = 34, 'Hospice'
|
||
AMBULANCE_LAND = 41, 'Ambulance - Land'
|
||
AMBULANCE_AIR_OR_WATER = 42, 'Ambulance - Air or Water'
|
||
INDEPENDENT_CLINIC = 49, 'Independent Clinic'
|
||
FEDERALLY_QUALIFIED_HEALTH_CENTER = 50, 'Federally Qualified Health Center'
|
||
INPATIENT_PSYCHIATRIC_FACILITY = 51, 'Inpatient Psychiatric Facility'
|
||
PSYCHIATRIC_PARTIAL_HOSPITALIZATION = 52, 'Psychiatric Facility-Partial Hospitalization'
|
||
COMMUNITY_MENTAL_HEALTH_CENTER = 53, 'Community Mental Health Center'
|
||
INTERMEDIATE_CARE_FACILITY_MR = 54, 'Intermediate Care Facility/Mentally Retarded'
|
||
RESIDENTIAL_SUBSTANCE_ABUSE_TREATMENT = 55, 'Residential Substance Abuse Treatment Facility'
|
||
PSYCHIATRIC_RESIDENTIAL_TREATMENT_CENTER = 56, 'Psychiatric Residential Treatment Center'
|
||
NON_RESIDENTIAL_SUBSTANCE_ABUSE_TREATMENT = 57, 'Non-residential Substance Abuse Treatment Facility'
|
||
MASS_IMMUNIZATION_CENTER = 60, 'Mass Immunization Center'
|
||
COMPREHENSIVE_INPATIENT_REHAB_FACILITY = 61, 'Comprehensive Inpatient Rehabilitation Facility'
|
||
COMPREHENSIVE_OUTPATIENT_REHAB_FACILITY = 62, 'Comprehensive Outpatient Rehabilitation Facility'
|
||
ESRD_TREATMENT_FACILITY = 65, 'End-Stage Renal Disease Treatment Facility'
|
||
PUBLIC_HEALTH_CLINIC = 71, 'Public Health Clinic'
|
||
RURAL_HEALTH_CLINIC = 72, 'Rural Health Clinic'
|
||
INDEPENDENT_LABORATORY = 81, 'Independent Laboratory'
|
||
OTHER_PLACE_OF_SERVICE = 99, 'Other Place of Service'
|
||
|
||
class ServiceStatus(models.TextChoices):
|
||
ACTIVE = 'ACTIVE', 'Active'
|
||
DENIED = 'DENIED', 'Denied'
|
||
ADJUSTED = 'ADJUSTED', 'Adjusted'
|
||
VOIDED = '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=ServiceCategory.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=ServiceUnitOfMeasure.choices,
|
||
default=ServiceUnitOfMeasure.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.IntegerField(
|
||
choices=PlaceOfService.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=ServiceStatus.choices,
|
||
default=ServiceStatus.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.
|
||
"""
|
||
|
||
class ClaimSubmissionType(models.TextChoices):
|
||
PRIMARY = 'PRIMARY', 'Primary Claim'
|
||
SECONDARY = 'SECONDARY', 'Secondary Claim'
|
||
TERTIARY = 'TERTIARY', 'Tertiary Claim'
|
||
CORRECTED = 'CORRECTED', 'Corrected Claim'
|
||
VOID = 'VOID', 'Void Claim'
|
||
REPLACEMENT = 'REPLACEMENT', 'Replacement Claim'
|
||
|
||
class ClaimProcessingStatus(models.TextChoices):
|
||
DRAFT = 'DRAFT', 'Draft'
|
||
SUBMITTED = 'SUBMITTED', 'Submitted'
|
||
PENDING = 'PENDING', 'Pending'
|
||
PROCESSING = 'PROCESSING', 'Processing'
|
||
PAID = 'PAID', 'Paid'
|
||
DENIED = 'DENIED', 'Denied'
|
||
REJECTED = 'REJECTED', 'Rejected'
|
||
APPEALED = 'APPEALED', 'Appealed'
|
||
VOIDED = '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=ClaimSubmissionType.choices,
|
||
default=ClaimSubmissionType.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=ClaimProcessingStatus.choices,
|
||
default=ClaimProcessingStatus.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.
|
||
"""
|
||
|
||
class PaymentMethod(models.TextChoices):
|
||
CASH = 'CASH', 'Cash'
|
||
CHECK = 'CHECK', 'Check'
|
||
CREDIT_CARD = 'CREDIT_CARD', 'Credit Card'
|
||
DEBIT_CARD = 'DEBIT_CARD', 'Debit Card'
|
||
BANK_TRANSFER = 'BANK_TRANSFER', 'Bank Transfer'
|
||
ACH = 'ACH', 'ACH Transfer'
|
||
WIRE = 'WIRE', 'Wire Transfer'
|
||
MONEY_ORDER = 'MONEY_ORDER', 'Money Order'
|
||
INSURANCE = 'INSURANCE', 'Insurance Payment'
|
||
ADJUSTMENT = 'ADJUSTMENT', 'Adjustment'
|
||
WRITE_OFF = 'WRITE_OFF', 'Write Off'
|
||
OTHER = 'OTHER', 'Other'
|
||
|
||
class PaymentSource(models.TextChoices):
|
||
PATIENT = 'PATIENT', 'Patient'
|
||
INSURANCE = 'INSURANCE', 'Insurance'
|
||
GUARANTOR = 'GUARANTOR', 'Guarantor'
|
||
GOVERNMENT = 'GOVERNMENT', 'Government'
|
||
CHARITY = 'CHARITY', 'Charity'
|
||
OTHER = 'OTHER', 'Other'
|
||
|
||
class PaymentStatus(models.TextChoices):
|
||
PENDING = 'PENDING', 'Pending'
|
||
PROCESSED = 'PROCESSED', 'Processed'
|
||
CLEARED = 'CLEARED', 'Cleared'
|
||
BOUNCED = 'BOUNCED', 'Bounced'
|
||
REVERSED = 'REVERSED', 'Reversed'
|
||
REFUNDED = 'REFUNDED', 'Refunded'
|
||
|
||
class CardType(models.TextChoices):
|
||
VISA = 'VISA', 'Visa'
|
||
MASTERCARD = 'MASTERCARD', 'MasterCard'
|
||
AMEX = 'AMEX', 'American Express'
|
||
DISCOVER = 'DISCOVER', 'Discover'
|
||
OTHER = '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=PaymentMethod.choices,
|
||
help_text='Payment method'
|
||
)
|
||
|
||
# Payment Source
|
||
payment_source = models.CharField(
|
||
max_length=20,
|
||
choices=PaymentSource.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=CardType.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=PaymentStatus.choices,
|
||
default=PaymentStatus.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.
|
||
"""
|
||
|
||
class UpdateSource(models.TextChoices):
|
||
MANUAL = 'MANUAL', 'Manual Update'
|
||
EDI = 'EDI', 'EDI Response'
|
||
PHONE = 'PHONE', 'Phone Call'
|
||
PORTAL = 'PORTAL', 'Insurance Portal'
|
||
EMAIL = 'EMAIL', 'Email'
|
||
FAX = 'FAX', 'Fax'
|
||
MAIL = 'MAIL', 'Mail'
|
||
SYSTEM = '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=UpdateSource.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.
|
||
"""
|
||
|
||
class DefaultPaymentTerms(models.TextChoices):
|
||
NET_30 = 'NET_30', 'Net 30 Days'
|
||
NET_60 = 'NET_60', 'Net 60 Days'
|
||
NET_90 = 'NET_90', 'Net 90 Days'
|
||
IMMEDIATE = 'IMMEDIATE', 'Immediate'
|
||
|
||
class StatementFrequency(models.TextChoices):
|
||
MONTHLY = 'MONTHLY', 'Monthly'
|
||
QUARTERLY = 'QUARTERLY', 'Quarterly'
|
||
ON_DEMAND = 'ON_DEMAND', 'On Demand'
|
||
|
||
class ClaimSubmissionFrequency(models.TextChoices):
|
||
DAILY = 'DAILY', 'Daily'
|
||
WEEKLY = 'WEEKLY', 'Weekly'
|
||
MANUAL = '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=DefaultPaymentTerms.choices,
|
||
default=DefaultPaymentTerms.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=StatementFrequency.choices,
|
||
default=StatementFrequency.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=ClaimSubmissionFrequency.choices,
|
||
default=ClaimSubmissionFrequency.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}"
|
||
|