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.
- MediBot/MediBot.bat +56 -16
- MediBot/MediBot.py +100 -78
- MediBot/MediBot_Crosswalk_Library.py +496 -194
- MediBot/MediBot_Preprocessor.py +22 -14
- MediBot/MediBot_Preprocessor_lib.py +301 -143
- MediBot/MediBot_UI.py +25 -24
- MediBot/MediBot_dataformat_library.py +17 -25
- MediBot/MediBot_docx_decoder.py +267 -110
- MediBot/update_json.py +26 -1
- MediBot/update_medicafe.py +134 -44
- MediLink/MediLink.py +95 -53
- MediLink/MediLink_837p_encoder.py +83 -66
- MediLink/MediLink_837p_encoder_library.py +159 -102
- MediLink/MediLink_API_Generator.py +1 -7
- MediLink/MediLink_API_v3.py +348 -63
- MediLink/MediLink_APIs.py +1 -2
- MediLink/MediLink_ClaimStatus.py +21 -6
- MediLink/MediLink_ConfigLoader.py +9 -9
- MediLink/MediLink_DataMgmt.py +321 -100
- MediLink/MediLink_Decoder.py +249 -87
- MediLink/MediLink_Deductible.py +62 -56
- MediLink/MediLink_Down.py +115 -121
- MediLink/MediLink_Gmail.py +2 -11
- MediLink/MediLink_Parser.py +63 -36
- MediLink/MediLink_UI.py +36 -23
- MediLink/MediLink_Up.py +188 -115
- {medicafe-0.240716.2.dist-info → medicafe-0.240925.9.dist-info}/METADATA +1 -1
- medicafe-0.240925.9.dist-info/RECORD +47 -0
- medicafe-0.240716.2.dist-info/RECORD +0 -47
- {medicafe-0.240716.2.dist-info → medicafe-0.240925.9.dist-info}/LICENSE +0 -0
- {medicafe-0.240716.2.dist-info → medicafe-0.240925.9.dist-info}/WHEEL +0 -0
- {medicafe-0.240716.2.dist-info → medicafe-0.240925.9.dist-info}/top_level.txt +0 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
107
|
-
excluded_insurance =
|
|
108
|
-
csv_data[:] = [row for row in csv_data if row.get('Patient ID'
|
|
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
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
#
|
|
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
|
|
113
|
+
if patient_id not in unique_patients:
|
|
125
114
|
unique_patients[patient_id] = row
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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'] =
|
|
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'] =
|
|
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
|
-
|
|
149
|
-
row
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
224
|
+
MediLink_ConfigLoader.log("Processing row with Ins1 Payer ID: '{}'.".format(ins1_payer_id), level="DEBUG")
|
|
225
|
+
|
|
159
226
|
if ins1_payer_id:
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
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
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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,
|
|
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
|
-
#
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
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="
|
|
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
|
-
#
|
|
276
|
-
|
|
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="
|
|
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="
|
|
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
|
-
|
|
302
|
-
medisoft_shorthand
|
|
303
|
-
|
|
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
|
-
|
|
2
|
-
import ctypes
|
|
1
|
+
#MediBot_UI.py
|
|
2
|
+
import ctypes, time, re, os, sys
|
|
3
3
|
from ctypes import wintypes
|
|
4
|
-
import
|
|
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
|
|
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
|
-
|
|
18
|
-
|
|
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
|
-
|
|
215
|
-
|
|
216
|
-
if response
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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("
|
|
224
|
-
|
|
225
|
-
print("
|
|
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']:
|