medicafe 0.240716.2__py3-none-any.whl → 0.240925.9__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.

@@ -1,20 +1,18 @@
1
+ #MediBot_Preprocessor_lib.py
1
2
  from collections import OrderedDict, defaultdict
2
- from datetime import datetime
3
- import os
4
- import csv
5
- import sys
3
+ from datetime import datetime, timedelta
4
+ import os, csv, sys
6
5
 
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)
6
+ # Add the parent directory of the project to the Python path
7
+ sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
10
8
 
9
+ # Attempt to import necessary modules, falling back if they are not found
11
10
  try:
12
11
  import MediLink_ConfigLoader
13
12
  import MediLink_DataMgmt
14
13
  except ImportError:
15
- from MediLink import MediLink_ConfigLoader
16
- from MediLink import MediLink_DataMgmt
17
-
14
+ from MediLink import MediLink_ConfigLoader, MediLink_DataMgmt
15
+
18
16
  try:
19
17
  from MediBot_UI import app_control
20
18
  from MediBot_docx_decoder import parse_docx
@@ -32,26 +30,18 @@ class InitializationError(Exception):
32
30
  def initialize(config):
33
31
  global AHK_EXECUTABLE, CSV_FILE_PATH, field_mapping, page_end_markers
34
32
 
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.")
33
+ required_keys = {
34
+ 'AHK_EXECUTABLE': "",
35
+ 'CSV_FILE_PATH': "",
36
+ 'field_mapping': {},
37
+ 'page_end_markers': []
38
+ }
49
39
 
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
-
40
+ for key, default in required_keys.items():
41
+ try:
42
+ globals()[key] = config.get(key, default) if key != 'field_mapping' else OrderedDict(config.get(key, default))
43
+ except AttributeError:
44
+ raise InitializationError("Error: '{}' not found in config.".format(key))
55
45
 
56
46
  def open_csv_for_editing(csv_file_path):
57
47
  try:
@@ -103,107 +93,237 @@ def add_columns(csv_data, column_headers):
103
93
 
104
94
  # Extracting the list to a variable for future refactoring:
105
95
  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]
96
+ # TODO: This should be handled in the crosswalk.
97
+ excluded_insurance = {'AETNA', 'AETNA MEDICARE', 'HUMANA MED HMO'}
98
+ csv_data[:] = [row for row in csv_data if row.get('Patient ID') and row.get('Primary Insurance') not in excluded_insurance]
110
99
 
111
100
  def convert_surgery_date(csv_data):
112
101
  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
102
+ surgery_date_str = row.get('Surgery Date', '')
103
+ row['Surgery Date'] = (datetime.strptime(surgery_date_str, '%m/%d/%Y')
104
+ if surgery_date_str else datetime.min) # Assign a minimum datetime value if empty
117
105
 
118
106
  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()))
107
+ # Create a dictionary to hold unique patients based on Patient ID
121
108
  unique_patients = {}
109
+
110
+ # Iterate through the CSV data and populate the unique_patients dictionary
122
111
  for row in csv_data:
123
112
  patient_id = row.get('Patient ID')
124
- if patient_id not in unique_patients or row['Surgery Date'] < unique_patients[patient_id]['Surgery Date']:
113
+ if patient_id not in unique_patients:
125
114
  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()))
115
+ else:
116
+ # If the patient ID already exists, compare surgery dates
117
+ existing_row = unique_patients[patient_id]
118
+ if row['Surgery Date'] < existing_row['Surgery Date']:
119
+ unique_patients[patient_id] = row
120
+
121
+ # Convert the unique_patients dictionary back to a list and sort it
122
+ csv_data[:] = sorted(unique_patients.values(), key=lambda x: (x['Surgery Date'], x.get('Patient Last', '').strip())) # TODO Does this need to be sorted twice? once before and once after?
123
+
124
+ # TODO: Consider adding an option in the config to sort based on Surgery Schedules when available.
125
+ # If no schedule is available, the current sorting strategy will be used.
130
126
 
131
127
  def combine_fields(csv_data):
132
128
  for row in csv_data:
133
- row['Surgery Date'] = row['Surgery Date'].strftime('%m/%d/%Y')
129
+ # Safely handle the 'Surgery Date' conversion
130
+ surgery_date = row.get('Surgery Date')
131
+ row['Surgery Date'] = surgery_date.strftime('%m/%d/%Y') if surgery_date else ''
132
+
134
133
  first_name = row.get('Patient First', '').strip()
135
134
  middle_name = row.get('Patient Middle', '').strip()
136
- if len(middle_name) > 1:
137
- middle_name = middle_name[0] # Take only the first character
135
+ middle_name = middle_name[0] if len(middle_name) > 1 else '' # Take only the first character or empty
138
136
  last_name = row.get('Patient Last', '').strip()
139
- row['Patient Name'] = "{}, {} {}".format(last_name, first_name, middle_name).strip()
137
+ row['Patient Name'] = ', '.join(filter(None, [last_name, first_name, middle_name])) # Join non-empty parts
138
+
140
139
  address1 = row.get('Patient Address1', '').strip()
141
140
  address2 = row.get('Patient Address2', '').strip()
142
- row['Patient Street'] = "{} {}".format(address1, address2).strip()
141
+ row['Patient Street'] = ' '.join(filter(None, [address1, address2])) # Join non-empty addresses
143
142
 
144
143
  def apply_replacements(csv_data, crosswalk):
145
144
  replacements = crosswalk.get('csv_replacements', {})
146
145
  for row in csv_data:
147
146
  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):
147
+ for key in ['Patient SSN', 'Primary Insurance', 'Ins1 Payer ID']:
148
+ if row.get(key) == old_value:
149
+ row[key] = new_value
150
+ break # Exit the loop once a replacement is made
151
+
152
+ import difflib
153
+ from collections import defaultdict
154
+
155
+ def find_best_medisoft_id(insurance_name, medisoft_ids, medisoft_to_mains_names):
156
+ """
157
+ Finds the best matching Medisoft ID for a given insurance name using fuzzy matching.
158
+
159
+ Parameters:
160
+ - insurance_name (str): The insurance name from the CSV row.
161
+ - medisoft_ids (list): List of Medisoft IDs associated with the Payer ID.
162
+ - medisoft_to_mains_names (dict): Mapping from Medisoft ID to list of MAINS names.
163
+
164
+ Returns:
165
+ - int or None: The best matching Medisoft ID or None if no match is found.
166
+ """
167
+ best_match_ratio = 0
168
+ best_medisoft_id = None
169
+
170
+ for medisoft_id in medisoft_ids:
171
+ mains_names = medisoft_to_mains_names.get(medisoft_id, [])
172
+ for mains_name in mains_names:
173
+ # Preprocess names by extracting non-numeric characters and converting to uppercase
174
+ processed_mains = ''.join(filter(lambda x: not x.isdigit(), mains_name)).upper()
175
+ processed_insurance = ''.join(filter(lambda x: not x.isdigit(), insurance_name)).upper()
176
+
177
+ # Log the processed names before computing the match ratio
178
+ MediLink_ConfigLoader.log("Processing Medisoft ID '{}': Comparing processed insurance '{}' with processed mains '{}'.".format(medisoft_id, processed_insurance, processed_mains), level="DEBUG")
179
+
180
+ # Compute the similarity ratio
181
+ match_ratio = difflib.SequenceMatcher(None, processed_insurance, processed_mains).ratio()
182
+
183
+ # Log the match ratio
184
+ MediLink_ConfigLoader.log("Match ratio for Medisoft ID '{}': {:.2f}".format(medisoft_id, match_ratio), level="DEBUG")
185
+
186
+ if match_ratio > best_match_ratio:
187
+ best_match_ratio = match_ratio
188
+ best_medisoft_id = medisoft_id
189
+ # Log the current best match
190
+ MediLink_ConfigLoader.log("New best match found: Medisoft ID '{}' with match ratio {:.2f}".format(best_medisoft_id, best_match_ratio), level="DEBUG")
191
+
192
+ # Log the final best match ratio and ID
193
+ MediLink_ConfigLoader.log("Final best match ratio: {:.2f} for Medisoft ID '{}'".format(best_match_ratio, best_medisoft_id), level="DEBUG")
194
+
195
+ # No threshold applied, return the best match found
196
+ return best_medisoft_id
197
+
198
+ def NEW_update_insurance_ids(csv_data, config, crosswalk):
199
+ """
200
+ Updates the 'Ins1 Insurance ID' field in each row of csv_data based on the crosswalk and MAINS data.
201
+
202
+ Parameters:
203
+ - csv_data (list of dict): The CSV data where each row is represented as a dictionary.
204
+ - config (dict): Configuration object containing necessary paths and parameters.
205
+ - crosswalk (dict): Crosswalk data containing mappings between Payer IDs and Medisoft IDs.
206
+
207
+ Returns:
208
+ - None: The function modifies the csv_data in place.
209
+ """
210
+ processed_payer_ids = set() # Track processed Payer IDs
211
+ MediLink_ConfigLoader.log("Starting update of insurance IDs.", level="INFO")
212
+
213
+ # Load MAINS data to get mapping from Medisoft ID to MAINS names
214
+ insurance_to_id = load_insurance_data_from_mains(config) # Assuming it returns a dict mapping insurance names to IDs
215
+ MediLink_ConfigLoader.log("Loaded MAINS data for insurance to ID mapping.", level="DEBUG")
216
+
217
+ # Invert the mapping to get Medisoft ID to MAINS names
218
+ medisoft_to_mains_names = defaultdict(list)
219
+ for insurance_name, medisoft_id in insurance_to_id.items():
220
+ medisoft_to_mains_names[medisoft_id].append(insurance_name)
221
+
156
222
  for row in csv_data:
157
223
  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")))
224
+ MediLink_ConfigLoader.log("Processing row with Ins1 Payer ID: '{}'.".format(ins1_payer_id), level="DEBUG")
225
+
159
226
  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
- }
227
+ # Mark this Payer ID as processed
228
+ if ins1_payer_id not in processed_payer_ids:
229
+ processed_payer_ids.add(ins1_payer_id) # Add to set
230
+ MediLink_ConfigLoader.log("Marked Payer ID '{}' as processed.".format(ins1_payer_id), level="DEBUG")
231
+
232
+ # Retrieve Medisoft IDs for the current Payer ID
233
+ medisoft_ids = crosswalk.get('payer_id', {}).get(ins1_payer_id, {}).get('medisoft_id', [])
234
+ MediLink_ConfigLoader.log("Retrieved Medisoft IDs for Payer ID '{}': {}".format(ins1_payer_id, medisoft_ids), level="DEBUG")
235
+
236
+ if not medisoft_ids:
237
+ MediLink_ConfigLoader.log("No Medisoft IDs available for Payer ID '{}', creating placeholder entry.".format(ins1_payer_id), level="WARNING")
238
+ # Create a placeholder entry in the crosswalk
239
+ if 'payer_id' not in crosswalk:
240
+ crosswalk['payer_id'] = {}
241
+ crosswalk['payer_id'][ins1_payer_id] = {
242
+ 'medisoft_id': [], # Placeholder for future Medisoft IDs
243
+ 'medisoft_medicare_id': [], # Placeholder for future Medicare IDs
244
+ 'endpoint': None # Placeholder for future endpoint
245
+ }
246
+ continue # Skip further processing for this Payer ID
247
+
248
+ # If only one Medisoft ID is associated, assign it directly
249
+ if len(medisoft_ids) == 1:
250
+ try:
251
+ medisoft_id = int(medisoft_ids[0])
252
+ row['Ins1 Insurance ID'] = medisoft_id
253
+ MediLink_ConfigLoader.log("Assigned Medisoft ID '{}' to row number {} with Payer ID '{}'.".format(medisoft_id, csv_data.index(row) + 1, ins1_payer_id), level="DEBUG")
254
+ except ValueError as e:
255
+ MediLink_ConfigLoader.log("Error converting Medisoft ID '{}' to integer for Payer ID '{}': {}".format(medisoft_ids[0], ins1_payer_id, e), level="ERROR")
256
+ row['Ins1 Insurance ID'] = None
257
+ continue # Move to the next row
258
+
259
+ # If multiple Medisoft IDs are associated, perform fuzzy matching
260
+ insurance_name = row.get('Primary Insurance', '').strip()
261
+ if not insurance_name:
262
+ MediLink_ConfigLoader.log("Row with Payer ID '{}' missing 'Primary Insurance', skipping assignment.".format(ins1_payer_id), level="WARNING")
263
+ continue # Skip if insurance name is missing
264
+
265
+ best_medisoft_id = find_best_medisoft_id(insurance_name, medisoft_ids, medisoft_to_mains_names)
266
+
267
+ if best_medisoft_id:
268
+ row['Ins1 Insurance ID'] = best_medisoft_id
269
+ MediLink_ConfigLoader.log("Assigned Medisoft ID '{}' to row with Payer ID '{}' based on fuzzy match.".format(best_medisoft_id, ins1_payer_id), level="INFO")
270
+ else:
271
+ # Default to the first Medisoft ID if no good match is found
272
+ try:
273
+ default_medisoft_id = int(medisoft_ids[0])
274
+ row['Ins1 Insurance ID'] = default_medisoft_id
275
+ MediLink_ConfigLoader.log("No suitable match found. Defaulted to Medisoft ID '{}' for Payer ID '{}'.".format(default_medisoft_id, ins1_payer_id), level="INFO")
276
+ except ValueError as e:
277
+ MediLink_ConfigLoader.log("Error converting default Medisoft ID '{}' to integer for Payer ID '{}': {}".format(medisoft_ids[0], ins1_payer_id, e), level="ERROR")
278
+ row['Ins1 Insurance ID'] = None
279
+
280
+ def update_insurance_ids(csv_data, config, crosswalk):
281
+ MediLink_ConfigLoader.log("Starting update_insurance_ids function.", level="DEBUG")
282
+
283
+ # Create a dictionary to hold Medisoft IDs for each payer ID in the crosswalk
284
+ payer_id_to_medisoft = {}
285
+ MediLink_ConfigLoader.log("Initialized payer_id_to_medisoft dictionary.", level="DEBUG")
286
+
287
+ # Populate the dictionary with data from the crosswalk
288
+ for payer_id, details in crosswalk.get('payer_id', {}).items():
289
+ medisoft_ids = details.get('medisoft_id', [])
290
+ # Filter out empty strings and take the first valid ID
291
+ medisoft_ids = [id for id in medisoft_ids if id]
292
+ payer_id_to_medisoft[payer_id] = int(medisoft_ids[0]) if medisoft_ids else None
293
+ MediLink_ConfigLoader.log("Processed Payer ID '{}': Medisoft IDs found: {}".format(payer_id, medisoft_ids), level="DEBUG")
294
+
295
+ # Process the csv_data
296
+ for row in csv_data:
297
+ ins1_payer_id = row.get('Ins1 Payer ID', '').strip()
298
+ MediLink_ConfigLoader.log("Processing row #{} with Ins1 Payer ID '{}'.".format(csv_data.index(row) + 1, ins1_payer_id), level="DEBUG")
299
+
300
+ if ins1_payer_id not in payer_id_to_medisoft:
301
+ # Add placeholder entry for new payer ID
302
+ payer_id_to_medisoft[ins1_payer_id] = None # No Medisoft ID available
303
+ crosswalk.setdefault('payer_id', {})[ins1_payer_id] = {
304
+ 'medisoft_id': [], # Placeholder for future Medisoft IDs
305
+ 'medisoft_medicare_id': [], # Placeholder for future Medicare IDs
306
+ 'endpoint': None # Placeholder for future endpoint
307
+ }
308
+ MediLink_ConfigLoader.log("Added placeholder entry for new Payer ID '{}'.".format(ins1_payer_id), level="INFO")
309
+
310
+ # Assign the Medisoft ID to the row
311
+ row['Ins1 Insurance ID'] = payer_id_to_medisoft[ins1_payer_id]
312
+ MediLink_ConfigLoader.log("Assigned Medisoft ID '{}' to row with Ins1 Payer ID '{}'.".format(row['Ins1 Insurance ID'], ins1_payer_id), level="DEBUG")
193
313
 
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"]
314
+ def update_procedure_codes(csv_data, crosswalk):
315
+
316
+ # Get Medisoft shorthand dictionary from crosswalk and reverse it
317
+ diagnosis_to_medisoft = crosswalk.get('diagnosis_to_medisoft', {}) # BUG We need to be careful here in case we decide we need to change the crosswalk data specifically with regard to the T8/H usage.
318
+ medisoft_to_diagnosis = {v: k for k, v in diagnosis_to_medisoft.items()}
319
+
320
+ # Get procedure code to diagnosis dictionary from crosswalk and reverse it for easier lookup
321
+ diagnosis_to_procedure = {
322
+ diagnosis_code: procedure_code
323
+ for procedure_code, diagnosis_codes in crosswalk.get('procedure_to_diagnosis', {}).items()
324
+ for diagnosis_code in diagnosis_codes
199
325
  }
200
326
 
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
327
  # Initialize counter for updated rows
208
328
  updated_count = 0
209
329
 
@@ -232,7 +352,7 @@ def update_procedure_codes(csv_data):
232
352
  def update_diagnosis_codes(csv_data):
233
353
  try:
234
354
  # Load configuration and crosswalk
235
- config, _ = MediLink_ConfigLoader.load_configuration()
355
+ config, crosswalk = MediLink_ConfigLoader.load_configuration()
236
356
 
237
357
  # Extract the local storage path from the configuration
238
358
  local_storage_path = config['MediLink_Config']['local_storage_path']
@@ -240,48 +360,80 @@ def update_diagnosis_codes(csv_data):
240
360
  # Initialize a dictionary to hold diagnosis codes from all DOCX files
241
361
  all_patient_data = {}
242
362
 
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")
363
+ # Convert surgery dates in CSV data
364
+ convert_surgery_date(csv_data)
365
+
366
+ # Extract all valid surgery dates from csv_data
367
+ surgery_dates = [row['Surgery Date'] for row in csv_data if row['Surgery Date'] != datetime.min]
368
+
369
+ if not surgery_dates:
370
+ raise ValueError("No valid surgery dates found in csv_data.")
371
+
372
+ # Determine the minimum and maximum surgery dates
373
+ min_surgery_date = min(surgery_dates)
374
+ max_surgery_date = max(surgery_dates)
375
+
376
+ # Apply a ±5-day margin to the surgery dates
377
+ margin = timedelta(days=5)
378
+ threshold_start = min_surgery_date - margin
379
+ threshold_end = max_surgery_date + margin
380
+
381
+ MediLink_ConfigLoader.log("Processing DOCX files modified between {} and {}.".format(threshold_start, threshold_end), level="INFO")
382
+
383
+ # Gather all relevant DOCX files in the specified directory
384
+ docx_files = [
385
+ os.path.join(local_storage_path, filename)
386
+ for filename in os.listdir(local_storage_path)
387
+ if filename.endswith(".docx")
388
+ ]
389
+
390
+ # Filter files based on modification time
391
+ valid_files = [
392
+ filepath for filepath in docx_files
393
+ if threshold_start <= datetime.fromtimestamp(os.path.getmtime(filepath)) <= threshold_end
394
+ ]
395
+
396
+ # Process valid DOCX files
397
+ for filepath in valid_files:
398
+ MediLink_ConfigLoader.log("Processing DOCX file: {}".format(filepath), level="INFO")
399
+ try:
400
+ patient_data = parse_docx(filepath, surgery_dates) # Pass surgery_dates to parse_docx
401
+ for patient_id, service_dates in patient_data.items():
402
+ if patient_id not in all_patient_data:
403
+ all_patient_data[patient_id] = {}
404
+ for date_of_service, diagnosis_data in service_dates.items():
405
+ all_patient_data[patient_id][date_of_service] = diagnosis_data
406
+ except Exception as e:
407
+ MediLink_ConfigLoader.log("Error parsing DOCX file {}: {}".format(filepath, e), level="ERROR")
408
+
409
+ # Log if no valid files were found
410
+ if not valid_files:
411
+ MediLink_ConfigLoader.log("No valid DOCX files found within the modification time threshold.", level="INFO")
257
412
 
258
413
  # 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
- }
414
+ MediLink_ConfigLoader.log("All patient data collected from DOCX files: {}".format(all_patient_data), level="DEBUG")
274
415
 
275
- # Convert surgery dates in CSV data
276
- convert_surgery_date(csv_data)
416
+ # Extract patient IDs from csv_data for efficient matching
417
+ patient_ids_in_csv = {row.get('Patient ID', '').strip() for row in csv_data}
418
+
419
+ # Check if any patient data was collected
420
+ if not all_patient_data or not patient_ids_in_csv.intersection(all_patient_data.keys()):
421
+ MediLink_ConfigLoader.log("No patient data collected or no matching Patient IDs found. Skipping further processing.", level="INFO")
422
+ return # Exit the function early if no data is available
423
+
424
+ # Get Medisoft shorthand dictionary from crosswalk.
425
+ diagnosis_to_medisoft = crosswalk.get('diagnosis_to_medisoft', {})
277
426
 
278
427
  # Initialize counter for updated rows
279
428
  updated_count = 0
280
429
 
281
430
  # Update the "Default Diagnosis #1" column in the CSV data
282
431
  for row_num, row in enumerate(csv_data, start=1):
283
- MediLink_ConfigLoader.log("Processing row number {}.".format(row_num), level="INFO")
284
432
  patient_id = row.get('Patient ID', '').strip()
433
+ if patient_id not in patient_ids_in_csv:
434
+ continue # Skip rows that do not match any patient ID
435
+
436
+ MediLink_ConfigLoader.log("Processing row number {}.".format(row_num), level="DEBUG")
285
437
  surgery_date = row.get('Surgery Date', '')
286
438
 
287
439
  # Convert surgery_date to string format for lookup
@@ -290,17 +442,19 @@ def update_diagnosis_codes(csv_data):
290
442
  else:
291
443
  surgery_date_str = ''
292
444
 
293
- MediLink_ConfigLoader.log("Patient ID: {}, Surgery Date: {}".format(patient_id, surgery_date_str), level="INFO")
445
+ MediLink_ConfigLoader.log("Patient ID: {}, Surgery Date: {}".format(patient_id, surgery_date_str), level="DEBUG")
294
446
 
295
447
  if patient_id in all_patient_data:
296
448
  if surgery_date_str in all_patient_data[patient_id]:
297
449
  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")
450
+ MediLink_ConfigLoader.log("Found diagnosis data for Patient ID: {}, Surgery Date: {}".format(patient_id, surgery_date_str), level="DEBUG")
299
451
 
300
452
  # 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")
453
+ medisoft_shorthand = diagnosis_to_medisoft.get(diagnosis_code, None)
454
+ if medisoft_shorthand is None and diagnosis_code:
455
+ defaulted_code = diagnosis_code.lstrip('H').lstrip('T8').replace('.', '')[-5:]
456
+ medisoft_shorthand = defaulted_code
457
+ MediLink_ConfigLoader.log("Converted diagnosis code to Medisoft shorthand: {}".format(medisoft_shorthand), level="DEBUG")
304
458
 
305
459
  row['Default Diagnosis #1'] = medisoft_shorthand
306
460
  updated_count += 1
@@ -367,6 +521,9 @@ def load_insurance_data_from_mains(config):
367
521
  # Retrieve MAINS path and slicing information from the configuration
368
522
  # TODO (Low) For secondary insurance, this needs to be pulling from the correct MAINS (there are 2)
369
523
  # TODO (Low) Performance: There probably needs to be a dictionary proxy for MAINS that gets updated.
524
+ # Meh, this just has to be part of the new architecture plan where we make Medisoft a downstream
525
+ # recipient from the db.
526
+ # TODO (High) The Medisoft Medicare flag needs to be brought in here.
370
527
  mains_path = config['MAINS_MED_PATH']
371
528
  mains_slices = crosswalk['mains_mapping']['slices']
372
529
 
@@ -407,7 +564,7 @@ def load_insurance_data_from_mapat(config, crosswalk):
407
564
 
408
565
  return patient_id_to_insurance_id
409
566
 
410
- def parse_z_dat(z_dat_path, config):
567
+ def parse_z_dat(z_dat_path, config): # Why is this in MediBot and not MediLink?
411
568
  """
412
569
  Parses the Z.dat file to map Patient IDs to Insurance Names using the provided fixed-width file format.
413
570
 
@@ -482,11 +639,12 @@ def load_historical_payer_to_patient_mappings(config):
482
639
 
483
640
  # Log the accumulated count for this CSV file
484
641
  if patient_count > 0:
485
- MediLink_ConfigLoader.log("CSV file '{}' has {} Patient IDs with Payer IDs.".format(filename, patient_count))
642
+ MediLink_ConfigLoader.log("CSV file '{}' has {} Patient IDs with Payer IDs.".format(filename, patient_count), level="DEBUG")
486
643
  else:
487
- MediLink_ConfigLoader.log("CSV file '{}' is empty or does not have valid Patient ID or Payer ID mappings.".format(filename))
644
+ MediLink_ConfigLoader.log("CSV file '{}' is empty or does not have valid Patient ID or Payer ID mappings.".format(filename), level="DEBUG")
488
645
  except Exception as e:
489
646
  print("Error processing file {}: {}".format(filename, e))
647
+ MediLink_ConfigLoader.log("Error processing file '{}': {}".format(filename, e), level="ERROR")
490
648
  except FileNotFoundError as e:
491
649
  print("Error: {}".format(e))
492
650
 
MediBot/MediBot_UI.py CHANGED
@@ -1,22 +1,18 @@
1
- from sys import exit
2
- import ctypes
1
+ #MediBot_UI.py
2
+ import ctypes, time, re, os, sys
3
3
  from ctypes import wintypes
4
- import time
5
- import re
6
-
7
- # Add parent directory of the project to the Python path
8
- import os
9
- import sys
4
+ from sys import exit
10
5
  project_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
11
- sys.path.append(project_dir)
6
+ if project_dir not in sys.path:
7
+ sys.path.append(project_dir)
12
8
 
13
9
  try:
14
10
  from MediLink import MediLink_ConfigLoader
15
- config, crosswalk = MediLink_ConfigLoader.load_configuration()
16
11
  except ImportError:
17
- from MediLink_ConfigLoader import load_configuration
18
- config, crosswalk = load_configuration()
19
-
12
+ import MediLink_ConfigLoader
13
+
14
+ # Load configuration
15
+ config, crosswalk = MediLink_ConfigLoader.load_configuration()
20
16
 
21
17
  # Function to check if a specific key is pressed
22
18
  VK_END = int(config.get('VK_END', ""), 16) # Try F12 (7B). Virtual key code for 'End' (23)
@@ -50,6 +46,7 @@ class AppControl:
50
46
 
51
47
  def load_paths_from_config(self, medicare=False):
52
48
  # Assuming `config` is a module or a globally accessible configuration dictionary
49
+ # TODO Is this where the MAINS paths should also be set?
53
50
  if medicare:
54
51
  self.mapat_med_path = config.get('MEDICARE_MAPAT_MED_PATH', "")
55
52
  self.medisoft_shortcut = config.get('MEDICARE_SHORTCUT', "")
@@ -211,18 +208,22 @@ def user_interaction(csv_data, interaction_mode, error_message, reverse_mapping)
211
208
  display_menu_header(" =(^.^)= Welcome to MediBot! =(^.^)=")
212
209
 
213
210
  while True:
214
- response = input("\nAm I processing Medicare patients? (yes/no): ").lower().strip()
215
- if response:
216
- if response in ['yes', 'y']:
217
- app_control.load_paths_from_config(medicare=True)
218
- break
219
- elif response in ['no', 'n']:
220
- app_control.load_paths_from_config(medicare=False)
221
- break
211
+ try:
212
+ response = input("\nAm I processing Medicare patients? (yes/no): ").lower().strip()
213
+ if response:
214
+ if response in ['yes', 'y']:
215
+ app_control.load_paths_from_config(medicare=True)
216
+ break
217
+ elif response in ['no', 'n']:
218
+ app_control.load_paths_from_config(medicare=False)
219
+ break
220
+ else:
221
+ print("Invalid entry. Please enter 'yes' or 'no'.")
222
222
  else:
223
- print("Invalid entry. Please enter 'yes' or 'no'.")
224
- else:
225
- print("A response is required. Please try again.")
223
+ print("A response is required. Please try again.")
224
+ except KeyboardInterrupt:
225
+ print("\nOperation cancelled by user. Exiting script.")
226
+ exit()
226
227
 
227
228
  fixed_values = config.get('fixed_values', {}) # Get fixed values from config json
228
229
  if response in ['yes', 'y']: