hospital-management/billing_data.py
Marwan Alwali 4d06ca4b5e update
2025-09-20 14:26:19 +03:00

709 lines
32 KiB
Python
Raw Permalink Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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()