709 lines
32 KiB
Python
709 lines
32 KiB
Python
import os
|
||
import django
|
||
|
||
# Set up Django environment
|
||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'hospital_management.settings')
|
||
django.setup()
|
||
|
||
import uuid
|
||
import random
|
||
from datetime import datetime, timedelta, date
|
||
from decimal import Decimal
|
||
from django.utils import timezone as django_timezone
|
||
from django.contrib.auth import get_user_model
|
||
from core.models import Tenant
|
||
from patients.models import PatientProfile, InsuranceInfo
|
||
from emr.models import Encounter
|
||
from inpatients.models import Admission
|
||
from billing.models import *
|
||
from accounts.models import User
|
||
|
||
|
||
|
||
from django.db import IntegrityError, transaction
|
||
from django.db.models import DecimalField, CharField, IntegerField
|
||
from django.db.models.fields import NOT_PROVIDED
|
||
|
||
def _model_fields(Model):
|
||
return {f.name for f in Model._meta.get_fields() if getattr(f, "concrete", False) and not f.auto_created}
|
||
|
||
def _filter_kwargs(Model, data: dict):
|
||
allowed = _model_fields(Model)
|
||
return {k: v for k, v in data.items() if k in allowed}
|
||
|
||
def _next_seq_number(prefix, Model, field):
|
||
today = django_timezone.now().date().strftime("%Y%m%d")
|
||
i = 1
|
||
while True:
|
||
candidate = f"{prefix}-{today}-{i:04d}"
|
||
if not Model.objects.filter(**{field: candidate}).exists():
|
||
return candidate
|
||
i += 1
|
||
|
||
|
||
def _quantize_for_field(value: Decimal, f: DecimalField) -> Decimal:
|
||
"""Quantize a Decimal to a field's decimal_places and clamp to max_digits."""
|
||
# Quantize to required decimal_places
|
||
q = Decimal(1).scaleb(-f.decimal_places) # 10^(-decimal_places)
|
||
v = Decimal(value).quantize(q)
|
||
|
||
# Ensure total digits <= max_digits (digits before + decimal_places)
|
||
# Count digits before decimal:
|
||
sign, digits, exp = v.as_tuple()
|
||
digits_str_len = len(digits)
|
||
# number of digits after decimal is decimal_places
|
||
digits_before = digits_str_len - f.decimal_places if f.decimal_places else digits_str_len
|
||
# if v is 0.x and decimal_places > digits, digits_before can be negative; normalize
|
||
if digits_before < 0:
|
||
digits_before = 0
|
||
|
||
max_before = f.max_digits - f.decimal_places
|
||
if max_before < 0:
|
||
max_before = 0
|
||
|
||
# If too many digits before decimal, clamp to the largest representable value
|
||
if digits_before > max_before:
|
||
# Largest integer part we can store is 10^max_before - 1
|
||
max_int = (10 ** max_before) - 1 if max_before > 0 else 0
|
||
v = Decimal(max_int).quantize(q)
|
||
return v
|
||
|
||
def _default_decimal_for(BillingConfiguration, f: DecimalField) -> Decimal:
|
||
"""Return a safe default decimal per field name & precision."""
|
||
name = f.name.lower()
|
||
if any(k in name for k in ["tax", "vat", "rate"]):
|
||
# Prefer 15% if it's a rate; use 15 if integer percent
|
||
base = Decimal("15") if f.decimal_places == 0 else Decimal("0.15")
|
||
else:
|
||
base = Decimal("0")
|
||
return _quantize_for_field(base, f)
|
||
|
||
# Saudi billing specific data
|
||
SAUDI_MEDICAL_SERVICES = [
|
||
# Consultation Services
|
||
('99213', 'Office/Outpatient Visit, Established Patient (Low Complexity)', 'CONSULTATION', 150.00),
|
||
('99214', 'Office/Outpatient Visit, Established Patient (Moderate Complexity)', 'CONSULTATION', 200.00),
|
||
('99215', 'Office/Outpatient Visit, Established Patient (High Complexity)', 'CONSULTATION', 280.00),
|
||
('99203', 'Office/Outpatient Visit, New Patient (Low Complexity)', 'CONSULTATION', 180.00),
|
||
('99204', 'Office/Outpatient Visit, New Patient (Moderate Complexity)', 'CONSULTATION', 250.00),
|
||
('99205', 'Office/Outpatient Visit, New Patient (High Complexity)', 'CONSULTATION', 350.00),
|
||
|
||
# Emergency Services
|
||
('99281', 'Emergency Department Visit (Low Complexity)', 'EMERGENCY', 300.00),
|
||
('99282', 'Emergency Department Visit (Low-Moderate Complexity)', 'EMERGENCY', 450.00),
|
||
('99283', 'Emergency Department Visit (Moderate Complexity)', 'EMERGENCY', 650.00),
|
||
('99284', 'Emergency Department Visit (Moderate-High Complexity)', 'EMERGENCY', 850.00),
|
||
('99285', 'Emergency Department Visit (High Complexity)', 'EMERGENCY', 1200.00),
|
||
|
||
# Laboratory Services
|
||
('80053', 'Comprehensive Metabolic Panel', 'LABORATORY', 85.00),
|
||
('85025', 'Blood Count; Complete (CBC)', 'LABORATORY', 45.00),
|
||
('80061', 'Lipid Panel', 'LABORATORY', 65.00),
|
||
('85610', 'Prothrombin Time (PT)', 'LABORATORY', 35.00),
|
||
('84443', 'Thyroid Stimulating Hormone (TSH)', 'LABORATORY', 75.00),
|
||
('82947', 'Glucose; Quantitative, Blood', 'LABORATORY', 25.00),
|
||
('83036', 'Hemoglobin A1C', 'LABORATORY', 55.00),
|
||
|
||
# Radiology Services
|
||
('71020', 'Chest X-ray, Two Views', 'RADIOLOGY', 120.00),
|
||
('73060', 'Knee X-ray, Two Views', 'RADIOLOGY', 100.00),
|
||
('72148', 'MRI Lumbar Spine', 'RADIOLOGY', 1800.00),
|
||
('70553', 'MRI Brain with Contrast', 'RADIOLOGY', 2200.00),
|
||
('76700', 'Abdominal Ultrasound', 'RADIOLOGY', 350.00),
|
||
('93000', 'Electrocardiogram (ECG)', 'RADIOLOGY', 80.00),
|
||
('93307', 'Echocardiography', 'RADIOLOGY', 450.00),
|
||
|
||
# Surgical Procedures
|
||
('44970', 'Laparoscopic Appendectomy', 'SURGERY', 8500.00),
|
||
('47563', 'Laparoscopic Cholecystectomy', 'SURGERY', 12000.00),
|
||
('29881', 'Arthroscopy, Knee, Surgical', 'SURGERY', 6500.00),
|
||
('64483', 'Epidural Injection, Lumbar', 'SURGERY', 1200.00),
|
||
('19307', 'Mastectomy, Modified Radical', 'SURGERY', 15000.00),
|
||
|
||
# Pharmacy Services
|
||
('J1050', 'Injection, Medroxyprogesterone Acetate', 'PHARMACY', 45.00),
|
||
('J3420', 'Injection, Vitamin B-12', 'PHARMACY', 25.00),
|
||
('J7050', 'Infusion, Normal Saline Solution', 'PHARMACY', 35.00),
|
||
('90788', 'Intramuscular Injection', 'PHARMACY', 15.00),
|
||
|
||
# Room and Board
|
||
('R0001', 'Private Room, Per Day', 'ACCOMMODATION', 800.00),
|
||
('R0002', 'Semi-Private Room, Per Day', 'ACCOMMODATION', 600.00),
|
||
('R0003', 'ICU Bed, Per Day', 'ACCOMMODATION', 2500.00),
|
||
('R0004', 'CCU Bed, Per Day', 'ACCOMMODATION', 2800.00),
|
||
('R0005', 'Emergency Department Bed, Per Day', 'ACCOMMODATION', 1200.00),
|
||
]
|
||
|
||
SAUDI_DIAGNOSIS_CODES = [
|
||
('Z00.00', 'Encounter for general adult medical examination without abnormal findings'),
|
||
('I10', 'Essential (primary) hypertension'),
|
||
('E11.9', 'Type 2 diabetes mellitus without complications'),
|
||
('J44.1', 'Chronic obstructive pulmonary disease with acute exacerbation'),
|
||
('M79.3', 'Panniculitis, unsystematic'),
|
||
('K21.9', 'Gastro-esophageal reflux disease without esophagitis'),
|
||
('R06.02', 'Shortness of breath'),
|
||
('M25.511', 'Pain in right shoulder'),
|
||
('R50.9', 'Fever, unspecified'),
|
||
('N39.0', 'Urinary tract infection, site not specified'),
|
||
('J06.9', 'Acute upper respiratory infection, unspecified'),
|
||
('K59.00', 'Constipation, unspecified'),
|
||
('R51', 'Headache'),
|
||
('M54.5', 'Low back pain'),
|
||
('F32.9', 'Major depressive disorder, single episode, unspecified'),
|
||
]
|
||
|
||
SAUDI_PROVIDERS = [
|
||
'Dr. Mohammed Al-Rashid', 'Dr. Fatima Al-Zahra', 'Dr. Abdullah Al-Mutairi',
|
||
'Dr. Aisha Al-Qarni', 'Dr. Ahmad Al-Harbi', 'Dr. Nora Al-Dawsari',
|
||
'Dr. Khalid Al-Subai', 'Dr. Maryam Al-Sharani', 'Dr. Omar Al-Ghamdi',
|
||
'Dr. Layla Al-Zahrani', 'Dr. Faisal Al-Maliki', 'Dr. Hala Al-Shehri'
|
||
]
|
||
|
||
SAUDI_CLEARINGHOUSES = ['Availity', 'Change Healthcare', 'Trizetto', 'Saudi Health Exchange']
|
||
|
||
SAUDI_PAYMENT_METHODS = ['CASH', 'CREDIT_CARD', 'DEBIT_CARD', 'BANK_TRANSFER', 'CHECK', 'INSURANCE']
|
||
|
||
SAUDI_PAYMENT_SOURCES = ['PATIENT', 'INSURANCE', 'GOVERNMENT', 'EMPLOYER']
|
||
|
||
def _next_seq_number(prefix, Model, field):
|
||
today = django_timezone.now().date().strftime("%Y%m%d")
|
||
i = 1
|
||
while True:
|
||
candidate = f"{prefix}-{today}-{i:04d}"
|
||
if not Model.objects.filter(**{field: candidate}).exists():
|
||
return candidate
|
||
i += 1
|
||
|
||
def create_saudi_medical_bills():
|
||
"""Create medical bills for Saudi patients"""
|
||
print("Creating Saudi medical bills...")
|
||
|
||
# Get patients with encounters or admissions
|
||
patients_with_encounters = list(PatientProfile.objects.filter(
|
||
encounters__isnull=False
|
||
).distinct()[:100])
|
||
|
||
patients_with_admissions = list(PatientProfile.objects.filter(
|
||
admissions__isnull=False
|
||
).distinct()[:50])
|
||
|
||
all_patients = list(set(patients_with_encounters + patients_with_admissions))
|
||
|
||
if not all_patients:
|
||
print("No patients with encounters or admissions found. Creating bills for random patients...")
|
||
all_patients = list(PatientProfile.objects.all()[:100])
|
||
|
||
# Get actual User instances for providers
|
||
users = list(User.objects.filter(is_active=True))
|
||
provider_users = list(User.objects.filter(
|
||
is_active=True,
|
||
employee_profile__role__in=['PHYSICIAN', 'NURSE_PRACTITIONER', 'PHYSICIAN_ASSISTANT', 'RADIOLOGIST']
|
||
))
|
||
|
||
if not provider_users:
|
||
provider_users = users
|
||
|
||
bills_created = 0
|
||
created_bills = []
|
||
|
||
# Keep track of bill numbers to avoid duplicates
|
||
used_bill_numbers = set()
|
||
|
||
for patient in all_patients:
|
||
num_bills = random.randint(1, 3)
|
||
|
||
for i in range(num_bills):
|
||
try:
|
||
service_date_from = django_timezone.now().date() - timedelta(days=random.randint(1, 180))
|
||
service_date_to = service_date_from + timedelta(days=random.randint(0, 7))
|
||
bill_date = service_date_from + timedelta(days=random.randint(0, 30))
|
||
due_date = bill_date + timedelta(days=30)
|
||
|
||
encounter = patient.encounters.first() if patient.encounters.exists() else None
|
||
admission = patient.admissions.first() if patient.admissions.exists() else None
|
||
|
||
primary_insurance = patient.insurance_info.filter(insurance_type='PRIMARY').first()
|
||
secondary_insurance = patient.insurance_info.filter(insurance_type='SECONDARY').first()
|
||
|
||
# Generate unique bill number
|
||
today = django_timezone.now().date()
|
||
bill_number = None
|
||
counter = 1
|
||
|
||
# while bill_number is None or bill_number in used_bill_numbers:
|
||
# bill_number = f"BILL-{today.strftime('%Y%m%d')}-{bills_created + counter:04d}"
|
||
# counter += 1
|
||
|
||
used_bill_numbers.add(bill_number)
|
||
bill_number = _next_seq_number("BILL", MedicalBill, "bill_number")
|
||
bill = MedicalBill.objects.create(
|
||
tenant=patient.tenant,
|
||
patient=patient,
|
||
bill_number=bill_number, # Explicitly set bill number
|
||
bill_type=random.choice(['INPATIENT', 'OUTPATIENT', 'EMERGENCY', 'PROFESSIONAL']),
|
||
service_date_from=service_date_from,
|
||
service_date_to=service_date_to,
|
||
bill_date=bill_date,
|
||
due_date=due_date,
|
||
subtotal=Decimal('0.00'),
|
||
tax_amount=Decimal('0.00'),
|
||
discount_amount=Decimal('0.00'),
|
||
adjustment_amount=Decimal('0.00'),
|
||
total_amount=Decimal('0.00'),
|
||
paid_amount=Decimal('0.00'),
|
||
balance_amount=Decimal('0.00'),
|
||
primary_insurance=primary_insurance,
|
||
secondary_insurance=secondary_insurance,
|
||
status=random.choice(['DRAFT', 'PENDING', 'SENT', 'PARTIAL_PAYMENT', 'PAID']),
|
||
attending_provider=random.choice(provider_users) if provider_users else None,
|
||
billing_provider=random.choice(provider_users) if provider_users else None,
|
||
encounter=encounter,
|
||
admission=admission,
|
||
notes=random.choice([
|
||
'Standard billing procedure completed',
|
||
'Insurance verification pending',
|
||
'Patient requires payment plan',
|
||
'Emergency services provided'
|
||
]) if random.choice([True, False]) else None,
|
||
payment_terms=30,
|
||
collection_status=random.choice(['ACTIVE', 'COLLECTIONS', 'WRITE_OFF', 'PAID_IN_FULL']),
|
||
last_statement_date=bill_date + timedelta(days=random.randint(1, 15)) if random.choice(
|
||
[True, False]) else None,
|
||
created_by=random.choice(users) if users else None,
|
||
)
|
||
|
||
created_bills.append(bill)
|
||
bills_created += 1
|
||
|
||
if bills_created % 10 == 0:
|
||
print(f"Created {bills_created} medical bills...")
|
||
|
||
except Exception as e:
|
||
print(f"Error creating medical bill for patient {patient.mrn}: {str(e)}")
|
||
continue
|
||
|
||
print(f"Successfully created {bills_created} Saudi medical bills.")
|
||
return created_bills
|
||
|
||
|
||
def create_saudi_bill_line_items(medical_bills):
|
||
"""Create bill line items for medical bills"""
|
||
print("Creating Saudi bill line items...")
|
||
|
||
if not medical_bills:
|
||
print("No medical bills provided for line items.")
|
||
return []
|
||
|
||
provider_users = list(User.objects.filter(
|
||
is_active=True,
|
||
employee_profile__role__in=['PHYSICIAN', 'NURSE_PRACTITIONER', 'PHYSICIAN_ASSISTANT', 'RADIOLOGIST']
|
||
)) or list(User.objects.filter(is_active=True))
|
||
|
||
created_line_items = []
|
||
|
||
for bill in medical_bills:
|
||
num_items = random.randint(2, 8)
|
||
bill_subtotal = Decimal('0.00')
|
||
|
||
for i in range(num_items):
|
||
try:
|
||
# Unpack tuple: (code, description, category, base_price)
|
||
code, description, category, base_price = random.choice(SAUDI_MEDICAL_SERVICES)
|
||
|
||
# Price/qty math
|
||
unit_price = (Decimal(str(base_price)) * Decimal(str(random.uniform(0.85, 1.2)))).quantize(Decimal('0.01'))
|
||
quantity = random.randint(1, 5)
|
||
line_total = (unit_price * quantity).quantize(Decimal('0.01'))
|
||
|
||
discount_percentage = random.choice([0, 5, 10, 15, 20]) if random.choice([True, False]) else 0
|
||
discount_amount = (line_total * (Decimal(discount_percentage) / 100)).quantize(Decimal('0.01'))
|
||
net_amount = (line_total - discount_amount).quantize(Decimal('0.01'))
|
||
|
||
# Diagnosis tuple: (icd_code, description)
|
||
diag_code, _ = random.choice(SAUDI_DIAGNOSIS_CODES)
|
||
|
||
# Safe service_date
|
||
delta_days = max(0, (bill.service_date_to - bill.service_date_from).days if bill.service_date_to else 0)
|
||
service_date = bill.service_date_from + timedelta(days=random.randint(0, delta_days))
|
||
|
||
item_kwargs = {
|
||
"medical_bill": bill,
|
||
"tenant": getattr(bill, "tenant", None),
|
||
"line_number": i + 1,
|
||
"service_code": code,
|
||
"service_description": description,
|
||
"service_category": category,
|
||
"cpt_code": code, # if your model has cpt_code
|
||
"icd_code": diag_code, # if your model has icd_code
|
||
"quantity": quantity,
|
||
"unit_price": unit_price,
|
||
"line_total": line_total,
|
||
"discount_amount": discount_amount,
|
||
"net_amount": net_amount,
|
||
"rendering_provider": random.choice(provider_users) if provider_users else None,
|
||
"service_date": service_date,
|
||
}
|
||
|
||
# Only pass fields that exist on the model
|
||
item = BillLineItem.objects.create(**_filter_kwargs(BillLineItem, item_kwargs))
|
||
created_line_items.append(item)
|
||
bill_subtotal += net_amount
|
||
|
||
except Exception as e:
|
||
print(f"Error creating line item for bill {bill.bill_number}: {e}")
|
||
|
||
# Update bill totals (and paid amount based on status)
|
||
try:
|
||
bill.subtotal = bill_subtotal.quantize(Decimal('0.01'))
|
||
bill.tax_amount = (bill.subtotal * Decimal('0.15')).quantize(Decimal('0.01'))
|
||
bill.total_amount = (bill.subtotal + bill.tax_amount).quantize(Decimal('0.01'))
|
||
|
||
if bill.status == 'PAID':
|
||
bill.paid_amount = bill.total_amount
|
||
elif bill.status == 'PARTIAL_PAYMENT':
|
||
bill.paid_amount = (bill.total_amount * Decimal(str(random.uniform(0.2, 0.8)))).quantize(Decimal('0.01'))
|
||
else:
|
||
bill.paid_amount = Decimal('0.00')
|
||
|
||
bill.balance_amount = (bill.total_amount - bill.paid_amount).quantize(Decimal('0.01'))
|
||
bill.save(update_fields=['subtotal', 'tax_amount', 'total_amount', 'paid_amount', 'balance_amount'])
|
||
|
||
except Exception as e:
|
||
print(f"Error updating bill totals for {bill.bill_number}: {e}")
|
||
|
||
print(f"Successfully created {len(created_line_items)} bill line items.")
|
||
return created_line_items
|
||
|
||
|
||
from billing.models import InsuranceClaim # ensure import
|
||
|
||
def _next_claim_number():
|
||
return _next_seq_number("CLM", InsuranceClaim, "claim_number")
|
||
|
||
def create_saudi_insurance_claims(medical_bills):
|
||
print("Creating Saudi insurance claims...")
|
||
if not medical_bills:
|
||
print("No medical bills provided for claims.")
|
||
return []
|
||
|
||
created = []
|
||
for bill in medical_bills:
|
||
if not bill.primary_insurance and not bill.secondary_insurance:
|
||
continue
|
||
|
||
def _base_claim_data():
|
||
return {
|
||
"medical_bill": bill,
|
||
"claim_number": _next_claim_number(),
|
||
"billed_amount": bill.total_amount,
|
||
"submission_date": bill.bill_date + timedelta(days=random.randint(1, 7)),
|
||
"service_date_from": bill.service_date_from, # <-- REQUIRED
|
||
"service_date_to": bill.service_date_to, # <-- REQUIRED (if your model is NOT NULL)
|
||
"status": random.choice(['SUBMITTED', 'RECEIVED', 'UNDER_REVIEW', 'APPROVED', 'DENIED']),
|
||
}
|
||
|
||
if bill.primary_insurance:
|
||
try:
|
||
data = _base_claim_data()
|
||
data["claim_type"] = "PRIMARY"
|
||
data["insurance_info"] = bill.primary_insurance
|
||
claim = InsuranceClaim.objects.create(**_filter_kwargs(InsuranceClaim, data))
|
||
created.append(claim)
|
||
except Exception as e:
|
||
print(f"Error creating primary insurance claim for bill {bill.bill_number}: {e}")
|
||
|
||
if bill.secondary_insurance:
|
||
try:
|
||
data = _base_claim_data()
|
||
data["claim_type"] = "SECONDARY"
|
||
data["insurance_info"] = bill.secondary_insurance
|
||
data["billed_amount"] = (bill.total_amount * Decimal('0.2')).quantize(Decimal('0.01'))
|
||
data["submission_date"] = bill.bill_date + timedelta(days=random.randint(7, 14))
|
||
claim = InsuranceClaim.objects.create(**_filter_kwargs(InsuranceClaim, data))
|
||
created.append(claim)
|
||
except Exception as e:
|
||
print(f"Error creating secondary insurance claim for bill {bill.bill_number}: {e}")
|
||
|
||
print(f"Successfully created {len(created)} insurance claims.")
|
||
return created
|
||
|
||
|
||
from billing.models import Payment # ensure this import is present
|
||
|
||
def _next_payment_number():
|
||
return _next_seq_number("PMT", Payment, "payment_number")
|
||
|
||
def create_saudi_payments(bills):
|
||
"""Create payments for medical bills"""
|
||
print("Creating Saudi payments...")
|
||
|
||
payments_created = 0
|
||
users = list(User.objects.filter(is_active=True))
|
||
bills_with_payments = [bill for bill in bills if bill.paid_amount and bill.paid_amount > 0]
|
||
|
||
for bill in bills_with_payments:
|
||
num_payments = random.randint(1, min(3, int(bill.paid_amount / 100) + 1))
|
||
remaining_payment = bill.paid_amount
|
||
|
||
for i in range(num_payments):
|
||
try:
|
||
payment_amount = remaining_payment if i == num_payments - 1 else (remaining_payment * Decimal(str(random.uniform(0.2, 0.7))))
|
||
remaining_payment -= payment_amount
|
||
if payment_amount <= 0:
|
||
continue
|
||
|
||
payment_date = bill.bill_date + timedelta(days=random.randint(1, 60))
|
||
payment_method = random.choice(SAUDI_PAYMENT_METHODS)
|
||
payment_source = random.choice(SAUDI_PAYMENT_SOURCES)
|
||
|
||
check_number = f"CHK{random.randint(100000, 999999)}" if payment_method == 'CHECK' else None
|
||
bank_name = random.choice(['Al Rajhi Bank','Saudi National Bank','Riyad Bank','Arab National Bank','Banque Saudi Fransi','Saudi Investment Bank']) if payment_method in ['CHECK','BANK_TRANSFER'] else None
|
||
card_type = random.choice(['VISA','MASTERCARD','AMEX']) if payment_method in ['CREDIT_CARD','DEBIT_CARD'] else None
|
||
card_last_four = f"{random.randint(1000, 9999)}" if payment_method in ['CREDIT_CARD','DEBIT_CARD'] else None
|
||
authorization_code = f"AUTH{random.randint(100000, 999999)}" if payment_method in ['CREDIT_CARD','DEBIT_CARD'] else None
|
||
transaction_id = f"TXN{random.randint(1000000, 9999999)}" if payment_method in ['CREDIT_CARD','DEBIT_CARD','BANK_TRANSFER'] else None
|
||
insurance_claim = random.choice(list(bill.insurance_claims.all())) if payment_source == 'INSURANCE' and bill.insurance_claims.exists() else None
|
||
|
||
# Build kwargs, include payment_number if field exists
|
||
kwargs = {
|
||
"medical_bill": bill,
|
||
"payment_date": payment_date,
|
||
"payment_amount": payment_amount.quantize(Decimal('0.01')),
|
||
"payment_method": payment_method,
|
||
"payment_source": payment_source,
|
||
"check_number": check_number,
|
||
"bank_name": bank_name,
|
||
"routing_number": f"{random.randint(100000000, 999999999)}" if payment_method in ['CHECK','BANK_TRANSFER'] else None,
|
||
"card_type": card_type,
|
||
"card_last_four": card_last_four,
|
||
"authorization_code": authorization_code,
|
||
"transaction_id": transaction_id,
|
||
"insurance_claim": insurance_claim,
|
||
"eob_number": f"EOB{random.randint(100000, 999999)}" if payment_source == 'INSURANCE' else None,
|
||
"status": random.choice(['PENDING','PROCESSED','CLEARED','RETURNED']),
|
||
"deposit_date": payment_date + timedelta(days=random.randint(0, 3)),
|
||
"deposit_slip": f"DEP{random.randint(100000, 999999)}" if random.choice([True, False]) else None,
|
||
"notes": random.choice(['Payment processed successfully','Partial payment received','Insurance payment - EOB attached','Patient payment plan installment']) if random.choice([True, False]) else None,
|
||
"refund_amount": Decimal('0.00'),
|
||
"received_by": random.choice(users) if users else None,
|
||
"processed_by": random.choice(users) if users else None,
|
||
}
|
||
|
||
if "payment_number" in _model_fields(Payment):
|
||
# retry loop for rare uniqueness races or model-level generators
|
||
attempts = 0
|
||
while True:
|
||
attempts += 1
|
||
kwargs["payment_number"] = _next_payment_number()
|
||
try:
|
||
Payment.objects.create(**_filter_kwargs(Payment, kwargs))
|
||
payments_created += 1
|
||
break
|
||
except IntegrityError:
|
||
if attempts >= 5:
|
||
raise
|
||
# try another number
|
||
continue
|
||
else:
|
||
Payment.objects.create(**_filter_kwargs(Payment, kwargs))
|
||
payments_created += 1
|
||
|
||
except Exception as e:
|
||
print(f"Error creating payment for bill {bill.bill_number}: {e}")
|
||
continue
|
||
|
||
print(f"Successfully created {payments_created} payments.")
|
||
return payments_created
|
||
|
||
|
||
def create_saudi_claim_status_updates(insurance_claims):
|
||
"""Create claim status updates with non-null previous_status."""
|
||
print("Creating Saudi claim status updates...")
|
||
|
||
if not insurance_claims:
|
||
print("No insurance claims provided for status updates.")
|
||
return []
|
||
|
||
users = list(User.objects.filter(is_active=True))
|
||
created_updates = []
|
||
|
||
for claim in insurance_claims:
|
||
num_updates = random.randint(1, 4)
|
||
|
||
# Define a sensible progression starting from the claim's current status
|
||
all_steps = ['SUBMITTED', 'RECEIVED', 'UNDER_REVIEW', 'APPROVED', 'PAID', 'DENIED']
|
||
try:
|
||
start_idx = all_steps.index(claim.status) if claim.status in all_steps else 0
|
||
except ValueError:
|
||
start_idx = 0
|
||
|
||
# ensure we have forward steps; if already terminal, just repeat last/nearby statuses
|
||
forward = all_steps[start_idx+1:] or [all_steps[min(start_idx, len(all_steps)-1)]]
|
||
|
||
previous_status = claim.status # NOT NULL for first update
|
||
|
||
for i in range(num_updates):
|
||
try:
|
||
new_status = forward[min(i, len(forward)-1)] if forward else previous_status
|
||
|
||
base_date = claim.submission_date + timedelta(days=(i+1) * random.randint(3, 14))
|
||
status_date = django_timezone.make_aware(
|
||
datetime.combine(base_date, datetime.min.time()),
|
||
timezone=django_timezone.get_current_timezone()
|
||
)
|
||
|
||
update_kwargs = {
|
||
"insurance_claim": claim,
|
||
"previous_status": previous_status, # never None
|
||
"new_status": new_status,
|
||
"status_date": status_date,
|
||
"update_source": random.choice(['EDI','PORTAL','PHONE','FAX','EMAIL']),
|
||
"response_code": f"R{random.randint(100, 999)}" if random.choice([True, False]) else None,
|
||
"response_message": random.choice([
|
||
'Claim processed successfully',
|
||
'Additional information required',
|
||
'Claim approved for payment',
|
||
'Claim denied - insufficient documentation'
|
||
]) if random.choice([True, False]) else None,
|
||
}
|
||
|
||
# Optional financials only for approved/paid
|
||
if new_status in ['APPROVED', 'PAID']:
|
||
allowed = (claim.billed_amount * Decimal(str(random.uniform(0.7, 1.0)))).quantize(Decimal('0.01'))
|
||
patient_resp = (claim.billed_amount * Decimal(str(random.uniform(0.1, 0.3)))).quantize(Decimal('0.01'))
|
||
update_kwargs["allowed_amount"] = allowed
|
||
update_kwargs["patient_responsibility"] = patient_resp
|
||
if new_status == 'PAID':
|
||
update_kwargs["paid_amount"] = (allowed - patient_resp).quantize(Decimal('0.01'))
|
||
|
||
update = ClaimStatusUpdate.objects.create(**_filter_kwargs(ClaimStatusUpdate, update_kwargs))
|
||
created_updates.append(update)
|
||
|
||
previous_status = new_status # move the chain forward
|
||
|
||
except Exception as e:
|
||
print(f"Error creating status update for claim {claim.claim_number}: {e}")
|
||
continue
|
||
|
||
print(f"Successfully created {len(created_updates)} claim status updates.")
|
||
return created_updates
|
||
|
||
|
||
def create_saudi_billing_configurations():
|
||
"""Create billing configurations with per-field-precision-safe decimals."""
|
||
print("Creating Saudi billing configurations...")
|
||
|
||
tenants = list(Tenant.objects.all())
|
||
if not tenants:
|
||
print("No tenants found for billing configurations.")
|
||
return []
|
||
|
||
from billing.models import BillingConfiguration
|
||
created = []
|
||
cfg_fields = _model_fields(BillingConfiguration)
|
||
|
||
for tenant in tenants:
|
||
try:
|
||
data = {}
|
||
|
||
# Currency-like field
|
||
for f in BillingConfiguration._meta.get_fields():
|
||
if isinstance(f, CharField) and f.name in {"currency", "currency_code", "base_currency"}:
|
||
data[f.name] = "SAR"
|
||
|
||
# Payment terms-like field
|
||
for f in BillingConfiguration._meta.get_fields():
|
||
if isinstance(f, IntegerField) and f.name in {"default_payment_terms", "payment_terms", "net_terms"}:
|
||
data[f.name] = 30
|
||
|
||
# Decimal fields: generate a safe value per field precision
|
||
for f in BillingConfiguration._meta.get_fields():
|
||
if isinstance(f, DecimalField):
|
||
has_default = getattr(f, "default", NOT_PROVIDED) is not NOT_PROVIDED
|
||
if not f.null and not has_default:
|
||
data[f.name] = _default_decimal_for(BillingConfiguration, f)
|
||
|
||
# Include tenant if the model has it
|
||
if "tenant" in cfg_fields:
|
||
obj, is_new = BillingConfiguration.objects.get_or_create(
|
||
tenant=tenant,
|
||
defaults=_filter_kwargs(BillingConfiguration, data)
|
||
)
|
||
if is_new:
|
||
created.append(obj)
|
||
print(f"Successfully created billing configuration for {tenant.name}")
|
||
else:
|
||
print(f"Billing configuration already exists for {tenant.name}")
|
||
else:
|
||
obj = BillingConfiguration.objects.create(**_filter_kwargs(BillingConfiguration, data))
|
||
created.append(obj)
|
||
print("Successfully created billing configuration (no tenant FK)")
|
||
|
||
except Exception as e:
|
||
print(f"Error creating billing configuration for tenant {tenant.name}: {e}")
|
||
|
||
print(f"Successfully created {len(created)} billing configurations.")
|
||
return created
|
||
|
||
|
||
def main():
|
||
"""Main function to create all Saudi billing data"""
|
||
print("🏥 Creating Saudi Billing Data")
|
||
|
||
try:
|
||
# Check if tenants exist
|
||
tenants = list(Tenant.objects.all())
|
||
if not tenants:
|
||
print("❌ No tenants found. Please run core_data.py first.")
|
||
return
|
||
|
||
# Check if patients exist
|
||
patients = list(PatientProfile.objects.all())
|
||
if not patients:
|
||
print("❌ No patients found. Please run patients_data.py first.")
|
||
return
|
||
|
||
print(f"📋 Found {len(tenants)} tenants and {len(patients)} patients")
|
||
|
||
# Create billing configurations first
|
||
print("\n1️⃣ Creating Billing Configurations...")
|
||
configs_count = create_saudi_billing_configurations()
|
||
|
||
# Create medical bills
|
||
print("\n2️⃣ Creating Medical Bills...")
|
||
bills = create_saudi_medical_bills()
|
||
if not bills:
|
||
print("❌ No bills created. Stopping.")
|
||
return
|
||
|
||
# Create bill line items
|
||
print("\n3️⃣ Creating Bill Line Items...")
|
||
line_items_count = create_saudi_bill_line_items(bills)
|
||
|
||
# Create insurance claims
|
||
print("\n4️⃣ Creating Insurance Claims...")
|
||
claims = create_saudi_insurance_claims(bills)
|
||
|
||
# Create payments
|
||
print("\n5️⃣ Creating Payments...")
|
||
payments_count = create_saudi_payments(bills)
|
||
|
||
# Create claim status updates
|
||
print("\n6️⃣ Creating Claim Status Updates...")
|
||
updates_count = create_saudi_claim_status_updates(claims)
|
||
|
||
print("\n🎉 Saudi Billing Data Creation Complete!")
|
||
print("📊 Summary:")
|
||
print(f" - Billing Configurations: {len(configs_count)}")
|
||
print(f" - Medical Bills: {len(bills)}")
|
||
print(f" - Bill Line Items: {len(line_items_count)}")
|
||
print(f" - Insurance Claims: {len(claims)}")
|
||
print(f" - Payments: {payments_count}")
|
||
print(f" - Claim Status Updates: {len(updates_count)}")
|
||
|
||
except Exception as e:
|
||
print(f"❌ Error in main execution: {str(e)}")
|
||
|
||
|
||
if __name__ == '__main__':
|
||
main() |