medicafe 0.240419.2__py3-none-any.whl → 0.240613.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 +174 -38
- MediBot/MediBot.py +80 -77
- MediBot/MediBot_Charges.py +0 -28
- MediBot/MediBot_Crosswalk_Library.py +281 -0
- MediBot/MediBot_Post.py +0 -0
- MediBot/MediBot_Preprocessor.py +138 -211
- MediBot/MediBot_Preprocessor_lib.py +496 -0
- MediBot/MediBot_UI.py +80 -35
- MediBot/MediBot_dataformat_library.py +79 -35
- MediBot/MediBot_docx_decoder.py +295 -0
- MediBot/update_medicafe.py +46 -8
- MediLink/MediLink.py +207 -108
- MediLink/MediLink_837p_encoder.py +299 -214
- MediLink/MediLink_837p_encoder_library.py +445 -245
- MediLink/MediLink_API_v2.py +174 -0
- MediLink/MediLink_APIs.py +139 -0
- MediLink/MediLink_ConfigLoader.py +44 -32
- MediLink/MediLink_DataMgmt.py +297 -89
- MediLink/MediLink_Decoder.py +63 -0
- MediLink/MediLink_Down.py +73 -102
- MediLink/MediLink_ERA_decoder.py +4 -4
- MediLink/MediLink_Gmail.py +479 -4
- MediLink/MediLink_Mailer.py +0 -0
- MediLink/MediLink_Parser.py +111 -0
- MediLink/MediLink_Scan.py +0 -0
- MediLink/MediLink_Scheduler.py +2 -131
- MediLink/MediLink_StatusCheck.py +0 -4
- MediLink/MediLink_UI.py +87 -27
- MediLink/MediLink_Up.py +301 -45
- MediLink/MediLink_batch.bat +1 -1
- MediLink/test.py +74 -0
- medicafe-0.240613.0.dist-info/METADATA +55 -0
- medicafe-0.240613.0.dist-info/RECORD +43 -0
- {medicafe-0.240419.2.dist-info → medicafe-0.240613.0.dist-info}/WHEEL +5 -5
- medicafe-0.240419.2.dist-info/METADATA +0 -19
- medicafe-0.240419.2.dist-info/RECORD +0 -32
- {medicafe-0.240419.2.dist-info → medicafe-0.240613.0.dist-info}/LICENSE +0 -0
- {medicafe-0.240419.2.dist-info → medicafe-0.240613.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
from collections import OrderedDict, defaultdict
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
import os
|
|
4
|
+
import csv
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
# Add parent directory of the project to the Python path
|
|
8
|
+
project_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
|
9
|
+
sys.path.append(project_dir)
|
|
10
|
+
|
|
11
|
+
try:
|
|
12
|
+
import MediLink_ConfigLoader
|
|
13
|
+
import MediLink_DataMgmt
|
|
14
|
+
except ImportError:
|
|
15
|
+
from MediLink import MediLink_ConfigLoader
|
|
16
|
+
from MediLink import MediLink_DataMgmt
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
from MediBot_UI import app_control
|
|
20
|
+
from MediBot_docx_decoder import parse_docx
|
|
21
|
+
except ImportError:
|
|
22
|
+
from MediBot import MediBot_UI
|
|
23
|
+
app_control = MediBot_UI.app_control
|
|
24
|
+
from MediBot import MediBot_docx_decoder
|
|
25
|
+
parse_docx = MediBot_docx_decoder.parse_docx
|
|
26
|
+
|
|
27
|
+
class InitializationError(Exception):
|
|
28
|
+
def __init__(self, message):
|
|
29
|
+
self.message = message
|
|
30
|
+
super().__init__(self.message)
|
|
31
|
+
|
|
32
|
+
def initialize(config):
|
|
33
|
+
global AHK_EXECUTABLE, CSV_FILE_PATH, field_mapping, page_end_markers
|
|
34
|
+
|
|
35
|
+
try:
|
|
36
|
+
AHK_EXECUTABLE = config.get('AHK_EXECUTABLE', "")
|
|
37
|
+
except AttributeError:
|
|
38
|
+
raise InitializationError("Error: 'AHK_EXECUTABLE' not found in config.")
|
|
39
|
+
|
|
40
|
+
try:
|
|
41
|
+
CSV_FILE_PATH = config.get('CSV_FILE_PATH', "")
|
|
42
|
+
except AttributeError:
|
|
43
|
+
raise InitializationError("Error: 'CSV_FILE_PATH' not found in config.")
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
field_mapping = OrderedDict(config.get('field_mapping', {}))
|
|
47
|
+
except AttributeError:
|
|
48
|
+
raise InitializationError("Error: 'field_mapping' not found in config.")
|
|
49
|
+
|
|
50
|
+
try:
|
|
51
|
+
page_end_markers = config.get('page_end_markers', [])
|
|
52
|
+
except AttributeError:
|
|
53
|
+
raise InitializationError("Error: 'page_end_markers' not found in config.")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def open_csv_for_editing(csv_file_path):
|
|
57
|
+
try:
|
|
58
|
+
# Open the CSV file with its associated application
|
|
59
|
+
os.system('start "" "{}"'.format(csv_file_path))
|
|
60
|
+
print("After saving the revised CSV, please re-run MediBot.")
|
|
61
|
+
except Exception as e:
|
|
62
|
+
print("Failed to open CSV file:", e)
|
|
63
|
+
|
|
64
|
+
# Function to load and process CSV data
|
|
65
|
+
def load_csv_data(csv_file_path):
|
|
66
|
+
try:
|
|
67
|
+
# Check if the file exists
|
|
68
|
+
if not os.path.exists(csv_file_path):
|
|
69
|
+
raise FileNotFoundError("***Error: CSV file '{}' not found.".format(csv_file_path))
|
|
70
|
+
|
|
71
|
+
with open(csv_file_path, 'r') as csvfile:
|
|
72
|
+
reader = csv.DictReader(csvfile)
|
|
73
|
+
return [row for row in reader] # Return a list of dictionaries
|
|
74
|
+
except FileNotFoundError as e:
|
|
75
|
+
print(e) # Print the informative error message
|
|
76
|
+
print("Hint: Check if CSV file is located in the expected directory or specify a different path in config file.")
|
|
77
|
+
print("Please correct the issue and re-run MediBot.")
|
|
78
|
+
sys.exit(1) # Halt the script
|
|
79
|
+
except IOError as e:
|
|
80
|
+
print("Error reading CSV file: {}. Please check the file path and permissions.".format(e))
|
|
81
|
+
sys.exit(1) # Halt the script in case of other IO errors
|
|
82
|
+
|
|
83
|
+
# CSV Pre-processor Helper functions
|
|
84
|
+
def add_columns(csv_data, column_headers):
|
|
85
|
+
"""
|
|
86
|
+
Adds one or multiple columns to the CSV data.
|
|
87
|
+
|
|
88
|
+
Parameters:
|
|
89
|
+
csv_data (list of dict): The CSV data where each row is represented as a dictionary.
|
|
90
|
+
column_headers (list of str or str): A list of column headers to be added to each row, or a single column header.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
None: The function modifies the csv_data in place.
|
|
94
|
+
"""
|
|
95
|
+
if isinstance(column_headers, str):
|
|
96
|
+
column_headers = [column_headers]
|
|
97
|
+
elif not isinstance(column_headers, list):
|
|
98
|
+
raise ValueError("column_headers should be a list or a string")
|
|
99
|
+
|
|
100
|
+
for row in csv_data:
|
|
101
|
+
for header in column_headers:
|
|
102
|
+
row[header] = '' # Initialize the column with empty values
|
|
103
|
+
|
|
104
|
+
# Extracting the list to a variable for future refactoring:
|
|
105
|
+
def filter_rows(csv_data):
|
|
106
|
+
# TODO This should go to the crosswalk.
|
|
107
|
+
excluded_insurance = ['AETNA', 'AETNA MEDICARE', 'HUMANA MED HMO']
|
|
108
|
+
csv_data[:] = [row for row in csv_data if row.get('Patient ID', '').strip()]
|
|
109
|
+
csv_data[:] = [row for row in csv_data if row.get('Primary Insurance', '').strip() not in excluded_insurance]
|
|
110
|
+
|
|
111
|
+
def convert_surgery_date(csv_data):
|
|
112
|
+
for row in csv_data:
|
|
113
|
+
try:
|
|
114
|
+
row['Surgery Date'] = datetime.strptime(row.get('Surgery Date', ''), '%m/%d/%Y')
|
|
115
|
+
except ValueError:
|
|
116
|
+
row['Surgery Date'] = datetime.min # Assign a minimum datetime value for sorting purposes
|
|
117
|
+
|
|
118
|
+
def sort_and_deduplicate(csv_data):
|
|
119
|
+
# TODO we need to figure out a new logic here for doing second-eye charges. I don't know what the flow should be yet.
|
|
120
|
+
csv_data.sort(key=lambda x: (x['Surgery Date'], x.get('Patient Last', '').strip()))
|
|
121
|
+
unique_patients = {}
|
|
122
|
+
for row in csv_data:
|
|
123
|
+
patient_id = row.get('Patient ID')
|
|
124
|
+
if patient_id not in unique_patients or row['Surgery Date'] < unique_patients[patient_id]['Surgery Date']:
|
|
125
|
+
unique_patients[patient_id] = row
|
|
126
|
+
csv_data[:] = list(unique_patients.values())
|
|
127
|
+
# TODO Sorting, now that we're going to have the Surgery Schedules available, should be ordered as the patients show up on the schedule.
|
|
128
|
+
# If we don't have that surgery schedule yet for some reason, we should default to the current ordering strategy.
|
|
129
|
+
csv_data.sort(key=lambda x: (x['Surgery Date'], x.get('Patient Last', '').strip()))
|
|
130
|
+
|
|
131
|
+
def combine_fields(csv_data):
|
|
132
|
+
for row in csv_data:
|
|
133
|
+
row['Surgery Date'] = row['Surgery Date'].strftime('%m/%d/%Y')
|
|
134
|
+
first_name = row.get('Patient First', '').strip()
|
|
135
|
+
middle_name = row.get('Patient Middle', '').strip()
|
|
136
|
+
if len(middle_name) > 1:
|
|
137
|
+
middle_name = middle_name[0] # Take only the first character
|
|
138
|
+
last_name = row.get('Patient Last', '').strip()
|
|
139
|
+
row['Patient Name'] = "{}, {} {}".format(last_name, first_name, middle_name).strip()
|
|
140
|
+
address1 = row.get('Patient Address1', '').strip()
|
|
141
|
+
address2 = row.get('Patient Address2', '').strip()
|
|
142
|
+
row['Patient Street'] = "{} {}".format(address1, address2).strip()
|
|
143
|
+
|
|
144
|
+
def apply_replacements(csv_data, crosswalk):
|
|
145
|
+
replacements = crosswalk.get('csv_replacements', {})
|
|
146
|
+
for row in csv_data:
|
|
147
|
+
for old_value, new_value in replacements.items():
|
|
148
|
+
if row.get('Patient SSN', '') == old_value:
|
|
149
|
+
row['Patient SSN'] = new_value
|
|
150
|
+
elif row.get('Primary Insurance', '') == old_value:
|
|
151
|
+
row['Primary Insurance'] = new_value
|
|
152
|
+
elif row.get('Ins1 Payer ID') == old_value:
|
|
153
|
+
row['Ins1 Payer ID'] = new_value
|
|
154
|
+
|
|
155
|
+
def update_insurance_ids(csv_data, crosswalk):
|
|
156
|
+
for row in csv_data:
|
|
157
|
+
ins1_payer_id = row.get('Ins1 Payer ID', '').strip()
|
|
158
|
+
# MediLink_ConfigLoader.log("Ins1 Payer ID '{}' associated with Patient ID {}.".format(ins1_payer_id, row.get('Patient ID', "None")))
|
|
159
|
+
if ins1_payer_id:
|
|
160
|
+
if ins1_payer_id in crosswalk.get('payer_id', {}):
|
|
161
|
+
medisoft_ids = crosswalk['payer_id'][ins1_payer_id].get('medisoft_id', [])
|
|
162
|
+
if medisoft_ids:
|
|
163
|
+
medisoft_ids = [int(id) for id in medisoft_ids]
|
|
164
|
+
# TODO Try to match OpenPM's Insurance Name to get a better match
|
|
165
|
+
row['Ins1 Insurance ID'] = medisoft_ids[0]
|
|
166
|
+
# MediLink_ConfigLoader.log("Ins1 Insurance ID '{}' used for Payer ID {} in crosswalk.".format(row.get('Ins1 Insurance ID', ''), ins1_payer_id))
|
|
167
|
+
else:
|
|
168
|
+
MediLink_ConfigLoader.log("Ins1 Payer ID '{}' not found in the crosswalk.".format(ins1_payer_id))
|
|
169
|
+
# Create a placeholder entry in the crosswalk, need to consider the medisoft_medicare_id handling later.
|
|
170
|
+
if 'payer_id' not in crosswalk:
|
|
171
|
+
crosswalk['payer_id'] = {}
|
|
172
|
+
crosswalk['payer_id'][ins1_payer_id] = {
|
|
173
|
+
'medisoft_id': [],
|
|
174
|
+
'medisoft_medicare_id': [],
|
|
175
|
+
'endpoint': 'OPTUMEDI' # Default probably should be a flag for the crosswalk update function to deal with. BUG HARDCODE THERE ARE 3 of these defaults
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
def update_procedure_codes(csv_data):
|
|
179
|
+
# Define the Medisoft shorthand to diagnostic codes dictionary
|
|
180
|
+
# TODO The reverse of this will be in the crosswalk. We'll need to reverse it here for lookup.
|
|
181
|
+
medisoft_to_diagnosis = {
|
|
182
|
+
"25811": "H25.811",
|
|
183
|
+
"25812": "H25.812",
|
|
184
|
+
"2512": "H25.12",
|
|
185
|
+
"2511": "H25.11",
|
|
186
|
+
"529XA": "T85.29XA",
|
|
187
|
+
"4301": "H43.01",
|
|
188
|
+
"4302": "H43.02",
|
|
189
|
+
"011X2": "H40.11X2",
|
|
190
|
+
"051X3": "H40.51X3",
|
|
191
|
+
"5398A": "T85.398A"
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
# Define the procedure codes to diagnostic codes dictionary
|
|
195
|
+
procedure_to_diagnosis = {
|
|
196
|
+
"00142": ["H25.811", "H25.812", "H25.12", "H25.11", "T85.29XA"],
|
|
197
|
+
"00145": ["H43.01", "H43.02"],
|
|
198
|
+
"00140": ["H40.11X2", "H40.51X3"]
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
# Reverse the dictionary for easier lookup from diagnostic code to procedure code
|
|
202
|
+
diagnosis_to_procedure = {}
|
|
203
|
+
for procedure_code, diagnosis_codes in procedure_to_diagnosis.items():
|
|
204
|
+
for diagnosis_code in diagnosis_codes:
|
|
205
|
+
diagnosis_to_procedure[diagnosis_code] = procedure_code
|
|
206
|
+
|
|
207
|
+
# Initialize counter for updated rows
|
|
208
|
+
updated_count = 0
|
|
209
|
+
|
|
210
|
+
# Update the "Procedure Code" column in the CSV data
|
|
211
|
+
for row_num, row in enumerate(csv_data, start=1):
|
|
212
|
+
try:
|
|
213
|
+
medisoft_code = row.get('Default Diagnosis #1', '').strip()
|
|
214
|
+
diagnosis_code = medisoft_to_diagnosis.get(medisoft_code)
|
|
215
|
+
if diagnosis_code:
|
|
216
|
+
procedure_code = diagnosis_to_procedure.get(diagnosis_code)
|
|
217
|
+
if procedure_code:
|
|
218
|
+
row['Procedure Code'] = procedure_code
|
|
219
|
+
updated_count += 1
|
|
220
|
+
else:
|
|
221
|
+
row['Procedure Code'] = "Unknown" # Or handle as appropriate
|
|
222
|
+
else:
|
|
223
|
+
row['Procedure Code'] = "Unknown" # Or handle as appropriate
|
|
224
|
+
except Exception as e:
|
|
225
|
+
MediLink_ConfigLoader.log("In update_procedure_codes, Error processing row {}: {}".format(row_num, e), level="ERROR")
|
|
226
|
+
|
|
227
|
+
# Log total count of updated rows
|
|
228
|
+
MediLink_ConfigLoader.log("Total {} 'Procedure Code' rows updated.".format(updated_count), level="INFO")
|
|
229
|
+
|
|
230
|
+
return True
|
|
231
|
+
|
|
232
|
+
def update_diagnosis_codes(csv_data):
|
|
233
|
+
try:
|
|
234
|
+
# Load configuration and crosswalk
|
|
235
|
+
config, _ = MediLink_ConfigLoader.load_configuration()
|
|
236
|
+
|
|
237
|
+
# Extract the local storage path from the configuration
|
|
238
|
+
local_storage_path = config['MediLink_Config']['local_storage_path']
|
|
239
|
+
|
|
240
|
+
# Initialize a dictionary to hold diagnosis codes from all DOCX files
|
|
241
|
+
all_patient_data = {}
|
|
242
|
+
|
|
243
|
+
# Iterate through all files in the specified directory
|
|
244
|
+
for filename in os.listdir(local_storage_path):
|
|
245
|
+
if filename.endswith(".docx"):
|
|
246
|
+
filepath = os.path.join(local_storage_path, filename)
|
|
247
|
+
MediLink_ConfigLoader.log("Processing DOCX file: {}".format(filepath), level="INFO")
|
|
248
|
+
try:
|
|
249
|
+
patient_data = parse_docx(filepath)
|
|
250
|
+
for patient_id, service_dates in patient_data.items():
|
|
251
|
+
if patient_id not in all_patient_data:
|
|
252
|
+
all_patient_data[patient_id] = {}
|
|
253
|
+
for date_of_service, diagnosis_data in service_dates.items():
|
|
254
|
+
all_patient_data[patient_id][date_of_service] = diagnosis_data
|
|
255
|
+
except Exception as e:
|
|
256
|
+
MediLink_ConfigLoader.log("Error parsing DOCX file {}: {}".format(filepath, e), level="ERROR")
|
|
257
|
+
|
|
258
|
+
# Debug logging for all_patient_data
|
|
259
|
+
MediLink_ConfigLoader.log("All patient data collected from DOCX files: {}".format(all_patient_data), level="INFO")
|
|
260
|
+
|
|
261
|
+
# Define the diagnosis to Medisoft shorthand dictionary
|
|
262
|
+
diagnosis_to_medisoft = {
|
|
263
|
+
"H25.811": "25811",
|
|
264
|
+
"H25.812": "25812",
|
|
265
|
+
"H25.12": "2512",
|
|
266
|
+
"H25.11": "2511",
|
|
267
|
+
"T85.29XA": "529XA",
|
|
268
|
+
"H43.01": "4301",
|
|
269
|
+
"H43.02": "4302",
|
|
270
|
+
"H40.11X2": "011X2",
|
|
271
|
+
"H40.51X3": "051X3",
|
|
272
|
+
"T85.398A": "5398A"
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
# Convert surgery dates in CSV data
|
|
276
|
+
convert_surgery_date(csv_data)
|
|
277
|
+
|
|
278
|
+
# Initialize counter for updated rows
|
|
279
|
+
updated_count = 0
|
|
280
|
+
|
|
281
|
+
# Update the "Default Diagnosis #1" column in the CSV data
|
|
282
|
+
for row_num, row in enumerate(csv_data, start=1):
|
|
283
|
+
MediLink_ConfigLoader.log("Processing row number {}.".format(row_num), level="INFO")
|
|
284
|
+
patient_id = row.get('Patient ID', '').strip()
|
|
285
|
+
surgery_date = row.get('Surgery Date', '')
|
|
286
|
+
|
|
287
|
+
# Convert surgery_date to string format for lookup
|
|
288
|
+
if surgery_date != datetime.min:
|
|
289
|
+
surgery_date_str = surgery_date.strftime("%m-%d-%Y")
|
|
290
|
+
else:
|
|
291
|
+
surgery_date_str = ''
|
|
292
|
+
|
|
293
|
+
MediLink_ConfigLoader.log("Patient ID: {}, Surgery Date: {}".format(patient_id, surgery_date_str), level="INFO")
|
|
294
|
+
|
|
295
|
+
if patient_id in all_patient_data:
|
|
296
|
+
if surgery_date_str in all_patient_data[patient_id]:
|
|
297
|
+
diagnosis_code, left_or_right_eye, femto_yes_or_no = all_patient_data[patient_id][surgery_date_str]
|
|
298
|
+
MediLink_ConfigLoader.log("Found diagnosis data for Patient ID: {}, Surgery Date: {}".format(patient_id, surgery_date_str), level="INFO")
|
|
299
|
+
|
|
300
|
+
# Convert diagnosis code to Medisoft shorthand format.
|
|
301
|
+
defaulted_code = diagnosis_code[1:].replace('.', '')[-5:] if diagnosis_code else ''
|
|
302
|
+
medisoft_shorthand = diagnosis_to_medisoft.get(diagnosis_code, defaulted_code)
|
|
303
|
+
MediLink_ConfigLoader.log("Converted diagnosis code to Medisoft shorthand: {}".format(medisoft_shorthand), level="INFO")
|
|
304
|
+
|
|
305
|
+
row['Default Diagnosis #1'] = medisoft_shorthand
|
|
306
|
+
updated_count += 1
|
|
307
|
+
MediLink_ConfigLoader.log("Updated row number {} with new diagnosis code.".format(row_num), level="INFO")
|
|
308
|
+
else:
|
|
309
|
+
MediLink_ConfigLoader.log("No matching surgery date found for Patient ID: {} in row {}.".format(patient_id, row_num), level="INFO")
|
|
310
|
+
else:
|
|
311
|
+
MediLink_ConfigLoader.log("Patient ID: {} not found in DOCX data for row {}.".format(patient_id, row_num), level="INFO")
|
|
312
|
+
|
|
313
|
+
# Log total count of updated rows
|
|
314
|
+
MediLink_ConfigLoader.log("Total {} 'Default Diagnosis #1' rows updated.".format(updated_count), level="INFO")
|
|
315
|
+
|
|
316
|
+
except Exception as e:
|
|
317
|
+
message = "An error occurred while updating diagnosis codes. Please check the DOCX files and configuration: {}".format(e)
|
|
318
|
+
MediLink_ConfigLoader.log(message, level="ERROR")
|
|
319
|
+
print(message)
|
|
320
|
+
|
|
321
|
+
def load_data_sources(config, crosswalk):
|
|
322
|
+
"""Loads historical mappings from MAPAT and Carol's CSVs."""
|
|
323
|
+
patient_id_to_insurance_id = load_insurance_data_from_mapat(config, crosswalk)
|
|
324
|
+
if not patient_id_to_insurance_id:
|
|
325
|
+
raise ValueError("Failed to load historical Patient ID to Insurance ID mappings from MAPAT.")
|
|
326
|
+
|
|
327
|
+
payer_id_to_patient_ids = load_historical_payer_to_patient_mappings(config)
|
|
328
|
+
if not payer_id_to_patient_ids:
|
|
329
|
+
raise ValueError("Failed to load historical Carol's CSVs.")
|
|
330
|
+
|
|
331
|
+
return patient_id_to_insurance_id, payer_id_to_patient_ids
|
|
332
|
+
|
|
333
|
+
def map_payer_ids_to_insurance_ids(patient_id_to_insurance_id, payer_id_to_patient_ids):
|
|
334
|
+
"""Maps Payer IDs to Insurance IDs based on the historical mappings."""
|
|
335
|
+
payer_id_to_details = {}
|
|
336
|
+
for payer_id, patient_ids in payer_id_to_patient_ids.items():
|
|
337
|
+
medisoft_ids = set()
|
|
338
|
+
for patient_id in patient_ids:
|
|
339
|
+
if patient_id in patient_id_to_insurance_id:
|
|
340
|
+
medisoft_id = patient_id_to_insurance_id[patient_id]
|
|
341
|
+
medisoft_ids.add(medisoft_id)
|
|
342
|
+
MediLink_ConfigLoader.log("Added Medisoft ID {} for Patient ID {} and Payer ID {}".format(medisoft_id, patient_id, payer_id))
|
|
343
|
+
else:
|
|
344
|
+
MediLink_ConfigLoader.log("No matching Insurance ID found for Patient ID {}".format(patient_id))
|
|
345
|
+
if medisoft_ids:
|
|
346
|
+
payer_id_to_details[payer_id] = {
|
|
347
|
+
"endpoint": "OPTUMEDI", # TODO Default, to be refined via API poll. There are 2 of these defaults!
|
|
348
|
+
"medisoft_id": list(medisoft_ids),
|
|
349
|
+
"medisoft_medicare_id": [] # Placeholder for future implementation
|
|
350
|
+
}
|
|
351
|
+
return payer_id_to_details
|
|
352
|
+
|
|
353
|
+
def load_insurance_data_from_mains(config):
|
|
354
|
+
"""
|
|
355
|
+
Loads insurance data from MAINS and creates a mapping from insurance names to their respective IDs.
|
|
356
|
+
This mapping is critical for the crosswalk update process to correctly associate payer IDs with insurance IDs.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
config (dict): Configuration object containing necessary paths and parameters.
|
|
360
|
+
|
|
361
|
+
Returns:
|
|
362
|
+
dict: A dictionary mapping insurance names to insurance IDs.
|
|
363
|
+
"""
|
|
364
|
+
# Reset config pull to make sure its not using the MediLink config key subset
|
|
365
|
+
config, crosswalk = MediLink_ConfigLoader.load_configuration()
|
|
366
|
+
|
|
367
|
+
# Retrieve MAINS path and slicing information from the configuration
|
|
368
|
+
# TODO (Low) For secondary insurance, this needs to be pulling from the correct MAINS (there are 2)
|
|
369
|
+
# TODO (Low) Performance: There probably needs to be a dictionary proxy for MAINS that gets updated.
|
|
370
|
+
mains_path = config['MAINS_MED_PATH']
|
|
371
|
+
mains_slices = crosswalk['mains_mapping']['slices']
|
|
372
|
+
|
|
373
|
+
# Initialize the dictionary to hold the insurance to insurance ID mappings
|
|
374
|
+
insurance_to_id = {}
|
|
375
|
+
|
|
376
|
+
# Read data from MAINS using a provided function to handle fixed-width data
|
|
377
|
+
for record, line_number in MediLink_DataMgmt.read_general_fixed_width_data(mains_path, mains_slices):
|
|
378
|
+
insurance_name = record['MAINSNAME']
|
|
379
|
+
# Assuming line_number gives the correct insurance ID without needing adjustment
|
|
380
|
+
insurance_to_id[insurance_name] = line_number
|
|
381
|
+
|
|
382
|
+
return insurance_to_id
|
|
383
|
+
|
|
384
|
+
def load_insurance_data_from_mapat(config, crosswalk):
|
|
385
|
+
"""
|
|
386
|
+
Loads insurance data from MAPAT and creates a mapping from patient ID to insurance ID.
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
config (dict): Configuration object containing necessary paths and parameters.
|
|
390
|
+
crosswalk ... ADD HERE.
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
dict: A dictionary mapping patient IDs to insurance IDs.
|
|
394
|
+
"""
|
|
395
|
+
# Retrieve MAPAT path and slicing information from the configuration
|
|
396
|
+
mapat_path = app_control.get_mapat_med_path()
|
|
397
|
+
mapat_slices = crosswalk['mapat_mapping']['slices']
|
|
398
|
+
|
|
399
|
+
# Initialize the dictionary to hold the patient ID to insurance ID mappings
|
|
400
|
+
patient_id_to_insurance_id = {}
|
|
401
|
+
|
|
402
|
+
# Read data from MAPAT using a provided function to handle fixed-width data
|
|
403
|
+
for record, _ in MediLink_DataMgmt.read_general_fixed_width_data(mapat_path, mapat_slices):
|
|
404
|
+
patient_id = record['MAPATPXID']
|
|
405
|
+
insurance_id = record['MAPATINID']
|
|
406
|
+
patient_id_to_insurance_id[patient_id] = insurance_id
|
|
407
|
+
|
|
408
|
+
return patient_id_to_insurance_id
|
|
409
|
+
|
|
410
|
+
def parse_z_dat(z_dat_path, config):
|
|
411
|
+
"""
|
|
412
|
+
Parses the Z.dat file to map Patient IDs to Insurance Names using the provided fixed-width file format.
|
|
413
|
+
|
|
414
|
+
Args:
|
|
415
|
+
z_dat_path (str): Path to the Z.dat file.
|
|
416
|
+
config (dict): Configuration object containing slicing information and other parameters.
|
|
417
|
+
|
|
418
|
+
Returns:
|
|
419
|
+
dict: A dictionary mapping Patient IDs to Insurance Names.
|
|
420
|
+
"""
|
|
421
|
+
patient_id_to_insurance_name = {}
|
|
422
|
+
|
|
423
|
+
try:
|
|
424
|
+
# Reading blocks of fixed-width data (up to 5 lines per record)
|
|
425
|
+
for personal_info, insurance_info, service_info, service_info_2, service_info_3 in MediLink_DataMgmt.read_fixed_width_data(z_dat_path):
|
|
426
|
+
# Parsing the data using slice definitions from the config
|
|
427
|
+
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))
|
|
428
|
+
|
|
429
|
+
# Extract Patient ID and Insurance Name from parsed data
|
|
430
|
+
patient_id = parsed_data.get('PATID')
|
|
431
|
+
insurance_name = parsed_data.get('INAME')
|
|
432
|
+
|
|
433
|
+
if patient_id and insurance_name:
|
|
434
|
+
patient_id_to_insurance_name[patient_id] = insurance_name
|
|
435
|
+
MediLink_ConfigLoader.log("Mapped Patient ID {} to Insurance Name {}".format(patient_id, insurance_name), config, level="INFO")
|
|
436
|
+
|
|
437
|
+
except FileNotFoundError:
|
|
438
|
+
MediLink_ConfigLoader.log("File not found: {}".format(z_dat_path), config, level="INFO")
|
|
439
|
+
except Exception as e:
|
|
440
|
+
MediLink_ConfigLoader.log("Failed to parse Z.dat: {}".format(str(e)), config, level="INFO")
|
|
441
|
+
|
|
442
|
+
return patient_id_to_insurance_name
|
|
443
|
+
|
|
444
|
+
def load_historical_payer_to_patient_mappings(config):
|
|
445
|
+
"""
|
|
446
|
+
Loads historical mappings from multiple Carol's CSV files in a specified directory,
|
|
447
|
+
mapping Payer IDs to sets of Patient IDs.
|
|
448
|
+
|
|
449
|
+
Args:
|
|
450
|
+
config (dict): Configuration object containing the directory path for Carol's CSV files
|
|
451
|
+
and other necessary parameters.
|
|
452
|
+
|
|
453
|
+
Returns:
|
|
454
|
+
dict: A dictionary where each key is a Payer ID and the value is a set of Patient IDs.
|
|
455
|
+
"""
|
|
456
|
+
directory_path = os.path.dirname(config['CSV_FILE_PATH'])
|
|
457
|
+
payer_to_patient_ids = defaultdict(set)
|
|
458
|
+
|
|
459
|
+
try:
|
|
460
|
+
# Check if the directory exists
|
|
461
|
+
if not os.path.isdir(directory_path):
|
|
462
|
+
raise FileNotFoundError("Directory '{}' not found.".format(directory_path))
|
|
463
|
+
|
|
464
|
+
# Loop through each file in the directory containing Carol's historical CSVs
|
|
465
|
+
for filename in os.listdir(directory_path):
|
|
466
|
+
file_path = os.path.join(directory_path, filename)
|
|
467
|
+
if filename.endswith('.csv'):
|
|
468
|
+
try:
|
|
469
|
+
with open(file_path, 'r', encoding='utf-8') as csvfile:
|
|
470
|
+
reader = csv.DictReader(csvfile)
|
|
471
|
+
patient_count = 0 # Counter for Patient IDs found in this CSV
|
|
472
|
+
for row in reader:
|
|
473
|
+
if 'Patient ID' not in row or 'Ins1 Payer ID' not in row:
|
|
474
|
+
continue # Skip this row if either key is missing
|
|
475
|
+
if not row.get('Patient ID').strip() or not row.get('Ins1 Payer ID').strip():
|
|
476
|
+
continue # Skip this row if either value is missing or empty
|
|
477
|
+
|
|
478
|
+
payer_id = row['Ins1 Payer ID'].strip()
|
|
479
|
+
patient_id = row['Patient ID'].strip()
|
|
480
|
+
payer_to_patient_ids[payer_id].add(patient_id)
|
|
481
|
+
patient_count += 1 # Increment the counter for each valid mapping
|
|
482
|
+
|
|
483
|
+
# Log the accumulated count for this CSV file
|
|
484
|
+
if patient_count > 0:
|
|
485
|
+
MediLink_ConfigLoader.log("CSV file '{}' has {} Patient IDs with Payer IDs.".format(filename, patient_count))
|
|
486
|
+
else:
|
|
487
|
+
MediLink_ConfigLoader.log("CSV file '{}' is empty or does not have valid Patient ID or Payer ID mappings.".format(filename))
|
|
488
|
+
except Exception as e:
|
|
489
|
+
print("Error processing file {}: {}".format(filename, e))
|
|
490
|
+
except FileNotFoundError as e:
|
|
491
|
+
print("Error: {}".format(e))
|
|
492
|
+
|
|
493
|
+
if not payer_to_patient_ids:
|
|
494
|
+
print("No historical mappings were generated.")
|
|
495
|
+
|
|
496
|
+
return dict(payer_to_patient_ids)
|
MediBot/MediBot_UI.py
CHANGED
|
@@ -3,22 +3,61 @@ import ctypes
|
|
|
3
3
|
from ctypes import wintypes
|
|
4
4
|
import time
|
|
5
5
|
import re
|
|
6
|
-
import re #for addresses
|
|
7
|
-
from MediBot_Preprocessor import config
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
7
|
+
# Add parent directory of the project to the Python path
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
project_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
|
11
|
+
sys.path.append(project_dir)
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
from MediLink import MediLink_ConfigLoader
|
|
15
|
+
config, crosswalk = MediLink_ConfigLoader.load_configuration()
|
|
16
|
+
except ImportError:
|
|
17
|
+
from MediLink_ConfigLoader import load_configuration
|
|
18
|
+
config, crosswalk = load_configuration()
|
|
19
|
+
|
|
14
20
|
|
|
15
|
-
"""
|
|
16
21
|
# Function to check if a specific key is pressed
|
|
17
22
|
VK_END = int(config.get('VK_END', ""), 16) # Try F12 (7B). Virtual key code for 'End' (23)
|
|
18
23
|
VK_PAUSE = int(config.get('VK_PAUSE', ""), 16) # Try F11 (7A). Virtual-key code for 'Home' (24)
|
|
19
24
|
|
|
20
|
-
|
|
21
|
-
|
|
25
|
+
class AppControl:
|
|
26
|
+
def __init__(self):
|
|
27
|
+
self.script_paused = False
|
|
28
|
+
self.mapat_med_path = ''
|
|
29
|
+
self.medisoft_shortcut = ''
|
|
30
|
+
# Load initial paths from config when instance is created
|
|
31
|
+
self.load_paths_from_config()
|
|
32
|
+
|
|
33
|
+
def get_pause_status(self):
|
|
34
|
+
return self.script_paused
|
|
35
|
+
|
|
36
|
+
def set_pause_status(self, status):
|
|
37
|
+
self.script_paused = status
|
|
38
|
+
|
|
39
|
+
def get_mapat_med_path(self):
|
|
40
|
+
return self.mapat_med_path
|
|
41
|
+
|
|
42
|
+
def set_mapat_med_path(self, path):
|
|
43
|
+
self.mapat_med_path = path
|
|
44
|
+
|
|
45
|
+
def get_medisoft_shortcut(self):
|
|
46
|
+
return self.medisoft_shortcut
|
|
47
|
+
|
|
48
|
+
def set_medisoft_shortcut(self, path):
|
|
49
|
+
self.medisoft_shortcut = path
|
|
50
|
+
|
|
51
|
+
def load_paths_from_config(self, medicare=False):
|
|
52
|
+
# Assuming `config` is a module or a globally accessible configuration dictionary
|
|
53
|
+
if medicare:
|
|
54
|
+
self.mapat_med_path = config.get('MEDICARE_MAPAT_MED_PATH', "")
|
|
55
|
+
self.medisoft_shortcut = config.get('MEDICARE_SHORTCUT', "")
|
|
56
|
+
else:
|
|
57
|
+
self.mapat_med_path = config.get('MAPAT_MED_PATH', "")
|
|
58
|
+
self.medisoft_shortcut = config.get('PRIVATE_SHORTCUT', "")
|
|
59
|
+
|
|
60
|
+
app_control = AppControl()
|
|
22
61
|
|
|
23
62
|
def is_key_pressed(key_code):
|
|
24
63
|
user32 = ctypes.WinDLL('user32', use_last_error=True)
|
|
@@ -27,19 +66,17 @@ def is_key_pressed(key_code):
|
|
|
27
66
|
return user32.GetAsyncKeyState(key_code) & 0x8000 != 0
|
|
28
67
|
|
|
29
68
|
def manage_script_pause(csv_data, error_message, reverse_mapping):
|
|
30
|
-
global script_paused
|
|
31
|
-
#print("Debug - Entered manage_script_pause with pause status: {}".format(script_paused))
|
|
32
69
|
user_action = 0 # initialize as 'continue'
|
|
33
70
|
|
|
34
|
-
if not
|
|
35
|
-
|
|
71
|
+
if not app_control.get_pause_status() and is_key_pressed(VK_PAUSE):
|
|
72
|
+
app_control.set_pause_status(True)
|
|
36
73
|
print("Script paused. Opening menu...")
|
|
37
74
|
interaction_mode = 'normal' # Assuming normal interaction mode for script pause
|
|
38
75
|
user_action = user_interaction(csv_data, interaction_mode, error_message, reverse_mapping)
|
|
39
76
|
|
|
40
|
-
while
|
|
77
|
+
while app_control.get_pause_status():
|
|
41
78
|
if is_key_pressed(VK_END):
|
|
42
|
-
|
|
79
|
+
app_control.set_pause_status(False)
|
|
43
80
|
print("Continuing...")
|
|
44
81
|
elif is_key_pressed(VK_PAUSE):
|
|
45
82
|
user_action = user_interaction(csv_data, 'normal', error_message, reverse_mapping)
|
|
@@ -80,7 +117,7 @@ def display_patient_selection_menu(csv_data, reverse_mapping, proceed_as_medicar
|
|
|
80
117
|
patient_name = row.get(patient_name_header, "Unknown")
|
|
81
118
|
surgery_date = row.get('Surgery Date', "Unknown Date") # Access 'Surgery Date' as string directly from the row
|
|
82
119
|
|
|
83
|
-
print("{0:
|
|
120
|
+
print("{0:03d}: {3:%m-%d} (ID: {2}) {1} ".format(index+1, patient_name, patient_id, surgery_date))
|
|
84
121
|
|
|
85
122
|
displayed_indices.append(index)
|
|
86
123
|
displayed_patient_ids.append(patient_id)
|
|
@@ -101,10 +138,23 @@ def display_patient_selection_menu(csv_data, reverse_mapping, proceed_as_medicar
|
|
|
101
138
|
display_menu_header("Patient Selection for Today's Data Entry")
|
|
102
139
|
selected_indices, selected_patient_ids = display_patient_list(csv_data, reverse_mapping)
|
|
103
140
|
print("-" * 60)
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
141
|
+
|
|
142
|
+
while True:
|
|
143
|
+
selection = input("\nEnter the number(s) of the patients you wish to proceed with \n(e.g., 1,3,5): ").strip()
|
|
144
|
+
|
|
145
|
+
if not selection:
|
|
146
|
+
print("Invalid entry. Please provide at least one number.")
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
selection = selection.replace('.', ',') # Replace '.' with ',' in the user input just in case
|
|
150
|
+
selected_indices = [int(x.strip()) - 1 for x in selection.split(',') if x.strip().isdigit()]
|
|
151
|
+
|
|
152
|
+
if not selected_indices:
|
|
153
|
+
print("Invalid entry. Please provide at least one integer.")
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
proceed = True
|
|
157
|
+
break
|
|
108
158
|
|
|
109
159
|
patient_id_header = reverse_mapping['Patient ID #2']
|
|
110
160
|
selected_patient_ids = [csv_data[i][patient_id_header] for i in selected_indices if i < len(csv_data)]
|
|
@@ -153,38 +203,33 @@ def handle_user_interaction(interaction_mode, error_message):
|
|
|
153
203
|
print("Invalid choice. Please enter a valid number.")
|
|
154
204
|
|
|
155
205
|
def user_interaction(csv_data, interaction_mode, error_message, reverse_mapping):
|
|
156
|
-
#
|
|
157
|
-
global MAPAT_MED_PATH, MEDISOFT_SHORTCUT # Initialize global constants
|
|
206
|
+
global app_control # Use the instance of AppControl
|
|
158
207
|
selected_patient_ids = []
|
|
159
208
|
selected_indices = []
|
|
160
209
|
|
|
161
210
|
if interaction_mode == 'triage':
|
|
162
|
-
|
|
163
211
|
display_menu_header(" =(^.^)= Welcome to MediBot! =(^.^)=")
|
|
164
|
-
|
|
212
|
+
|
|
165
213
|
while True:
|
|
166
214
|
response = input("\nAm I processing Medicare patients? (yes/no): ").lower().strip()
|
|
167
|
-
if response:
|
|
215
|
+
if response:
|
|
168
216
|
if response in ['yes', 'y']:
|
|
169
|
-
|
|
217
|
+
app_control.load_paths_from_config(medicare=True)
|
|
170
218
|
break
|
|
171
219
|
elif response in ['no', 'n']:
|
|
172
|
-
|
|
220
|
+
app_control.load_paths_from_config(medicare=False)
|
|
173
221
|
break
|
|
174
222
|
else:
|
|
175
223
|
print("Invalid entry. Please enter 'yes' or 'no'.")
|
|
176
224
|
else:
|
|
177
225
|
print("A response is required. Please try again.")
|
|
178
226
|
|
|
179
|
-
MAPAT_MED_PATH = config.get('MEDICARE_MAPAT_MED_PATH', "") if proceed_as_medicare else config.get('MAPAT_MED_PATH', "")
|
|
180
|
-
MEDISOFT_SHORTCUT = config.get('MEDICARE_SHORTCUT', "") if proceed_as_medicare else config.get('PRIVATE_SHORTCUT', "")
|
|
181
|
-
|
|
182
227
|
fixed_values = config.get('fixed_values', {}) # Get fixed values from config json
|
|
183
|
-
if
|
|
228
|
+
if response in ['yes', 'y']:
|
|
184
229
|
medicare_added_fixed_values = config.get('medicare_added_fixed_values', {})
|
|
185
|
-
fixed_values.update(medicare_added_fixed_values)
|
|
230
|
+
fixed_values.update(medicare_added_fixed_values) # Add any medicare-specific fixed values from config
|
|
186
231
|
|
|
187
|
-
proceed, selected_patient_ids, selected_indices = display_patient_selection_menu(csv_data, reverse_mapping,
|
|
232
|
+
proceed, selected_patient_ids, selected_indices = display_patient_selection_menu(csv_data, reverse_mapping, response in ['yes', 'y'])
|
|
188
233
|
return proceed, selected_patient_ids, selected_indices, fixed_values
|
|
189
|
-
|
|
234
|
+
|
|
190
235
|
return handle_user_interaction(interaction_mode, error_message)
|