medicafe 0.240517.0__py3-none-any.whl → 0.240716.2__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 +46 -6
- MediBot/MediBot.py +9 -36
- MediBot/MediBot_Charges.py +0 -28
- MediBot/MediBot_Crosswalk_Library.py +16 -8
- MediBot/MediBot_Post.py +0 -0
- MediBot/MediBot_Preprocessor.py +26 -63
- MediBot/MediBot_Preprocessor_lib.py +182 -43
- MediBot/MediBot_UI.py +2 -7
- MediBot/MediBot_dataformat_library.py +0 -9
- MediBot/MediBot_docx_decoder.py +283 -60
- MediLink/MediLink.py +80 -120
- MediLink/MediLink_837p_encoder.py +3 -28
- MediLink/MediLink_837p_encoder_library.py +19 -53
- MediLink/MediLink_API_Generator.py +246 -0
- MediLink/MediLink_API_v2.py +2 -0
- MediLink/MediLink_API_v3.py +325 -0
- MediLink/MediLink_APIs.py +2 -0
- MediLink/MediLink_ClaimStatus.py +144 -0
- MediLink/MediLink_ConfigLoader.py +13 -7
- MediLink/MediLink_DataMgmt.py +224 -68
- MediLink/MediLink_Decoder.py +165 -0
- MediLink/MediLink_Deductible.py +203 -0
- MediLink/MediLink_Down.py +122 -96
- MediLink/MediLink_Gmail.py +453 -74
- MediLink/MediLink_Mailer.py +0 -7
- MediLink/MediLink_Parser.py +193 -0
- MediLink/MediLink_Scan.py +0 -0
- MediLink/MediLink_Scheduler.py +2 -172
- MediLink/MediLink_StatusCheck.py +0 -4
- MediLink/MediLink_UI.py +54 -18
- MediLink/MediLink_Up.py +6 -15
- {medicafe-0.240517.0.dist-info → medicafe-0.240716.2.dist-info}/METADATA +4 -1
- medicafe-0.240716.2.dist-info/RECORD +47 -0
- {medicafe-0.240517.0.dist-info → medicafe-0.240716.2.dist-info}/WHEEL +1 -1
- medicafe-0.240517.0.dist-info/RECORD +0 -39
- {medicafe-0.240517.0.dist-info → medicafe-0.240716.2.dist-info}/LICENSE +0 -0
- {medicafe-0.240517.0.dist-info → medicafe-0.240716.2.dist-info}/top_level.txt +0 -0
MediBot/MediBot_docx_decoder.py
CHANGED
|
@@ -1,80 +1,303 @@
|
|
|
1
|
-
"""
|
|
2
|
-
Using docx-utils 0.1.3,
|
|
3
|
-
|
|
4
|
-
This script parses a .docx file containing a table of patient information and extracts
|
|
5
|
-
relevant data into a dictionary. Each row in the table corresponds to a new patient,
|
|
6
|
-
and the data from each cell is parsed into specific variables. The resulting dictionary
|
|
7
|
-
uses the 'Patient ID Number' as keys and lists containing 'Diagnosis Code',
|
|
8
|
-
'Left or Right Eye', and 'Femto yes or no' as values.
|
|
9
|
-
|
|
10
|
-
Functions:
|
|
11
|
-
parse_docx(filepath): Reads the .docx file and constructs the patient data dictionary.
|
|
12
|
-
parse_patient_id(text): Extracts the Patient ID Number from the text.
|
|
13
|
-
parse_diagnosis_code(text): Extracts the Diagnosis Code from the text.
|
|
14
|
-
parse_left_or_right_eye(text): Extracts the eye information (Left or Right) from the text.
|
|
15
|
-
parse_femto_yes_or_no(text): Extracts the Femto information (yes or no) from the text.
|
|
16
|
-
"""
|
|
17
|
-
|
|
18
1
|
from docx import Document
|
|
2
|
+
import re
|
|
3
|
+
from lxml import etree
|
|
4
|
+
import zipfile
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
import os
|
|
7
|
+
import sys
|
|
8
|
+
from collections import OrderedDict
|
|
9
|
+
|
|
10
|
+
# Add parent directory of the project to the Python path
|
|
11
|
+
project_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
|
12
|
+
sys.path.append(project_dir)
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
import MediLink_ConfigLoader
|
|
16
|
+
except ImportError:
|
|
17
|
+
from MediLink import MediLink_ConfigLoader
|
|
19
18
|
|
|
20
19
|
def parse_docx(filepath):
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
try:
|
|
21
|
+
doc = Document(filepath) # Open the .docx file
|
|
22
|
+
except Exception as e:
|
|
23
|
+
MediLink_ConfigLoader.log("Error opening document: {}".format(e)) # Log error
|
|
24
|
+
return {}
|
|
25
|
+
|
|
26
|
+
patient_data = OrderedDict() # Initialize OrderedDict to store data
|
|
27
|
+
MediLink_ConfigLoader.log("Extracting Date of Service from {}".format(filepath), level="DEBUG")
|
|
28
|
+
|
|
29
|
+
date_of_service = extract_date_of_service(filepath) # Extract date of service
|
|
30
|
+
MediLink_ConfigLoader.log("Date of Service recorded as: {}".format(date_of_service), level="DEBUG")
|
|
31
|
+
|
|
32
|
+
for table in doc.tables: # Iterate over tables in the document
|
|
33
|
+
for row in table.rows:
|
|
34
|
+
cells = [cell.text.strip() for cell in row.cells]
|
|
35
|
+
if len(cells) > 4 and cells[3].startswith('#'):
|
|
36
|
+
try:
|
|
37
|
+
patient_id = parse_patient_id(cells[3])
|
|
38
|
+
diagnosis_code = parse_diagnosis_code(cells[4])
|
|
39
|
+
left_or_right_eye = parse_left_or_right_eye(cells[4])
|
|
40
|
+
femto_yes_or_no = parse_femto_yes_or_no(cells[4])
|
|
41
|
+
|
|
42
|
+
if patient_id not in patient_data:
|
|
43
|
+
patient_data[patient_id] = {}
|
|
44
|
+
|
|
45
|
+
if date_of_service in patient_data[patient_id]:
|
|
46
|
+
MediLink_ConfigLoader.log("Duplicate entry for patient ID {} on date {}. Skipping.".format(patient_id, date_of_service))
|
|
47
|
+
else:
|
|
48
|
+
patient_data[patient_id][date_of_service] = [diagnosis_code, left_or_right_eye, femto_yes_or_no]
|
|
49
|
+
except Exception as e:
|
|
50
|
+
MediLink_ConfigLoader.log("Error processing row: {}. Error: {}".format(cells, e))
|
|
51
|
+
|
|
52
|
+
# Validation steps
|
|
53
|
+
validate_unknown_entries(patient_data)
|
|
54
|
+
validate_diagnostic_code(patient_data)
|
|
23
55
|
|
|
24
|
-
|
|
25
|
-
|
|
56
|
+
return patient_data
|
|
57
|
+
|
|
58
|
+
def validate_unknown_entries(patient_data):
|
|
59
|
+
for patient_id, dates in list(patient_data.items()):
|
|
60
|
+
for date, details in list(dates.items()):
|
|
61
|
+
if 'Unknown' in details:
|
|
62
|
+
warning_message = "Warning: 'Unknown' entry found. Patient ID: {}, Date: {}, Details: {}".format(patient_id, date, details)
|
|
63
|
+
MediLink_ConfigLoader.log(warning_message, level="WARNING")
|
|
64
|
+
print(warning_message)
|
|
65
|
+
del patient_data[patient_id][date]
|
|
66
|
+
if not patient_data[patient_id]: # If no dates left for the patient, remove the patient
|
|
67
|
+
del patient_data[patient_id]
|
|
68
|
+
|
|
69
|
+
def validate_diagnostic_code(patient_data):
|
|
70
|
+
for patient_id, dates in patient_data.items():
|
|
71
|
+
for date, details in dates.items():
|
|
72
|
+
diagnostic_code, eye, _ = details
|
|
73
|
+
if diagnostic_code[-1].isdigit():
|
|
74
|
+
if eye == 'Left' and not diagnostic_code.endswith('2'):
|
|
75
|
+
log_and_warn(patient_id, date, diagnostic_code, eye)
|
|
76
|
+
elif eye == 'Right' and not diagnostic_code.endswith('1'):
|
|
77
|
+
log_and_warn(patient_id, date, diagnostic_code, eye)
|
|
78
|
+
|
|
79
|
+
def log_and_warn(patient_id, date, diagnostic_code, eye):
|
|
80
|
+
warning_message = (
|
|
81
|
+
"Warning: Mismatch found for Patient ID: {}, Date: {}, "
|
|
82
|
+
"Diagnostic Code: {}, Eye: {}".format(patient_id, date, diagnostic_code, eye)
|
|
83
|
+
)
|
|
84
|
+
MediLink_ConfigLoader.log(warning_message, level="WARNING")
|
|
85
|
+
print(warning_message)
|
|
86
|
+
|
|
87
|
+
# Extract and parse the date of service from the .docx file
|
|
88
|
+
def extract_date_of_service(docx_path):
|
|
89
|
+
extract_to = "extracted_docx"
|
|
90
|
+
try:
|
|
91
|
+
if not os.path.exists(extract_to):
|
|
92
|
+
os.makedirs(extract_to)
|
|
93
|
+
with zipfile.ZipFile(docx_path, 'r') as docx:
|
|
94
|
+
docx.extractall(extract_to)
|
|
95
|
+
MediLink_ConfigLoader.log("Extracted DOCX to: {}".format(extract_to), level="DEBUG")
|
|
96
|
+
|
|
97
|
+
file_path = find_text_in_xml(extract_to, "Surgery Schedule")
|
|
98
|
+
if file_path:
|
|
99
|
+
return extract_date_from_file(file_path)
|
|
100
|
+
else:
|
|
101
|
+
MediLink_ConfigLoader.log("Target text 'Surgery Schedule' not found in any XML files.", level="WARNING")
|
|
102
|
+
return None
|
|
103
|
+
finally:
|
|
104
|
+
# Clean up the extracted files
|
|
105
|
+
remove_directory(extract_to)
|
|
106
|
+
MediLink_ConfigLoader.log("Cleaned up extracted files in: {}".format(extract_to), level="DEBUG")
|
|
107
|
+
|
|
108
|
+
def remove_directory(path):
|
|
109
|
+
if os.path.exists(path):
|
|
110
|
+
for root, dirs, files in os.walk(path, topdown=False):
|
|
111
|
+
for name in files:
|
|
112
|
+
os.remove(os.path.join(root, name))
|
|
113
|
+
for name in dirs:
|
|
114
|
+
os.rmdir(os.path.join(root, name))
|
|
115
|
+
os.rmdir(path)
|
|
116
|
+
|
|
117
|
+
# Find the target text in the extracted XML files
|
|
118
|
+
def find_text_in_xml(directory, target_text):
|
|
119
|
+
for root_dir, dirs, files in os.walk(directory):
|
|
120
|
+
for file in files:
|
|
121
|
+
if file.endswith('.xml'):
|
|
122
|
+
file_path = os.path.join(root_dir, file)
|
|
123
|
+
try:
|
|
124
|
+
tree = etree.parse(file_path)
|
|
125
|
+
root = tree.getroot()
|
|
126
|
+
namespaces = {'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'} # hardcoded for XP handling BUG
|
|
127
|
+
for elem in root.xpath('//w:t', namespaces=namespaces):
|
|
128
|
+
if elem.text and target_text in elem.text:
|
|
129
|
+
MediLink_ConfigLoader.log("Found target text in file: {}".format(file_path), level="DEBUG")
|
|
130
|
+
return file_path
|
|
131
|
+
except Exception as e:
|
|
132
|
+
MediLink_ConfigLoader.log("Error parsing XML file {}: {}".format(file_path, e))
|
|
133
|
+
print("Error parsing XML file {}: {}".format(file_path, e))
|
|
134
|
+
return None
|
|
135
|
+
|
|
136
|
+
# Normalize month and day abbreviations
|
|
137
|
+
def normalize_text(text):
|
|
138
|
+
month_map = {
|
|
139
|
+
'JAN': 'JANUARY', 'FEB': 'FEBRUARY', 'MAR': 'MARCH', 'APR': 'APRIL',
|
|
140
|
+
'MAY': 'MAY', 'JUN': 'JUNE', 'JUL': 'JULY', 'AUG': 'AUGUST',
|
|
141
|
+
'SEP': 'SEPTEMBER', 'OCT': 'OCTOBER', 'NOV': 'NOVEMBER', 'DEC': 'DECEMBER'
|
|
142
|
+
}
|
|
143
|
+
day_map = {
|
|
144
|
+
'MON': 'MONDAY', 'TUE': 'TUESDAY', 'WED': 'WEDNESDAY', 'THU': 'THURSDAY',
|
|
145
|
+
'FRI': 'FRIDAY', 'SAT': 'SATURDAY', 'SUN': 'SUNDAY'
|
|
146
|
+
}
|
|
26
147
|
|
|
27
|
-
|
|
28
|
-
|
|
148
|
+
for abbr, full in month_map.items():
|
|
149
|
+
text = re.sub(r'\b' + abbr + r'\b', full, text, flags=re.IGNORECASE)
|
|
150
|
+
for abbr, full in day_map.items():
|
|
151
|
+
text = re.sub(r'\b' + abbr + r'\b', full, text, flags=re.IGNORECASE)
|
|
29
152
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
153
|
+
return text
|
|
154
|
+
|
|
155
|
+
def reassemble_year(text):
|
|
156
|
+
# First, handle the most common case where a 4-digit year is split as (3,1), (1,3), or (2,2)
|
|
157
|
+
text = re.sub(r'(\d{3}) (\d{1})', r'\1\2', text)
|
|
158
|
+
text = re.sub(r'(\d{1}) (\d{3})', r'\1\2', text)
|
|
159
|
+
text = re.sub(r'(\d{2}) (\d{2})', r'\1\2', text)
|
|
160
|
+
|
|
161
|
+
# Handle the less common cases where the year might be split as (1,1,2) or (2,1,1) or (1,2,1)
|
|
162
|
+
parts = re.findall(r'\b(\d{1,2})\b', text)
|
|
163
|
+
if len(parts) >= 4:
|
|
164
|
+
for i in range(len(parts) - 3):
|
|
165
|
+
candidate = ''.join(parts[i:i + 4])
|
|
166
|
+
if len(candidate) == 4 and candidate.isdigit():
|
|
167
|
+
combined_year = candidate
|
|
168
|
+
text = re.sub(r'\b' + r'\b \b'.join(parts[i:i + 4]) + r'\b', combined_year, text)
|
|
169
|
+
break
|
|
170
|
+
|
|
171
|
+
return text
|
|
172
|
+
|
|
173
|
+
# Extract and parse the date from the file
|
|
174
|
+
def extract_date_from_file(file_path):
|
|
175
|
+
try:
|
|
176
|
+
tree = etree.parse(file_path)
|
|
177
|
+
root = tree.getroot()
|
|
178
|
+
collected_text = []
|
|
179
|
+
|
|
180
|
+
namespaces = {'w': 'http://schemas.openxmlformats.org/wordprocessingml/2006/main'} # hardcoded for XP handling BUG
|
|
181
|
+
for elem in root.xpath('//w:t', namespaces=namespaces):
|
|
182
|
+
if elem.text:
|
|
183
|
+
collected_text.append(elem.text.strip())
|
|
33
184
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
185
|
+
for elem in root.iter():
|
|
186
|
+
if elem.tag.endswith('t') and elem.text:
|
|
187
|
+
collected_text.append(elem.text.strip())
|
|
188
|
+
|
|
189
|
+
combined_text = ' '.join(collected_text)
|
|
190
|
+
combined_text = reassemble_year(combined_text) # Fix OCR splitting years
|
|
191
|
+
# combined_text = re.sub(r'(\d{3}) (\d{1})', r'\1\2', combined_text) # initial year regex.
|
|
192
|
+
combined_text = normalize_text(combined_text) # Normalize abbreviations
|
|
193
|
+
combined_text = re.sub(r',', '', combined_text) # Remove commas if they exist
|
|
194
|
+
|
|
195
|
+
# Log the combined text
|
|
196
|
+
MediLink_ConfigLoader.log("Combined text: {}".format(combined_text), level="DEBUG")
|
|
197
|
+
# print("DEBUG: Combined text: {}".format(combined_text))
|
|
198
|
+
|
|
199
|
+
day_week_pattern = r"(MONDAY|TUESDAY|WEDNESDAY|THURSDAY|FRIDAY|SATURDAY|SUNDAY)"
|
|
200
|
+
month_day_pattern = r"(JANUARY|FEBRUARY|MARCH|APRIL|MAY|JUNE|JULY|AUGUST|SEPTEMBER|OCTOBER|NOVEMBER|DECEMBER) \d{1,2}"
|
|
201
|
+
year_pattern = r"\d{4}"
|
|
202
|
+
|
|
203
|
+
day_of_week = re.search(day_week_pattern, combined_text, re.IGNORECASE)
|
|
204
|
+
month_day = re.search(month_day_pattern, combined_text, re.IGNORECASE)
|
|
205
|
+
year_match = re.search(year_pattern, combined_text, re.IGNORECASE)
|
|
39
206
|
|
|
40
|
-
#
|
|
41
|
-
|
|
207
|
+
# Log the results of the regex searches
|
|
208
|
+
MediLink_ConfigLoader.log("Day of week found: {}".format(day_of_week.group() if day_of_week else 'None'), level="DEBUG")
|
|
209
|
+
MediLink_ConfigLoader.log("Month and day found: {}".format(month_day.group() if month_day else 'None'), level="DEBUG")
|
|
210
|
+
MediLink_ConfigLoader.log("Year found: {}".format(year_match.group() if year_match else 'None'), level="DEBUG")
|
|
211
|
+
|
|
212
|
+
if day_of_week and month_day and year_match:
|
|
213
|
+
date_str = "{} {} {}".format(day_of_week.group(), month_day.group(), year_match.group())
|
|
214
|
+
try:
|
|
215
|
+
date_obj = datetime.strptime(date_str, '%A %B %d %Y')
|
|
216
|
+
return date_obj.strftime('%m-%d-%Y')
|
|
217
|
+
except ValueError as e:
|
|
218
|
+
MediLink_ConfigLoader.log("Error converting date: {}. Error: {}".format(date_str, e), level="ERROR")
|
|
219
|
+
else:
|
|
220
|
+
MediLink_ConfigLoader.log("Date components not found or incomplete in the text. Combined text: {}, Day of week: {}, Month and day: {}, Year: {}"
|
|
221
|
+
.format(combined_text,
|
|
222
|
+
day_of_week.group() if day_of_week else 'None',
|
|
223
|
+
month_day.group() if month_day else 'None',
|
|
224
|
+
year_match.group() if year_match else 'None'),
|
|
225
|
+
level="WARNING")
|
|
226
|
+
except Exception as e:
|
|
227
|
+
MediLink_ConfigLoader.log("Error extracting date from file: {}. Error: {}".format(file_path, e))
|
|
228
|
+
print("Error extracting date from file: {}. Error: {}".format(file_path, e))
|
|
42
229
|
|
|
43
|
-
return
|
|
230
|
+
return None
|
|
44
231
|
|
|
45
232
|
def parse_patient_id(text):
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
233
|
+
try:
|
|
234
|
+
return text.split()[0].lstrip('#') # Extract patient ID number (removing the '#')
|
|
235
|
+
except Exception as e:
|
|
236
|
+
MediLink_ConfigLoader.log("Error parsing patient ID: {}. Error: {}".format(text, e))
|
|
237
|
+
return None
|
|
49
238
|
|
|
50
239
|
def parse_diagnosis_code(text):
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
240
|
+
try:
|
|
241
|
+
# Regular expression to find all ICD-10 codes starting with 'H' and containing a period
|
|
242
|
+
pattern = re.compile(r'H\d{2}\.\d+')
|
|
243
|
+
matches = pattern.findall(text)
|
|
244
|
+
|
|
245
|
+
if matches:
|
|
246
|
+
return matches[0] # Return the first match
|
|
247
|
+
else:
|
|
248
|
+
# Fallback to original method if no match is found
|
|
249
|
+
if '(' in text and ')' in text: # Extract the diagnosis code before the '/'
|
|
250
|
+
full_code = text[text.index('(')+1:text.index(')')]
|
|
251
|
+
return full_code.split('/')[0]
|
|
252
|
+
return text.split('/')[0]
|
|
253
|
+
|
|
254
|
+
except Exception as e:
|
|
255
|
+
MediLink_ConfigLoader.log("Error parsing diagnosis code: {}. Error: {}".format(text, e))
|
|
256
|
+
return "Unknown"
|
|
54
257
|
|
|
55
258
|
def parse_left_or_right_eye(text):
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
259
|
+
try:
|
|
260
|
+
if 'LEFT EYE' in text.upper():
|
|
261
|
+
return 'Left'
|
|
262
|
+
elif 'RIGHT EYE' in text.upper():
|
|
263
|
+
return 'Right'
|
|
264
|
+
else:
|
|
265
|
+
return 'Unknown'
|
|
266
|
+
except Exception as e:
|
|
267
|
+
MediLink_ConfigLoader.log("Error parsing left or right eye: {}. Error: {}".format(text, e))
|
|
63
268
|
return 'Unknown'
|
|
64
269
|
|
|
65
270
|
def parse_femto_yes_or_no(text):
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return
|
|
271
|
+
try:
|
|
272
|
+
if 'FEMTO' in text.upper():
|
|
273
|
+
return True
|
|
274
|
+
else:
|
|
275
|
+
return False
|
|
276
|
+
except Exception as e:
|
|
277
|
+
MediLink_ConfigLoader.log("Error parsing femto yes or no: {}. Error: {}".format(text, e))
|
|
278
|
+
return False
|
|
279
|
+
|
|
280
|
+
def rotate_docx_files(directory):
|
|
281
|
+
# List all files in the directory
|
|
282
|
+
files = os.listdir(directory)
|
|
283
|
+
|
|
284
|
+
# Filter files that contain "DR" and "SS" in the filename
|
|
285
|
+
filtered_files = [file for file in files if "DR" in file and "SS" in file]
|
|
286
|
+
|
|
287
|
+
# Iterate through filtered files
|
|
288
|
+
for filename in filtered_files:
|
|
289
|
+
filepath = os.path.join(directory, filename)
|
|
290
|
+
# Parse each document and print the resulting dictionary
|
|
291
|
+
patient_data_dict = parse_docx(filepath)
|
|
292
|
+
print("Data from file '{}':".format(filename))
|
|
293
|
+
import pprint
|
|
294
|
+
pprint.pprint(patient_data_dict)
|
|
295
|
+
print()
|
|
74
296
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
297
|
+
def main():
|
|
298
|
+
# Call the function with the directory containing your .docx files
|
|
299
|
+
directory = "C:\\Users\\danie\\Downloads\\"
|
|
300
|
+
rotate_docx_files(directory)
|
|
78
301
|
|
|
79
|
-
|
|
80
|
-
|
|
302
|
+
if __name__ == "__main__":
|
|
303
|
+
main()
|
MediLink/MediLink.py
CHANGED
|
@@ -2,7 +2,7 @@ import os
|
|
|
2
2
|
import MediLink_Down
|
|
3
3
|
import MediLink_Up
|
|
4
4
|
import MediLink_ConfigLoader
|
|
5
|
-
import
|
|
5
|
+
import MediLink_DataMgmt
|
|
6
6
|
|
|
7
7
|
# For UI Functions
|
|
8
8
|
import os
|
|
@@ -18,93 +18,54 @@ from MediBot import MediBot_Preprocessor_lib
|
|
|
18
18
|
load_insurance_data_from_mains = MediBot_Preprocessor_lib.load_insurance_data_from_mains
|
|
19
19
|
from MediBot import MediBot_Crosswalk_Library
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
Verify file transmissions via WinSCP log analysis for successful endpoint acknowledgments and secure data transfer.
|
|
27
|
-
Automate response file handling from endpoints and integrate feedback into MediSoft with exception alerts.
|
|
28
|
-
De-persisting Intermediate Files.
|
|
29
|
-
When transmissions fail, there is some retaining of patient data in memory or something that seems to default
|
|
30
|
-
any new endpoint changes to Optum. May need to "de-confirm" patients, but leave the suggested endpoints as the previously
|
|
31
|
-
confirmed endpoints. This should be similar logic to if the user made a mistake and wants to go back and fix it.
|
|
32
|
-
These tasks involve backend enhancements such as dynamic configurations, file detection improvements, file transmission verification, automation of response file handling, and management of intermediate files and transmission failures.
|
|
33
|
-
|
|
34
|
-
TODO (Low) Availity has a response file that says "File was received at TIME. File was sent for processing." as a confirmation
|
|
35
|
-
that sits in the SendFiles folder after a submittal.
|
|
36
|
-
|
|
37
|
-
TODO (Crosswalk) When an endpoint is updated in the UI, the crosswalk should also be updated and saved for that payer ID because that payer ID
|
|
38
|
-
would basically forever need to be going to that endpoint for any patient. the suggested_endpoint should eventually be always correct.
|
|
39
|
-
|
|
40
|
-
BUG Suggested Endpoint when you say 'n' to proceed with transmission is not getting updated with the endpoint
|
|
41
|
-
that was selected previously by the user. However, when we go back to the confirmation list, we do have a persist of the assignment.
|
|
42
|
-
This can be confusing for the user.
|
|
43
|
-
|
|
44
|
-
MediLink
|
|
45
|
-
| - import MediLink_Down
|
|
46
|
-
| - import MediLink_ERA_decoder
|
|
47
|
-
| | - from MediLink_ConfigLoader import load_configuration
|
|
48
|
-
| | | - None
|
|
49
|
-
| | - from MediLink_DataMgmt import consolidate_csvs
|
|
50
|
-
| | | - from MediLink import MediLink_ConfigLoader
|
|
51
|
-
| | - None
|
|
52
|
-
| - from MediLink_DataMgmt import operate_winscp
|
|
53
|
-
| - from MediLink import MediLink_ConfigLoader
|
|
54
|
-
| - None
|
|
55
|
-
| - import MediLink_Up
|
|
56
|
-
| - None
|
|
57
|
-
| - import MediLink_ConfigLoader
|
|
58
|
-
| - None
|
|
59
|
-
| - import MediLink_837p_encoder
|
|
60
|
-
| - import MediLink_ConfigLoader
|
|
61
|
-
| | - None
|
|
62
|
-
| - from MediLink_DataMgmt import parse_fixed_width_data, read_fixed_width_data
|
|
63
|
-
| - from MediLink import MediLink_ConfigLoader
|
|
64
|
-
| - None
|
|
65
|
-
| - import MediLink_837p_encoder_library
|
|
66
|
-
| - from MediLink import MediLink_ConfigLoader
|
|
67
|
-
| - None
|
|
68
|
-
| - import MediLink_UI
|
|
69
|
-
| - None
|
|
70
|
-
"""
|
|
71
|
-
|
|
72
|
-
def detect_and_display_file_summaries(directory_path, config, crosswalk):
|
|
21
|
+
# Retrieve insurance options with codes and descriptions
|
|
22
|
+
config, _ = MediLink_ConfigLoader.load_configuration()
|
|
23
|
+
insurance_options = config['MediLink_Config'].get('insurance_options')
|
|
24
|
+
|
|
25
|
+
def collect_detailed_patient_data(selected_files, config, crosswalk):
|
|
73
26
|
"""
|
|
74
|
-
|
|
75
|
-
including suggestions for endpoints based on insurance provider information found in the config.
|
|
27
|
+
Collects detailed patient data from the selected files.
|
|
76
28
|
|
|
77
|
-
:param
|
|
29
|
+
:param selected_files: List of selected file paths.
|
|
78
30
|
:param config: Configuration settings loaded from a JSON file.
|
|
79
|
-
:
|
|
31
|
+
:param crosswalk: Crosswalk data for mapping purposes.
|
|
32
|
+
:return: A list of detailed patient data.
|
|
80
33
|
"""
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
print(" No new claims detected. Check Medisoft claims output.\n")
|
|
84
|
-
return False, []
|
|
85
|
-
|
|
86
|
-
detailed_patient_data = [] # Initialize list for detailed patient data
|
|
87
|
-
for file_path in new_files:
|
|
34
|
+
detailed_patient_data = []
|
|
35
|
+
for file_path in selected_files:
|
|
88
36
|
detailed_data = extract_and_suggest_endpoint(file_path, config, crosswalk)
|
|
89
37
|
detailed_patient_data.extend(detailed_data) # Accumulate detailed data for processing
|
|
38
|
+
|
|
39
|
+
# Enrich the detailed patient data with insurance type
|
|
40
|
+
detailed_patient_data = enrich_with_insurance_type(detailed_patient_data, insurance_options)
|
|
41
|
+
|
|
42
|
+
# Display summaries and provide an option for bulk edit
|
|
43
|
+
MediLink_UI.display_patient_summaries(detailed_patient_data)
|
|
90
44
|
|
|
91
|
-
|
|
92
|
-
return new_files, detailed_patient_data
|
|
45
|
+
return detailed_patient_data
|
|
93
46
|
|
|
94
|
-
def
|
|
47
|
+
def enrich_with_insurance_type(detailed_patient_data, patient_insurance_type_mapping=None):
|
|
95
48
|
"""
|
|
96
|
-
|
|
49
|
+
Enriches the detailed patient data with insurance type based on patient ID.
|
|
50
|
+
|
|
51
|
+
Parameters:
|
|
52
|
+
- detailed_patient_data: List of dictionaries containing detailed patient data.
|
|
53
|
+
- patient_insurance_mapping: Dictionary mapping patient IDs to their insurance types.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
- Enriched detailed patient data with insurance type added.
|
|
97
57
|
|
|
98
|
-
:
|
|
99
|
-
:param file_extension: Extension of the files to detect. Defaults to '.csv'.
|
|
100
|
-
:return: A list of paths to new files detected in the directory.
|
|
58
|
+
TODO: Implement a function to provide `patient_insurance_mapping` from a reliable source.
|
|
101
59
|
"""
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
60
|
+
if patient_insurance_type_mapping is None:
|
|
61
|
+
MediLink_ConfigLoader.log("No Patient:Insurance-Type mapping available.")
|
|
62
|
+
patient_insurance_type_mapping = {}
|
|
63
|
+
|
|
64
|
+
for data in detailed_patient_data:
|
|
65
|
+
patient_id = data.get('PATID') # I think this is the right name?
|
|
66
|
+
insurance_type = patient_insurance_type_mapping.get(patient_id, '12') # Default to '12' (PPO)
|
|
67
|
+
data['insurance_type'] = insurance_type
|
|
68
|
+
return detailed_patient_data
|
|
108
69
|
|
|
109
70
|
def extract_and_suggest_endpoint(file_path, config, crosswalk):
|
|
110
71
|
"""
|
|
@@ -127,8 +88,8 @@ def extract_and_suggest_endpoint(file_path, config, crosswalk):
|
|
|
127
88
|
insurance_to_id = load_insurance_data_from_mains(config)
|
|
128
89
|
MediLink_ConfigLoader.log("Insurance data loaded from MAINS. {} insurance providers found.".format(len(insurance_to_id)))
|
|
129
90
|
|
|
130
|
-
for personal_info, insurance_info, service_info in
|
|
131
|
-
parsed_data =
|
|
91
|
+
for personal_info, insurance_info, service_info, service_info_2, service_info_3 in MediLink_DataMgmt.read_fixed_width_data(file_path):
|
|
92
|
+
parsed_data = MediLink_DataMgmt.parse_fixed_width_data(personal_info, insurance_info, service_info, service_info_2, service_info_3, config.get('MediLink_Config', config))
|
|
132
93
|
|
|
133
94
|
primary_insurance = parsed_data.get('INAME')
|
|
134
95
|
|
|
@@ -174,25 +135,6 @@ def extract_and_suggest_endpoint(file_path, config, crosswalk):
|
|
|
174
135
|
# Return only the enriched detailed patient data, eliminating the need for a separate summary list
|
|
175
136
|
return detailed_patient_data
|
|
176
137
|
|
|
177
|
-
def organize_patient_data_by_endpoint(detailed_patient_data):
|
|
178
|
-
"""
|
|
179
|
-
Organizes detailed patient data by their confirmed endpoints.
|
|
180
|
-
This simplifies processing and conversion per endpoint basis, ensuring that claims are generated and submitted
|
|
181
|
-
according to the endpoint-specific requirements.
|
|
182
|
-
|
|
183
|
-
:param detailed_patient_data: A list of dictionaries, each containing detailed patient data including confirmed endpoint.
|
|
184
|
-
:return: A dictionary with endpoints as keys and lists of detailed patient data as values for processing.
|
|
185
|
-
"""
|
|
186
|
-
organized = {}
|
|
187
|
-
for data in detailed_patient_data:
|
|
188
|
-
# Retrieve confirmed endpoint from each patient's data
|
|
189
|
-
endpoint = data['confirmed_endpoint'] if 'confirmed_endpoint' in data else data['suggested_endpoint']
|
|
190
|
-
# Initialize a list for the endpoint if it doesn't exist
|
|
191
|
-
if endpoint not in organized:
|
|
192
|
-
organized[endpoint] = []
|
|
193
|
-
organized[endpoint].append(data)
|
|
194
|
-
return organized
|
|
195
|
-
|
|
196
138
|
def check_for_new_remittances(config):
|
|
197
139
|
print("\nChecking for new files across all endpoints...")
|
|
198
140
|
endpoints = config['MediLink_Config']['endpoints']
|
|
@@ -239,7 +181,7 @@ def user_decision_on_suggestions(detailed_patient_data, config):
|
|
|
239
181
|
|
|
240
182
|
BUG (Med suggested_endpoint) The display summary suggested_endpoint key isn't updating per the user's decision
|
|
241
183
|
although the user decision is persisting. Possibly consider making the current/suggested/confirmed endpoint
|
|
242
|
-
part of a class that the user can interact with via these menus
|
|
184
|
+
part of a class that the user can interact with via these menus? Probably better handling that way.
|
|
243
185
|
"""
|
|
244
186
|
# Display summaries of patient details and endpoints.
|
|
245
187
|
MediLink_UI.display_patient_summaries(detailed_patient_data)
|
|
@@ -249,20 +191,11 @@ def user_decision_on_suggestions(detailed_patient_data, config):
|
|
|
249
191
|
|
|
250
192
|
# If the user agrees to proceed with all suggested endpoints, confirm them.
|
|
251
193
|
if proceed:
|
|
252
|
-
return confirm_all_suggested_endpoints(detailed_patient_data)
|
|
194
|
+
return MediLink_DataMgmt.confirm_all_suggested_endpoints(detailed_patient_data)
|
|
253
195
|
# Otherwise, allow the user to adjust the endpoints manually.
|
|
254
196
|
else:
|
|
255
197
|
return select_and_adjust_files(detailed_patient_data, config)
|
|
256
|
-
|
|
257
|
-
def confirm_all_suggested_endpoints(detailed_patient_data):
|
|
258
|
-
"""
|
|
259
|
-
Confirms all suggested endpoints for each patient's detailed data.
|
|
260
|
-
"""
|
|
261
|
-
for data in detailed_patient_data:
|
|
262
|
-
if 'confirmed_endpoint' not in data:
|
|
263
|
-
data['confirmed_endpoint'] = data['suggested_endpoint']
|
|
264
|
-
return detailed_patient_data
|
|
265
|
-
|
|
198
|
+
|
|
266
199
|
def select_and_adjust_files(detailed_patient_data, config):
|
|
267
200
|
"""
|
|
268
201
|
Allows users to select patients and adjust their endpoints by interfacing with UI functions.
|
|
@@ -323,14 +256,14 @@ def main_menu():
|
|
|
323
256
|
# Normalize the directory path for file operations.
|
|
324
257
|
directory_path = os.path.normpath(config['MediLink_Config']['inputFilePath'])
|
|
325
258
|
|
|
326
|
-
# Detect
|
|
327
|
-
|
|
259
|
+
# Detect files and determine if a new file is flagged.
|
|
260
|
+
all_files, file_flagged = MediLink_DataMgmt.detect_new_files(directory_path)
|
|
328
261
|
|
|
329
262
|
while True:
|
|
330
263
|
# Define the menu options. Base options include checking remittances and exiting the program.
|
|
331
264
|
options = ["Check for new remittances", "Exit"]
|
|
332
|
-
# If
|
|
333
|
-
if
|
|
265
|
+
# If any files are detected, add the option to submit claims.
|
|
266
|
+
if all_files:
|
|
334
267
|
options.insert(1, "Submit claims")
|
|
335
268
|
|
|
336
269
|
# Display the dynamically adjusted menu options.
|
|
@@ -341,11 +274,22 @@ def main_menu():
|
|
|
341
274
|
if choice == '1':
|
|
342
275
|
# Handle remittance checking.
|
|
343
276
|
check_for_new_remittances(config)
|
|
344
|
-
elif choice == '2' and
|
|
345
|
-
# Handle the claims submission flow if
|
|
277
|
+
elif choice == '2' and all_files:
|
|
278
|
+
# Handle the claims submission flow if any files are present.
|
|
279
|
+
if file_flagged:
|
|
280
|
+
# Extract the newest single latest file from the list if a new file is flagged.
|
|
281
|
+
selected_files = [max(all_files, key=os.path.getctime)]
|
|
282
|
+
else:
|
|
283
|
+
# Prompt the user to select files if no new file is flagged.
|
|
284
|
+
selected_files = MediLink_UI.user_select_files(all_files)
|
|
285
|
+
|
|
286
|
+
# Collect detailed patient data for selected files.
|
|
287
|
+
detailed_patient_data = collect_detailed_patient_data(selected_files, config, crosswalk)
|
|
288
|
+
|
|
289
|
+
# Process the claims submission.
|
|
346
290
|
handle_submission(detailed_patient_data, config)
|
|
347
|
-
elif choice == '3' or (choice == '2' and not
|
|
348
|
-
# Exit the program if the user chooses to exit or if no
|
|
291
|
+
elif choice == '3' or (choice == '2' and not all_files):
|
|
292
|
+
# Exit the program if the user chooses to exit or if no files are present.
|
|
349
293
|
MediLink_UI.display_exit_message()
|
|
350
294
|
break
|
|
351
295
|
else:
|
|
@@ -357,13 +301,29 @@ def handle_submission(detailed_patient_data, config):
|
|
|
357
301
|
Handles the submission process for claims based on detailed patient data.
|
|
358
302
|
This function orchestrates the flow from user decision on endpoint suggestions to the actual submission of claims.
|
|
359
303
|
"""
|
|
304
|
+
# TODO If we get here via a user decline we end up not displaying the patient summary data, but this doesn't happen in the first round. Can be de-tangled later.
|
|
305
|
+
|
|
306
|
+
# Ask the user if they want to edit insurance types
|
|
307
|
+
edit_insurance = input("Do you want to edit insurance types? (y/n): ").strip().lower()
|
|
308
|
+
if edit_insurance in ['y', 'yes', '']:
|
|
309
|
+
while True:
|
|
310
|
+
# Bulk edit insurance types
|
|
311
|
+
MediLink_DataMgmt.bulk_edit_insurance_types(detailed_patient_data, insurance_options)
|
|
312
|
+
|
|
313
|
+
# Review and confirm changes
|
|
314
|
+
if MediLink_DataMgmt.review_and_confirm_changes(detailed_patient_data, insurance_options):
|
|
315
|
+
break # Exit the loop if changes are confirmed
|
|
316
|
+
else:
|
|
317
|
+
print("Returning to bulk edit insurance types.")
|
|
318
|
+
|
|
360
319
|
# Initiate user interaction to confirm or adjust suggested endpoints.
|
|
361
320
|
adjusted_data = user_decision_on_suggestions(detailed_patient_data, config)
|
|
321
|
+
|
|
362
322
|
# Confirm all remaining suggested endpoints.
|
|
363
|
-
confirmed_data = confirm_all_suggested_endpoints(adjusted_data)
|
|
323
|
+
confirmed_data = MediLink_DataMgmt.confirm_all_suggested_endpoints(adjusted_data)
|
|
364
324
|
if confirmed_data: # Proceed if there are confirmed data entries.
|
|
365
325
|
# Organize data by confirmed endpoints for submission.
|
|
366
|
-
organized_data = organize_patient_data_by_endpoint(confirmed_data)
|
|
326
|
+
organized_data = MediLink_DataMgmt.organize_patient_data_by_endpoint(confirmed_data)
|
|
367
327
|
# Confirm transmission with the user and check for internet connectivity.
|
|
368
328
|
if MediLink_Up.confirm_transmission(organized_data):
|
|
369
329
|
if MediLink_Up.check_internet_connection():
|