medicafe 0.240517.0__py3-none-any.whl → 0.240613.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of medicafe might be problematic. Click here for more details.
- MediBot/MediBot.bat +9 -1
- MediBot/MediBot.py +9 -36
- MediBot/MediBot_Charges.py +0 -28
- MediBot/MediBot_Crosswalk_Library.py +1 -0
- 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 +275 -60
- MediLink/MediLink.py +102 -107
- MediLink/MediLink_837p_encoder.py +3 -28
- MediLink/MediLink_837p_encoder_library.py +14 -25
- MediLink/MediLink_APIs.py +2 -0
- MediLink/MediLink_DataMgmt.py +224 -68
- MediLink/MediLink_Decoder.py +63 -0
- MediLink/MediLink_Down.py +73 -79
- MediLink/MediLink_Gmail.py +453 -74
- MediLink/MediLink_Mailer.py +0 -7
- MediLink/MediLink_Parser.py +111 -0
- MediLink/MediLink_Scan.py +0 -0
- MediLink/MediLink_Scheduler.py +2 -172
- MediLink/MediLink_StatusCheck.py +0 -4
- MediLink/MediLink_UI.py +76 -18
- MediLink/MediLink_Up.py +4 -14
- {medicafe-0.240517.0.dist-info → medicafe-0.240613.0.dist-info}/METADATA +3 -1
- medicafe-0.240613.0.dist-info/RECORD +43 -0
- medicafe-0.240517.0.dist-info/RECORD +0 -39
- {medicafe-0.240517.0.dist-info → medicafe-0.240613.0.dist-info}/LICENSE +0 -0
- {medicafe-0.240517.0.dist-info → medicafe-0.240613.0.dist-info}/WHEEL +0 -0
- {medicafe-0.240517.0.dist-info → medicafe-0.240613.0.dist-info}/top_level.txt +0 -0
MediBot/MediBot_docx_decoder.py
CHANGED
|
@@ -1,80 +1,295 @@
|
|
|
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)
|
|
55
|
+
|
|
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
|
+
}
|
|
23
147
|
|
|
24
|
-
|
|
25
|
-
|
|
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)
|
|
26
152
|
|
|
27
|
-
|
|
28
|
-
|
|
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)
|
|
29
160
|
|
|
30
|
-
#
|
|
31
|
-
|
|
32
|
-
|
|
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 = []
|
|
33
179
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
femto_yes_or_no = parse_femto_yes_or_no(cells[3].text.strip())
|
|
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())
|
|
39
184
|
|
|
40
|
-
|
|
41
|
-
|
|
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)
|
|
206
|
+
|
|
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
|
+
if '(' in text and ')' in text: # Extract the diagnosis code before the '/'
|
|
242
|
+
full_code = text[text.index('(')+1:text.index(')')]
|
|
243
|
+
return full_code.split('/')[0]
|
|
244
|
+
return text.split('/')[0]
|
|
245
|
+
except Exception as e:
|
|
246
|
+
MediLink_ConfigLoader.log("Error parsing diagnosis code: {}. Error: {}".format(text, e))
|
|
247
|
+
return "Unknown"
|
|
54
248
|
|
|
55
249
|
def parse_left_or_right_eye(text):
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
250
|
+
try:
|
|
251
|
+
if 'LEFT EYE' in text.upper():
|
|
252
|
+
return 'Left'
|
|
253
|
+
elif 'RIGHT EYE' in text.upper():
|
|
254
|
+
return 'Right'
|
|
255
|
+
else:
|
|
256
|
+
return 'Unknown'
|
|
257
|
+
except Exception as e:
|
|
258
|
+
MediLink_ConfigLoader.log("Error parsing left or right eye: {}. Error: {}".format(text, e))
|
|
63
259
|
return 'Unknown'
|
|
64
260
|
|
|
65
261
|
def parse_femto_yes_or_no(text):
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return
|
|
262
|
+
try:
|
|
263
|
+
if 'FEMTO' in text.upper():
|
|
264
|
+
return True
|
|
265
|
+
else:
|
|
266
|
+
return False
|
|
267
|
+
except Exception as e:
|
|
268
|
+
MediLink_ConfigLoader.log("Error parsing femto yes or no: {}. Error: {}".format(text, e))
|
|
269
|
+
return False
|
|
270
|
+
|
|
271
|
+
|
|
272
|
+
def rotate_docx_files(directory):
|
|
273
|
+
# List all files in the directory
|
|
274
|
+
files = os.listdir(directory)
|
|
275
|
+
|
|
276
|
+
# Filter files that contain "DR" and "SS" in the filename
|
|
277
|
+
filtered_files = [file for file in files if "DR" in file and "SS" in file]
|
|
278
|
+
|
|
279
|
+
# Iterate through filtered files
|
|
280
|
+
for filename in filtered_files:
|
|
281
|
+
filepath = os.path.join(directory, filename)
|
|
282
|
+
# Parse each document and print the resulting dictionary
|
|
283
|
+
patient_data_dict = parse_docx(filepath)
|
|
284
|
+
print("Data from file '{}':".format(filename))
|
|
285
|
+
import pprint
|
|
286
|
+
pprint.pprint(patient_data_dict)
|
|
287
|
+
print()
|
|
74
288
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
289
|
+
def main():
|
|
290
|
+
# Call the function with the directory containing your .docx files
|
|
291
|
+
directory = "C:\\Users\\danie\\Downloads\\"
|
|
292
|
+
rotate_docx_files(directory)
|
|
78
293
|
|
|
79
|
-
|
|
80
|
-
|
|
294
|
+
if __name__ == "__main__":
|
|
295
|
+
main()
|
MediLink/MediLink.py
CHANGED
|
@@ -3,6 +3,7 @@ import MediLink_Down
|
|
|
3
3
|
import MediLink_Up
|
|
4
4
|
import MediLink_ConfigLoader
|
|
5
5
|
import MediLink_837p_encoder
|
|
6
|
+
import MediLink_DataMgmt
|
|
6
7
|
|
|
7
8
|
# For UI Functions
|
|
8
9
|
import os
|
|
@@ -18,56 +19,33 @@ from MediBot import MediBot_Preprocessor_lib
|
|
|
18
19
|
load_insurance_data_from_mains = MediBot_Preprocessor_lib.load_insurance_data_from_mains
|
|
19
20
|
from MediBot import MediBot_Crosswalk_Library
|
|
20
21
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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
|
-
"""
|
|
22
|
+
# Define insurance options with codes and descriptions.
|
|
23
|
+
# TODO This needs to move to the config file
|
|
24
|
+
insurance_options = {
|
|
25
|
+
"11": "Other Non-Federal Programs",
|
|
26
|
+
"12": "Preferred Provider Organization (PPO)",
|
|
27
|
+
"13": "Point of Service (POS)",
|
|
28
|
+
"14": "Exclusive Provider Organization (EPO)",
|
|
29
|
+
"15": "Indemnity Insurance",
|
|
30
|
+
"16": "Health Maintenance Organization (HMO) Medicare Risk",
|
|
31
|
+
"17": "Dental Maintenance Organization",
|
|
32
|
+
"AM": "Automobile Medical",
|
|
33
|
+
"BL": "Blue Cross/Blue Shield",
|
|
34
|
+
"CH": "Champus",
|
|
35
|
+
"CI": "Commercial Insurance Co.",
|
|
36
|
+
"DS": "Disability",
|
|
37
|
+
"FI": "Federal Employees Program",
|
|
38
|
+
"HM": "Health Maintenance Organization",
|
|
39
|
+
"LM": "Liability Medical",
|
|
40
|
+
"MA": "Medicare Part A",
|
|
41
|
+
"MB": "Medicare Part B",
|
|
42
|
+
"MC": "Medicaid",
|
|
43
|
+
"OF": "Other Federal Program",
|
|
44
|
+
"TV": "Title V",
|
|
45
|
+
"VA": "Veterans Affairs Plan",
|
|
46
|
+
"WC": "Workers Compensation Health Claim",
|
|
47
|
+
"ZZ": "Mutually Defined"
|
|
48
|
+
}
|
|
71
49
|
|
|
72
50
|
def detect_and_display_file_summaries(directory_path, config, crosswalk):
|
|
73
51
|
"""
|
|
@@ -76,35 +54,61 @@ def detect_and_display_file_summaries(directory_path, config, crosswalk):
|
|
|
76
54
|
|
|
77
55
|
:param directory_path: Path to the directory containing files to be detected.
|
|
78
56
|
:param config: Configuration settings loaded from a JSON file.
|
|
57
|
+
:param crosswalk: Crosswalk data for mapping purposes.
|
|
79
58
|
:return: A tuple containing a list of new file paths and the detailed patient data.
|
|
80
59
|
"""
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
60
|
+
try:
|
|
61
|
+
new_files, file_flagged = MediLink_DataMgmt.detect_new_files(directory_path)
|
|
62
|
+
if not new_files:
|
|
63
|
+
print("No new claims detected. Check Medisoft claims output.")
|
|
64
|
+
MediLink_ConfigLoader.log("No new claims detected. Check Medisoft claims output.")
|
|
65
|
+
return False, []
|
|
66
|
+
|
|
67
|
+
if not file_flagged:
|
|
68
|
+
selected_files = MediLink_UI.user_select_files(new_files)
|
|
69
|
+
else:
|
|
70
|
+
# Extract the newest single latest file from the list
|
|
71
|
+
selected_files = [max(new_files, key=os.path.getctime)]
|
|
72
|
+
|
|
73
|
+
detailed_patient_data = [] # Initialize list for detailed patient data
|
|
74
|
+
for file_path in selected_files:
|
|
75
|
+
detailed_data = extract_and_suggest_endpoint(file_path, config, crosswalk)
|
|
76
|
+
detailed_patient_data.extend(detailed_data) # Accumulate detailed data for processing
|
|
77
|
+
|
|
78
|
+
# Enrich the detailed patient data with insurance type
|
|
79
|
+
detailed_patient_data = enrich_with_insurance_type(detailed_patient_data, insurance_options)
|
|
80
|
+
|
|
81
|
+
# Display summaries and provide an option for bulk edit
|
|
82
|
+
MediLink_UI.display_patient_summaries(detailed_patient_data)
|
|
83
|
+
|
|
84
|
+
# Return the list of new files and the enriched detailed patient data
|
|
85
|
+
return selected_files, detailed_patient_data
|
|
86
|
+
except Exception as e:
|
|
87
|
+
MediLink_ConfigLoader.log("Error in detect_and_display_file_summaries: {}".format(e))
|
|
84
88
|
return False, []
|
|
85
89
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
detailed_patient_data.extend(detailed_data) # Accumulate detailed data for processing
|
|
90
|
+
def enrich_with_insurance_type(detailed_patient_data, patient_insurance_type_mapping=None):
|
|
91
|
+
"""
|
|
92
|
+
Enriches the detailed patient data with insurance type based on patient ID.
|
|
90
93
|
|
|
91
|
-
|
|
92
|
-
|
|
94
|
+
Parameters:
|
|
95
|
+
- detailed_patient_data: List of dictionaries containing detailed patient data.
|
|
96
|
+
- patient_insurance_mapping: Dictionary mapping patient IDs to their insurance types.
|
|
93
97
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
Scans the specified directory for new files with a given extension.
|
|
98
|
+
Returns:
|
|
99
|
+
- Enriched detailed patient data with insurance type added.
|
|
97
100
|
|
|
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.
|
|
101
|
+
TODO: Implement a function to provide `patient_insurance_mapping` from a reliable source.
|
|
101
102
|
"""
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
103
|
+
if patient_insurance_type_mapping is None:
|
|
104
|
+
MediLink_ConfigLoader.log("No Patient:Insurance-Type mapping available.")
|
|
105
|
+
patient_insurance_type_mapping = {}
|
|
106
|
+
|
|
107
|
+
for data in detailed_patient_data:
|
|
108
|
+
patient_id = data.get('PATID') # I think this is the right name?
|
|
109
|
+
insurance_type = patient_insurance_type_mapping.get(patient_id, '12') # Default to '12' (PPO)
|
|
110
|
+
data['insurance_type'] = insurance_type
|
|
111
|
+
return detailed_patient_data
|
|
108
112
|
|
|
109
113
|
def extract_and_suggest_endpoint(file_path, config, crosswalk):
|
|
110
114
|
"""
|
|
@@ -127,8 +131,8 @@ def extract_and_suggest_endpoint(file_path, config, crosswalk):
|
|
|
127
131
|
insurance_to_id = load_insurance_data_from_mains(config)
|
|
128
132
|
MediLink_ConfigLoader.log("Insurance data loaded from MAINS. {} insurance providers found.".format(len(insurance_to_id)))
|
|
129
133
|
|
|
130
|
-
for personal_info, insurance_info, service_info in
|
|
131
|
-
parsed_data =
|
|
134
|
+
for personal_info, insurance_info, service_info, service_info_2, service_info_3 in MediLink_DataMgmt.read_fixed_width_data(file_path):
|
|
135
|
+
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
136
|
|
|
133
137
|
primary_insurance = parsed_data.get('INAME')
|
|
134
138
|
|
|
@@ -174,25 +178,6 @@ def extract_and_suggest_endpoint(file_path, config, crosswalk):
|
|
|
174
178
|
# Return only the enriched detailed patient data, eliminating the need for a separate summary list
|
|
175
179
|
return detailed_patient_data
|
|
176
180
|
|
|
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
181
|
def check_for_new_remittances(config):
|
|
197
182
|
print("\nChecking for new files across all endpoints...")
|
|
198
183
|
endpoints = config['MediLink_Config']['endpoints']
|
|
@@ -239,7 +224,7 @@ def user_decision_on_suggestions(detailed_patient_data, config):
|
|
|
239
224
|
|
|
240
225
|
BUG (Med suggested_endpoint) The display summary suggested_endpoint key isn't updating per the user's decision
|
|
241
226
|
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
|
|
227
|
+
part of a class that the user can interact with via these menus? Probably better handling that way.
|
|
243
228
|
"""
|
|
244
229
|
# Display summaries of patient details and endpoints.
|
|
245
230
|
MediLink_UI.display_patient_summaries(detailed_patient_data)
|
|
@@ -249,20 +234,11 @@ def user_decision_on_suggestions(detailed_patient_data, config):
|
|
|
249
234
|
|
|
250
235
|
# If the user agrees to proceed with all suggested endpoints, confirm them.
|
|
251
236
|
if proceed:
|
|
252
|
-
return confirm_all_suggested_endpoints(detailed_patient_data)
|
|
237
|
+
return MediLink_DataMgmt.confirm_all_suggested_endpoints(detailed_patient_data)
|
|
253
238
|
# Otherwise, allow the user to adjust the endpoints manually.
|
|
254
239
|
else:
|
|
255
240
|
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
|
-
|
|
241
|
+
|
|
266
242
|
def select_and_adjust_files(detailed_patient_data, config):
|
|
267
243
|
"""
|
|
268
244
|
Allows users to select patients and adjust their endpoints by interfacing with UI functions.
|
|
@@ -325,6 +301,9 @@ def main_menu():
|
|
|
325
301
|
|
|
326
302
|
# Detect new files and collect detailed patient data if available.
|
|
327
303
|
new_files, detailed_patient_data = detect_and_display_file_summaries(directory_path, config, crosswalk)
|
|
304
|
+
|
|
305
|
+
if new_files:
|
|
306
|
+
handle_submission(detailed_patient_data, config)
|
|
328
307
|
|
|
329
308
|
while True:
|
|
330
309
|
# Define the menu options. Base options include checking remittances and exiting the program.
|
|
@@ -343,7 +322,7 @@ def main_menu():
|
|
|
343
322
|
check_for_new_remittances(config)
|
|
344
323
|
elif choice == '2' and new_files:
|
|
345
324
|
# Handle the claims submission flow if new files are present.
|
|
346
|
-
handle_submission(detailed_patient_data, config)
|
|
325
|
+
handle_submission(detailed_patient_data, config) # Since we have the bulk edit, we should actually go there first
|
|
347
326
|
elif choice == '3' or (choice == '2' and not new_files):
|
|
348
327
|
# Exit the program if the user chooses to exit or if no new files are present.
|
|
349
328
|
MediLink_UI.display_exit_message()
|
|
@@ -357,13 +336,29 @@ def handle_submission(detailed_patient_data, config):
|
|
|
357
336
|
Handles the submission process for claims based on detailed patient data.
|
|
358
337
|
This function orchestrates the flow from user decision on endpoint suggestions to the actual submission of claims.
|
|
359
338
|
"""
|
|
339
|
+
# 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.
|
|
340
|
+
|
|
341
|
+
# Ask the user if they want to edit insurance types
|
|
342
|
+
edit_insurance = input("Do you want to edit insurance types? (y/n): ").strip().lower()
|
|
343
|
+
if edit_insurance in ['y', 'yes', '']:
|
|
344
|
+
while True:
|
|
345
|
+
# Bulk edit insurance types
|
|
346
|
+
MediLink_DataMgmt.bulk_edit_insurance_types(detailed_patient_data, insurance_options)
|
|
347
|
+
|
|
348
|
+
# Review and confirm changes
|
|
349
|
+
if MediLink_DataMgmt.review_and_confirm_changes(detailed_patient_data, insurance_options):
|
|
350
|
+
break # Exit the loop if changes are confirmed
|
|
351
|
+
else:
|
|
352
|
+
print("Returning to bulk edit insurance types.")
|
|
353
|
+
|
|
360
354
|
# Initiate user interaction to confirm or adjust suggested endpoints.
|
|
361
355
|
adjusted_data = user_decision_on_suggestions(detailed_patient_data, config)
|
|
356
|
+
|
|
362
357
|
# Confirm all remaining suggested endpoints.
|
|
363
|
-
confirmed_data = confirm_all_suggested_endpoints(adjusted_data)
|
|
358
|
+
confirmed_data = MediLink_DataMgmt.confirm_all_suggested_endpoints(adjusted_data)
|
|
364
359
|
if confirmed_data: # Proceed if there are confirmed data entries.
|
|
365
360
|
# Organize data by confirmed endpoints for submission.
|
|
366
|
-
organized_data = organize_patient_data_by_endpoint(confirmed_data)
|
|
361
|
+
organized_data = MediLink_DataMgmt.organize_patient_data_by_endpoint(confirmed_data)
|
|
367
362
|
# Confirm transmission with the user and check for internet connectivity.
|
|
368
363
|
if MediLink_Up.confirm_transmission(organized_data):
|
|
369
364
|
if MediLink_Up.check_internet_connection():
|