medicafe 0.240419.2__zip
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.
- medicafe-0.240419.2/LICENSE +21 -0
- medicafe-0.240419.2/MANIFEST.in +2 -0
- medicafe-0.240419.2/MediBot/MediBot.bat +70 -0
- medicafe-0.240419.2/MediBot/MediBot.py +316 -0
- medicafe-0.240419.2/MediBot/MediBot_Charges.py +28 -0
- medicafe-0.240419.2/MediBot/MediBot_Preprocessor.py +283 -0
- medicafe-0.240419.2/MediBot/MediBot_UI.py +190 -0
- medicafe-0.240419.2/MediBot/MediBot_dataformat_library.py +145 -0
- medicafe-0.240419.2/MediBot/MediPost.py +5 -0
- medicafe-0.240419.2/MediBot/PDF_to_CSV_Cleaner.py +211 -0
- medicafe-0.240419.2/MediBot/__init__.py +0 -0
- medicafe-0.240419.2/MediBot/update_json.py +43 -0
- medicafe-0.240419.2/MediBot/update_medicafe.py +19 -0
- medicafe-0.240419.2/MediLink/MediLink.py +277 -0
- medicafe-0.240419.2/MediLink/MediLink_277_decoder.py +92 -0
- medicafe-0.240419.2/MediLink/MediLink_837p_encoder.py +392 -0
- medicafe-0.240419.2/MediLink/MediLink_837p_encoder_library.py +679 -0
- medicafe-0.240419.2/MediLink/MediLink_ConfigLoader.py +69 -0
- medicafe-0.240419.2/MediLink/MediLink_DataMgmt.py +206 -0
- medicafe-0.240419.2/MediLink/MediLink_Down.py +151 -0
- medicafe-0.240419.2/MediLink/MediLink_ERA_decoder.py +192 -0
- medicafe-0.240419.2/MediLink/MediLink_Gmail.py +4 -0
- medicafe-0.240419.2/MediLink/MediLink_Scheduler.py +132 -0
- medicafe-0.240419.2/MediLink/MediLink_StatusCheck.py +4 -0
- medicafe-0.240419.2/MediLink/MediLink_UI.py +116 -0
- medicafe-0.240419.2/MediLink/MediLink_Up.py +117 -0
- medicafe-0.240419.2/MediLink/MediLink_batch.bat +7 -0
- medicafe-0.240419.2/MediLink/Soumit_api.py +22 -0
- medicafe-0.240419.2/MediLink/__init__.py +0 -0
- medicafe-0.240419.2/PKG-INFO +11 -0
- medicafe-0.240419.2/README.md +28 -0
- medicafe-0.240419.2/medicafe.egg-info/PKG-INFO +11 -0
- medicafe-0.240419.2/medicafe.egg-info/SOURCES.txt +37 -0
- medicafe-0.240419.2/medicafe.egg-info/dependency_links.txt +1 -0
- medicafe-0.240419.2/medicafe.egg-info/not-zip-safe +1 -0
- medicafe-0.240419.2/medicafe.egg-info/requires.txt +5 -0
- medicafe-0.240419.2/medicafe.egg-info/top_level.txt +2 -0
- medicafe-0.240419.2/setup.cfg +5 -0
- medicafe-0.240419.2/setup.py +28 -0
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import csv
|
|
2
|
+
import subprocess
|
|
3
|
+
import os
|
|
4
|
+
import re
|
|
5
|
+
from datetime import datetime
|
|
6
|
+
from collections import OrderedDict # so that the field_mapping stays in order.
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
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
|
+
from MediLink import MediLink_ConfigLoader
|
|
15
|
+
from MediLink import MediLink_DataMgmt
|
|
16
|
+
|
|
17
|
+
"""
|
|
18
|
+
Preprocessing Enhancements
|
|
19
|
+
- [X] Preprocess Insurance Policy Numbers and Group Numbers to replace '-' with ''.
|
|
20
|
+
- [X] De-duplicate entries in the CSV and only entering the px once even if they show up twice in the file.
|
|
21
|
+
- [ ] Implement dynamic field combination in CSV pre-processing for flexibility with various CSV formats.
|
|
22
|
+
- [ ] Enhance SSN cleaning logic to handle more variations of sensitive data masking.
|
|
23
|
+
- [ ] Optimize script startup and CSV loading to reduce initial latency.
|
|
24
|
+
|
|
25
|
+
Data Integrity and Validation
|
|
26
|
+
- [ ] Conduct a thorough CSV integrity check before processing to flag potential issues upfront.
|
|
27
|
+
- [ ] Implement a mechanism to confirm the accuracy of entered data, potentially through a verification step or summary report.
|
|
28
|
+
- [ ] Explore the possibility of integrating direct database queries for existing patient checks to streamline the process.
|
|
29
|
+
- [ ] Automate the replacement of spaces with underscores ('_') in last names for Medicare entries, ensuring data consistency.
|
|
30
|
+
- [ ] Enhance CSV integrity checks to identify and report potential issues with data format, especially concerning insurance policy numbers and special character handling.
|
|
31
|
+
|
|
32
|
+
Known Issues and Bugs
|
|
33
|
+
- [ ] Address the handling of '.' and other special characters that may disrupt parsing, especially under Windows XP.
|
|
34
|
+
- [ ] Investigate the issue with Excel modifying long policy numbers in the CSV and provide guidance or a workaround.
|
|
35
|
+
|
|
36
|
+
Future Work
|
|
37
|
+
- [ ] Consolidate data from multiple sources (Provider_Notes.csv, Surgery_Schedule.csv, and Carols_CSV.csv) into a single table with Patient ID as the key, ensuring all data elements are aligned and duplicate entries are minimized.
|
|
38
|
+
- [ ] Implement logic to verify and match Patient IDs across different files to ensure data integrity before consolidation.
|
|
39
|
+
- [ ] Optimize the preprocessing of surgery dates and diagnosis codes for use in patient billing and scheduling systems.
|
|
40
|
+
- [ ] This needs to be able to take in the Surgery Schedule doc and parse out a Patient ID : Diagnosis Code table
|
|
41
|
+
- [ ] The Minutes & Cacncellation data with logic to consolidate into one table in memory.
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
Future Work: crosswalk_update() automates the process of updating the crosswalk.json file with new Medisoft insurance information.
|
|
45
|
+
|
|
46
|
+
Development Roadmap:
|
|
47
|
+
1. Problem Statement:
|
|
48
|
+
- The need to update the crosswalk.json file arises whenever a new Medisoft insurance is discovered. Automation of this process is required for accuracy and efficiency.
|
|
49
|
+
|
|
50
|
+
2. Identifying New Insurance:
|
|
51
|
+
- New Medisoft insurances are identified based on the payer ID number.
|
|
52
|
+
- The existence of the payer ID number is checked in the crosswalk.json under existing endpoints.
|
|
53
|
+
|
|
54
|
+
3. Adding New Insurance:
|
|
55
|
+
- If the payer ID number does not exist in any endpoint, the tool prompts the user, assisted by endpoint APIs, to add the payer ID to a specific endpoint.
|
|
56
|
+
- The corresponding name from Carol's spreadsheet is used as the value for the new payer ID.
|
|
57
|
+
|
|
58
|
+
4. Mapping to Main Insurance:
|
|
59
|
+
- The tool presents the user with a list of the top 5-7 insurances, scored higher on a fuzzy search or above a certain score.
|
|
60
|
+
- The user selects the appropriate insurance based on the identified Medisoft insurance, establishing the medisoft_insurance_to_payer_id relationship.
|
|
61
|
+
|
|
62
|
+
5. Confirming Mapping:
|
|
63
|
+
- The tool implicitly establishes the insurance_to_endpoint_mapping based on the selected MediSoft name and endpoint.
|
|
64
|
+
- This step is confirmed or re-evaluated to ensure accuracy.
|
|
65
|
+
|
|
66
|
+
6. User Interaction:
|
|
67
|
+
- Unrecognized payer IDs are presented to the user.
|
|
68
|
+
- Users can assign these payer IDs to MediSoft custom names individually.
|
|
69
|
+
- Grouping of payer IDs may be facilitated, especially for insurances like CIGNA with multiple addresses but few payer IDs.
|
|
70
|
+
|
|
71
|
+
7. Handling Unavailable Payer IDs:
|
|
72
|
+
- An extra endpoint named "Fax/Mail or Other" is created to handle cases where the payer ID is unavailable.
|
|
73
|
+
- The tool retains payer IDs not existing in any endpoint, allowing users to assign them to the "Fax/Mail or Other" key in the crosswalk.
|
|
74
|
+
|
|
75
|
+
8. Implementation Considerations:
|
|
76
|
+
- The tool should handle various scenarios, including checking for free payer IDs and determining the appropriate endpoint for assignment.
|
|
77
|
+
- Integration of API checks to verify payer ID availability and associated information is recommended.
|
|
78
|
+
- Validation mechanisms should be implemented to prevent incorrect mappings and ensure data integrity.
|
|
79
|
+
|
|
80
|
+
NOTE: this needs to also pull from the CSV the listed address of the insruance.
|
|
81
|
+
NOTE: La Forma Z can have the PatientID number which can link back to Carol's table which can then map the Medisoft insurance name to the payerID
|
|
82
|
+
and payer name and address when the insurance is already selected in Medisoft so the program can learn retroactively and would know the Medisoft # from
|
|
83
|
+
the sequencing rather than trying to feed it from the beginning. so that'll be out of ["fixedWidthSlices"]["personal_slices"]["PATID"].
|
|
84
|
+
NOTE: Also check MAPAT because maybe the PatientID to Medisoft custom insurance name might exist there enmasse + the PatientID to PayerID link from Carol's CSV
|
|
85
|
+
gives us the Medisoft custom insurance name to Payer ID. Then, the endpoint mapping is the clearinghouse PayerID list (API?). MAPAT has the PatientID to Medisoft
|
|
86
|
+
insruance reference number which is the MAINS offset by 1 for the header. MAPAT has columns [159,162] for insurance and [195,200] for patient ID.
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
# Load configuration
|
|
90
|
+
# Should this also take args? Path for ./MediLink needed to be added for this to resolve
|
|
91
|
+
config, _ = MediLink_ConfigLoader.load_configuration()
|
|
92
|
+
|
|
93
|
+
class InitializationError(Exception):
|
|
94
|
+
def __init__(self, message):
|
|
95
|
+
self.message = message
|
|
96
|
+
super().__init__(self.message)
|
|
97
|
+
|
|
98
|
+
def initialize(config):
|
|
99
|
+
global AHK_EXECUTABLE, CSV_FILE_PATH, field_mapping, page_end_markers
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
AHK_EXECUTABLE = config.get('AHK_EXECUTABLE', "")
|
|
103
|
+
except AttributeError:
|
|
104
|
+
raise InitializationError("Error: 'AHK_EXECUTABLE' not found in config.")
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
CSV_FILE_PATH = config.get('CSV_FILE_PATH', "")
|
|
108
|
+
except AttributeError:
|
|
109
|
+
raise InitializationError("Error: 'CSV_FILE_PATH' not found in config.")
|
|
110
|
+
|
|
111
|
+
try:
|
|
112
|
+
field_mapping = OrderedDict(config.get('field_mapping', {}))
|
|
113
|
+
except AttributeError:
|
|
114
|
+
raise InitializationError("Error: 'field_mapping' not found in config.")
|
|
115
|
+
|
|
116
|
+
try:
|
|
117
|
+
page_end_markers = config.get('page_end_markers', [])
|
|
118
|
+
except AttributeError:
|
|
119
|
+
raise InitializationError("Error: 'page_end_markers' not found in config.")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def open_csv_for_editing(csv_file_path):
|
|
123
|
+
try:
|
|
124
|
+
# Open the CSV file in the default program
|
|
125
|
+
subprocess.run(['open' if os.name == 'posix' else 'start', csv_file_path], check=True, shell=True)
|
|
126
|
+
print("After saving the revised CSV, please re-run MediBot.")
|
|
127
|
+
except subprocess.CalledProcessError as e:
|
|
128
|
+
print("Failed to open CSV file:", e)
|
|
129
|
+
|
|
130
|
+
# Function to load and process CSV data
|
|
131
|
+
def load_csv_data(csv_file_path):
|
|
132
|
+
try:
|
|
133
|
+
# Check if the file exists
|
|
134
|
+
if not os.path.exists(csv_file_path):
|
|
135
|
+
raise FileNotFoundError("***Error: CSV file '{}' not found.".format(csv_file_path))
|
|
136
|
+
|
|
137
|
+
with open(csv_file_path, 'r') as csvfile:
|
|
138
|
+
reader = csv.DictReader(csvfile)
|
|
139
|
+
return [row for row in reader] # Return a list of dictionaries
|
|
140
|
+
except FileNotFoundError as e:
|
|
141
|
+
print(e) # Print the informative error message
|
|
142
|
+
print("Hint: Check if CSV file is located in the expected directory or specify a different path in config file.")
|
|
143
|
+
print("Please correct the issue and re-run MediBot.")
|
|
144
|
+
sys.exit(1) # Halt the script
|
|
145
|
+
except IOError as e:
|
|
146
|
+
print("Error reading CSV file: {}. Please check the file path and permissions.".format(e))
|
|
147
|
+
sys.exit(1) # Halt the script in case of other IO errors
|
|
148
|
+
|
|
149
|
+
# CSV Preprocessor built for Carol
|
|
150
|
+
def preprocess_csv_data(csv_data):
|
|
151
|
+
try:
|
|
152
|
+
# Filter out rows without a Patient ID
|
|
153
|
+
csv_data[:] = [row for row in csv_data if row.get('Patient ID', '').strip()]
|
|
154
|
+
|
|
155
|
+
# Remove Patients (rows) that are Primary Insurance: 'AETNA', 'AETNA MEDICARE', or 'HUMANA MED HMO'.
|
|
156
|
+
csv_data[:] = [row for row in csv_data if row.get('Primary Insurance', '').strip() not in ['AETNA', 'AETNA MEDICARE', 'HUMANA MED HMO']]
|
|
157
|
+
|
|
158
|
+
# Convert 'Surgery Date' to datetime objects for sorting
|
|
159
|
+
for row in csv_data:
|
|
160
|
+
try:
|
|
161
|
+
row['Surgery Date'] = datetime.strptime(row.get('Surgery Date', ''), '%m/%d/%Y')
|
|
162
|
+
except ValueError:
|
|
163
|
+
# Handle or log the error if the date is invalid
|
|
164
|
+
row['Surgery Date'] = datetime.min # Assign a minimum datetime value for sorting purposes
|
|
165
|
+
|
|
166
|
+
# Initially sort the patients first by 'Surgery Date' and then by 'Patient Last' alphabetically
|
|
167
|
+
csv_data.sort(key=lambda x: (x['Surgery Date'], x.get('Patient Last', '').strip()))
|
|
168
|
+
|
|
169
|
+
# Deduplicate patient records based on Patient ID, keeping the entry with the earliest surgery date
|
|
170
|
+
unique_patients = {}
|
|
171
|
+
for row in csv_data:
|
|
172
|
+
patient_id = row.get('Patient ID')
|
|
173
|
+
if patient_id not in unique_patients or row['Surgery Date'] < unique_patients[patient_id]['Surgery Date']:
|
|
174
|
+
unique_patients[patient_id] = row
|
|
175
|
+
|
|
176
|
+
# Update csv_data to only include unique patient records
|
|
177
|
+
csv_data[:] = list(unique_patients.values())
|
|
178
|
+
|
|
179
|
+
# Re-sort the csv_data after deduplication to ensure correct order
|
|
180
|
+
csv_data.sort(key=lambda x: (x['Surgery Date'], x.get('Patient Last', '').strip()))
|
|
181
|
+
|
|
182
|
+
# Maybe make a dataformat_library function for this? csv_data = format_preprocessor(csv_data)?
|
|
183
|
+
for row in csv_data:
|
|
184
|
+
# Convert 'Surgery Date' back to string format if needed for further processing (cleanup)
|
|
185
|
+
row['Surgery Date'] = row['Surgery Date'].strftime('%m/%d/%Y')
|
|
186
|
+
|
|
187
|
+
# Combine name fields
|
|
188
|
+
first_name = row.get('Patient First', '').strip()
|
|
189
|
+
middle_name = row.get('Patient Middle', '').strip()
|
|
190
|
+
last_name = row.get('Patient Last', '').strip()
|
|
191
|
+
row['Patient Name'] = "{}, {} {}".format(last_name, first_name, middle_name).strip()
|
|
192
|
+
|
|
193
|
+
# Combine address fields
|
|
194
|
+
address1 = row.get('Patient Address1', '').strip()
|
|
195
|
+
address2 = row.get('Patient Address2', '').strip()
|
|
196
|
+
row['Patient Street'] = "{} {}".format(address1, address2).strip()
|
|
197
|
+
|
|
198
|
+
# Probably make a data_format function for this:
|
|
199
|
+
# Define the replacements as a dictionary
|
|
200
|
+
replacements = {
|
|
201
|
+
'777777777': '', # Replace '777777777' with an empty string
|
|
202
|
+
'RAILROAD MEDICARE': 'RAILROAD', # Replace 'RAILROAD MEDICARE' with 'RAILROAD'
|
|
203
|
+
'AARP MEDICARE COMPLETE': 'AARP COMPLETE' # Replace 'AARP MEDICARE COMPLETE' with 'AARP COMPLETE'
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
# Iterate over each key-value pair in the replacements dictionary
|
|
207
|
+
for old_value, new_value in replacements.items():
|
|
208
|
+
# Replace the old value with the new value if it exists in the row
|
|
209
|
+
if row.get('Patient SSN', '') == old_value:
|
|
210
|
+
row['Patient SSN'] = new_value
|
|
211
|
+
elif row.get('Primary Insurance', '') == old_value:
|
|
212
|
+
row['Primary Insurance'] = new_value
|
|
213
|
+
|
|
214
|
+
except Exception as e:
|
|
215
|
+
print("An error occurred while pre-processing CSV data. Please repair the CSV directly and try again:", e)
|
|
216
|
+
|
|
217
|
+
def check_existing_patients(selected_patient_ids, MAPAT_MED_PATH):
|
|
218
|
+
existing_patients = []
|
|
219
|
+
patients_to_process = list(selected_patient_ids) # Clone the selected patient IDs list
|
|
220
|
+
|
|
221
|
+
try:
|
|
222
|
+
with open(MAPAT_MED_PATH, 'r') as file:
|
|
223
|
+
next(file) # Skip header row
|
|
224
|
+
for line in file:
|
|
225
|
+
if line.startswith("0"): # 1 is a flag for a deleted record so it would need to be re-entered.
|
|
226
|
+
patient_id = line[194:202].strip() # Extract Patient ID (Columns 195-202)
|
|
227
|
+
patient_name = line[9:39].strip() # Extract Patient Name (Columns 10-39)
|
|
228
|
+
|
|
229
|
+
if patient_id in selected_patient_ids:
|
|
230
|
+
existing_patients.append((patient_id, patient_name))
|
|
231
|
+
# Remove all occurrences of this patient_id from patients_to_process as a filter rather than .remove because
|
|
232
|
+
# then it only makes one pass and removes the first instance.
|
|
233
|
+
except FileNotFoundError:
|
|
234
|
+
# Handle the case where MAPAT_MED_PATH is not found
|
|
235
|
+
print("MAPAT.med was not found at location indicated in config file.")
|
|
236
|
+
print("Skipping existing patient check and continuing...")
|
|
237
|
+
|
|
238
|
+
# Filter out all instances of existing patient IDs
|
|
239
|
+
patients_to_process = [id for id in patients_to_process if id not in [patient[0] for patient in existing_patients]]
|
|
240
|
+
|
|
241
|
+
return existing_patients, patients_to_process
|
|
242
|
+
|
|
243
|
+
def intake_scan(csv_headers, field_mapping):
|
|
244
|
+
identified_fields = OrderedDict()
|
|
245
|
+
missing_fields_warnings = []
|
|
246
|
+
required_fields = config["required_fields"]
|
|
247
|
+
|
|
248
|
+
# Iterate over the Medisoft fields defined in field_mapping
|
|
249
|
+
for medisoft_field in field_mapping.keys():
|
|
250
|
+
for pattern in field_mapping[medisoft_field]:
|
|
251
|
+
matched_headers = [header for header in csv_headers if re.search(pattern, header, re.IGNORECASE)]
|
|
252
|
+
if matched_headers:
|
|
253
|
+
# Assuming the first matched header is the desired one
|
|
254
|
+
identified_fields[matched_headers[0]] = medisoft_field
|
|
255
|
+
break
|
|
256
|
+
else:
|
|
257
|
+
# Check if the missing field is a required field before appending the warning
|
|
258
|
+
if medisoft_field in required_fields:
|
|
259
|
+
missing_fields_warnings.append("WARNING: No matching CSV header found for Medisoft field '{0}'".format(medisoft_field))
|
|
260
|
+
|
|
261
|
+
#-----------------------
|
|
262
|
+
# CSV Integrity Check
|
|
263
|
+
#-----------------------
|
|
264
|
+
|
|
265
|
+
# This section needs to be revamped further so that it can interpret the information from here and decide
|
|
266
|
+
# if it's significant or not.
|
|
267
|
+
# e.g. If the 'Street' value:key is 'Address', then any warnings about City, State, Zip can be ignored.
|
|
268
|
+
# Insurance Policy Numbers should be all alphanumeric with no other characters.
|
|
269
|
+
# Make sure that the name field has at least one name under it (basically check for a blank or
|
|
270
|
+
# partially blank csv with just a header)
|
|
271
|
+
|
|
272
|
+
# Display the identified fields and missing fields warnings
|
|
273
|
+
#print("The following Medisoft fields have been identified in the CSV:\n")
|
|
274
|
+
#for header, medisoft_field in identified_fields.items():
|
|
275
|
+
# print("{0} (CSV header: {1})".format(medisoft_field, header))
|
|
276
|
+
|
|
277
|
+
#if missing_fields_warnings:
|
|
278
|
+
# print("\nSome required fields could not be matched:")
|
|
279
|
+
# for warning in missing_fields_warnings:
|
|
280
|
+
# print(warning)
|
|
281
|
+
|
|
282
|
+
#print("Debug - Identified fields mapping (intake scan):", identified_fields)
|
|
283
|
+
return identified_fields
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
from sys import exit
|
|
2
|
+
import ctypes
|
|
3
|
+
from ctypes import wintypes
|
|
4
|
+
import time
|
|
5
|
+
import re
|
|
6
|
+
import re #for addresses
|
|
7
|
+
from MediBot_Preprocessor import config
|
|
8
|
+
|
|
9
|
+
"""
|
|
10
|
+
User Interaction Refinements
|
|
11
|
+
- [ ] Refine the menu options for clearer user guidance during script pauses and errors.
|
|
12
|
+
- [ ] Add functionality for user to easily repeat or skip specific entries without script restart.
|
|
13
|
+
Develop more intuitive skip and retry mechanisms that are responsive to user input during data entry sessions.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
# Function to check if a specific key is pressed
|
|
17
|
+
VK_END = int(config.get('VK_END', ""), 16) # Try F12 (7B). Virtual key code for 'End' (23)
|
|
18
|
+
VK_PAUSE = int(config.get('VK_PAUSE', ""), 16) # Try F11 (7A). Virtual-key code for 'Home' (24)
|
|
19
|
+
|
|
20
|
+
MAPAT_MED_PATH = '' # Initialize global constant for MAPAT path
|
|
21
|
+
MEDISOFT_SHORTCUT = '' # Initialize global constant for LNK path
|
|
22
|
+
|
|
23
|
+
def is_key_pressed(key_code):
|
|
24
|
+
user32 = ctypes.WinDLL('user32', use_last_error=True)
|
|
25
|
+
user32.GetAsyncKeyState.restype = wintypes.SHORT
|
|
26
|
+
user32.GetAsyncKeyState.argtypes = [wintypes.INT]
|
|
27
|
+
return user32.GetAsyncKeyState(key_code) & 0x8000 != 0
|
|
28
|
+
|
|
29
|
+
def manage_script_pause(csv_data, error_message, reverse_mapping):
|
|
30
|
+
global script_paused
|
|
31
|
+
#print("Debug - Entered manage_script_pause with pause status: {}".format(script_paused))
|
|
32
|
+
user_action = 0 # initialize as 'continue'
|
|
33
|
+
|
|
34
|
+
if not script_paused and is_key_pressed(VK_PAUSE):
|
|
35
|
+
script_paused = True
|
|
36
|
+
print("Script paused. Opening menu...")
|
|
37
|
+
interaction_mode = 'normal' # Assuming normal interaction mode for script pause
|
|
38
|
+
user_action = user_interaction(csv_data, interaction_mode, error_message, reverse_mapping)
|
|
39
|
+
|
|
40
|
+
while script_paused:
|
|
41
|
+
if is_key_pressed(VK_END):
|
|
42
|
+
script_paused = False
|
|
43
|
+
print("Continuing...")
|
|
44
|
+
elif is_key_pressed(VK_PAUSE):
|
|
45
|
+
user_action = user_interaction(csv_data, 'normal', error_message, reverse_mapping)
|
|
46
|
+
time.sleep(0.1)
|
|
47
|
+
|
|
48
|
+
return user_action
|
|
49
|
+
|
|
50
|
+
# Menu Display & User Interaction
|
|
51
|
+
def display_patient_selection_menu(csv_data, reverse_mapping, proceed_as_medicare):
|
|
52
|
+
selected_patient_ids = []
|
|
53
|
+
selected_indices = []
|
|
54
|
+
|
|
55
|
+
def display_menu_header(title):
|
|
56
|
+
print("\n" + "-" * 60)
|
|
57
|
+
print(title)
|
|
58
|
+
print("-" * 60)
|
|
59
|
+
|
|
60
|
+
def display_patient_list(csv_data, reverse_mapping, medicare_filter=False, exclude_medicare=False):
|
|
61
|
+
medicare_policy_pattern = r"^[a-zA-Z0-9]{11}$" # Regex pattern for 11 alpha-numeric characters
|
|
62
|
+
primary_policy_number_header = reverse_mapping.get('Primary Policy Number', 'Primary Policy Number')
|
|
63
|
+
primary_insurance_header = reverse_mapping.get('Primary Insurance', 'Primary Insurance') # Adjust field name as needed
|
|
64
|
+
|
|
65
|
+
displayed_indices = []
|
|
66
|
+
displayed_patient_ids = []
|
|
67
|
+
|
|
68
|
+
for index, row in enumerate(csv_data):
|
|
69
|
+
policy_number = row.get(primary_policy_number_header, "")
|
|
70
|
+
primary_insurance = row.get(primary_insurance_header, "").upper()
|
|
71
|
+
|
|
72
|
+
if medicare_filter and (not re.match(medicare_policy_pattern, policy_number) or "MEDICARE" not in primary_insurance):
|
|
73
|
+
continue
|
|
74
|
+
if exclude_medicare and re.match(medicare_policy_pattern, policy_number) and "MEDICARE" in primary_insurance:
|
|
75
|
+
continue
|
|
76
|
+
|
|
77
|
+
patient_id_header = reverse_mapping['Patient ID #2']
|
|
78
|
+
patient_name_header = reverse_mapping['Patient Name']
|
|
79
|
+
patient_id = row.get(patient_id_header, "N/A")
|
|
80
|
+
patient_name = row.get(patient_name_header, "Unknown")
|
|
81
|
+
surgery_date = row.get('Surgery Date', "Unknown Date") # Access 'Surgery Date' as string directly from the row
|
|
82
|
+
|
|
83
|
+
print("{0:02d}: {3:.5s} (ID: {2}) {1} ".format(index+1, patient_name, patient_id, surgery_date))
|
|
84
|
+
|
|
85
|
+
displayed_indices.append(index)
|
|
86
|
+
displayed_patient_ids.append(patient_id)
|
|
87
|
+
|
|
88
|
+
return displayed_indices, displayed_patient_ids
|
|
89
|
+
|
|
90
|
+
if proceed_as_medicare:
|
|
91
|
+
display_menu_header("MEDICARE Patient Selection for Today's Data Entry")
|
|
92
|
+
selected_indices, selected_patient_ids = display_patient_list(csv_data, reverse_mapping, medicare_filter=True)
|
|
93
|
+
else:
|
|
94
|
+
display_menu_header("PRIVATE Patient Selection for Today's Data Entry")
|
|
95
|
+
selected_indices, selected_patient_ids = display_patient_list(csv_data, reverse_mapping, exclude_medicare=True)
|
|
96
|
+
|
|
97
|
+
print("-" * 60)
|
|
98
|
+
proceed = input("\nDo you want to proceed with the selected patients? (yes/no): ").lower().strip() in ['yes', 'y']
|
|
99
|
+
|
|
100
|
+
if not proceed:
|
|
101
|
+
display_menu_header("Patient Selection for Today's Data Entry")
|
|
102
|
+
selected_indices, selected_patient_ids = display_patient_list(csv_data, reverse_mapping)
|
|
103
|
+
print("-" * 60)
|
|
104
|
+
selection = input("\nEnter the number(s) of the patients you wish to proceed with \n(e.g., 1,3,5): ")
|
|
105
|
+
selection = selection.replace('.', ',') # Replace '.' with ',' in the user input just in case
|
|
106
|
+
selected_indices = [int(x.strip()) - 1 for x in selection.split(',')]
|
|
107
|
+
proceed = True
|
|
108
|
+
|
|
109
|
+
patient_id_header = reverse_mapping['Patient ID #2']
|
|
110
|
+
selected_patient_ids = [csv_data[i][patient_id_header] for i in selected_indices if i < len(csv_data)]
|
|
111
|
+
|
|
112
|
+
return proceed, selected_patient_ids, selected_indices
|
|
113
|
+
|
|
114
|
+
def display_menu_header(title):
|
|
115
|
+
print("\n" + "-" * 60)
|
|
116
|
+
print(title)
|
|
117
|
+
print("-" * 60)
|
|
118
|
+
|
|
119
|
+
def handle_user_interaction(interaction_mode, error_message):
|
|
120
|
+
while True:
|
|
121
|
+
# If interaction_mode is neither 'triage' nor 'error', then it's normal mode.
|
|
122
|
+
title = "Error Occurred" if interaction_mode == 'error' else "Data Entry Options"
|
|
123
|
+
display_menu_header(title)
|
|
124
|
+
|
|
125
|
+
if interaction_mode == 'error':
|
|
126
|
+
print("\nERROR: ", error_message)
|
|
127
|
+
|
|
128
|
+
# Need to tell the user which patient we're talking about because it won't be obvious anymore.
|
|
129
|
+
# Also, this ERROR might be called from a location where the menu below isn't relevant like selecting patients
|
|
130
|
+
# -- need a better way to handle that.
|
|
131
|
+
print("1: Retry last entry")
|
|
132
|
+
print("2: Skip to next patient and continue")
|
|
133
|
+
print("3: Go back two patients and redo")
|
|
134
|
+
print("4: Exit script")
|
|
135
|
+
print("-" * 60)
|
|
136
|
+
choice = input("Enter your choice (1/2/3/4): ").strip()
|
|
137
|
+
|
|
138
|
+
if choice == '1':
|
|
139
|
+
print("Selected: 'Retry last entry'. Please press 'F12' to continue.")
|
|
140
|
+
return -1
|
|
141
|
+
elif choice == '2':
|
|
142
|
+
print("Selected: 'Skip to next patient and continue'. Please press 'F12' to continue.")
|
|
143
|
+
return 1
|
|
144
|
+
elif choice == '3':
|
|
145
|
+
print("Selected: 'Go back two patients and redo'. Please press 'F12' to continue.")
|
|
146
|
+
# Returning a specific value to indicate the action of going back two patients
|
|
147
|
+
# but we might run into a problem if we stop mid-run on the first row?
|
|
148
|
+
return -2
|
|
149
|
+
elif choice == '4':
|
|
150
|
+
print("Exiting the script.")
|
|
151
|
+
exit()
|
|
152
|
+
else:
|
|
153
|
+
print("Invalid choice. Please enter a valid number.")
|
|
154
|
+
|
|
155
|
+
def user_interaction(csv_data, interaction_mode, error_message, reverse_mapping):
|
|
156
|
+
# Consider logging the actions taken during user interaction for audit purposes.
|
|
157
|
+
global MAPAT_MED_PATH, MEDISOFT_SHORTCUT # Initialize global constants
|
|
158
|
+
selected_patient_ids = []
|
|
159
|
+
selected_indices = []
|
|
160
|
+
|
|
161
|
+
if interaction_mode == 'triage':
|
|
162
|
+
|
|
163
|
+
display_menu_header(" =(^.^)= Welcome to MediBot! =(^.^)=")
|
|
164
|
+
|
|
165
|
+
while True:
|
|
166
|
+
response = input("\nAm I processing Medicare patients? (yes/no): ").lower().strip()
|
|
167
|
+
if response: # Check if the response is not empty
|
|
168
|
+
if response in ['yes', 'y']:
|
|
169
|
+
proceed_as_medicare = True
|
|
170
|
+
break
|
|
171
|
+
elif response in ['no', 'n']:
|
|
172
|
+
proceed_as_medicare = False
|
|
173
|
+
break
|
|
174
|
+
else:
|
|
175
|
+
print("Invalid entry. Please enter 'yes' or 'no'.")
|
|
176
|
+
else:
|
|
177
|
+
print("A response is required. Please try again.")
|
|
178
|
+
|
|
179
|
+
MAPAT_MED_PATH = config.get('MEDICARE_MAPAT_MED_PATH', "") if proceed_as_medicare else config.get('MAPAT_MED_PATH', "")
|
|
180
|
+
MEDISOFT_SHORTCUT = config.get('MEDICARE_SHORTCUT', "") if proceed_as_medicare else config.get('PRIVATE_SHORTCUT', "")
|
|
181
|
+
|
|
182
|
+
fixed_values = config.get('fixed_values', {}) # Get fixed values from config json
|
|
183
|
+
if proceed_as_medicare:
|
|
184
|
+
medicare_added_fixed_values = config.get('medicare_added_fixed_values', {})
|
|
185
|
+
fixed_values.update(medicare_added_fixed_values) # Add any medicare-specific fixed values from config
|
|
186
|
+
|
|
187
|
+
proceed, selected_patient_ids, selected_indices = display_patient_selection_menu(csv_data, reverse_mapping, proceed_as_medicare)
|
|
188
|
+
return proceed, selected_patient_ids, selected_indices, fixed_values
|
|
189
|
+
|
|
190
|
+
return handle_user_interaction(interaction_mode, error_message)
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
import re #for addresses
|
|
4
|
+
from MediBot_Preprocessor import open_csv_for_editing, config, initialize
|
|
5
|
+
from MediBot_UI import manage_script_pause
|
|
6
|
+
|
|
7
|
+
# Bring in all the constants
|
|
8
|
+
initialize(config)
|
|
9
|
+
|
|
10
|
+
# Format Data
|
|
11
|
+
def format_name(value):
|
|
12
|
+
if ',' in value:
|
|
13
|
+
return value
|
|
14
|
+
hyphenated_name_pattern = r'(?P<First>[\w-]+)\s+(?P<Middle>[\w-]+)?\s+(?P<Last>[\w-]+)'
|
|
15
|
+
match = re.match(hyphenated_name_pattern, value)
|
|
16
|
+
if match:
|
|
17
|
+
first_name = match.group('First')
|
|
18
|
+
middle_name = match.group('Middle') or ''
|
|
19
|
+
last_name = match.group('Last')
|
|
20
|
+
return '{}, {} {}'.format(last_name, first_name, middle_name).strip()
|
|
21
|
+
parts = value.split()
|
|
22
|
+
return '{}, {}'.format(parts[-1], ' '.join(parts[:-1]))
|
|
23
|
+
|
|
24
|
+
def format_date(value):
|
|
25
|
+
try:
|
|
26
|
+
date_obj = datetime.strptime(value, '%m/%d/%Y')
|
|
27
|
+
return date_obj.strftime('%m%d%Y')
|
|
28
|
+
except ValueError as e:
|
|
29
|
+
print("Date format error:", e)
|
|
30
|
+
return value
|
|
31
|
+
|
|
32
|
+
def format_phone(value):
|
|
33
|
+
digits = ''.join(filter(str.isdigit, value))
|
|
34
|
+
if len(digits) == 10:
|
|
35
|
+
return digits
|
|
36
|
+
print("Phone number format error: Invalid number of digits")
|
|
37
|
+
return value
|
|
38
|
+
|
|
39
|
+
def format_policy(value):
|
|
40
|
+
alphanumeric = ''.join(filter(str.isalnum, value))
|
|
41
|
+
return alphanumeric
|
|
42
|
+
|
|
43
|
+
def format_gender(value):
|
|
44
|
+
return value[0].upper()
|
|
45
|
+
|
|
46
|
+
def format_street(value, csv_data, reverse_mapping, parsed_address_components):
|
|
47
|
+
global script_paused
|
|
48
|
+
script_paused = False
|
|
49
|
+
|
|
50
|
+
# Remove periods from the input (seems to be an XP-only issue?)
|
|
51
|
+
value = value.replace('.', '')
|
|
52
|
+
|
|
53
|
+
# Only proceed with parsing if a comma is present in the value
|
|
54
|
+
if ',' in value:
|
|
55
|
+
try:
|
|
56
|
+
# Access the common cities from the loaded configuration
|
|
57
|
+
common_cities = config.get('cities', [])
|
|
58
|
+
|
|
59
|
+
# Convert cities to a case-insensitive regex pattern
|
|
60
|
+
city_pattern = '|'.join(re.escape(city) for city in common_cities)
|
|
61
|
+
city_regex_pattern = r'(?P<City>{})'.format(city_pattern)
|
|
62
|
+
city_regex = re.compile(city_regex_pattern, re.IGNORECASE)
|
|
63
|
+
|
|
64
|
+
# Check if the address contains one of the common cities
|
|
65
|
+
city_match = city_regex.search(value)
|
|
66
|
+
|
|
67
|
+
if city_match:
|
|
68
|
+
city = city_match.group('City').upper() # Normalize city name to uppercase
|
|
69
|
+
street, _, remainder = value.partition(city)
|
|
70
|
+
|
|
71
|
+
# Extract state and zip code from the remainder
|
|
72
|
+
address_pattern = r',\s*(?P<State>[A-Z]{2})\s*(?P<Zip>\d{5}(?:-\d{4})?)?'
|
|
73
|
+
|
|
74
|
+
match = re.search(address_pattern, remainder)
|
|
75
|
+
|
|
76
|
+
if match:
|
|
77
|
+
# Update global parsed address components
|
|
78
|
+
parsed_address_components['City'] = city
|
|
79
|
+
parsed_address_components['State'] = match.group('State')
|
|
80
|
+
parsed_address_components['Zip Code'] = match.group('Zip')
|
|
81
|
+
|
|
82
|
+
return street.strip() # Return formatted street address
|
|
83
|
+
else:
|
|
84
|
+
# Fallback to old regex
|
|
85
|
+
# value = street + ', ' + city + remainder
|
|
86
|
+
address_pattern = r'(?P<Street>[\w\s]+),?\s+(?P<City>[\w\s]+),\s*(?P<State>[A-Z]{2})\s*(?P<Zip>\d{5}(-\d{4})?)'
|
|
87
|
+
match = re.match(address_pattern, value)
|
|
88
|
+
|
|
89
|
+
if match:
|
|
90
|
+
# Update global parsed address components
|
|
91
|
+
parsed_address_components['City'] = match.group('City')
|
|
92
|
+
parsed_address_components['State'] = match.group('State')
|
|
93
|
+
parsed_address_components['Zip Code'] = match.group('Zip')
|
|
94
|
+
|
|
95
|
+
return match.group('Street').strip() # Return formatted street address
|
|
96
|
+
|
|
97
|
+
except Exception as e:
|
|
98
|
+
print("Address format error: Unable to parse address '{}'. Error: {}".format(value, e))
|
|
99
|
+
print("Please update the CSV file for this record.")
|
|
100
|
+
script_paused = True
|
|
101
|
+
open_csv_for_editing(CSV_FILE_PATH) # Offer to open the CSV for manual correction
|
|
102
|
+
manage_script_pause(csv_data, e, reverse_mapping)
|
|
103
|
+
return value.replace(' ', '{Space}') # Fallback to return original value with spaces escaped
|
|
104
|
+
else:
|
|
105
|
+
# If no comma is present, return the value as is, assuming it's just a street name
|
|
106
|
+
return value.replace(' ', '{Space}')
|
|
107
|
+
|
|
108
|
+
# This return acts as a fallback in case the initial comma check passes but no address components are matched
|
|
109
|
+
return value.replace(' ', '{Space}')
|
|
110
|
+
|
|
111
|
+
def format_zip(value):
|
|
112
|
+
# Ensure the value is a string, in case it's provided as an integer
|
|
113
|
+
value_str = str(value)
|
|
114
|
+
# Return only the first 5 characters of the zip code
|
|
115
|
+
return value_str[:5]
|
|
116
|
+
|
|
117
|
+
def format_data(medisoft_field, value, csv_data, reverse_mapping, parsed_address_components):
|
|
118
|
+
if medisoft_field == 'Patient Name':
|
|
119
|
+
formatted_value = format_name(value)
|
|
120
|
+
elif medisoft_field == 'Birth Date':
|
|
121
|
+
formatted_value = format_date(value)
|
|
122
|
+
elif medisoft_field == 'Phone':
|
|
123
|
+
formatted_value = format_phone(value)
|
|
124
|
+
elif medisoft_field == 'Phone #2':
|
|
125
|
+
formatted_value = format_phone(value)
|
|
126
|
+
elif medisoft_field == 'Gender':
|
|
127
|
+
formatted_value = format_gender(value)
|
|
128
|
+
elif medisoft_field == 'Street':
|
|
129
|
+
formatted_value = format_street(value, csv_data, reverse_mapping, parsed_address_components)
|
|
130
|
+
elif medisoft_field == 'Zip Code':
|
|
131
|
+
formatted_value = format_zip(value)
|
|
132
|
+
elif medisoft_field == 'Primary Policy Number':
|
|
133
|
+
formatted_value = format_policy(value)
|
|
134
|
+
elif medisoft_field == 'Secondary Policy Number':
|
|
135
|
+
formatted_value = format_policy(value)
|
|
136
|
+
elif medisoft_field == 'Primary Group Number':
|
|
137
|
+
formatted_value = format_policy(value)
|
|
138
|
+
elif medisoft_field == 'Secondary Group Number':
|
|
139
|
+
formatted_value = format_policy(value)
|
|
140
|
+
else:
|
|
141
|
+
formatted_value = value
|
|
142
|
+
|
|
143
|
+
formatted_value = formatted_value.replace(',', '{,}').replace(' ', '{Space}')
|
|
144
|
+
ahk_command = 'SendInput, {}{{Enter}}'.format(formatted_value)
|
|
145
|
+
return ahk_command
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Takes CSV from MediLink_Down.py and utilizes config and MediBot to post the CSV to Medisoft.
|
|
3
|
+
This script now also processes ERAs and responses (277CA/277A) to update claims status and finalize billing records.
|
|
4
|
+
Handles parsing and cleaning of input CSV files to ensure data accuracy and compliance with Medisoft requirements.
|
|
5
|
+
"""
|