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.

Files changed (38) hide show
  1. MediBot/MediBot.bat +174 -38
  2. MediBot/MediBot.py +80 -77
  3. MediBot/MediBot_Charges.py +0 -28
  4. MediBot/MediBot_Crosswalk_Library.py +281 -0
  5. MediBot/MediBot_Post.py +0 -0
  6. MediBot/MediBot_Preprocessor.py +138 -211
  7. MediBot/MediBot_Preprocessor_lib.py +496 -0
  8. MediBot/MediBot_UI.py +80 -35
  9. MediBot/MediBot_dataformat_library.py +79 -35
  10. MediBot/MediBot_docx_decoder.py +295 -0
  11. MediBot/update_medicafe.py +46 -8
  12. MediLink/MediLink.py +207 -108
  13. MediLink/MediLink_837p_encoder.py +299 -214
  14. MediLink/MediLink_837p_encoder_library.py +445 -245
  15. MediLink/MediLink_API_v2.py +174 -0
  16. MediLink/MediLink_APIs.py +139 -0
  17. MediLink/MediLink_ConfigLoader.py +44 -32
  18. MediLink/MediLink_DataMgmt.py +297 -89
  19. MediLink/MediLink_Decoder.py +63 -0
  20. MediLink/MediLink_Down.py +73 -102
  21. MediLink/MediLink_ERA_decoder.py +4 -4
  22. MediLink/MediLink_Gmail.py +479 -4
  23. MediLink/MediLink_Mailer.py +0 -0
  24. MediLink/MediLink_Parser.py +111 -0
  25. MediLink/MediLink_Scan.py +0 -0
  26. MediLink/MediLink_Scheduler.py +2 -131
  27. MediLink/MediLink_StatusCheck.py +0 -4
  28. MediLink/MediLink_UI.py +87 -27
  29. MediLink/MediLink_Up.py +301 -45
  30. MediLink/MediLink_batch.bat +1 -1
  31. MediLink/test.py +74 -0
  32. medicafe-0.240613.0.dist-info/METADATA +55 -0
  33. medicafe-0.240613.0.dist-info/RECORD +43 -0
  34. {medicafe-0.240419.2.dist-info → medicafe-0.240613.0.dist-info}/WHEEL +5 -5
  35. medicafe-0.240419.2.dist-info/METADATA +0 -19
  36. medicafe-0.240419.2.dist-info/RECORD +0 -32
  37. {medicafe-0.240419.2.dist-info → medicafe-0.240613.0.dist-info}/LICENSE +0 -0
  38. {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
- User Interaction Refinements
11
- - [ ] Refine the menu options for clearer user guidance during script pauses and errors.
12
- - [ ] Add functionality for user to easily repeat or skip specific entries without script restart.
13
- Develop more intuitive skip and retry mechanisms that are responsive to user input during data entry sessions.
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
- MAPAT_MED_PATH = '' # Initialize global constant for MAPAT path
21
- MEDISOFT_SHORTCUT = '' # Initialize global constant for LNK path
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 script_paused and is_key_pressed(VK_PAUSE):
35
- script_paused = True
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 script_paused:
77
+ while app_control.get_pause_status():
41
78
  if is_key_pressed(VK_END):
42
- script_paused = False
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:02d}: {3:.5s} (ID: {2}) {1} ".format(index+1, patient_name, patient_id, surgery_date))
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
- selection = input("\nEnter the number(s) of the patients you wish to proceed with \n(e.g., 1,3,5): ")
105
- selection = selection.replace('.', ',') # Replace '.' with ',' in the user input just in case
106
- selected_indices = [int(x.strip()) - 1 for x in selection.split(',')]
107
- proceed = True
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
- # Consider logging the actions taken during user interaction for audit purposes.
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: # Check if the response is not empty
215
+ if response:
168
216
  if response in ['yes', 'y']:
169
- proceed_as_medicare = True
217
+ app_control.load_paths_from_config(medicare=True)
170
218
  break
171
219
  elif response in ['no', 'n']:
172
- proceed_as_medicare = False
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 proceed_as_medicare:
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) # Add any medicare-specific fixed values from config
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, proceed_as_medicare)
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)