medicafe 0.240517.0__tar.gz → 0.240613.0__tar.gz
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.240517.0 → medicafe-0.240613.0}/MediBot/MediBot.bat +9 -1
- {medicafe-0.240517.0 → medicafe-0.240613.0}/MediBot/MediBot.py +9 -36
- {medicafe-0.240517.0 → medicafe-0.240613.0}/MediBot/MediBot_Crosswalk_Library.py +1 -0
- {medicafe-0.240517.0 → medicafe-0.240613.0}/MediBot/MediBot_Preprocessor.py +26 -63
- {medicafe-0.240517.0 → medicafe-0.240613.0}/MediBot/MediBot_Preprocessor_lib.py +182 -43
- {medicafe-0.240517.0 → medicafe-0.240613.0}/MediBot/MediBot_UI.py +2 -7
- {medicafe-0.240517.0 → medicafe-0.240613.0}/MediBot/MediBot_dataformat_library.py +0 -9
- medicafe-0.240613.0/MediBot/MediBot_docx_decoder.py +295 -0
- medicafe-0.240613.0/MediBot/__init__.py +0 -0
- {medicafe-0.240517.0 → medicafe-0.240613.0}/MediLink/MediLink.py +102 -107
- {medicafe-0.240517.0 → medicafe-0.240613.0}/MediLink/MediLink_837p_encoder.py +3 -28
- {medicafe-0.240517.0 → medicafe-0.240613.0}/MediLink/MediLink_837p_encoder_library.py +14 -25
- {medicafe-0.240517.0 → medicafe-0.240613.0}/MediLink/MediLink_APIs.py +2 -0
- medicafe-0.240613.0/MediLink/MediLink_DataMgmt.py +414 -0
- medicafe-0.240613.0/MediLink/MediLink_Decoder.py +63 -0
- medicafe-0.240613.0/MediLink/MediLink_Down.py +122 -0
- medicafe-0.240613.0/MediLink/MediLink_Gmail.py +479 -0
- medicafe-0.240613.0/MediLink/MediLink_Mailer.py +0 -0
- medicafe-0.240613.0/MediLink/MediLink_Parser.py +111 -0
- medicafe-0.240613.0/MediLink/MediLink_Scan.py +0 -0
- medicafe-0.240613.0/MediLink/MediLink_Scheduler.py +3 -0
- medicafe-0.240613.0/MediLink/MediLink_StatusCheck.py +0 -0
- {medicafe-0.240517.0 → medicafe-0.240613.0}/MediLink/MediLink_UI.py +76 -18
- {medicafe-0.240517.0 → medicafe-0.240613.0}/MediLink/MediLink_Up.py +4 -14
- medicafe-0.240613.0/MediLink/__init__.py +0 -0
- {medicafe-0.240517.0/medicafe.egg-info → medicafe-0.240613.0}/PKG-INFO +1 -1
- {medicafe-0.240517.0 → medicafe-0.240613.0/medicafe.egg-info}/PKG-INFO +1 -1
- {medicafe-0.240517.0 → medicafe-0.240613.0}/medicafe.egg-info/SOURCES.txt +4 -3
- {medicafe-0.240517.0 → medicafe-0.240613.0}/medicafe.egg-info/requires.txt +2 -0
- {medicafe-0.240517.0 → medicafe-0.240613.0}/setup.py +4 -2
- medicafe-0.240517.0/MediBot/MediBot_Charges.py +0 -28
- medicafe-0.240517.0/MediBot/MediBot_docx_decoder.py +0 -80
- medicafe-0.240517.0/MediBot/MediPost.py +0 -5
- medicafe-0.240517.0/MediLink/MediLink_277_decoder.py +0 -92
- medicafe-0.240517.0/MediLink/MediLink_DataMgmt.py +0 -258
- medicafe-0.240517.0/MediLink/MediLink_Down.py +0 -128
- medicafe-0.240517.0/MediLink/MediLink_ERA_decoder.py +0 -192
- medicafe-0.240517.0/MediLink/MediLink_Gmail.py +0 -100
- medicafe-0.240517.0/MediLink/MediLink_Mailer.py +0 -7
- medicafe-0.240517.0/MediLink/MediLink_Scheduler.py +0 -173
- medicafe-0.240517.0/MediLink/MediLink_StatusCheck.py +0 -4
- {medicafe-0.240517.0 → medicafe-0.240613.0}/LICENSE +0 -0
- {medicafe-0.240517.0 → medicafe-0.240613.0}/MANIFEST.in +0 -0
- /medicafe-0.240517.0/MediBot/__init__.py → /medicafe-0.240613.0/MediBot/MediBot_Charges.py +0 -0
- /medicafe-0.240517.0/MediLink/__init__.py → /medicafe-0.240613.0/MediBot/MediBot_Post.py +0 -0
- {medicafe-0.240517.0 → medicafe-0.240613.0}/MediBot/PDF_to_CSV_Cleaner.py +0 -0
- {medicafe-0.240517.0 → medicafe-0.240613.0}/MediBot/update_json.py +0 -0
- {medicafe-0.240517.0 → medicafe-0.240613.0}/MediBot/update_medicafe.py +0 -0
- {medicafe-0.240517.0 → medicafe-0.240613.0}/MediLink/MediLink_API_v2.py +0 -0
- {medicafe-0.240517.0 → medicafe-0.240613.0}/MediLink/MediLink_ConfigLoader.py +0 -0
- {medicafe-0.240517.0 → medicafe-0.240613.0}/MediLink/MediLink_batch.bat +0 -0
- {medicafe-0.240517.0 → medicafe-0.240613.0}/MediLink/Soumit_api.py +0 -0
- {medicafe-0.240517.0 → medicafe-0.240613.0}/MediLink/test.py +0 -0
- {medicafe-0.240517.0 → medicafe-0.240613.0}/README.md +0 -0
- {medicafe-0.240517.0 → medicafe-0.240613.0}/medicafe.egg-info/dependency_links.txt +0 -0
- {medicafe-0.240517.0 → medicafe-0.240613.0}/medicafe.egg-info/not-zip-safe +0 -0
- {medicafe-0.240517.0 → medicafe-0.240613.0}/medicafe.egg-info/top_level.txt +0 -0
- {medicafe-0.240517.0 → medicafe-0.240613.0}/setup.cfg +0 -0
|
@@ -133,6 +133,7 @@ if "!internet_available!"=="0" (
|
|
|
133
133
|
goto main_menu
|
|
134
134
|
)
|
|
135
135
|
echo Downloading emails...
|
|
136
|
+
:: move this path.
|
|
136
137
|
py "../MediLink/MediLink_Gmail.py" "%firefox_path%"
|
|
137
138
|
if errorlevel 1 (
|
|
138
139
|
echo Failed to download emails.
|
|
@@ -161,6 +162,7 @@ if "!internet_available!"=="0" (
|
|
|
161
162
|
)
|
|
162
163
|
call :process_csvs
|
|
163
164
|
cls
|
|
165
|
+
:: move this path.
|
|
164
166
|
py "C:\Python34\Lib\site-packages\MediLink\MediLink.py"
|
|
165
167
|
if errorlevel 1 echo MediLink failed to execute.
|
|
166
168
|
pause
|
|
@@ -173,7 +175,13 @@ for /f "tokens=1-5 delims=/: " %%a in ('echo %time%') do (
|
|
|
173
175
|
set "minute=%%b"
|
|
174
176
|
set "second=%%c"
|
|
175
177
|
)
|
|
176
|
-
|
|
178
|
+
for /f "tokens=2-4 delims=/ " %%a in ('echo %date%') do (
|
|
179
|
+
set "day=%%a"
|
|
180
|
+
set "month=%%b"
|
|
181
|
+
set "year=%%c"
|
|
182
|
+
)
|
|
183
|
+
set "timestamp=!year!!month!!day!_!hour!!minute!"
|
|
184
|
+
|
|
177
185
|
set "latest_csv="
|
|
178
186
|
for /f "delims=" %%a in ('dir /b /a-d /o-d "%source_folder%\*.csv" 2^>nul') do (
|
|
179
187
|
set "latest_csv=%%a"
|
|
@@ -22,41 +22,6 @@ except ImportError:
|
|
|
22
22
|
from MediBot import MediBot_Crosswalk_Library
|
|
23
23
|
crosswalk_update = MediBot_Crosswalk_Library.crosswalk_update
|
|
24
24
|
|
|
25
|
-
"""
|
|
26
|
-
# Development Task List for MediBot
|
|
27
|
-
|
|
28
|
-
Error Handling Improvements
|
|
29
|
-
- [ ] Its really difficult to get out of the main menu if you go open MediBot by accident
|
|
30
|
-
- [ ] Develop a centralized error handling and logging mechanism for improved troubleshooting.
|
|
31
|
-
- [ ] Implement validation checks during patient data entry to prevent submission of incomplete or incorrect records.
|
|
32
|
-
|
|
33
|
-
Insurance Mode Adjustments
|
|
34
|
-
- [ ] Integrate a comprehensive list of insurance company codes for automatic selection.
|
|
35
|
-
- [ ] Automate insurance-specific data entry adjustments, such as character replacements specific to Medicare.
|
|
36
|
-
|
|
37
|
-
Diagnosis Entry
|
|
38
|
-
- [ ] Automate data extraction from Surgery Schedule to import to CSV a column indicating diagnosis code per px.
|
|
39
|
-
|
|
40
|
-
Script Efficiency and Reliability
|
|
41
|
-
- [ ] Ensure robust handling of Medisoft's field navigation quirks, particularly for fields that are skipped or require special access.
|
|
42
|
-
|
|
43
|
-
Documentation and Support
|
|
44
|
-
- [ ] Create detailed documentation for setup, configuration, and usage of the script.
|
|
45
|
-
- [ ] Establish a support channel for users to report issues or request features.
|
|
46
|
-
|
|
47
|
-
Future Directions
|
|
48
|
-
- [ ] Consider developing a graphical user interface (GUI) for non-technical users for easier script management and execution.
|
|
49
|
-
|
|
50
|
-
Medisoft Field Navigation:
|
|
51
|
-
Investigate and optimize navigation for fields that Medisoft skips or requires backward navigation to access.
|
|
52
|
-
|
|
53
|
-
Insurance Mode Features:
|
|
54
|
-
Evaluate the feasibility and implement the use of the F6 search for insurance address verification, enhancing user verification processes.
|
|
55
|
-
|
|
56
|
-
Error Handling and Logging:
|
|
57
|
-
Implement a check for AHK script execution status, providing feedback or troubleshooting steps if the script encounters issues.
|
|
58
|
-
"""
|
|
59
|
-
|
|
60
25
|
def identify_field(header, field_mapping):
|
|
61
26
|
for medisoft_field, patterns in field_mapping.items():
|
|
62
27
|
for pattern in patterns:
|
|
@@ -223,7 +188,7 @@ class ExecutionState:
|
|
|
223
188
|
MediLink_ConfigLoader.log("Constants initialized...")
|
|
224
189
|
|
|
225
190
|
except Exception as e:
|
|
226
|
-
print("Failed to load or update configuration: {}".format(e))
|
|
191
|
+
print("MediBot: Failed to load or update configuration: {}".format(e))
|
|
227
192
|
raise # Re-throwing the exception or using a more sophisticated error handling mechanism might be needed
|
|
228
193
|
# Handle the exception somehow (e.g., retry, halt, log)??
|
|
229
194
|
|
|
@@ -261,6 +226,7 @@ if __name__ == "__main__":
|
|
|
261
226
|
MediLink_ConfigLoader.log("Pre-processing CSV Data...", level="INFO")
|
|
262
227
|
MediBot_Preprocessor.preprocess_csv_data(csv_data, e_state.crosswalk)
|
|
263
228
|
headers = csv_data[0].keys() # Make sure all the headers are in place
|
|
229
|
+
# Patient charges should be added prior to the intake_scan.
|
|
264
230
|
|
|
265
231
|
print("Performing Intake Scan...")
|
|
266
232
|
MediLink_ConfigLoader.log("Performing Intake Scan...", level="INFO")
|
|
@@ -294,9 +260,12 @@ if __name__ == "__main__":
|
|
|
294
260
|
|
|
295
261
|
if existing_patients:
|
|
296
262
|
print("\nNOTE: The following patient(s) already EXIST in the system and \n will be excluded from processing:")
|
|
263
|
+
# TODO ... not excluded anymore, because charges may need to be added.
|
|
264
|
+
# So at this point in the processing, we should have already processed the surgery schedules and enriched the data with Charges.
|
|
297
265
|
for patient_id, patient_name in existing_patients:
|
|
298
266
|
print("(ID: {0}) {1}".format(patient_id, patient_name))
|
|
299
267
|
# Update csv_data to exclude existing patients
|
|
268
|
+
# TODO This now has to be updated to handle patients that exist but need new charges added.
|
|
300
269
|
csv_data = [row for row in csv_data if row[reverse_mapping['Patient ID #2']] in patients_to_process]
|
|
301
270
|
else:
|
|
302
271
|
print("\nSelected patient(s) are NEW patients and will be processed.")
|
|
@@ -306,6 +275,10 @@ if __name__ == "__main__":
|
|
|
306
275
|
else:
|
|
307
276
|
proceed = input("\nDo you want to proceed with the {} remaining patient(s)? (yes/no): ".format(len(patients_to_process))).lower().strip() in ['yes', 'y']
|
|
308
277
|
|
|
278
|
+
# TODO Here is where we need to add the step where we move to MediBot_Charges.
|
|
279
|
+
# The return is an enriched dataset to be picked up by medibot which means we need to return:
|
|
280
|
+
# csv_data, field_mapping, reverse_mapping, and fixed_values.
|
|
281
|
+
|
|
309
282
|
if proceed:
|
|
310
283
|
|
|
311
284
|
# Would be nice to have some kind of self-test here.
|
|
@@ -167,6 +167,7 @@ def crosswalk_update(config, crosswalk):
|
|
|
167
167
|
|
|
168
168
|
# Load incremental mapping data from Z.dat (Patient ID to Insurance Name)
|
|
169
169
|
# TODO This may be a redundant approach?
|
|
170
|
+
# This is a singular path. This is fine though because any time we process a Z.DAT we'd have the crosswalk incremented.
|
|
170
171
|
patient_id_to_insurance_name = MediBot_Preprocessor_lib.parse_z_dat(config['MediLink_Config']['Z_DAT_PATH'], config['MediLink_Config'])
|
|
171
172
|
MediLink_ConfigLoader.log("Parsed Z data...")
|
|
172
173
|
|
|
@@ -20,61 +20,6 @@ try:
|
|
|
20
20
|
except ImportError:
|
|
21
21
|
from MediBot import MediBot_Preprocessor_lib
|
|
22
22
|
|
|
23
|
-
"""
|
|
24
|
-
Preprocessing Enhancements
|
|
25
|
-
- [X] Preprocess Insurance Policy Numbers and Group Numbers to replace '-' with ''.
|
|
26
|
-
- [X] De-duplicate entries in the CSV and only entering the px once even if they show up twice in the file.
|
|
27
|
-
- [ ] Implement dynamic field combination in CSV pre-processing for flexibility with various CSV formats.
|
|
28
|
-
- [ ] Enhance SSN cleaning logic to handle more variations of sensitive data masking.
|
|
29
|
-
- [ ] Optimize script startup and CSV loading to reduce initial latency.
|
|
30
|
-
|
|
31
|
-
Data Integrity and Validation
|
|
32
|
-
- [ ] Conduct a thorough CSV integrity check before processing to flag potential issues upfront.
|
|
33
|
-
- [ ] Implement a mechanism to confirm the accuracy of entered data, potentially through a verification step or summary report.
|
|
34
|
-
- [ ] Explore the possibility of integrating direct database queries for existing patient checks to streamline the process.
|
|
35
|
-
- [ ] Automate the replacement of spaces with underscores ('_') in last names for Medicare entries.
|
|
36
|
-
- [ ] Enhance CSV integrity checks to identify and report potential issues with data format, especially concerning insurance policy numbers and special character handling.
|
|
37
|
-
|
|
38
|
-
Known Issues and Bugs
|
|
39
|
-
- [ ] Address the handling of '.' and other special characters that may disrupt parsing, especially under Windows XP.
|
|
40
|
-
- [ ] Investigate the issue with Excel modifying long policy numbers in the CSV and provide guidance or a workaround.
|
|
41
|
-
|
|
42
|
-
Future Work
|
|
43
|
-
- [X] Check for PatientID number in La Forma Z to link back to Carol's table for mapping Medisoft insurance name to payerID and payer name and address.
|
|
44
|
-
- [X] Check for PatientID to Medisoft custom insurance name mapping in MAPAT.
|
|
45
|
-
- [X] Middle Names should all be single letters. Make sure it gets truncated before submitting.
|
|
46
|
-
- [ ] 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.
|
|
47
|
-
- [ ] Implement logic to verify and match Patient IDs across different files to ensure data integrity before consolidation. (Catching errors between source data)
|
|
48
|
-
- [ ] Optimize the preprocessing of surgery dates and diagnosis codes for use in patient billing and scheduling systems.
|
|
49
|
-
- [ ] Read Surgery Schedule doc and parse out a Patient ID : Diagnosis Code table.
|
|
50
|
-
- [ ] The Minutes & Cancellation data with logic to consolidate into one table in memory.
|
|
51
|
-
- [ ] Dynamically list the endpoint for a new Payer ID via API or user interaction to update the crosswalk.json efficiently.
|
|
52
|
-
- [ ] Pull listed addresses of insurance from the CSV. (Not really necessary)
|
|
53
|
-
- [ ] Retroactively learn Medisoft insurance name and payerID from the provided data sources.
|
|
54
|
-
|
|
55
|
-
Development Roadmap for crosswalk_update():
|
|
56
|
-
- [X] Automation required for updating the crosswalk.json when new Medisoft insurance is discovered.
|
|
57
|
-
- [X] New Medisoft insurances are identified based on the payer ID number.
|
|
58
|
-
- [X] Check the existence of the payer ID in crosswalk.json under existing endpoints.
|
|
59
|
-
- [X] Facilitate grouping of IDs for insurances like CIGNA with multiple addresses but few payer IDs.
|
|
60
|
-
- [X] Retroactive learning based on selected insurances in Medisoft
|
|
61
|
-
- [ ] Prompt user via endpoint APIs to add new payer ID to an endpoint if it does not exist.
|
|
62
|
-
- [ ] Retain payer IDs without Insurance ID for future assignments.
|
|
63
|
-
- [ ] Check for free payer IDs and determine the appropriate endpoint for assignment.
|
|
64
|
-
- [ ] Present unrecognized payer IDs with Carol's Insurance Name to users for assignment to Insurance ID. (Try API Call)
|
|
65
|
-
- [ ] Integrate API checks to verify payer ID availability and related information.
|
|
66
|
-
- [ ] Implement "Fax/Mail or Other" endpoint for unavailable payer IDs.
|
|
67
|
-
- [ ] Present user with a list of top insurances for selection based on fuzzy search scores.
|
|
68
|
-
- [ ] Establish payer ID to insurance ID relationship based on user selection.
|
|
69
|
-
- [ ] Implicitly establish payer ID to endpoint mapping based on user selection.
|
|
70
|
-
- [ ] Implement validation mechanisms to prevent incorrect mappings and ensure data integrity.
|
|
71
|
-
- [ ] Considerations for extracting insurance addresses (if necessary)
|
|
72
|
-
- [ ] Handle better the case where a payer_id doesn't exist (When Carol's CSV doesn't bring the Payer ID).
|
|
73
|
-
Maybe ask the user what the payer ID is for that patient? I dont know.
|
|
74
|
-
- [ ] TODO (MED) Crosswalk (both initializing and updating) needs to pull AFTER the Preprocessor for Carol's CSV because
|
|
75
|
-
all that data lives in-memory and then gets corrections or replacements before being used so we need
|
|
76
|
-
the post-correction data to be used to build and update the crosswalk.
|
|
77
|
-
"""
|
|
78
23
|
# Load configuration
|
|
79
24
|
# Should this also take args? Path for ./MediLink needed to be added for this to resolve
|
|
80
25
|
config, crosswalk = MediLink_ConfigLoader.load_configuration()
|
|
@@ -82,10 +27,11 @@ config, crosswalk = MediLink_ConfigLoader.load_configuration()
|
|
|
82
27
|
# CSV Preprocessor built for Carol
|
|
83
28
|
def preprocess_csv_data(csv_data, crosswalk):
|
|
84
29
|
try:
|
|
85
|
-
# Add the "Ins1 Insurance ID"
|
|
86
|
-
# This initializes the
|
|
87
|
-
|
|
88
|
-
|
|
30
|
+
# Add the "Ins1 Insurance ID" and "Default Diagnosis #1" columns to the CSV data.
|
|
31
|
+
# This initializes the columns with empty values for each row.
|
|
32
|
+
columns_to_add = ['Ins1 Insurance ID', 'Default Diagnosis #1', 'Procedure Code', 'Minutes', 'Amount']
|
|
33
|
+
MediLink_ConfigLoader.log("CSV Pre-processor: Initializing empty columns to the CSV data...", level="INFO")
|
|
34
|
+
MediBot_Preprocessor_lib.add_columns(csv_data, columns_to_add)
|
|
89
35
|
|
|
90
36
|
# Filter out rows without a Patient ID and rows where the Primary Insurance
|
|
91
37
|
# is 'AETNA', 'AETNA MEDICARE', or 'HUMANA MED HMO'.
|
|
@@ -93,13 +39,20 @@ def preprocess_csv_data(csv_data, crosswalk):
|
|
|
93
39
|
MediBot_Preprocessor_lib.filter_rows(csv_data)
|
|
94
40
|
|
|
95
41
|
# Convert 'Surgery Date' from string format to datetime objects for sorting purposes.
|
|
96
|
-
|
|
97
|
-
|
|
42
|
+
MediBot_Preprocessor_lib.convert_surgery_date(csv_data)
|
|
43
|
+
|
|
98
44
|
# Update the CSV data to include only unique patient records.
|
|
99
45
|
# Re-sort the CSV data after deduplication to ensure the correct order.
|
|
46
|
+
# Sort the patients by 'Surgery Date' and then by 'Patient Last' name alphabetically.
|
|
47
|
+
# Deduplicate patient records based on Patient ID, keeping the entry with the earliest surgery date.
|
|
100
48
|
MediLink_ConfigLoader.log("CSV Pre-processor: Sorting and de-duplicating patient records...", level="INFO")
|
|
101
|
-
MediBot_Preprocessor_lib.
|
|
102
|
-
|
|
49
|
+
MediBot_Preprocessor_lib.sort_and_deduplicate(csv_data)
|
|
50
|
+
# TODO This eventually needs to be handled differently because now we're wanting to handle both surgery dates.
|
|
51
|
+
# Instead of deleting, maybe we need to make a secondary dataset or some kind of flag or isolate here where
|
|
52
|
+
# MediBot knows to skip it when entering the patient data but is ready to put Charges for the second surgery date.
|
|
53
|
+
# MediLink_Scheduler will have a dictionary persist somewhere that would tell us which patients were billed
|
|
54
|
+
# and which haven't been yet. So, if the patient 'exists' in the system, the next quetion is about claims/billing status.
|
|
55
|
+
# Eventually, we really want to get out of Medisoft...
|
|
103
56
|
|
|
104
57
|
# Convert 'Surgery Date' back to string format if needed for further processing.
|
|
105
58
|
# Combine 'Patient First', 'Patient Middle', and 'Patient Last' into a single 'Patient Name' field.
|
|
@@ -117,6 +70,16 @@ def preprocess_csv_data(csv_data, crosswalk):
|
|
|
117
70
|
# If the Payer ID is not found in the crosswalk, create a placeholder entry in the crosswalk and mark the row for review.
|
|
118
71
|
MediLink_ConfigLoader.log("CSV Pre-processor: Populating 'Ins1 Insurance ID' based on Crosswalk...", level="INFO")
|
|
119
72
|
MediBot_Preprocessor_lib.update_insurance_ids(csv_data, crosswalk)
|
|
73
|
+
|
|
74
|
+
# Enrich the "Default Diagnosis #1" column based on the parsed docx for each row.
|
|
75
|
+
# This needs to handle the different patient dates correctly so we get the right diagnosis code assigned to the right patient on the right date of service.
|
|
76
|
+
# Currently, we've deleted all the second date entries for patients. As long as they exist in the system, they're just deleted.
|
|
77
|
+
MediLink_ConfigLoader.log("CSV Pre-processor: Populating 'Default Diagnosis #1' based on Surgery Schedule and Crosswalk...", level="INFO")
|
|
78
|
+
MediBot_Preprocessor_lib.update_diagnosis_codes(csv_data)
|
|
79
|
+
|
|
80
|
+
# Enrich the procedure code column based on the diagnosis code for each patient.
|
|
81
|
+
MediLink_ConfigLoader.log("CSV Pre-processor: Populating 'Procedure Code' based on Crosswalk...", level="INFO")
|
|
82
|
+
MediBot_Preprocessor_lib.update_procedure_codes(csv_data)
|
|
120
83
|
|
|
121
84
|
except Exception as e:
|
|
122
85
|
message = "An error occurred while pre-processing CSV data. Please repair the CSV directly and try again: {}".format(e)
|
|
@@ -17,37 +17,13 @@ except ImportError:
|
|
|
17
17
|
|
|
18
18
|
try:
|
|
19
19
|
from MediBot_UI import app_control
|
|
20
|
+
from MediBot_docx_decoder import parse_docx
|
|
20
21
|
except ImportError:
|
|
21
22
|
from MediBot import MediBot_UI
|
|
22
23
|
app_control = MediBot_UI.app_control
|
|
24
|
+
from MediBot import MediBot_docx_decoder
|
|
25
|
+
parse_docx = MediBot_docx_decoder.parse_docx
|
|
23
26
|
|
|
24
|
-
"""
|
|
25
|
-
Draft Docstring to move over from Preprocessor.
|
|
26
|
-
|
|
27
|
-
Data Integrity and Validation
|
|
28
|
-
Implement a mechanism to confirm the accuracy of entered data, potentially through a verification step or summary report.
|
|
29
|
-
Enhance CSV integrity checks to identify and report potential issues with data format, especially concerning insurance policy numbers and special character handling.
|
|
30
|
-
|
|
31
|
-
Development Roadmap for crosswalk_update()
|
|
32
|
-
Automation required for updating the crosswalk.json when new Medisoft insurance is discovered.
|
|
33
|
-
For open_csv_for_editing
|
|
34
|
-
|
|
35
|
-
Known Issues and Bugs
|
|
36
|
-
Address the handling of '.' and other special characters that may disrupt parsing, especially under Windows XP.
|
|
37
|
-
|
|
38
|
-
For load_csv_data
|
|
39
|
-
|
|
40
|
-
Preprocessing Enhancements
|
|
41
|
-
Optimize script startup and CSV loading to reduce initial latency.
|
|
42
|
-
|
|
43
|
-
Data Integrity and Validation
|
|
44
|
-
Conduct a thorough CSV integrity check before processing to flag potential issues upfront.
|
|
45
|
-
|
|
46
|
-
Future Work
|
|
47
|
-
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.
|
|
48
|
-
Implement logic to verify and match Patient IDs across different files to ensure data integrity before consolidation. (Catching errors between source data)
|
|
49
|
-
Optimize the preprocessing of surgery dates and diagnosis codes for use in patient billing and scheduling systems.
|
|
50
|
-
"""
|
|
51
27
|
class InitializationError(Exception):
|
|
52
28
|
def __init__(self, message):
|
|
53
29
|
self.message = message
|
|
@@ -105,13 +81,32 @@ def load_csv_data(csv_file_path):
|
|
|
105
81
|
sys.exit(1) # Halt the script in case of other IO errors
|
|
106
82
|
|
|
107
83
|
# CSV Pre-processor Helper functions
|
|
108
|
-
def
|
|
84
|
+
def add_columns(csv_data, column_headers):
|
|
85
|
+
"""
|
|
86
|
+
Adds one or multiple columns to the CSV data.
|
|
87
|
+
|
|
88
|
+
Parameters:
|
|
89
|
+
csv_data (list of dict): The CSV data where each row is represented as a dictionary.
|
|
90
|
+
column_headers (list of str or str): A list of column headers to be added to each row, or a single column header.
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
None: The function modifies the csv_data in place.
|
|
94
|
+
"""
|
|
95
|
+
if isinstance(column_headers, str):
|
|
96
|
+
column_headers = [column_headers]
|
|
97
|
+
elif not isinstance(column_headers, list):
|
|
98
|
+
raise ValueError("column_headers should be a list or a string")
|
|
99
|
+
|
|
109
100
|
for row in csv_data:
|
|
110
|
-
|
|
101
|
+
for header in column_headers:
|
|
102
|
+
row[header] = '' # Initialize the column with empty values
|
|
111
103
|
|
|
104
|
+
# Extracting the list to a variable for future refactoring:
|
|
112
105
|
def filter_rows(csv_data):
|
|
106
|
+
# TODO This should go to the crosswalk.
|
|
107
|
+
excluded_insurance = ['AETNA', 'AETNA MEDICARE', 'HUMANA MED HMO']
|
|
113
108
|
csv_data[:] = [row for row in csv_data if row.get('Patient ID', '').strip()]
|
|
114
|
-
csv_data[:] = [row for row in csv_data if row.get('Primary Insurance', '').strip() not in
|
|
109
|
+
csv_data[:] = [row for row in csv_data if row.get('Primary Insurance', '').strip() not in excluded_insurance]
|
|
115
110
|
|
|
116
111
|
def convert_surgery_date(csv_data):
|
|
117
112
|
for row in csv_data:
|
|
@@ -121,6 +116,7 @@ def convert_surgery_date(csv_data):
|
|
|
121
116
|
row['Surgery Date'] = datetime.min # Assign a minimum datetime value for sorting purposes
|
|
122
117
|
|
|
123
118
|
def sort_and_deduplicate(csv_data):
|
|
119
|
+
# TODO we need to figure out a new logic here for doing second-eye charges. I don't know what the flow should be yet.
|
|
124
120
|
csv_data.sort(key=lambda x: (x['Surgery Date'], x.get('Patient Last', '').strip()))
|
|
125
121
|
unique_patients = {}
|
|
126
122
|
for row in csv_data:
|
|
@@ -128,6 +124,8 @@ def sort_and_deduplicate(csv_data):
|
|
|
128
124
|
if patient_id not in unique_patients or row['Surgery Date'] < unique_patients[patient_id]['Surgery Date']:
|
|
129
125
|
unique_patients[patient_id] = row
|
|
130
126
|
csv_data[:] = list(unique_patients.values())
|
|
127
|
+
# TODO Sorting, now that we're going to have the Surgery Schedules available, should be ordered as the patients show up on the schedule.
|
|
128
|
+
# If we don't have that surgery schedule yet for some reason, we should default to the current ordering strategy.
|
|
131
129
|
csv_data.sort(key=lambda x: (x['Surgery Date'], x.get('Patient Last', '').strip()))
|
|
132
130
|
|
|
133
131
|
def combine_fields(csv_data):
|
|
@@ -177,6 +175,149 @@ def update_insurance_ids(csv_data, crosswalk):
|
|
|
177
175
|
'endpoint': 'OPTUMEDI' # Default probably should be a flag for the crosswalk update function to deal with. BUG HARDCODE THERE ARE 3 of these defaults
|
|
178
176
|
}
|
|
179
177
|
|
|
178
|
+
def update_procedure_codes(csv_data):
|
|
179
|
+
# Define the Medisoft shorthand to diagnostic codes dictionary
|
|
180
|
+
# TODO The reverse of this will be in the crosswalk. We'll need to reverse it here for lookup.
|
|
181
|
+
medisoft_to_diagnosis = {
|
|
182
|
+
"25811": "H25.811",
|
|
183
|
+
"25812": "H25.812",
|
|
184
|
+
"2512": "H25.12",
|
|
185
|
+
"2511": "H25.11",
|
|
186
|
+
"529XA": "T85.29XA",
|
|
187
|
+
"4301": "H43.01",
|
|
188
|
+
"4302": "H43.02",
|
|
189
|
+
"011X2": "H40.11X2",
|
|
190
|
+
"051X3": "H40.51X3",
|
|
191
|
+
"5398A": "T85.398A"
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
# Define the procedure codes to diagnostic codes dictionary
|
|
195
|
+
procedure_to_diagnosis = {
|
|
196
|
+
"00142": ["H25.811", "H25.812", "H25.12", "H25.11", "T85.29XA"],
|
|
197
|
+
"00145": ["H43.01", "H43.02"],
|
|
198
|
+
"00140": ["H40.11X2", "H40.51X3"]
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
# Reverse the dictionary for easier lookup from diagnostic code to procedure code
|
|
202
|
+
diagnosis_to_procedure = {}
|
|
203
|
+
for procedure_code, diagnosis_codes in procedure_to_diagnosis.items():
|
|
204
|
+
for diagnosis_code in diagnosis_codes:
|
|
205
|
+
diagnosis_to_procedure[diagnosis_code] = procedure_code
|
|
206
|
+
|
|
207
|
+
# Initialize counter for updated rows
|
|
208
|
+
updated_count = 0
|
|
209
|
+
|
|
210
|
+
# Update the "Procedure Code" column in the CSV data
|
|
211
|
+
for row_num, row in enumerate(csv_data, start=1):
|
|
212
|
+
try:
|
|
213
|
+
medisoft_code = row.get('Default Diagnosis #1', '').strip()
|
|
214
|
+
diagnosis_code = medisoft_to_diagnosis.get(medisoft_code)
|
|
215
|
+
if diagnosis_code:
|
|
216
|
+
procedure_code = diagnosis_to_procedure.get(diagnosis_code)
|
|
217
|
+
if procedure_code:
|
|
218
|
+
row['Procedure Code'] = procedure_code
|
|
219
|
+
updated_count += 1
|
|
220
|
+
else:
|
|
221
|
+
row['Procedure Code'] = "Unknown" # Or handle as appropriate
|
|
222
|
+
else:
|
|
223
|
+
row['Procedure Code'] = "Unknown" # Or handle as appropriate
|
|
224
|
+
except Exception as e:
|
|
225
|
+
MediLink_ConfigLoader.log("In update_procedure_codes, Error processing row {}: {}".format(row_num, e), level="ERROR")
|
|
226
|
+
|
|
227
|
+
# Log total count of updated rows
|
|
228
|
+
MediLink_ConfigLoader.log("Total {} 'Procedure Code' rows updated.".format(updated_count), level="INFO")
|
|
229
|
+
|
|
230
|
+
return True
|
|
231
|
+
|
|
232
|
+
def update_diagnosis_codes(csv_data):
|
|
233
|
+
try:
|
|
234
|
+
# Load configuration and crosswalk
|
|
235
|
+
config, _ = MediLink_ConfigLoader.load_configuration()
|
|
236
|
+
|
|
237
|
+
# Extract the local storage path from the configuration
|
|
238
|
+
local_storage_path = config['MediLink_Config']['local_storage_path']
|
|
239
|
+
|
|
240
|
+
# Initialize a dictionary to hold diagnosis codes from all DOCX files
|
|
241
|
+
all_patient_data = {}
|
|
242
|
+
|
|
243
|
+
# Iterate through all files in the specified directory
|
|
244
|
+
for filename in os.listdir(local_storage_path):
|
|
245
|
+
if filename.endswith(".docx"):
|
|
246
|
+
filepath = os.path.join(local_storage_path, filename)
|
|
247
|
+
MediLink_ConfigLoader.log("Processing DOCX file: {}".format(filepath), level="INFO")
|
|
248
|
+
try:
|
|
249
|
+
patient_data = parse_docx(filepath)
|
|
250
|
+
for patient_id, service_dates in patient_data.items():
|
|
251
|
+
if patient_id not in all_patient_data:
|
|
252
|
+
all_patient_data[patient_id] = {}
|
|
253
|
+
for date_of_service, diagnosis_data in service_dates.items():
|
|
254
|
+
all_patient_data[patient_id][date_of_service] = diagnosis_data
|
|
255
|
+
except Exception as e:
|
|
256
|
+
MediLink_ConfigLoader.log("Error parsing DOCX file {}: {}".format(filepath, e), level="ERROR")
|
|
257
|
+
|
|
258
|
+
# Debug logging for all_patient_data
|
|
259
|
+
MediLink_ConfigLoader.log("All patient data collected from DOCX files: {}".format(all_patient_data), level="INFO")
|
|
260
|
+
|
|
261
|
+
# Define the diagnosis to Medisoft shorthand dictionary
|
|
262
|
+
diagnosis_to_medisoft = {
|
|
263
|
+
"H25.811": "25811",
|
|
264
|
+
"H25.812": "25812",
|
|
265
|
+
"H25.12": "2512",
|
|
266
|
+
"H25.11": "2511",
|
|
267
|
+
"T85.29XA": "529XA",
|
|
268
|
+
"H43.01": "4301",
|
|
269
|
+
"H43.02": "4302",
|
|
270
|
+
"H40.11X2": "011X2",
|
|
271
|
+
"H40.51X3": "051X3",
|
|
272
|
+
"T85.398A": "5398A"
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
# Convert surgery dates in CSV data
|
|
276
|
+
convert_surgery_date(csv_data)
|
|
277
|
+
|
|
278
|
+
# Initialize counter for updated rows
|
|
279
|
+
updated_count = 0
|
|
280
|
+
|
|
281
|
+
# Update the "Default Diagnosis #1" column in the CSV data
|
|
282
|
+
for row_num, row in enumerate(csv_data, start=1):
|
|
283
|
+
MediLink_ConfigLoader.log("Processing row number {}.".format(row_num), level="INFO")
|
|
284
|
+
patient_id = row.get('Patient ID', '').strip()
|
|
285
|
+
surgery_date = row.get('Surgery Date', '')
|
|
286
|
+
|
|
287
|
+
# Convert surgery_date to string format for lookup
|
|
288
|
+
if surgery_date != datetime.min:
|
|
289
|
+
surgery_date_str = surgery_date.strftime("%m-%d-%Y")
|
|
290
|
+
else:
|
|
291
|
+
surgery_date_str = ''
|
|
292
|
+
|
|
293
|
+
MediLink_ConfigLoader.log("Patient ID: {}, Surgery Date: {}".format(patient_id, surgery_date_str), level="INFO")
|
|
294
|
+
|
|
295
|
+
if patient_id in all_patient_data:
|
|
296
|
+
if surgery_date_str in all_patient_data[patient_id]:
|
|
297
|
+
diagnosis_code, left_or_right_eye, femto_yes_or_no = all_patient_data[patient_id][surgery_date_str]
|
|
298
|
+
MediLink_ConfigLoader.log("Found diagnosis data for Patient ID: {}, Surgery Date: {}".format(patient_id, surgery_date_str), level="INFO")
|
|
299
|
+
|
|
300
|
+
# Convert diagnosis code to Medisoft shorthand format.
|
|
301
|
+
defaulted_code = diagnosis_code[1:].replace('.', '')[-5:] if diagnosis_code else ''
|
|
302
|
+
medisoft_shorthand = diagnosis_to_medisoft.get(diagnosis_code, defaulted_code)
|
|
303
|
+
MediLink_ConfigLoader.log("Converted diagnosis code to Medisoft shorthand: {}".format(medisoft_shorthand), level="INFO")
|
|
304
|
+
|
|
305
|
+
row['Default Diagnosis #1'] = medisoft_shorthand
|
|
306
|
+
updated_count += 1
|
|
307
|
+
MediLink_ConfigLoader.log("Updated row number {} with new diagnosis code.".format(row_num), level="INFO")
|
|
308
|
+
else:
|
|
309
|
+
MediLink_ConfigLoader.log("No matching surgery date found for Patient ID: {} in row {}.".format(patient_id, row_num), level="INFO")
|
|
310
|
+
else:
|
|
311
|
+
MediLink_ConfigLoader.log("Patient ID: {} not found in DOCX data for row {}.".format(patient_id, row_num), level="INFO")
|
|
312
|
+
|
|
313
|
+
# Log total count of updated rows
|
|
314
|
+
MediLink_ConfigLoader.log("Total {} 'Default Diagnosis #1' rows updated.".format(updated_count), level="INFO")
|
|
315
|
+
|
|
316
|
+
except Exception as e:
|
|
317
|
+
message = "An error occurred while updating diagnosis codes. Please check the DOCX files and configuration: {}".format(e)
|
|
318
|
+
MediLink_ConfigLoader.log(message, level="ERROR")
|
|
319
|
+
print(message)
|
|
320
|
+
|
|
180
321
|
def load_data_sources(config, crosswalk):
|
|
181
322
|
"""Loads historical mappings from MAPAT and Carol's CSVs."""
|
|
182
323
|
patient_id_to_insurance_id = load_insurance_data_from_mapat(config, crosswalk)
|
|
@@ -269,36 +410,34 @@ def load_insurance_data_from_mapat(config, crosswalk):
|
|
|
269
410
|
def parse_z_dat(z_dat_path, config):
|
|
270
411
|
"""
|
|
271
412
|
Parses the Z.dat file to map Patient IDs to Insurance Names using the provided fixed-width file format.
|
|
272
|
-
|
|
413
|
+
|
|
273
414
|
Args:
|
|
274
415
|
z_dat_path (str): Path to the Z.dat file.
|
|
275
416
|
config (dict): Configuration object containing slicing information and other parameters.
|
|
276
|
-
|
|
417
|
+
|
|
277
418
|
Returns:
|
|
278
419
|
dict: A dictionary mapping Patient IDs to Insurance Names.
|
|
279
420
|
"""
|
|
280
421
|
patient_id_to_insurance_name = {}
|
|
281
|
-
|
|
422
|
+
|
|
282
423
|
try:
|
|
283
|
-
# Reading blocks of fixed-width data (
|
|
284
|
-
for personal_info, insurance_info, service_info in MediLink_DataMgmt.read_fixed_width_data(z_dat_path):
|
|
424
|
+
# Reading blocks of fixed-width data (up to 5 lines per record)
|
|
425
|
+
for personal_info, insurance_info, service_info, service_info_2, service_info_3 in MediLink_DataMgmt.read_fixed_width_data(z_dat_path):
|
|
285
426
|
# Parsing the data using slice definitions from the config
|
|
286
|
-
parsed_data = MediLink_DataMgmt.parse_fixed_width_data(personal_info, insurance_info, service_info, config.get('MediLink_Config', config))
|
|
287
|
-
|
|
427
|
+
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))
|
|
428
|
+
|
|
288
429
|
# Extract Patient ID and Insurance Name from parsed data
|
|
289
430
|
patient_id = parsed_data.get('PATID')
|
|
290
431
|
insurance_name = parsed_data.get('INAME')
|
|
291
|
-
|
|
432
|
+
|
|
292
433
|
if patient_id and insurance_name:
|
|
293
434
|
patient_id_to_insurance_name[patient_id] = insurance_name
|
|
294
435
|
MediLink_ConfigLoader.log("Mapped Patient ID {} to Insurance Name {}".format(patient_id, insurance_name), config, level="INFO")
|
|
295
|
-
|
|
436
|
+
|
|
296
437
|
except FileNotFoundError:
|
|
297
|
-
MediLink_ConfigLoader.log("File not found: {}".format(z_dat_path), config, level="
|
|
298
|
-
raise
|
|
438
|
+
MediLink_ConfigLoader.log("File not found: {}".format(z_dat_path), config, level="INFO")
|
|
299
439
|
except Exception as e:
|
|
300
|
-
MediLink_ConfigLoader.log("Failed to parse Z.dat: {}".format(str(e)), config, level="
|
|
301
|
-
raise
|
|
440
|
+
MediLink_ConfigLoader.log("Failed to parse Z.dat: {}".format(str(e)), config, level="INFO")
|
|
302
441
|
|
|
303
442
|
return patient_id_to_insurance_name
|
|
304
443
|
|
|
@@ -17,12 +17,7 @@ except ImportError:
|
|
|
17
17
|
from MediLink_ConfigLoader import load_configuration
|
|
18
18
|
config, crosswalk = load_configuration()
|
|
19
19
|
|
|
20
|
-
|
|
21
|
-
User Interaction Refinements
|
|
22
|
-
- [ ] Refine the menu options for clearer user guidance during script pauses and errors.
|
|
23
|
-
- [ ] Add functionality for user to easily repeat or skip specific entries without script restart.
|
|
24
|
-
Develop more intuitive skip and retry mechanisms that are responsive to user input during data entry sessions.
|
|
25
|
-
"""
|
|
20
|
+
|
|
26
21
|
# Function to check if a specific key is pressed
|
|
27
22
|
VK_END = int(config.get('VK_END', ""), 16) # Try F12 (7B). Virtual key code for 'End' (23)
|
|
28
23
|
VK_PAUSE = int(config.get('VK_PAUSE', ""), 16) # Try F11 (7A). Virtual-key code for 'Home' (24)
|
|
@@ -122,7 +117,7 @@ def display_patient_selection_menu(csv_data, reverse_mapping, proceed_as_medicar
|
|
|
122
117
|
patient_name = row.get(patient_name_header, "Unknown")
|
|
123
118
|
surgery_date = row.get('Surgery Date', "Unknown Date") # Access 'Surgery Date' as string directly from the row
|
|
124
119
|
|
|
125
|
-
print("{0:03d}: {3
|
|
120
|
+
print("{0:03d}: {3:%m-%d} (ID: {2}) {1} ".format(index+1, patient_name, patient_id, surgery_date))
|
|
126
121
|
|
|
127
122
|
displayed_indices.append(index)
|
|
128
123
|
displayed_patient_ids.append(patient_id)
|
|
@@ -19,15 +19,6 @@ from MediBot_Preprocessor_lib import open_csv_for_editing, initialize
|
|
|
19
19
|
from MediBot_UI import manage_script_pause, app_control
|
|
20
20
|
|
|
21
21
|
|
|
22
|
-
"""
|
|
23
|
-
- [X] (TEST) Address Formatting 30-character Limit:
|
|
24
|
-
(LOW) Address the issue where the format_street function in Medibot may produce addresses exceeding
|
|
25
|
-
the 30-character limit. Current stop-gap is removing period characters and the abbreviation "APT"
|
|
26
|
-
surrounded by spaces from all records as a temporary solution.
|
|
27
|
-
If the address still exceeds 30 characters, the function will attempt to remove spaces from right to left
|
|
28
|
-
until it reaches 30 significant digits or runs out of spaces, then truncate to 30 characters if necessary.
|
|
29
|
-
"""
|
|
30
|
-
|
|
31
22
|
# Bring in all the constants
|
|
32
23
|
initialize(config)
|
|
33
24
|
|