medicafe 0.240613.0__py3-none-any.whl → 0.240809.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of medicafe might be problematic. Click here for more details.
- MediBot/MediBot.bat +37 -5
- MediBot/MediBot.py +13 -2
- MediBot/MediBot_Crosswalk_Library.py +15 -8
- MediBot/MediBot_Preprocessor_lib.py +14 -2
- MediBot/MediBot_docx_decoder.py +13 -5
- MediLink/MediLink.py +42 -77
- MediLink/MediLink_837p_encoder.py +64 -47
- MediLink/MediLink_837p_encoder_library.py +24 -35
- MediLink/MediLink_API_Generator.py +246 -0
- MediLink/MediLink_API_v2.py +2 -0
- MediLink/MediLink_API_v3.py +429 -0
- MediLink/MediLink_ClaimStatus.py +144 -0
- MediLink/MediLink_ConfigLoader.py +13 -7
- MediLink/MediLink_DataMgmt.py +4 -4
- MediLink/MediLink_Decoder.py +122 -20
- MediLink/MediLink_Deductible.py +210 -0
- MediLink/MediLink_Down.py +97 -66
- MediLink/MediLink_Parser.py +106 -24
- MediLink/MediLink_UI.py +12 -26
- MediLink/MediLink_Up.py +181 -111
- {medicafe-0.240613.0.dist-info → medicafe-0.240809.0.dist-info}/METADATA +2 -1
- medicafe-0.240809.0.dist-info/RECORD +47 -0
- medicafe-0.240613.0.dist-info/RECORD +0 -43
- {medicafe-0.240613.0.dist-info → medicafe-0.240809.0.dist-info}/LICENSE +0 -0
- {medicafe-0.240613.0.dist-info → medicafe-0.240809.0.dist-info}/WHEEL +0 -0
- {medicafe-0.240613.0.dist-info → medicafe-0.240809.0.dist-info}/top_level.txt +0 -0
MediBot/MediBot.bat
CHANGED
|
@@ -11,6 +11,8 @@ set "medicafe_package=medicafe"
|
|
|
11
11
|
set "upgrade_medicafe=F:\Medibot\update_medicafe.py"
|
|
12
12
|
set "temp_file=F:\Medibot\last_update_timestamp.txt"
|
|
13
13
|
set "firefox_path=C:\Program Files\Mozilla Firefox\firefox.exe"
|
|
14
|
+
set "claims_status_script=..\MediLink\MediLink_ClaimStatus.py"
|
|
15
|
+
set "deductible_script=..\MediLink\MediLink_Deductible.py"
|
|
14
16
|
set "package_version="
|
|
15
17
|
set PYTHONWARNINGS=ignore
|
|
16
18
|
|
|
@@ -99,14 +101,18 @@ if "!internet_available!"=="1" (
|
|
|
99
101
|
echo 1. Check for MediCafe Package Updates
|
|
100
102
|
echo 2. Download Email de Carol
|
|
101
103
|
echo 3. MediLink Claims
|
|
104
|
+
echo 4. United Claims Status
|
|
105
|
+
echo 5. United Deductible
|
|
102
106
|
)
|
|
103
|
-
echo
|
|
104
|
-
echo
|
|
107
|
+
echo 6. Run MediBot
|
|
108
|
+
echo 7. Exit
|
|
105
109
|
echo.
|
|
106
110
|
set /p choice=Enter your choice:
|
|
107
111
|
|
|
108
|
-
if "!choice!"=="
|
|
109
|
-
if "!choice!"=="
|
|
112
|
+
if "!choice!"=="7" goto end_script
|
|
113
|
+
if "!choice!"=="6" goto medibot_flow
|
|
114
|
+
if "!choice!"=="5" goto united_deductible
|
|
115
|
+
if "!choice!"=="4" goto united_claims_status
|
|
110
116
|
if "!choice!"=="3" goto medilink_flow
|
|
111
117
|
if "!choice!"=="2" goto download_emails
|
|
112
118
|
if "!choice!"=="1" goto check_updates
|
|
@@ -168,6 +174,32 @@ if errorlevel 1 echo MediLink failed to execute.
|
|
|
168
174
|
pause
|
|
169
175
|
goto main_menu
|
|
170
176
|
|
|
177
|
+
:: United Claims Status
|
|
178
|
+
:united_claims_status
|
|
179
|
+
if "!internet_available!"=="0" (
|
|
180
|
+
echo No internet connection available.
|
|
181
|
+
goto main_menu
|
|
182
|
+
)
|
|
183
|
+
cls
|
|
184
|
+
echo Checking United Claims Status...
|
|
185
|
+
py "%claims_status_script%"
|
|
186
|
+
if errorlevel 1 echo Failed to check United Claims Status.
|
|
187
|
+
pause
|
|
188
|
+
goto main_menu
|
|
189
|
+
|
|
190
|
+
:: United Deductible
|
|
191
|
+
:united_deductible
|
|
192
|
+
if "!internet_available!"=="0" (
|
|
193
|
+
echo No internet connection available.
|
|
194
|
+
goto main_menu
|
|
195
|
+
)
|
|
196
|
+
cls
|
|
197
|
+
echo Checking United Deductible...
|
|
198
|
+
py "%deductible_script%"
|
|
199
|
+
if errorlevel 1 echo Failed to check United Deductible.
|
|
200
|
+
pause
|
|
201
|
+
goto main_menu
|
|
202
|
+
|
|
171
203
|
:: Process CSV Files
|
|
172
204
|
:process_csvs
|
|
173
205
|
for /f "tokens=1-5 delims=/: " %%a in ('echo %time%') do (
|
|
@@ -203,4 +235,4 @@ goto :eof
|
|
|
203
235
|
:end_script
|
|
204
236
|
echo Exiting MediCafe.
|
|
205
237
|
pause
|
|
206
|
-
exit /b
|
|
238
|
+
exit /b
|
MediBot/MediBot.py
CHANGED
|
@@ -262,10 +262,21 @@ if __name__ == "__main__":
|
|
|
262
262
|
print("\nNOTE: The following patient(s) already EXIST in the system and \n will be excluded from processing:")
|
|
263
263
|
# TODO ... not excluded anymore, because charges may need to be added.
|
|
264
264
|
# So at this point in the processing, we should have already processed the surgery schedules and enriched the data with Charges.
|
|
265
|
+
# Find the corresponding rows in csv_data to get the surgery dates and store them along with patient info
|
|
266
|
+
patient_info = []
|
|
265
267
|
for patient_id, patient_name in existing_patients:
|
|
266
|
-
|
|
268
|
+
surgery_date = next((row.get('Surgery Date', 'Unknown Date') for row in csv_data if row.get(reverse_mapping['Patient ID #2']) == patient_id), 'Unknown Date')
|
|
269
|
+
patient_info.append((surgery_date, patient_name, patient_id))
|
|
270
|
+
|
|
271
|
+
# Sort by surgery date first and then by patient name
|
|
272
|
+
patient_info.sort(key=lambda x: (x[0], x[1]))
|
|
273
|
+
|
|
274
|
+
# Print the sorted patient info
|
|
275
|
+
for index, (surgery_date, patient_name, patient_id) in enumerate(patient_info):
|
|
276
|
+
print("{0:03d}: {3:%m-%d} (ID: {2}) {1} ".format(index + 1, patient_name, patient_id, surgery_date))
|
|
277
|
+
|
|
267
278
|
# Update csv_data to exclude existing patients
|
|
268
|
-
# TODO This now has to be updated to handle patients that exist but need new charges added.
|
|
279
|
+
# TODO This now has to be updated to handle patients that exist but need new charges added.
|
|
269
280
|
csv_data = [row for row in csv_data if row[reverse_mapping['Patient ID #2']] in patients_to_process]
|
|
270
281
|
else:
|
|
271
282
|
print("\nSelected patient(s) are NEW patients and will be processed.")
|
|
@@ -14,10 +14,10 @@ except ImportError:
|
|
|
14
14
|
from MediLink import MediLink_ConfigLoader
|
|
15
15
|
|
|
16
16
|
try:
|
|
17
|
-
from
|
|
17
|
+
from MediLink_API_v3 import fetch_payer_name_from_api
|
|
18
18
|
except ImportError:
|
|
19
|
-
from MediLink import
|
|
20
|
-
fetch_payer_name_from_api =
|
|
19
|
+
from MediLink import MediLink_API_v3
|
|
20
|
+
fetch_payer_name_from_api = MediLink_API_v3.fetch_payer_name_from_api
|
|
21
21
|
|
|
22
22
|
try:
|
|
23
23
|
from MediBot import MediBot_Preprocessor_lib
|
|
@@ -127,7 +127,7 @@ def initialize_crosswalk_from_mapat():
|
|
|
127
127
|
validate_and_correct_payer_ids(crosswalk, config)
|
|
128
128
|
|
|
129
129
|
# Save the initial crosswalk
|
|
130
|
-
if save_crosswalk(config
|
|
130
|
+
if save_crosswalk(config, crosswalk):
|
|
131
131
|
message = "Crosswalk initialized with mappings for {} payers.".format(len(crosswalk.get('payer_id', {})))
|
|
132
132
|
print(message)
|
|
133
133
|
MediLink_ConfigLoader.log(message, config, level="INFO")
|
|
@@ -197,7 +197,7 @@ def crosswalk_update(config, crosswalk):
|
|
|
197
197
|
crosswalk['payer_id'][payer_id]['medisoft_medicare_id'] = list(crosswalk['payer_id'][payer_id]['medisoft_medicare_id'])
|
|
198
198
|
|
|
199
199
|
# Save the updated crosswalk to the specified file
|
|
200
|
-
return save_crosswalk(config
|
|
200
|
+
return save_crosswalk(config, crosswalk)
|
|
201
201
|
|
|
202
202
|
def update_crosswalk_with_corrected_payer_id(old_payer_id, corrected_payer_id, config, crosswalk):
|
|
203
203
|
"""Updates the crosswalk with the corrected payer ID."""
|
|
@@ -216,7 +216,7 @@ def update_crosswalk_with_corrected_payer_id(old_payer_id, corrected_payer_id, c
|
|
|
216
216
|
MediLink_ConfigLoader.log("Crosswalk csv_replacements updated: added {} -> {}".format(old_payer_id, corrected_payer_id), config, level="INFO")
|
|
217
217
|
|
|
218
218
|
# Save the updated crosswalk
|
|
219
|
-
return save_crosswalk(config
|
|
219
|
+
return save_crosswalk(config, crosswalk)
|
|
220
220
|
|
|
221
221
|
def update_crosswalk_with_new_payer_id(insurance_name, payer_id, config):
|
|
222
222
|
"""Updates the crosswalk with a new payer ID."""
|
|
@@ -229,14 +229,14 @@ def update_crosswalk_with_new_payer_id(insurance_name, payer_id, config):
|
|
|
229
229
|
crosswalk['payer_id'][payer_id] = {"medisoft_id": [medisoft_id_str], "medisoft_medicare_id": []}
|
|
230
230
|
else:
|
|
231
231
|
crosswalk['payer_id'][payer_id]['medisoft_id'].append(medisoft_id_str)
|
|
232
|
-
save_crosswalk(config
|
|
232
|
+
save_crosswalk(config, crosswalk)
|
|
233
233
|
MediLink_ConfigLoader.log("Updated crosswalk with new payer ID {} for insurance name {}".format(payer_id, insurance_name), config, level="INFO")
|
|
234
234
|
else:
|
|
235
235
|
message = "Failed to update crosswalk: Medisoft ID not found for insurance name {}".format(insurance_name)
|
|
236
236
|
print(message)
|
|
237
237
|
MediLink_ConfigLoader.log(message, config, level="ERROR")
|
|
238
238
|
|
|
239
|
-
def save_crosswalk(
|
|
239
|
+
def save_crosswalk(config, crosswalk):
|
|
240
240
|
"""
|
|
241
241
|
Saves the updated crosswalk to a JSON file.
|
|
242
242
|
Args:
|
|
@@ -245,6 +245,13 @@ def save_crosswalk(crosswalk_path, crosswalk):
|
|
|
245
245
|
Returns:
|
|
246
246
|
bool: True if the file was successfully saved, False otherwise.
|
|
247
247
|
"""
|
|
248
|
+
# Attempt to fetch crosswalkPath from MediLink_Config
|
|
249
|
+
try:
|
|
250
|
+
crosswalk_path = config['MediLink_Config']['crosswalkPath']
|
|
251
|
+
except KeyError:
|
|
252
|
+
# If KeyError occurs, fall back to fetching crosswalkPath directly
|
|
253
|
+
crosswalk_path = config.get('crosswalkPath', None) # Replace None with a default value if needed
|
|
254
|
+
|
|
248
255
|
try:
|
|
249
256
|
# Initialize 'payer_id' key if not present
|
|
250
257
|
if 'payer_id' not in crosswalk:
|
|
@@ -124,7 +124,8 @@ def sort_and_deduplicate(csv_data):
|
|
|
124
124
|
if patient_id not in unique_patients or row['Surgery Date'] < unique_patients[patient_id]['Surgery Date']:
|
|
125
125
|
unique_patients[patient_id] = row
|
|
126
126
|
csv_data[:] = list(unique_patients.values())
|
|
127
|
-
# TODO Sorting, now that we're going to have the Surgery Schedules available, should
|
|
127
|
+
# TODO Sorting, now that we're going to have the Surgery Schedules available, should (or shouldn't??
|
|
128
|
+
# maybe we should build in the option as liek a 'setting' in the config) be ordered as the patients show up on the schedule.
|
|
128
129
|
# If we don't have that surgery schedule yet for some reason, we should default to the current ordering strategy.
|
|
129
130
|
csv_data.sort(key=lambda x: (x['Surgery Date'], x.get('Patient Last', '').strip()))
|
|
130
131
|
|
|
@@ -161,7 +162,16 @@ def update_insurance_ids(csv_data, crosswalk):
|
|
|
161
162
|
medisoft_ids = crosswalk['payer_id'][ins1_payer_id].get('medisoft_id', [])
|
|
162
163
|
if medisoft_ids:
|
|
163
164
|
medisoft_ids = [int(id) for id in medisoft_ids]
|
|
164
|
-
# TODO Try to match OpenPM's Insurance Name to get a better match
|
|
165
|
+
# TODO Try to match OpenPM's Insurance Name to get a better match.
|
|
166
|
+
# Potential approach:
|
|
167
|
+
# 1. Retrieve the insurance name from the current row
|
|
168
|
+
# insurance_name = row.get('Primary Insurnace', '').strip()
|
|
169
|
+
# 2. Check if the insurance name exists in the subset of MAINS names associated with
|
|
170
|
+
# crosswalk medisoft ID values for the given payer ID.
|
|
171
|
+
# 3. If an approximate match is found above a certain confidence, use the corresponding medisoft_id.
|
|
172
|
+
# else: 4. If no match is found, default to the first medisoft_id
|
|
173
|
+
# row['Ins1 Insurance ID'] = medisoft_ids[0]
|
|
174
|
+
|
|
165
175
|
row['Ins1 Insurance ID'] = medisoft_ids[0]
|
|
166
176
|
# MediLink_ConfigLoader.log("Ins1 Insurance ID '{}' used for Payer ID {} in crosswalk.".format(row.get('Ins1 Insurance ID', ''), ins1_payer_id))
|
|
167
177
|
else:
|
|
@@ -367,6 +377,8 @@ def load_insurance_data_from_mains(config):
|
|
|
367
377
|
# Retrieve MAINS path and slicing information from the configuration
|
|
368
378
|
# TODO (Low) For secondary insurance, this needs to be pulling from the correct MAINS (there are 2)
|
|
369
379
|
# TODO (Low) Performance: There probably needs to be a dictionary proxy for MAINS that gets updated.
|
|
380
|
+
# Meh, this just has to be part of the new architecture plan where we make Medisoft a downstream
|
|
381
|
+
# recipient from the db.
|
|
370
382
|
mains_path = config['MAINS_MED_PATH']
|
|
371
383
|
mains_slices = crosswalk['mains_mapping']['slices']
|
|
372
384
|
|
MediBot/MediBot_docx_decoder.py
CHANGED
|
@@ -238,10 +238,19 @@ def parse_patient_id(text):
|
|
|
238
238
|
|
|
239
239
|
def parse_diagnosis_code(text):
|
|
240
240
|
try:
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
241
|
+
# Regular expression to find all ICD-10 codes starting with 'H' and containing a period
|
|
242
|
+
pattern = re.compile(r'H\d{2}\.\d+')
|
|
243
|
+
matches = pattern.findall(text)
|
|
244
|
+
|
|
245
|
+
if matches:
|
|
246
|
+
return matches[0] # Return the first match
|
|
247
|
+
else:
|
|
248
|
+
# Fallback to original method if no match is found
|
|
249
|
+
if '(' in text and ')' in text: # Extract the diagnosis code before the '/'
|
|
250
|
+
full_code = text[text.index('(')+1:text.index(')')]
|
|
251
|
+
return full_code.split('/')[0]
|
|
252
|
+
return text.split('/')[0]
|
|
253
|
+
|
|
245
254
|
except Exception as e:
|
|
246
255
|
MediLink_ConfigLoader.log("Error parsing diagnosis code: {}. Error: {}".format(text, e))
|
|
247
256
|
return "Unknown"
|
|
@@ -268,7 +277,6 @@ def parse_femto_yes_or_no(text):
|
|
|
268
277
|
MediLink_ConfigLoader.log("Error parsing femto yes or no: {}. Error: {}".format(text, e))
|
|
269
278
|
return False
|
|
270
279
|
|
|
271
|
-
|
|
272
280
|
def rotate_docx_files(directory):
|
|
273
281
|
# List all files in the directory
|
|
274
282
|
files = os.listdir(directory)
|
MediLink/MediLink.py
CHANGED
|
@@ -2,7 +2,6 @@ import os
|
|
|
2
2
|
import MediLink_Down
|
|
3
3
|
import MediLink_Up
|
|
4
4
|
import MediLink_ConfigLoader
|
|
5
|
-
import MediLink_837p_encoder
|
|
6
5
|
import MediLink_DataMgmt
|
|
7
6
|
|
|
8
7
|
# For UI Functions
|
|
@@ -19,73 +18,31 @@ from MediBot import MediBot_Preprocessor_lib
|
|
|
19
18
|
load_insurance_data_from_mains = MediBot_Preprocessor_lib.load_insurance_data_from_mains
|
|
20
19
|
from MediBot import MediBot_Crosswalk_Library
|
|
21
20
|
|
|
22
|
-
#
|
|
23
|
-
|
|
24
|
-
insurance_options =
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"13": "Point of Service (POS)",
|
|
28
|
-
"14": "Exclusive Provider Organization (EPO)",
|
|
29
|
-
"15": "Indemnity Insurance",
|
|
30
|
-
"16": "Health Maintenance Organization (HMO) Medicare Risk",
|
|
31
|
-
"17": "Dental Maintenance Organization",
|
|
32
|
-
"AM": "Automobile Medical",
|
|
33
|
-
"BL": "Blue Cross/Blue Shield",
|
|
34
|
-
"CH": "Champus",
|
|
35
|
-
"CI": "Commercial Insurance Co.",
|
|
36
|
-
"DS": "Disability",
|
|
37
|
-
"FI": "Federal Employees Program",
|
|
38
|
-
"HM": "Health Maintenance Organization",
|
|
39
|
-
"LM": "Liability Medical",
|
|
40
|
-
"MA": "Medicare Part A",
|
|
41
|
-
"MB": "Medicare Part B",
|
|
42
|
-
"MC": "Medicaid",
|
|
43
|
-
"OF": "Other Federal Program",
|
|
44
|
-
"TV": "Title V",
|
|
45
|
-
"VA": "Veterans Affairs Plan",
|
|
46
|
-
"WC": "Workers Compensation Health Claim",
|
|
47
|
-
"ZZ": "Mutually Defined"
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
def detect_and_display_file_summaries(directory_path, config, crosswalk):
|
|
21
|
+
# Retrieve insurance options with codes and descriptions
|
|
22
|
+
config, _ = MediLink_ConfigLoader.load_configuration()
|
|
23
|
+
insurance_options = config['MediLink_Config'].get('insurance_options')
|
|
24
|
+
|
|
25
|
+
def collect_detailed_patient_data(selected_files, config, crosswalk):
|
|
51
26
|
"""
|
|
52
|
-
|
|
53
|
-
including suggestions for endpoints based on insurance provider information found in the config.
|
|
27
|
+
Collects detailed patient data from the selected files.
|
|
54
28
|
|
|
55
|
-
:param
|
|
29
|
+
:param selected_files: List of selected file paths.
|
|
56
30
|
:param config: Configuration settings loaded from a JSON file.
|
|
57
31
|
:param crosswalk: Crosswalk data for mapping purposes.
|
|
58
|
-
:return: A
|
|
32
|
+
:return: A list of detailed patient data.
|
|
59
33
|
"""
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
MediLink_ConfigLoader.log("No new claims detected. Check Medisoft claims output.")
|
|
65
|
-
return False, []
|
|
66
|
-
|
|
67
|
-
if not file_flagged:
|
|
68
|
-
selected_files = MediLink_UI.user_select_files(new_files)
|
|
69
|
-
else:
|
|
70
|
-
# Extract the newest single latest file from the list
|
|
71
|
-
selected_files = [max(new_files, key=os.path.getctime)]
|
|
72
|
-
|
|
73
|
-
detailed_patient_data = [] # Initialize list for detailed patient data
|
|
74
|
-
for file_path in selected_files:
|
|
75
|
-
detailed_data = extract_and_suggest_endpoint(file_path, config, crosswalk)
|
|
76
|
-
detailed_patient_data.extend(detailed_data) # Accumulate detailed data for processing
|
|
77
|
-
|
|
78
|
-
# Enrich the detailed patient data with insurance type
|
|
79
|
-
detailed_patient_data = enrich_with_insurance_type(detailed_patient_data, insurance_options)
|
|
34
|
+
detailed_patient_data = []
|
|
35
|
+
for file_path in selected_files:
|
|
36
|
+
detailed_data = extract_and_suggest_endpoint(file_path, config, crosswalk)
|
|
37
|
+
detailed_patient_data.extend(detailed_data) # Accumulate detailed data for processing
|
|
80
38
|
|
|
81
|
-
|
|
82
|
-
|
|
39
|
+
# Enrich the detailed patient data with insurance type
|
|
40
|
+
detailed_patient_data = enrich_with_insurance_type(detailed_patient_data, insurance_options)
|
|
41
|
+
|
|
42
|
+
# Display summaries and provide an option for bulk edit
|
|
43
|
+
MediLink_UI.display_patient_summaries(detailed_patient_data)
|
|
83
44
|
|
|
84
|
-
|
|
85
|
-
return selected_files, detailed_patient_data
|
|
86
|
-
except Exception as e:
|
|
87
|
-
MediLink_ConfigLoader.log("Error in detect_and_display_file_summaries: {}".format(e))
|
|
88
|
-
return False, []
|
|
45
|
+
return detailed_patient_data
|
|
89
46
|
|
|
90
47
|
def enrich_with_insurance_type(detailed_patient_data, patient_insurance_type_mapping=None):
|
|
91
48
|
"""
|
|
@@ -101,7 +58,7 @@ def enrich_with_insurance_type(detailed_patient_data, patient_insurance_type_map
|
|
|
101
58
|
TODO: Implement a function to provide `patient_insurance_mapping` from a reliable source.
|
|
102
59
|
"""
|
|
103
60
|
if patient_insurance_type_mapping is None:
|
|
104
|
-
MediLink_ConfigLoader.log("No Patient:Insurance-Type mapping available.")
|
|
61
|
+
MediLink_ConfigLoader.log("No Patient:Insurance-Type mapping available.", level="WARNING")
|
|
105
62
|
patient_insurance_type_mapping = {}
|
|
106
63
|
|
|
107
64
|
for data in detailed_patient_data:
|
|
@@ -129,7 +86,7 @@ def extract_and_suggest_endpoint(file_path, config, crosswalk):
|
|
|
129
86
|
|
|
130
87
|
# Load insurance data from MAINS to create a mapping from insurance names to their respective IDs
|
|
131
88
|
insurance_to_id = load_insurance_data_from_mains(config)
|
|
132
|
-
MediLink_ConfigLoader.log("Insurance data loaded from MAINS. {} insurance providers found.".format(len(insurance_to_id)))
|
|
89
|
+
MediLink_ConfigLoader.log("Insurance data loaded from MAINS. {} insurance providers found.".format(len(insurance_to_id)), level="INFO")
|
|
133
90
|
|
|
134
91
|
for personal_info, insurance_info, service_info, service_info_2, service_info_3 in MediLink_DataMgmt.read_fixed_width_data(file_path):
|
|
135
92
|
parsed_data = MediLink_DataMgmt.parse_fixed_width_data(personal_info, insurance_info, service_info, service_info_2, service_info_3, config.get('MediLink_Config', config))
|
|
@@ -191,13 +148,13 @@ def check_for_new_remittances(config):
|
|
|
191
148
|
try:
|
|
192
149
|
ERA_path = MediLink_Down.main(desired_endpoint=endpoint_key)
|
|
193
150
|
processed_endpoints.append((endpoint_info['name'], ERA_path))
|
|
194
|
-
MediLink_ConfigLoader.log("Results for {} saved to: {}".format(endpoint_info['name'], ERA_path))
|
|
151
|
+
MediLink_ConfigLoader.log("Results for {} saved to: {}".format(endpoint_info['name'], ERA_path), level="DEBUG")
|
|
195
152
|
# TODO (Low SFTP - Download side) This needs to check to see if this actually worked maybe winscplog before saying it completed successfully
|
|
196
153
|
# Check if there is commonality with the upload side so we can use the same validation function.
|
|
197
154
|
except Exception as e:
|
|
198
155
|
print("An error occurred while checking remittances for {}: {}".format(endpoint_info['name'], e))
|
|
199
156
|
else:
|
|
200
|
-
MediLink_ConfigLoader.log("Skipping endpoint '{}' as it does not have 'remote_directory_down' configured.".format(endpoint_info['name']))
|
|
157
|
+
MediLink_ConfigLoader.log("Skipping endpoint '{}' as it does not have 'remote_directory_down' configured.".format(endpoint_info['name']), level="WARNING")
|
|
201
158
|
else:
|
|
202
159
|
print("Error: Endpoint config is not a 'dictionary' as expected.")
|
|
203
160
|
# Check if all ERA paths are the same
|
|
@@ -299,17 +256,14 @@ def main_menu():
|
|
|
299
256
|
# Normalize the directory path for file operations.
|
|
300
257
|
directory_path = os.path.normpath(config['MediLink_Config']['inputFilePath'])
|
|
301
258
|
|
|
302
|
-
# Detect
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
if new_files:
|
|
306
|
-
handle_submission(detailed_patient_data, config)
|
|
259
|
+
# Detect files and determine if a new file is flagged.
|
|
260
|
+
all_files, file_flagged = MediLink_DataMgmt.detect_new_files(directory_path)
|
|
307
261
|
|
|
308
262
|
while True:
|
|
309
263
|
# Define the menu options. Base options include checking remittances and exiting the program.
|
|
310
264
|
options = ["Check for new remittances", "Exit"]
|
|
311
|
-
# If
|
|
312
|
-
if
|
|
265
|
+
# If any files are detected, add the option to submit claims.
|
|
266
|
+
if all_files:
|
|
313
267
|
options.insert(1, "Submit claims")
|
|
314
268
|
|
|
315
269
|
# Display the dynamically adjusted menu options.
|
|
@@ -320,11 +274,22 @@ def main_menu():
|
|
|
320
274
|
if choice == '1':
|
|
321
275
|
# Handle remittance checking.
|
|
322
276
|
check_for_new_remittances(config)
|
|
323
|
-
elif choice == '2' and
|
|
324
|
-
# Handle the claims submission flow if
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
277
|
+
elif choice == '2' and all_files:
|
|
278
|
+
# Handle the claims submission flow if any files are present.
|
|
279
|
+
if file_flagged:
|
|
280
|
+
# Extract the newest single latest file from the list if a new file is flagged.
|
|
281
|
+
selected_files = [max(all_files, key=os.path.getctime)]
|
|
282
|
+
else:
|
|
283
|
+
# Prompt the user to select files if no new file is flagged.
|
|
284
|
+
selected_files = MediLink_UI.user_select_files(all_files)
|
|
285
|
+
|
|
286
|
+
# Collect detailed patient data for selected files.
|
|
287
|
+
detailed_patient_data = collect_detailed_patient_data(selected_files, config, crosswalk)
|
|
288
|
+
|
|
289
|
+
# Process the claims submission.
|
|
290
|
+
handle_submission(detailed_patient_data, config)
|
|
291
|
+
elif choice == '3' or (choice == '2' and not all_files):
|
|
292
|
+
# Exit the program if the user chooses to exit or if no files are present.
|
|
328
293
|
MediLink_UI.display_exit_message()
|
|
329
294
|
break
|
|
330
295
|
else:
|
|
@@ -77,33 +77,27 @@ def format_single_claim(patient_data, config, endpoint, transaction_set_control_
|
|
|
77
77
|
|
|
78
78
|
return formatted_837p
|
|
79
79
|
|
|
80
|
-
def write_output_file(document_segments, output_directory, endpoint_key, input_file_path, config):
|
|
80
|
+
def write_output_file(document_segments, output_directory, endpoint_key, input_file_path, config, suffix=""):
|
|
81
81
|
"""
|
|
82
82
|
Writes formatted 837P document segments to an output file with a dynamically generated name.
|
|
83
|
-
|
|
84
|
-
Development Roadmap:
|
|
85
|
-
- Ensure input `document_segments` is a non-empty list to avoid creating empty files.
|
|
86
|
-
- Verify `output_directory` exists and is writable before proceeding. Create the directory if it does not exist.
|
|
87
|
-
- Consider parameterizing the file naming convention or providing options for customization to accommodate different organizational needs.
|
|
88
|
-
- Implement error handling to gracefully manage file writing failures, potentially returning a status or error message alongside the file path.
|
|
89
|
-
- Incorporate logging directly within the function, accepting an optional `config` or `logger` parameter to facilitate tracking of the file writing process and outcomes.
|
|
90
|
-
- Update the return value to include both the path to the output file and any relevant status information (e.g., success flag, error message) to enhance downstream error handling and user feedback.
|
|
91
|
-
|
|
83
|
+
|
|
92
84
|
Parameters:
|
|
93
85
|
- document_segments: List of strings, where each string is a segment of the 837P document to be written.
|
|
94
86
|
- output_directory: String specifying the directory where the output file will be saved.
|
|
95
87
|
- endpoint_key: String specifying the endpoint for which the claim is processed, used in naming the output file.
|
|
96
88
|
- input_file_path: String specifying the path to the input file being processed, used in naming the output file.
|
|
97
|
-
|
|
89
|
+
- config: Configuration settings for logging and other purposes.
|
|
90
|
+
- suffix: Optional string to differentiate filenames, useful for single-patient processing.
|
|
91
|
+
|
|
98
92
|
Returns:
|
|
99
|
-
- String specifying the path to the successfully created output file
|
|
93
|
+
- String specifying the path to the successfully created output file, or None if an error occurred.
|
|
100
94
|
"""
|
|
101
|
-
#
|
|
95
|
+
# Ensure the document segments are not empty
|
|
102
96
|
if not document_segments:
|
|
103
97
|
MediLink_ConfigLoader.log("Error: Empty document segments provided. No output file created.", config, level="ERROR")
|
|
104
98
|
return None
|
|
105
99
|
|
|
106
|
-
#
|
|
100
|
+
# Verify the output directory exists and is writable, create if necessary
|
|
107
101
|
if not os.path.exists(output_directory):
|
|
108
102
|
try:
|
|
109
103
|
os.makedirs(output_directory)
|
|
@@ -114,13 +108,13 @@ def write_output_file(document_segments, output_directory, endpoint_key, input_f
|
|
|
114
108
|
MediLink_ConfigLoader.log("Error: Output directory is not writable.", config, level="ERROR")
|
|
115
109
|
return None
|
|
116
110
|
|
|
117
|
-
# Generate new output file path
|
|
111
|
+
# Generate the new output file path
|
|
118
112
|
base_name = os.path.splitext(os.path.basename(input_file_path))[0]
|
|
119
113
|
timestamp = datetime.now().strftime("%m%d%H%M")
|
|
120
|
-
new_output_file_name = "{}_{}_{}.txt".format(base_name, endpoint_key.lower(), timestamp)
|
|
114
|
+
new_output_file_name = "{}_{}_{}{}.txt".format(base_name, endpoint_key.lower(), timestamp, suffix)
|
|
121
115
|
new_output_file_path = os.path.join(output_directory, new_output_file_name)
|
|
122
116
|
|
|
123
|
-
# Write
|
|
117
|
+
# Write the document to the output file
|
|
124
118
|
try:
|
|
125
119
|
with open(new_output_file_path, 'w') as output_file:
|
|
126
120
|
output_file.write('\n'.join(document_segments))
|
|
@@ -335,7 +329,7 @@ def main():
|
|
|
335
329
|
parser.add_argument(
|
|
336
330
|
"-e", "--endpoint",
|
|
337
331
|
required=True,
|
|
338
|
-
choices=["AVAILITY", "OPTUMEDI", "PNT_DATA"],
|
|
332
|
+
choices=["AVAILITY", "OPTUMEDI", "PNT_DATA", "UHCAPI"],
|
|
339
333
|
help="Specify the endpoint for which the conversion is intended."
|
|
340
334
|
)
|
|
341
335
|
parser.add_argument(
|
|
@@ -388,9 +382,9 @@ if __name__ == "__main__":
|
|
|
388
382
|
#######################################################################################
|
|
389
383
|
|
|
390
384
|
def convert_files_for_submission(detailed_patient_data, config):
|
|
391
|
-
"""
|
|
385
|
+
"""
|
|
392
386
|
Processes detailed patient data for submission based on their confirmed endpoints,
|
|
393
|
-
generating
|
|
387
|
+
generating separate 837P files for each endpoint according to the configured submission type.
|
|
394
388
|
|
|
395
389
|
Parameters:
|
|
396
390
|
- detailed_patient_data: A list containing detailed patient data with endpoint information.
|
|
@@ -398,73 +392,93 @@ def convert_files_for_submission(detailed_patient_data, config):
|
|
|
398
392
|
|
|
399
393
|
Returns:
|
|
400
394
|
- A list of paths to the converted files ready for submission.
|
|
395
|
+
|
|
396
|
+
Note:
|
|
397
|
+
- This function currently supports batch and single-patient submissions based on the configuration.
|
|
398
|
+
- Future implementation may include progress tracking using tools like `tqdm`.
|
|
401
399
|
"""
|
|
402
400
|
|
|
403
401
|
# Initialize a dictionary to hold patient data segregated by confirmed endpoints
|
|
404
402
|
data_by_endpoint = {}
|
|
405
|
-
|
|
406
|
-
#
|
|
403
|
+
|
|
404
|
+
# Group patient data by endpoint
|
|
407
405
|
for data in detailed_patient_data:
|
|
408
|
-
endpoint = data
|
|
409
|
-
if endpoint
|
|
410
|
-
|
|
411
|
-
|
|
406
|
+
endpoint = data.get('confirmed_endpoint')
|
|
407
|
+
if endpoint:
|
|
408
|
+
if endpoint not in data_by_endpoint:
|
|
409
|
+
data_by_endpoint[endpoint] = []
|
|
410
|
+
data_by_endpoint[endpoint].append(data)
|
|
412
411
|
|
|
413
412
|
# List to store paths of converted files for each endpoint
|
|
414
413
|
converted_files_paths = []
|
|
415
414
|
|
|
416
|
-
# Determine the total number of unique endpoints for progress bar
|
|
417
|
-
# total_endpoints = len(data_by_endpoint)
|
|
418
|
-
|
|
419
415
|
# Iterate over each endpoint and process its corresponding patient data
|
|
420
416
|
for endpoint, patient_data_list in data_by_endpoint.items():
|
|
421
|
-
#
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
417
|
+
# Retrieve submission type from config; default to "batch" if not specified
|
|
418
|
+
submission_type = config.get('MediLink_Config', {}).get('endpoints', {}).get(endpoint, {}).get('submission_type', 'batch')
|
|
419
|
+
|
|
420
|
+
if submission_type == 'single':
|
|
421
|
+
# Process each patient's data individually for single-patient submissions
|
|
422
|
+
for patient_data in patient_data_list:
|
|
423
|
+
# Generate a unique suffix for each patient, e.g., using a truncated chart number
|
|
424
|
+
chart_number = patient_data.get('CHART', 'UNKNOWN')#[:5] truncation might cause collisions.
|
|
425
|
+
suffix = "_{}".format(chart_number)
|
|
426
|
+
# Process and convert each patient's data to a separate file
|
|
427
|
+
converted_path = process_claim(config, endpoint, [patient_data], suffix)
|
|
428
|
+
if converted_path:
|
|
429
|
+
converted_files_paths.append(converted_path)
|
|
430
|
+
else:
|
|
431
|
+
# Process all patient data together for batch submissions
|
|
432
|
+
converted_path = process_claim(config, endpoint, patient_data_list)
|
|
425
433
|
if converted_path:
|
|
426
434
|
converted_files_paths.append(converted_path)
|
|
427
|
-
|
|
435
|
+
|
|
428
436
|
return converted_files_paths
|
|
429
437
|
|
|
430
|
-
def process_claim(config, endpoint, patient_data_list):
|
|
438
|
+
def process_claim(config, endpoint, patient_data_list, suffix=""):
|
|
431
439
|
"""
|
|
432
440
|
Processes patient data for a specified endpoint, converting it into the 837P format.
|
|
433
|
-
|
|
441
|
+
Can handle both batch and single-patient submissions.
|
|
434
442
|
|
|
435
443
|
Parameters:
|
|
436
444
|
- config: Configuration settings loaded from a JSON file.
|
|
437
|
-
-
|
|
445
|
+
- endpoint: The key representing the endpoint for which the data is being processed.
|
|
438
446
|
- patient_data_list: A list of dictionaries, each containing detailed patient data.
|
|
447
|
+
- suffix: An optional suffix to differentiate filenames for single-patient processing.
|
|
439
448
|
|
|
440
449
|
Returns:
|
|
441
450
|
- Path to the converted file, or None if an error occurs.
|
|
442
|
-
|
|
443
|
-
TODO (LOW) Why are there duplicated interchange flows? Because the arg if we're doing a .dat directory or not.
|
|
444
|
-
Although, that shouldn't be making duplicates of these interchange headers. That's still confusing and could end up making
|
|
445
|
-
duplicate interchange headers because processing .dat in batch might be fast enough to be a problem.
|
|
446
451
|
"""
|
|
452
|
+
# Ensure we're accessing the correct configuration key
|
|
453
|
+
config = config.get('MediLink_Config', config)
|
|
454
|
+
|
|
455
|
+
# Retrieve the output directory from the configuration
|
|
447
456
|
output_directory = MediLink_837p_encoder_library.get_output_directory(config)
|
|
448
457
|
if not output_directory:
|
|
449
458
|
return None
|
|
450
459
|
|
|
451
|
-
# Initialize the transaction set control number and document segments
|
|
452
460
|
transaction_set_control_number = 1
|
|
453
461
|
document_segments = []
|
|
454
462
|
|
|
455
|
-
# Process each patient's data in the list
|
|
456
463
|
for patient_data in patient_data_list:
|
|
457
|
-
# Validate each patient's data
|
|
464
|
+
# Validate each patient's data before processing
|
|
458
465
|
is_valid, validation_errors = validate_claim_data(patient_data, config)
|
|
459
466
|
if is_valid:
|
|
460
|
-
# Format the claim
|
|
467
|
+
# Format the claim into 837P segments
|
|
461
468
|
formatted_claim = format_single_claim(patient_data, config, endpoint, transaction_set_control_number)
|
|
462
469
|
document_segments.append(formatted_claim)
|
|
463
470
|
transaction_set_control_number += 1
|
|
464
471
|
else:
|
|
472
|
+
# Log any validation errors encountered
|
|
473
|
+
MediLink_ConfigLoader.log("Validation errors for patient data: {}".format(validation_errors), config, level="ERROR")
|
|
465
474
|
if MediLink_837p_encoder_library.handle_validation_errors(transaction_set_control_number, validation_errors, config):
|
|
466
475
|
continue # Skip the current patient
|
|
467
476
|
|
|
477
|
+
if not document_segments:
|
|
478
|
+
# If no valid segments were created, log the issue and return None
|
|
479
|
+
MediLink_ConfigLoader.log("No valid document segments created.", config, level="ERROR")
|
|
480
|
+
return None
|
|
481
|
+
|
|
468
482
|
# Create interchange elements with the final transaction set control number
|
|
469
483
|
isa_header, gs_header, ge_trailer, iea_trailer = MediLink_837p_encoder_library.create_interchange_elements(config, endpoint, transaction_set_control_number - 1)
|
|
470
484
|
|
|
@@ -473,5 +487,8 @@ def process_claim(config, endpoint, patient_data_list):
|
|
|
473
487
|
document_segments.insert(0, isa_header)
|
|
474
488
|
document_segments.extend([ge_trailer, iea_trailer])
|
|
475
489
|
|
|
476
|
-
#
|
|
477
|
-
|
|
490
|
+
# Use the first patient's file path as a reference for output file naming
|
|
491
|
+
input_file_path = patient_data_list[0].get('file_path', 'UNKNOWN')
|
|
492
|
+
# Write the complete 837P document to an output file
|
|
493
|
+
converted_file_path = write_output_file(document_segments, output_directory, endpoint, input_file_path, config, suffix)
|
|
494
|
+
return converted_file_path
|