Marwan Alwali ab2c4a36c5 update
2025-10-02 10:13:03 +03:00

1463 lines
42 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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}"