Last active
May 27, 2025 22:00
-
-
Save minherz/3cd11ab20408c743ef7fdaa8eec2a461 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from datetime import datetime | |
# --- EDI Configuration --- | |
ELEMENT_SEPARATOR = "*" | |
SEGMENT_TERMINATOR = "~" | |
COMPONENT_SEPARATOR = ":" # Used for composite elements | |
# --- Data Structures (Example Data) --- | |
# Submitter of the EDI file (e.g., Billing Service or Provider) | |
default_submitter_info = { | |
"name": "ABC BILLING SERVICES", | |
"id_qualifier": "41", # Submitter ID qualifier (e.g., 41 for Submitter ID) | |
"id": "SUBMITTER_ABC", # Submitter's unique ID | |
"contact_name": "EDI Support", | |
"contact_comm_type_1": "TE", "contact_comm_number_1": "8005551000", | |
"contact_comm_type_2": "EM", "contact_comm_number_2": "[email protected]" | |
} | |
# Receiver of the EDI file (e.g., Insurance Payer) | |
default_receiver_info = { | |
"name": "MAJOR HEALTH PLAN", | |
"id_qualifier": "PI", # Receiver ID qualifier (e.g., PI for Payer ID, or mutually agreed) | |
"id": "PAYERID_MHP" # Receiver's unique ID (often Payer ID) | |
} | |
# Billing Provider Information (e.g., Hospital) | |
default_billing_provider_info = { | |
"name": "COMMUNITY GENERAL HOSPITAL", | |
"npi": "1987654321", | |
"address_1": "1 HOSPITAL PLAZA", "address_2": "FLOOR 3", | |
"city": "HEALTHVILLE", "state": "CA", "zip_code": "90210", | |
"tax_id_type": "EI", "tax_id": "99-9999999", # Employer Identification Number | |
"contact_name": "Billing Dept", | |
"contact_comm_type_1": "TE", "contact_comm_number_1": "8005552000" | |
} | |
# Subscriber Information (Primary Insured) | |
default_subscriber_info = { | |
"last_name": "SMITH", "first_name": "ROBERT", "middle_name": "J", | |
"id_qualifier": "MI", "id": "RJS2024001P", # Member ID | |
"address_1": "25 OAK LANE", "address_2": "", | |
"city": "ANYTOWN", "state": "CA", "zip_code": "90211", | |
"dob": "19800520", "gender": "M", # YYYYMMDD | |
"group_number": "CORPGRP001" | |
} | |
# Patient Information | |
# Scenario 1: Patient is the Subscriber | |
# default_patient_info = default_subscriber_info.copy() | |
# default_patient_info["relationship_to_subscriber"] = "18" # 18 = Self | |
# Scenario 2: Patient is a Dependent (e.g., Spouse of Subscriber) | |
default_patient_info = { | |
"last_name": "SMITH", "first_name": "MARY", "middle_name": "L", | |
# Optional: Patient specific ID if different from subscriber's context for them | |
# "id_qualifier_for_loop_2010ca": "MI", "id_for_loop_2010ca": "MLS2024001D", | |
"address_1": "25 OAK LANE", "address_2": "", # Assuming same address | |
"city": "ANYTOWN", "state": "CA", "zip_code": "90211", | |
"dob": "19820810", "gender": "F", | |
"relationship_to_subscriber": "01" # 01 = Spouse, 19 = Child, etc. | |
} | |
# Payer Information (Who is being billed) | |
default_payer_info = { | |
"name": "MAJOR HEALTH PLAN", # Should match receiver name if primary payer is the receiver | |
"id_qualifier_pi_or_xv": "PI", "id": "PAYERID_MHP", # Primary Payer ID | |
"address_1": "PO BOX 1000", "city": "INSURANCECITY", "state": "CA", "zip_code": "90000", | |
# Optional secondary payer ID, e.g. CMS Plan ID | |
# "secondary_id_qualifier": "XV", "secondary_id": "MCP001" | |
} | |
# Claim Information | |
default_claim_info = { | |
"submitter_claim_id": "HOSCLAIM202400A1", # CLM01 (Patient Control Number / Provider's Claim ID) | |
"total_claim_charge_amount": "2750.50", # CLM02 (e.g., 1234.56 -> "1234.56") | |
"facility_code_value": "11", # CLM05-1 (Bill Type - Facility Part, e.g., 11 for Hospital Inpatient) | |
"claim_frequency_type_code": "1", # CLM05-3 (1=Original, 7=Replacement) | |
"assignment_of_benefits": "Y", # CLM07 | |
"benefits_assignment_certification_indicator": "Y", # CLM08 | |
"release_of_information_code": "I", # CLM09 | |
"patient_signature_source_code": "P", # CLM10 | |
"claim_filing_indicator_code": "CI", # SBR09 (e.g., CI=Commercial, MB=Medicare B, MC=Medicaid) | |
"admission_date": "20240501", "admission_hour": "1030", # DTP*435 | |
"statement_from_date": "20240501", "statement_to_date": "20240503", # DTP*434 | |
"type_of_admission": "1", # CL101 (1=Emergency, 3=Elective) | |
"source_of_admission": "1", # CL102 (1=Physician Referral) | |
"patient_status_code": "01", # CL104 (Discharge Status, e.g., 01=Discharged to home) | |
"principal_diagnosis_code": "S8261XA", # HI*BK (ICD-10) | |
"other_diagnosis_codes": [ # HI*BF (or other qualifiers like ABF for Admitting Diagnosis) | |
{"qualifier": "BF", "code": "I10"}, | |
{"qualifier": "BF", "code": "E119"} | |
], | |
"principal_procedure": { "code": "0SR90JZ", "date": "20240501"}, # HI*BBR | |
"attending_provider": { | |
"npi": "1122334455", "last_name": "PRIMARYDOC", "first_name": "ALICE", "qualifier": "XX" | |
}, | |
"operating_physician": { | |
"npi": "5544332211", "last_name": "SURGEONMD", "first_name": "BOB", "qualifier": "XX" | |
}, | |
"service_lines": [ | |
{ | |
"line_number": "1", "revenue_code": "0300", # Lab | |
"procedure_code_qualifier": "HC", "procedure_code": "80048", # Basic Metabolic Panel | |
"modifiers": ["", "", "", ""], | |
"service_charge_amount": "150.00", "units_of_service": "1", | |
"unit_or_basis_for_measurement_code": "UN", "service_date": "20240501" | |
}, | |
{ | |
"line_number": "2", "revenue_code": "0450", # Emergency Room | |
"procedure_code_qualifier": "HC", "procedure_code": "99283", # ER Visit Level 3 | |
"modifiers": ["25", "", "", ""], | |
"service_charge_amount": "1200.00", "units_of_service": "1", | |
"unit_or_basis_for_measurement_code": "UN", "service_date": "20240501" | |
}, | |
{ | |
"line_number": "3", "revenue_code": "0250", # Pharmacy | |
"procedure_code_qualifier": "HC", "procedure_code": "J0129", # Injection, abatacept | |
"modifiers": ["", "", "", ""], | |
"service_charge_amount": "1400.50", "units_of_service": "50", # e.g., 50 MG | |
"unit_or_basis_for_measurement_code": "MG", "service_date": "20240502" | |
} | |
] | |
} | |
# --- EDI Generation Logic --- | |
# Global date/time stamps for the current run | |
_current_datetime = datetime.now() | |
_edi_current_date_yyyymmdd = _current_datetime.strftime("%Y%m%d") | |
_edi_current_date_yymmdd = _current_datetime.strftime("%y%m%d") | |
_edi_current_time_hhmm = _current_datetime.strftime("%H%M") | |
_edi_current_time_hhmmss = _current_datetime.strftime("%H%M%S") | |
def format_edi_value(value, fixed_length=None, is_numeric=False, decimals=0): | |
"""Helper to format values for EDI elements.""" | |
if value is None or value == "": | |
return "" | |
s_value = str(value) | |
if is_numeric: | |
try: | |
num = float(s_value) | |
if decimals > 0: | |
s_value = f"{num:.{decimals}f}".replace('.', '') | |
else: | |
s_value = str(int(num)) | |
if fixed_length: | |
return s_value.zfill(fixed_length) | |
except ValueError: | |
s_value = "0" # Default for unparseable numeric | |
if fixed_length: | |
return s_value.zfill(fixed_length) | |
return s_value | |
if fixed_length: | |
return s_value.ljust(fixed_length) # Pad with spaces for alphanumeric | |
return s_value | |
def build_segment(*elements): | |
"""Builds an EDI segment from its elements.""" | |
return ELEMENT_SEPARATOR.join(str(el) if el is not None else "" for el in elements) + SEGMENT_TERMINATOR | |
def generate_837i( | |
submitter, receiver, billing_provider, | |
subscriber, patient, claim, payer, | |
interchange_ctrl_num, group_ctrl_num, transaction_ctrl_num | |
): | |
edi_segments = [] | |
segment_count = 0 | |
# ISA - Interchange Control Header | |
isa = build_segment( | |
"ISA", "00", " ", "00", " ", | |
format_edi_value(submitter['id_qualifier'], 2), format_edi_value(submitter['id'], 15), | |
format_edi_value(receiver['id_qualifier'], 2), format_edi_value(receiver['id'], 15), | |
_edi_current_date_yymmdd, _edi_current_time_hhmm, | |
"^", "00501", | |
format_edi_value(interchange_ctrl_num, 9, is_numeric=True), | |
"0", "P", COMPONENT_SEPARATOR | |
) | |
edi_segments.append(isa) | |
# GS - Functional Group Header | |
gs = build_segment( | |
"GS", "HC", | |
format_edi_value(submitter['id'], 15), # Application Sender's Code | |
format_edi_value(receiver['id'], 15), # Application Receiver's Code | |
_edi_current_date_yyyymmdd, _edi_current_time_hhmm, | |
format_edi_value(group_ctrl_num, 9, is_numeric=True), | |
"X", "005010X223A2" # X223A2 for 837I | |
) | |
edi_segments.append(gs) | |
# ST - Transaction Set Header | |
st = build_segment("ST", "837", format_edi_value(transaction_ctrl_num, 4)) # Min 4, Max 9 for Trx Ctrl Num | |
edi_segments.append(st) | |
segment_count += 1 | |
# BHT - Beginning of Hierarchical Transaction | |
bht = build_segment( | |
"BHT", "0019", "00", | |
claim.get("submitter_reference_id", format_edi_value(transaction_ctrl_num, 10)), # Originator's app trx id | |
_edi_current_date_yyyymmdd, _edi_current_time_hhmmss, "CH" | |
) | |
edi_segments.append(bht) | |
segment_count += 1 | |
# Loop 1000A - Submitter Name | |
edi_segments.append(build_segment("NM1", "41", "2", submitter['name'], "", "", "", "", submitter['id_qualifier'], submitter['id'])) | |
segment_count += 1 | |
edi_segments.append(build_segment( | |
"PER", "IC", submitter.get('contact_name', ''), | |
submitter.get('contact_comm_type_1', ''), submitter.get('contact_comm_number_1', ''), | |
submitter.get('contact_comm_type_2', ''), submitter.get('contact_comm_number_2', '') | |
)) | |
segment_count += 1 | |
# Loop 1000B - Receiver Name | |
edi_segments.append(build_segment("NM1", "40", "2", receiver['name'], "", "", "", "", receiver['id_qualifier'], receiver['id'])) | |
segment_count += 1 | |
# --- Hierarchical Levels (HL) --- | |
hl_counter = 0 | |
# Loop 2000A - Billing Provider HL | |
hl_counter += 1 | |
parent_hl_billing = hl_counter | |
edi_segments.append(build_segment("HL", str(hl_counter), "", "20", "1")) | |
segment_count += 1 | |
# Loop 2010AA - Billing Provider Name | |
edi_segments.append(build_segment("NM1", "85", "2", billing_provider['name'], "", "", "", "", "XX", billing_provider['npi'])) | |
segment_count += 1 | |
edi_segments.append(build_segment("N3", billing_provider['address_1'], billing_provider.get('address_2', ''))) | |
segment_count += 1 | |
edi_segments.append(build_segment("N4", billing_provider['city'], billing_provider['state'], billing_provider['zip_code'])) | |
segment_count += 1 | |
edi_segments.append(build_segment("REF", billing_provider['tax_id_type'], billing_provider['tax_id'])) | |
segment_count += 1 | |
if billing_provider.get('contact_name'): | |
edi_segments.append(build_segment( | |
"PER", "IC", billing_provider['contact_name'], | |
billing_provider.get('contact_comm_type_1', ''), billing_provider.get('contact_comm_number_1', '') | |
)) | |
segment_count += 1 | |
# Loop 2000B - Subscriber HL | |
hl_counter += 1 | |
parent_hl_subscriber = hl_counter | |
is_patient_subscriber = patient.get("relationship_to_subscriber") == "18" | |
edi_segments.append(build_segment("HL", str(hl_counter), str(parent_hl_billing), "22", "0" if is_patient_subscriber else "1")) | |
segment_count += 1 | |
# SBR - Subscriber Information | |
edi_segments.append(build_segment( | |
"SBR", "P", # P=Primary | |
patient.get("relationship_to_subscriber", "18"), | |
subscriber.get("group_number", ""), "", "", "", "", "", | |
claim.get("claim_filing_indicator_code", "CI") | |
)) | |
segment_count += 1 | |
# Loop 2010BA - Subscriber Name | |
edi_segments.append(build_segment( | |
"NM1", "IL", "1", subscriber['last_name'], subscriber['first_name'], | |
subscriber.get('middle_name', ''), "", "", | |
subscriber['id_qualifier'], subscriber['id'] | |
)) | |
segment_count += 1 | |
if subscriber.get('address_1'): | |
edi_segments.append(build_segment("N3", subscriber['address_1'], subscriber.get('address_2', ''))) | |
segment_count += 1 | |
if subscriber.get('city'): | |
edi_segments.append(build_segment("N4", subscriber['city'], subscriber['state'], subscriber['zip_code'])) | |
segment_count += 1 | |
if subscriber.get('dob') and subscriber.get('gender'): | |
edi_segments.append(build_segment("DMG", "D8", subscriber['dob'], subscriber['gender'])) | |
segment_count += 1 | |
# Loop 2010BB - Payer Name | |
edi_segments.append(build_segment( | |
"NM1", "PR", "2", payer['name'], "", "", "", "", | |
payer['id_qualifier_pi_or_xv'], payer['id'] | |
)) | |
segment_count += 1 | |
if payer.get('address_1'): | |
edi_segments.append(build_segment("N3", payer['address_1'])) # Payer address often not needed if NPI/ID is electronic | |
segment_count += 1 | |
if payer.get('city'): | |
edi_segments.append(build_segment("N4", payer['city'], payer['state'], payer['zip_code'])) | |
segment_count += 1 | |
# REF*G2 or REF*2U for Payer secondary ID (e.g. specific plan ID) | |
if payer.get('secondary_id_qualifier') and payer.get('secondary_id'): | |
edi_segments.append(build_segment("REF", payer['secondary_id_qualifier'], payer['secondary_id'])) | |
segment_count += 1 | |
# Loop 2000C - Patient HL (if patient is not subscriber) | |
if not is_patient_subscriber: | |
hl_counter += 1 | |
edi_segments.append(build_segment("HL", str(hl_counter), str(parent_hl_subscriber), "23", "0")) | |
segment_count += 1 | |
# PAT - Patient Information | |
edi_segments.append(build_segment( | |
"PAT", patient['relationship_to_subscriber'], "", "", "", | |
"D8", patient.get('dob', ''), patient.get('gender', '') | |
)) | |
segment_count += 1 | |
# Loop 2010CA - Patient Name | |
nm1_patient_elements = ["NM1", "QC", "1", patient['last_name'], patient['first_name'], patient.get('middle_name', ''), "", ""] | |
if patient.get('id_qualifier_for_loop_2010ca') and patient.get('id_for_loop_2010ca'): | |
nm1_patient_elements.extend([patient['id_qualifier_for_loop_2010ca'], patient['id_for_loop_2010ca']]) | |
else: # If no specific patient ID, NM1 ends after suffix | |
nm1_patient_elements.extend(["", ""]) # Empty ID qualifier and ID | |
edi_segments.append(build_segment(*nm1_patient_elements)) | |
segment_count += 1 | |
if patient.get('address_1'): | |
edi_segments.append(build_segment("N3", patient['address_1'], patient.get('address_2', ''))) | |
segment_count += 1 | |
if patient.get('city'): | |
edi_segments.append(build_segment("N4", patient['city'], patient['state'], patient['zip_code'])) | |
segment_count += 1 | |
# DMG for patient if not in PAT or if more detail is needed. Often PAT is sufficient. | |
# Loop 2300 - Claim Information | |
clm05_composite = f"{claim['facility_code_value']}{COMPONENT_SEPARATOR}B{COMPONENT_SEPARATOR}{claim['claim_frequency_type_code']}" | |
edi_segments.append(build_segment( | |
"CLM", claim['submitter_claim_id'], format_edi_value(claim['total_claim_charge_amount'], is_numeric=True, decimals=2), | |
"", "", clm05_composite, "Y", # Assuming provider signature on file | |
claim['assignment_of_benefits'], claim['benefits_assignment_certification_indicator'], | |
claim['release_of_information_code'], claim.get('patient_signature_source_code', "P"), | |
"", "", "", "", "", "Y" # CLM11-15 are situational, CLM16: Delay Reason (Y=info available). Actual DelayReasonCode in CLM20 | |
)) | |
segment_count += 1 | |
# DTP - Statement Dates | |
edi_segments.append(build_segment("DTP", "434", "RD8", f"{claim['statement_from_date']}-{claim['statement_to_date']}")) | |
segment_count += 1 | |
# DTP - Admission Date/Hour | |
if claim.get('admission_date'): | |
dtp_format = "D8" | |
dtp_value = claim['admission_date'] | |
if claim.get('admission_hour'): | |
dtp_format = "DT" | |
dtp_value += claim['admission_hour'] | |
edi_segments.append(build_segment("DTP", "435", dtp_format, dtp_value)) | |
segment_count += 1 | |
# CL1 - Institutional Claim Codes | |
edi_segments.append(build_segment( | |
"CL1", claim.get('type_of_admission',''), claim.get('source_of_admission',''), "", claim.get('patient_status_code','') | |
)) | |
segment_count += 1 | |
# HI - Diagnosis Codes | |
edi_segments.append(build_segment("HI", f"BK{COMPONENT_SEPARATOR}{claim['principal_diagnosis_code']}")) | |
segment_count += 1 | |
for diag in claim.get('other_diagnosis_codes', []): | |
qual = diag.get("qualifier", "BF") # BF = Other, ABF = Admitting | |
edi_segments.append(build_segment("HI", f"{qual}{COMPONENT_SEPARATOR}{diag['code']}")) | |
segment_count += 1 | |
# HI - Procedure Codes (Claim Level, e.g., ICD-10-PCS) | |
if claim.get('principal_procedure'): | |
pp = claim['principal_procedure'] | |
edi_segments.append(build_segment("HI", f"BBR{COMPONENT_SEPARATOR}{pp['code']}", "D8", pp['date'])) | |
segment_count += 1 | |
# Add other HI types: Occurrence (BI), Occurrence Span (BH), Value (BE), Condition (BG) as needed. | |
# Provider Loops at Claim Level (Attending, Operating, etc.) | |
if claim.get('attending_provider'): | |
ap = claim['attending_provider'] | |
edi_segments.append(build_segment("NM1", "71", "1", ap['last_name'], ap.get('first_name',''), ap.get('middle_name',''), "", "", ap.get('qualifier','XX'), ap['npi'])) | |
segment_count += 1 | |
if claim.get('operating_physician'): | |
op = claim['operating_physician'] | |
edi_segments.append(build_segment("NM1", "72", "1", op['last_name'], op.get('first_name',''), op.get('middle_name',''), "", "", op.get('qualifier','XX'), op['npi'])) | |
segment_count += 1 | |
# Other provider loops (e.g., NM1*77 for Service Facility Location if different from Billing Provider) go here. | |
# Loop 2400 - Service Line | |
for line in claim.get('service_lines', []): | |
edi_segments.append(build_segment("LX", line['line_number'])) | |
segment_count += 1 | |
sv2_elements = ["SV2", line['revenue_code']] | |
sv202_composite = [] | |
if line.get('procedure_code_qualifier') and line.get('procedure_code'): | |
sv202_composite.append(line['procedure_code_qualifier']) | |
sv202_composite.append(line['procedure_code']) | |
mods = line.get('modifiers', ["", "", "", ""]) | |
for i in range(4): | |
sv202_composite.append(mods[i] if i < len(mods) and mods[i] else "") | |
while sv202_composite and sv202_composite[-1] == "": sv202_composite.pop() # Trim trailing empty mods | |
sv2_elements.append(COMPONENT_SEPARATOR.join(sv202_composite) if sv202_composite else "") | |
sv2_elements.extend([ | |
format_edi_value(line['service_charge_amount'], is_numeric=True, decimals=2), | |
line.get('unit_or_basis_for_measurement_code', 'UN'), | |
format_edi_value(line['units_of_service'], is_numeric=True), # No decimals for units | |
line.get('non_covered_charge_amount', '').replace('.', '') if line.get('non_covered_charge_amount') else "" # SV206 | |
]) | |
edi_segments.append(build_segment(*sv2_elements)) | |
segment_count += 1 | |
edi_segments.append(build_segment("DTP", "472", "D8", line['service_date'])) # Service Date | |
segment_count += 1 | |
# REF for line level (e.g. NDC for drugs) would go here (REF*XZ) | |
# SE - Transaction Set Trailer | |
segment_count += 1 # Account for SE itself | |
edi_segments.append(build_segment("SE", str(segment_count), format_edi_value(transaction_ctrl_num, 4))) | |
# GE - Functional Group Trailer | |
edi_segments.append(build_segment("GE", "1", format_edi_value(group_ctrl_num, is_numeric=True))) # Match GS06 | |
# IEA - Interchange Control Trailer | |
edi_segments.append(build_segment("IEA", "1", format_edi_value(interchange_ctrl_num, 9, is_numeric=True))) # Match ISA13 | |
return "".join(edi_segments) | |
# --- Main execution example --- | |
if __name__ == "__main__": | |
# These control numbers should be unique and managed in a production system | |
interchange_control_num = 1001 | |
group_control_num = 1 | |
transaction_set_control_num = "0001" # String, min 4 chars | |
# Generate the EDI 837I string | |
edi_output = generate_837i( | |
default_submitter_info, | |
default_receiver_info, | |
default_billing_provider_info, | |
default_subscriber_info, | |
default_patient_info, | |
default_claim_info, | |
default_payer_info, | |
interchange_control_num, | |
group_control_num, | |
transaction_set_control_num | |
) | |
# Output to console | |
print("Generated EDI 837I String:") | |
print(edi_output) | |
# Write to a file | |
file_name = f"edi_837i_{_edi_current_date_yyyymmdd}_{transaction_set_control_num}.txt" | |
try: | |
with open(file_name, "w") as f: | |
f.write(edi_output) | |
print(f"\nSuccessfully wrote EDI 837I to file: {file_name}") | |
except IOError as e: | |
print(f"\nError writing EDI 837I to file: {e}") |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import datetime | |
# --- Configuration --- | |
PATIENT_IS_SUBSCRIBER = False # Set to True if the patient is also the subscriber | |
OUTPUT_FILENAME = "output_837i.edi" | |
# --- Data Structures --- | |
# Patient Information (Used if patient is not the subscriber, or if PATIENT_IS_SUBSCRIBER is True and this data is primary for subscriber) | |
patient_data = { | |
"last_name": "DOE", | |
"first_name": "JOHN", | |
"middle_initial": "M", | |
"patient_id": "PATID12345", # Can be Medical Record Number or other identifier | |
"date_of_birth": "19800101", # YYYYMMDD | |
"gender": "M", # M, F, U | |
"address_street": "123 MAIN ST", | |
"address_city": "ANYTOWN", | |
"address_state": "CA", | |
"address_zip": "90210", | |
"relationship_to_subscriber": "19" # e.g., 01-Spouse, 19-Child. Used in PAT01 if patient is not subscriber. | |
} | |
# Subscriber Information | |
subscriber_data = { | |
"last_name": "SMITH", | |
"first_name": "JANE", | |
"middle_initial": "S", | |
"member_id": "SUBID98765", # Member ID from Payer | |
"date_of_birth": "19750515", # YYYYMMDD | |
"gender": "F", # M, F, U | |
"address_street": "456 OAK AVE", | |
"address_city": "OTHERVILLE", | |
"address_state": "NY", | |
"address_zip": "10001", | |
"group_number": "GRP555" | |
} | |
# If patient is the subscriber, update subscriber_data with patient_data details. | |
if PATIENT_IS_SUBSCRIBER: | |
subscriber_data["last_name"] = patient_data["last_name"] | |
subscriber_data["first_name"] = patient_data["first_name"] | |
subscriber_data["middle_initial"] = patient_data["middle_initial"] | |
# Assuming member_id for subscriber is distinct, otherwise, it might be patient_data["patient_id"] | |
# subscriber_data["member_id"] = patient_data["patient_id"] | |
subscriber_data["date_of_birth"] = patient_data["date_of_birth"] | |
subscriber_data["gender"] = patient_data["gender"] | |
subscriber_data["address_street"] = patient_data["address_street"] | |
subscriber_data["address_city"] = patient_data["address_city"] | |
subscriber_data["address_state"] = patient_data["address_state"] | |
subscriber_data["address_zip"] = patient_data["address_zip"] | |
# group_number would typically still come from the subscriber's specific plan. | |
# Claim Information | |
claim_data = { | |
"claim_id": "CLAIM00001", # Used in CLM01 | |
"total_claim_charge_amount": "1500.75", # CLM02 | |
"payer_name": "HEALTHCARE PAYER INC", # NM103 in Loop 2010BB (Payer) | |
"payer_id": "PAYERIDXYZ", # NM109 in Loop 2010BB (Payer) | |
"submitter_name": "MEDICAL SUBMITTER LLC", # NM103 in Loop 1000A (Submitter) | |
"submitter_id": "SUBMITTER123", # ISA06, GS02, NM109 in Loop 1000A | |
"submitter_contact_name": "EDI DEPT", # PER02 in Loop 1000A | |
"submitter_contact_phone": "5551234567", # PER04 in Loop 1000A | |
"receiver_name": "PAYER EDI GATEWAY", # NM103 in Loop 1000B (Receiver) | |
"receiver_id": "RECEIVEREDI456", # ISA08, GS03, NM109 in Loop 1000B | |
"billing_provider_name": "GENERAL HOSPITAL", # NM103 in Loop 2010AA (Billing Provider) | |
"billing_provider_npi": "1234567890", # NM109 in Loop 2010AA | |
"billing_provider_tax_id": "987654321", # REF02 in Loop 2010AA (Billing Provider Tax ID) | |
"billing_provider_address_street": "789 HOSPITAL DR", # N301 in Loop 2010AA | |
"billing_provider_address_city": "HEALTHCITY", # N401 in Loop 2010AA | |
"billing_provider_address_state": "TX", # N402 in Loop 2010AA | |
"billing_provider_address_zip": "75001", # N403 in Loop 2010AA | |
"statement_from_date": "20240501", # YYYYMMDD, DTP*434 in Loop 2300 | |
"statement_to_date": "20240505", # YYYYMMDD, DTP*434 in Loop 2300 | |
"admission_date": "20240501", # YYYYMMDD, DTP*435 in Loop 2300 | |
"admission_type_code": "1", # CL101: e.g., 1-Emergency, 2-Urgent, 3-Elective | |
"admission_source_code": "7", # CL102: e.g., 1-Physician Referral, 7-Emergency Room | |
"patient_status_code": "01", # CL103: e.g., 01-Discharged to home | |
"facility_code_value": "22", # CLM05-1: e.g., 22 - Outpatient Hospital (NUBC Type of Bill - first two digits) | |
"claim_frequency_type_code": "1", # CLM05-3: 1-Original, 7-Replacement, 8-Void | |
"service_lines": [ | |
{ | |
"line_number": "1", # LX01 | |
"service_date": "20240501", # DTP*472 in Loop 2400 | |
"procedure_code": "99283", # SV202-2 (Composite with HC qualifier) | |
"line_item_charge_amount": "750.25", # SV203 | |
"revenue_code": "0450", # SV201 (e.g., Emergency Room) | |
"service_unit_count": "1", # SV205 | |
"unit_for_measurement_code": "UN" # SV204 (UN for Units) | |
}, | |
{ | |
"line_number": "2", | |
"service_date": "20240501", | |
"procedure_code": "71045", | |
"line_item_charge_amount": "250.50", | |
"revenue_code": "0320", # Radiology - Diagnostic | |
"service_unit_count": "1", | |
"unit_for_measurement_code": "UN" | |
}, | |
{ | |
"line_number": "3", | |
"service_date": "20240502", | |
"procedure_code": "85025", | |
"line_item_charge_amount": "500.00", | |
"revenue_code": "0300", # Laboratory | |
"service_unit_count": "1", | |
"unit_for_measurement_code": "UN" | |
} | |
] | |
} | |
# --- Control Numbers and EDI Settings --- | |
ISA_CONTROL_NUMBER = "000000001" # Must be 9 digits | |
GS_CONTROL_NUMBER = "1" | |
ST_CONTROL_NUMBER = "0001" # Must be 4-9 digits | |
ELEMENT_DELIMITER = "*" | |
SEGMENT_TERMINATOR = "~" | |
SUB_ELEMENT_DELIMITER = ":" # Component Element Separator for ISA16 and composite elements | |
# --- Helper Functions --- | |
def get_current_datetime_stamps(): | |
"""Gets current date and time in EDI formats.""" | |
now = datetime.datetime.now() | |
return { | |
"yyyymmdd_isa": now.strftime("%y%m%d"), # For ISA date (YYMMDD) | |
"ccyymmdd_gs_bht": now.strftime("%Y%m%d"), # For GS, BHT date (CCYYMMDD) | |
"hhmm_time": now.strftime("%H%M") # For ISA, GS, BHT time | |
} | |
def create_edi_segment(segment_id, *elements): | |
"""Helper function to build an EDI segment string.""" | |
return segment_id + ELEMENT_DELIMITER + ELEMENT_DELIMITER.join(map(str, elements)) + SEGMENT_TERMINATOR | |
# --- Main EDI Generation Logic --- | |
def generate_837i_edi(): | |
"""Generates the full 837I EDI string.""" | |
edi_segments_list = [] | |
st_to_se_segment_count = 0 # Counts segments from ST to SE (inclusive of ST and SE) | |
current_dt = get_current_datetime_stamps() | |
# ISA - Interchange Control Header | |
# ISA*AuthInfoQual*AuthInfo*SecInfoQual*SecInfo*SenderIDQual*SenderID*ReceiverIDQual*ReceiverID*Date*Time*RepSeparator*CtrlVersion*CtrlNumber*AckRequested*UsageIndicator*ComponentSeparator~ | |
isa_segment_str = ( | |
f"ISA{ELEMENT_DELIMITER}00{ELEMENT_DELIMITER}{'':<10}{ELEMENT_DELIMITER}00{ELEMENT_DELIMITER}{'':<10}" # Auth and Security Info (blank) | |
f"{ELEMENT_DELIMITER}ZZ{ELEMENT_DELIMITER}{claim_data['submitter_id']:<15}{ELEMENT_DELIMITER}ZZ{ELEMENT_DELIMITER}{claim_data['receiver_id']:<15}" # Sender/Receiver ID | |
f"{ELEMENT_DELIMITER}{current_dt['yyyymmdd_isa']}{ELEMENT_DELIMITER}{current_dt['hhmm_time']}{ELEMENT_DELIMITER}^" # Date, Time, Repetition Separator (ISA11, using '^') | |
f"{ELEMENT_DELIMITER}00501{ELEMENT_DELIMITER}{ISA_CONTROL_NUMBER}" # Version, Control Number | |
f"{ELEMENT_DELIMITER}0{ELEMENT_DELIMITER}P{ELEMENT_DELIMITER}{SUB_ELEMENT_DELIMITER}{SEGMENT_TERMINATOR}" # Ack Requested, Usage (P=Production), Component Separator | |
) | |
edi_segments_list.append(isa_segment_str) | |
# GS - Functional Group Header | |
# GS*FuncIDCode*AppSenderCode*AppReceiverCode*Date*Time*GroupCtrlNum*ResponsibleAgency*VersionReleaseID~ | |
gs_segment_str = create_edi_segment("GS", "HC", claim_data['submitter_id'], claim_data['receiver_id'], | |
current_dt['ccyymmdd_gs_bht'], current_dt['hhmm_time'], GS_CONTROL_NUMBER, "X", "005010X223A1") # X223A1 for 837I | |
edi_segments_list.append(gs_segment_str) | |
# ST - Transaction Set Header | |
st_segment_str = create_edi_segment("ST", "837", ST_CONTROL_NUMBER, "005010X223A1") | |
edi_segments_list.append(st_segment_str) | |
st_to_se_segment_count += 1 | |
# BHT - Beginning of Hierarchical Transaction | |
# BHT*HierStructCode*PurposeCode*OrigAppTransID*Date*Time*TransTypeCode~ | |
bht_reference_id = claim_data['claim_id'][:30] # Originator Application Transaction Identifier (max length varies, 30 is safe for many systems) | |
bht_segment_str = create_edi_segment("BHT", "0019", "00", bht_reference_id, current_dt['ccyymmdd_gs_bht'], current_dt['hhmm_time'], "CH") # CH = Chargeable | |
edi_segments_list.append(bht_segment_str) | |
st_to_se_segment_count += 1 | |
# Loop 1000A - Submitter Name | |
# NM1*EntityIDCode*EntityTypeQual*Name*IDCodeQual*IDCode~ | |
nm1_submitter = create_edi_segment("NM1", "41", "2", claim_data['submitter_name'], "", "", "", "", "46", claim_data['submitter_id']) # 41=Submitter, 2=Non-Person Org, 46=ETIN | |
edi_segments_list.append(nm1_submitter) | |
st_to_se_segment_count += 1 | |
# PER*ContactFuncCode*Name*CommNumQual*CommNum~ | |
per_submitter_contact = create_edi_segment("PER", "IC", claim_data['submitter_contact_name'], "TE", claim_data['submitter_contact_phone']) # IC=Information Contact, TE=Telephone | |
edi_segments_list.append(per_submitter_contact) | |
st_to_se_segment_count += 1 | |
# Loop 1000B - Receiver Name | |
nm1_receiver = create_edi_segment("NM1", "40", "2", claim_data['receiver_name'], "", "", "", "", "PI", claim_data['receiver_id']) # 40=Receiver, PI=Payer Identification | |
edi_segments_list.append(nm1_receiver) | |
st_to_se_segment_count += 1 | |
# --- Hierarchical Levels (HL) --- | |
hierarchical_level_counter = 0 | |
# Loop 2000A - Billing Provider Hierarchical Level | |
hierarchical_level_counter += 1 | |
billing_provider_hl_number = str(hierarchical_level_counter) | |
# HL*ID*ParentID*LevelCode*ChildCode~ (20=Information Source/Billing Provider, 1=Subordinate HL (Subscriber) present) | |
hl_billing_provider = create_edi_segment("HL", billing_provider_hl_number, "", "20", "1") | |
edi_segments_list.append(hl_billing_provider) | |
st_to_se_segment_count += 1 | |
# Loop 2010AA - Billing Provider Name | |
nm1_billing_provider = create_edi_segment("NM1", "85", "2", claim_data['billing_provider_name'], "", "", "", "", "XX", claim_data['billing_provider_npi']) # 85=Billing Provider, XX=NPI | |
edi_segments_list.append(nm1_billing_provider) | |
st_to_se_segment_count += 1 | |
n3_billing_provider_address = create_edi_segment("N3", claim_data['billing_provider_address_street']) | |
edi_segments_list.append(n3_billing_provider_address) | |
st_to_se_segment_count += 1 | |
n4_billing_provider_city = create_edi_segment("N4", claim_data['billing_provider_address_city'], claim_data['billing_provider_address_state'], claim_data['billing_provider_address_zip']) | |
edi_segments_list.append(n4_billing_provider_city) | |
st_to_se_segment_count += 1 | |
ref_billing_provider_tax_id = create_edi_segment("REF", "EI", claim_data['billing_provider_tax_id']) # EI=Employer's ID Number (Tax ID) | |
edi_segments_list.append(ref_billing_provider_tax_id) | |
st_to_se_segment_count += 1 | |
# Loop 2000B - Subscriber Hierarchical Level | |
hierarchical_level_counter += 1 | |
subscriber_hl_number = str(hierarchical_level_counter) | |
# HL04: ChildCode '1' if Patient Loop 2000C follows, '0' otherwise. | |
subscriber_hl_child_code = "1" if not PATIENT_IS_SUBSCRIBER else "0" | |
hl_subscriber = create_edi_segment("HL", subscriber_hl_number, billing_provider_hl_number, "22", subscriber_hl_child_code) # 22=Subscriber | |
edi_segments_list.append(hl_subscriber) | |
st_to_se_segment_count += 1 | |
# SBR - Subscriber Information | |
sbr_individual_relationship_code = "18" if PATIENT_IS_SUBSCRIBER else "" # SBR02: 18=Self (if patient is subscriber) | |
sbr_subscriber_info = create_edi_segment("SBR", "P", sbr_individual_relationship_code, subscriber_data['group_number'], "", "", "", "", "CI") # P=Primary, CI=Commercial Insurance | |
edi_segments_list.append(sbr_subscriber_info) | |
st_to_se_segment_count += 1 | |
# Loop 2010BA - Subscriber Name | |
nm1_subscriber_name = create_edi_segment("NM1", "IL", "1", subscriber_data['last_name'], subscriber_data['first_name'], subscriber_data['middle_initial'], "", "", "MI", subscriber_data['member_id']) # IL=Insured/Subscriber, 1=Person, MI=Member ID | |
edi_segments_list.append(nm1_subscriber_name) | |
st_to_se_segment_count += 1 | |
n3_subscriber_address = create_edi_segment("N3", subscriber_data['address_street']) | |
edi_segments_list.append(n3_subscriber_address) | |
st_to_se_segment_count += 1 | |
n4_subscriber_city = create_edi_segment("N4", subscriber_data['address_city'], subscriber_data['address_state'], subscriber_data['address_zip']) | |
edi_segments_list.append(n4_subscriber_city) | |
st_to_se_segment_count += 1 | |
dmg_subscriber_demographics = create_edi_segment("DMG", "D8", subscriber_data['date_of_birth'], subscriber_data['gender']) # D8=CCYYMMDD format | |
edi_segments_list.append(dmg_subscriber_demographics) | |
st_to_se_segment_count += 1 | |
# Loop 2010BB - Payer Name | |
nm1_payer_name = create_edi_segment("NM1", "PR", "2", claim_data['payer_name'], "", "", "", "", "PI", claim_data['payer_id']) # PR=Payer, PI=Payer ID | |
edi_segments_list.append(nm1_payer_name) | |
st_to_se_segment_count += 1 | |
# Loop 2000C - Patient Hierarchical Level (if patient is not the subscriber) | |
if not PATIENT_IS_SUBSCRIBER: | |
hierarchical_level_counter += 1 | |
patient_hl_number = str(hierarchical_level_counter) | |
# HL*ID*ParentID(Subscriber HL)*LevelCode*ChildCode~ (23=Patient, 0=No subordinate HL for patient in this basic structure) | |
hl_patient = create_edi_segment("HL", patient_hl_number, subscriber_hl_number, "23", "0") | |
edi_segments_list.append(hl_patient) | |
st_to_se_segment_count += 1 | |
# PAT - Patient Information | |
pat_patient_relationship = create_edi_segment("PAT", patient_data['relationship_to_subscriber']) # PAT01: Patient Relationship to Insured | |
edi_segments_list.append(pat_patient_relationship) | |
st_to_se_segment_count += 1 | |
# Loop 2010CA - Patient Name | |
nm1_patient_name = create_edi_segment("NM1", "QC", "1", patient_data['last_name'], patient_data['first_name'], patient_data['middle_initial']) # QC=Patient, 1=Person | |
edi_segments_list.append(nm1_patient_name) | |
st_to_se_segment_count += 1 | |
n3_patient_address = create_edi_segment("N3", patient_data['address_street']) | |
edi_segments_list.append(n3_patient_address) | |
st_to_se_segment_count += 1 | |
n4_patient_city = create_edi_segment("N4", patient_data['address_city'], patient_data['address_state'], patient_data['address_zip']) | |
edi_segments_list.append(n4_patient_city) | |
st_to_se_segment_count += 1 | |
dmg_patient_demographics = create_edi_segment("DMG", "D8", patient_data['date_of_birth'], patient_data['gender']) | |
edi_segments_list.append(dmg_patient_demographics) | |
st_to_se_segment_count += 1 | |
# Loop 2300 - Claim Information | |
# CLM*ClaimSubmitterID*TotalChargeAmt***FacilityCode:FacilityQual:FreqCode*ProvSigOnFile*AssignmentOfBenefits*BenAssignCert*ReleaseOfInfo~ | |
# CLM05 is a composite: CLM05-1 Facility Code Value, CLM05-2 Facility Code Qualifier ('B'), CLM05-3 Claim Frequency Type Code | |
clm05_facility_composite = f"{claim_data['facility_code_value']}{SUB_ELEMENT_DELIMITER}B{SUB_ELEMENT_DELIMITER}{claim_data['claim_frequency_type_code']}" | |
clm_claim_info = create_edi_segment("CLM", claim_data['claim_id'], claim_data['total_claim_charge_amount'], "", "", | |
clm05_facility_composite, "Y", "A", "Y", "I") # Y=Yes (Provider Signature), A=Assigned, Y=Benefits Cert, I=Release of Info | |
edi_segments_list.append(clm_claim_info) | |
st_to_se_segment_count += 1 | |
# DTP - Statement Dates (Required in 2300 for Institutional) | |
dtp_statement_dates_range = create_edi_segment("DTP", "434", "RD8", f"{claim_data['statement_from_date']}-{claim_data['statement_to_date']}") # 434=Statement Dates, RD8=Range CCYYMMDD-CCYYMMDD | |
edi_segments_list.append(dtp_statement_dates_range) | |
st_to_se_segment_count += 1 | |
# DTP - Admission Date (Often required for Institutional claims) | |
if claim_data.get("admission_date"): | |
dtp_admission = create_edi_segment("DTP", "435", "D8", claim_data['admission_date']) # 435=Admission Date/Hour, D8=CCYYMMDD | |
edi_segments_list.append(dtp_admission) | |
st_to_se_segment_count += 1 | |
# CL1 - Institutional Claim Code | |
cl1_institutional_codes = create_edi_segment("CL1", claim_data['admission_type_code'], claim_data['admission_source_code'], claim_data['patient_status_code']) | |
edi_segments_list.append(cl1_institutional_codes) | |
st_to_se_segment_count += 1 | |
# Loop 2400 - Service Line (at least one) | |
for service_line_item in claim_data['service_lines']: | |
# LX - Service Line Number | |
lx_line_number = create_edi_segment("LX", service_line_item['line_number']) | |
edi_segments_list.append(lx_line_number) | |
st_to_se_segment_count += 1 | |
# SV2 - Institutional Service Line | |
# SV2*RevenueCode*<HC:ProcedureCode>*ChargeAmt*UnitMeasure*UnitCount~ | |
# SV202 is a composite: SV202-1 Procedure Code Qualifier (HC), SV202-2 Procedure Code | |
sv202_procedure_composite = f"HC{SUB_ELEMENT_DELIMITER}{service_line_item['procedure_code']}" # HC = HCPCS/CPT | |
sv2_service_details = create_edi_segment("SV2", service_line_item['revenue_code'], sv202_procedure_composite, | |
service_line_item['line_item_charge_amount'], service_line_item['unit_for_measurement_code'], | |
service_line_item['service_unit_count']) | |
edi_segments_list.append(sv2_service_details) | |
st_to_se_segment_count += 1 | |
# DTP - Service Date (for this service line) | |
dtp_line_service_date = create_edi_segment("DTP", "472", "D8", service_line_item['service_date']) # 472=Service Date, D8=CCYYMMDD | |
edi_segments_list.append(dtp_line_service_date) | |
st_to_se_segment_count += 1 | |
# SE - Transaction Set Trailer | |
st_to_se_segment_count += 1 # For the SE segment itself | |
se_trailer = create_edi_segment("SE", st_to_se_segment_count, ST_CONTROL_NUMBER) | |
edi_segments_list.append(se_trailer) | |
# GE - Functional Group Trailer | |
ge_trailer = create_edi_segment("GE", "1", GS_CONTROL_NUMBER) # "1" = Number of transaction sets included in this functional group | |
edi_segments_list.append(ge_trailer) | |
# IEA - Interchange Control Trailer | |
iea_trailer = create_edi_segment("IEA", "1", ISA_CONTROL_NUMBER) # "1" = Number of functional groups in this interchange | |
edi_segments_list.append(iea_trailer) | |
return "".join(edi_segments_list) | |
if __name__ == "__main__": | |
# Generate the EDI 837I string | |
edi_837i_output_string = generate_837i_edi() | |
# Write the string to the output file | |
try: | |
with open(OUTPUT_FILENAME, "w", encoding="utf-8") as edi_file: | |
edi_file.write(edi_837i_output_string) | |
print(f"EDI 837I file generated successfully: {OUTPUT_FILENAME}") | |
except IOError as e: | |
print(f"Error writing EDI file: {e}") | |
# Optional: Print the generated EDI to console for review | |
# print("\n--- Generated EDI 837I Content ---") | |
# print(edi_837i_output_string) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment