Skip to content

Instantly share code, notes, and snippets.

@minherz
Last active May 27, 2025 22:00
Show Gist options
  • Save minherz/3cd11ab20408c743ef7fdaa8eec2a461 to your computer and use it in GitHub Desktop.
Save minherz/3cd11ab20408c743ef7fdaa8eec2a461 to your computer and use it in GitHub Desktop.
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}")
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