medicafe 0.240809.0__py3-none-any.whl → 0.241015.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 +73 -16
- MediBot/MediBot.py +90 -79
- MediBot/MediBot_Crosswalk_Library.py +496 -194
- MediBot/MediBot_Preprocessor.py +22 -14
- MediBot/MediBot_Preprocessor_lib.py +299 -153
- MediBot/MediBot_UI.py +25 -24
- MediBot/MediBot_dataformat_library.py +17 -25
- MediBot/MediBot_docx_decoder.py +267 -110
- MediBot/update_json.py +26 -1
- MediBot/update_medicafe.py +134 -44
- MediLink/MediLink.py +93 -51
- MediLink/MediLink_837p_encoder.py +23 -23
- MediLink/MediLink_837p_encoder_library.py +141 -96
- MediLink/MediLink_API_Generator.py +1 -7
- MediLink/MediLink_API_v3.py +241 -59
- MediLink/MediLink_APIs.py +1 -2
- MediLink/MediLink_ClaimStatus.py +21 -6
- MediLink/MediLink_ConfigLoader.py +8 -8
- MediLink/MediLink_DataMgmt.py +321 -100
- MediLink/MediLink_Decoder.py +249 -87
- MediLink/MediLink_Deductible.py +7 -8
- MediLink/MediLink_Down.py +115 -120
- MediLink/MediLink_Gmail.py +7 -16
- MediLink/MediLink_Parser.py +63 -36
- MediLink/MediLink_UI.py +29 -24
- MediLink/MediLink_Up.py +12 -8
- {medicafe-0.240809.0.dist-info → medicafe-0.241015.0.dist-info}/METADATA +1 -1
- medicafe-0.241015.0.dist-info/RECORD +47 -0
- {medicafe-0.240809.0.dist-info → medicafe-0.241015.0.dist-info}/WHEEL +1 -1
- medicafe-0.240809.0.dist-info/RECORD +0 -47
- {medicafe-0.240809.0.dist-info → medicafe-0.241015.0.dist-info}/LICENSE +0 -0
- {medicafe-0.240809.0.dist-info → medicafe-0.241015.0.dist-info}/top_level.txt +0 -0
MediBot/update_json.py
CHANGED
|
@@ -1,7 +1,25 @@
|
|
|
1
|
+
# update_json.py
|
|
1
2
|
import json
|
|
2
3
|
import sys
|
|
3
4
|
from collections import OrderedDict
|
|
4
5
|
|
|
6
|
+
def get_current_csv_path(json_file):
|
|
7
|
+
try:
|
|
8
|
+
with open(json_file, 'r', encoding='utf-8') as file:
|
|
9
|
+
try:
|
|
10
|
+
data = json.load(file, object_pairs_hook=OrderedDict)
|
|
11
|
+
return data.get('CSV_FILE_PATH', None)
|
|
12
|
+
except ValueError as decode_err:
|
|
13
|
+
print("Error decoding JSON file '{}': {}".format(json_file, decode_err))
|
|
14
|
+
sys.exit(1)
|
|
15
|
+
except IOError as io_err:
|
|
16
|
+
print("Error accessing file '{}': {}".format(json_file, io_err))
|
|
17
|
+
sys.exit(1)
|
|
18
|
+
except Exception as e:
|
|
19
|
+
print("An unexpected error occurred: {}".format(e))
|
|
20
|
+
sys.exit(1)
|
|
21
|
+
return None
|
|
22
|
+
|
|
5
23
|
def update_csv_path(json_file, new_path):
|
|
6
24
|
try:
|
|
7
25
|
with open(json_file, 'r', encoding='utf-8') as file:
|
|
@@ -38,6 +56,13 @@ if __name__ == "__main__":
|
|
|
38
56
|
json_path = sys.argv[1]
|
|
39
57
|
new_csv_path = sys.argv[2]
|
|
40
58
|
update_csv_path(json_path, new_csv_path)
|
|
59
|
+
elif len(sys.argv) == 2:
|
|
60
|
+
json_path = sys.argv[1]
|
|
61
|
+
current_csv_path = get_current_csv_path(json_path)
|
|
62
|
+
if current_csv_path:
|
|
63
|
+
print(current_csv_path)
|
|
64
|
+
else:
|
|
65
|
+
print("No CSV path found in config.")
|
|
41
66
|
else:
|
|
42
|
-
print("Usage: update_json.py <path_to_json_file> <new_csv_path>")
|
|
67
|
+
print("Usage: update_json.py <path_to_json_file> [<new_csv_path>]")
|
|
43
68
|
sys.exit(1)
|
MediBot/update_medicafe.py
CHANGED
|
@@ -1,8 +1,57 @@
|
|
|
1
|
-
|
|
2
|
-
import sys
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
#update_medicafe.py
|
|
2
|
+
import subprocess, sys, requests, time
|
|
3
|
+
|
|
4
|
+
def get_installed_version(package):
|
|
5
|
+
try:
|
|
6
|
+
process = subprocess.Popen(
|
|
7
|
+
[sys.executable, '-m', 'pip', 'show', package],
|
|
8
|
+
stdout=subprocess.PIPE,
|
|
9
|
+
stderr=subprocess.PIPE
|
|
10
|
+
)
|
|
11
|
+
stdout, stderr = process.communicate()
|
|
12
|
+
if process.returncode == 0:
|
|
13
|
+
for line in stdout.decode().splitlines():
|
|
14
|
+
if line.startswith("Version:"):
|
|
15
|
+
return line.split(":", 1)[1].strip()
|
|
16
|
+
return None
|
|
17
|
+
except Exception as e:
|
|
18
|
+
print("Error retrieving installed version: {}".format(e))
|
|
19
|
+
return None
|
|
20
|
+
|
|
21
|
+
def get_latest_version(package, retries=3, delay=1):
|
|
22
|
+
"""
|
|
23
|
+
Fetch the latest version of the specified package from PyPI with retries.
|
|
24
|
+
"""
|
|
25
|
+
for attempt in range(1, retries + 1):
|
|
26
|
+
try:
|
|
27
|
+
response = requests.get("https://pypi.org/pypi/{}/json".format(package), timeout=10)
|
|
28
|
+
response.raise_for_status() # Raise an error for bad responses
|
|
29
|
+
data = response.json()
|
|
30
|
+
latest_version = data['info']['version']
|
|
31
|
+
|
|
32
|
+
# Print the version with attempt information
|
|
33
|
+
if attempt == 1:
|
|
34
|
+
print("Latest available version: {}".format(latest_version))
|
|
35
|
+
else:
|
|
36
|
+
print("Latest available version: {} ({} attempt)".format(latest_version, attempt))
|
|
37
|
+
|
|
38
|
+
# Check if the latest version is different from the current version
|
|
39
|
+
current_version = get_installed_version(package)
|
|
40
|
+
if current_version and compare_versions(latest_version, current_version) == 0:
|
|
41
|
+
# If the versions are the same, perform a second request
|
|
42
|
+
time.sleep(delay)
|
|
43
|
+
response = requests.get("https://pypi.org/pypi/{}/json".format(package), timeout=10)
|
|
44
|
+
response.raise_for_status()
|
|
45
|
+
data = response.json()
|
|
46
|
+
latest_version = data['info']['version']
|
|
47
|
+
|
|
48
|
+
return latest_version # Return the version after the check
|
|
49
|
+
except requests.RequestException as e:
|
|
50
|
+
print("Attempt {}: Error fetching latest version: {}".format(attempt, e))
|
|
51
|
+
if attempt < retries:
|
|
52
|
+
print("Retrying in {} seconds...".format(delay))
|
|
53
|
+
time.sleep(delay)
|
|
54
|
+
return None
|
|
6
55
|
|
|
7
56
|
def check_internet_connection():
|
|
8
57
|
try:
|
|
@@ -11,47 +60,88 @@ def check_internet_connection():
|
|
|
11
60
|
except requests.ConnectionError:
|
|
12
61
|
return False
|
|
13
62
|
|
|
14
|
-
def
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
63
|
+
def compare_versions(version1, version2):
|
|
64
|
+
v1_parts = list(map(int, version1.split(".")))
|
|
65
|
+
v2_parts = list(map(int, version2.split(".")))
|
|
66
|
+
return (v1_parts > v2_parts) - (v1_parts < v2_parts)
|
|
67
|
+
|
|
68
|
+
def upgrade_package(package, retries=3, delay=2): # Updated retries to 3
|
|
69
|
+
"""
|
|
70
|
+
Attempts to upgrade the package multiple times with delays in between.
|
|
71
|
+
"""
|
|
72
|
+
if not check_internet_connection():
|
|
73
|
+
print("Error: No internet connection detected. Please check your internet connection and try again.")
|
|
74
|
+
sys.exit(1)
|
|
75
|
+
|
|
76
|
+
for attempt in range(1, retries + 1):
|
|
77
|
+
print("Attempt {} to upgrade {}...".format(attempt, package))
|
|
78
|
+
process = subprocess.Popen(
|
|
79
|
+
[
|
|
80
|
+
sys.executable, '-m', 'pip', 'install', '--upgrade',
|
|
81
|
+
package, '--no-cache-dir', '--disable-pip-version-check', '-q'
|
|
82
|
+
],
|
|
83
|
+
stdout=subprocess.PIPE,
|
|
84
|
+
stderr=subprocess.PIPE
|
|
85
|
+
)
|
|
20
86
|
|
|
21
|
-
|
|
87
|
+
stdout, stderr = process.communicate()
|
|
22
88
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
print("
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
time.sleep(
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
89
|
+
if process.returncode == 0:
|
|
90
|
+
print(stdout.decode().strip())
|
|
91
|
+
new_version = get_installed_version(package) # Get new version after upgrade
|
|
92
|
+
if compare_versions(new_version, get_latest_version(package)) >= 0: # Compare versions
|
|
93
|
+
if attempt == 1:
|
|
94
|
+
print("Upgrade succeeded!")
|
|
95
|
+
else:
|
|
96
|
+
print("Attempt {}: Upgrade succeeded!".format(attempt))
|
|
97
|
+
time.sleep(delay)
|
|
98
|
+
return True
|
|
99
|
+
else:
|
|
100
|
+
print("Upgrade failed. Current version remains: {}".format(new_version))
|
|
101
|
+
if attempt < retries:
|
|
102
|
+
print("Retrying in {} seconds...".format(delay))
|
|
103
|
+
time.sleep(delay)
|
|
104
|
+
else:
|
|
105
|
+
print(stderr.decode().strip())
|
|
106
|
+
print("Attempt {}: Upgrade failed.".format(attempt))
|
|
107
|
+
if attempt < retries:
|
|
108
|
+
print("Retrying in {} seconds...".format(delay))
|
|
109
|
+
time.sleep(delay)
|
|
110
|
+
|
|
111
|
+
print("Error: All upgrade attempts failed.")
|
|
112
|
+
return False
|
|
113
|
+
|
|
114
|
+
def main():
|
|
115
|
+
package = "medicafe"
|
|
116
|
+
|
|
117
|
+
current_version = get_installed_version(package)
|
|
118
|
+
if not current_version:
|
|
119
|
+
print("{} is not installed.".format(package))
|
|
53
120
|
sys.exit(1)
|
|
121
|
+
|
|
122
|
+
latest_version = get_latest_version(package)
|
|
123
|
+
if not latest_version:
|
|
124
|
+
print("Could not retrieve the latest version information.")
|
|
125
|
+
sys.exit(1)
|
|
126
|
+
|
|
127
|
+
print("Current version of {}: {}".format(package, current_version))
|
|
128
|
+
print("Latest version of {}: {}".format(package, latest_version))
|
|
129
|
+
|
|
130
|
+
if compare_versions(latest_version, current_version) > 0:
|
|
131
|
+
print("A newer version is available. Proceeding with upgrade.")
|
|
132
|
+
if upgrade_package(package):
|
|
133
|
+
# Verify upgrade
|
|
134
|
+
time.sleep(3)
|
|
135
|
+
new_version = get_installed_version(package)
|
|
136
|
+
if compare_versions(new_version, latest_version) >= 0:
|
|
137
|
+
print("Upgrade successful. New version: {}".format(new_version))
|
|
138
|
+
else:
|
|
139
|
+
print("Upgrade failed. Current version remains: {}".format(new_version))
|
|
140
|
+
sys.exit(1)
|
|
141
|
+
else:
|
|
142
|
+
sys.exit(1)
|
|
143
|
+
else:
|
|
144
|
+
print("You already have the latest version installed.")
|
|
54
145
|
|
|
55
146
|
if __name__ == "__main__":
|
|
56
|
-
|
|
57
|
-
upgrade_medicafe(medicafe_package)
|
|
147
|
+
main()
|
MediLink/MediLink.py
CHANGED
|
@@ -1,22 +1,21 @@
|
|
|
1
|
-
|
|
1
|
+
# MediLink.py
|
|
2
|
+
import os, sys
|
|
2
3
|
import MediLink_Down
|
|
3
4
|
import MediLink_Up
|
|
4
5
|
import MediLink_ConfigLoader
|
|
5
6
|
import MediLink_DataMgmt
|
|
6
7
|
|
|
7
8
|
# For UI Functions
|
|
8
|
-
import os
|
|
9
9
|
import MediLink_UI # Import UI module for handling all user interfaces
|
|
10
10
|
from tqdm import tqdm
|
|
11
11
|
|
|
12
12
|
# Add parent directory of the project to the Python path
|
|
13
|
-
import sys
|
|
14
13
|
project_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
|
15
|
-
sys.path
|
|
14
|
+
if project_dir not in sys.path:
|
|
15
|
+
sys.path.append(project_dir)
|
|
16
16
|
|
|
17
17
|
from MediBot import MediBot_Preprocessor_lib
|
|
18
18
|
load_insurance_data_from_mains = MediBot_Preprocessor_lib.load_insurance_data_from_mains
|
|
19
|
-
from MediBot import MediBot_Crosswalk_Library
|
|
20
19
|
|
|
21
20
|
# Retrieve insurance options with codes and descriptions
|
|
22
21
|
config, _ = MediLink_ConfigLoader.load_configuration()
|
|
@@ -135,46 +134,86 @@ def extract_and_suggest_endpoint(file_path, config, crosswalk):
|
|
|
135
134
|
# Return only the enriched detailed patient data, eliminating the need for a separate summary list
|
|
136
135
|
return detailed_patient_data
|
|
137
136
|
|
|
138
|
-
def check_for_new_remittances(config):
|
|
137
|
+
def check_for_new_remittances(config=None):
|
|
138
|
+
"""
|
|
139
|
+
Function to check for new remittance files across all configured endpoints.
|
|
140
|
+
Loads the configuration, validates it, and processes each endpoint to download and handle files.
|
|
141
|
+
Accumulates results from all endpoints and processes them together at the end.
|
|
142
|
+
"""
|
|
143
|
+
# Start the process and log the initiation
|
|
144
|
+
MediLink_ConfigLoader.log("Starting check_for_new_remittances function")
|
|
139
145
|
print("\nChecking for new files across all endpoints...")
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
if
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
146
|
+
MediLink_ConfigLoader.log("Checking for new files across all endpoints...")
|
|
147
|
+
|
|
148
|
+
# Step 1: Load and validate the configuration
|
|
149
|
+
if config is None:
|
|
150
|
+
config, _ = MediLink_ConfigLoader.load_configuration()
|
|
151
|
+
|
|
152
|
+
if not config or 'MediLink_Config' not in config or 'endpoints' not in config['MediLink_Config']:
|
|
153
|
+
MediLink_ConfigLoader.log("Error: Config is missing necessary sections. Aborting...", level="ERROR")
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
endpoints = config['MediLink_Config'].get('endpoints')
|
|
157
|
+
if not isinstance(endpoints, dict):
|
|
158
|
+
MediLink_ConfigLoader.log("Error: 'endpoints' is not a dictionary. Aborting...", level="ERROR")
|
|
159
|
+
return
|
|
160
|
+
|
|
161
|
+
# Lists to accumulate all consolidated records and translated files across all endpoints
|
|
162
|
+
all_consolidated_records = []
|
|
163
|
+
all_translated_files = []
|
|
164
|
+
|
|
165
|
+
# Step 2: Process each endpoint and accumulate results
|
|
166
|
+
for endpoint_key, endpoint_info in tqdm(endpoints.items(), desc="Processing endpoints"):
|
|
167
|
+
# Validate endpoint structure
|
|
168
|
+
if not endpoint_info or not isinstance(endpoint_info, dict):
|
|
169
|
+
MediLink_ConfigLoader.log("Error: Invalid endpoint structure for {}. Skipping...".format(endpoint_key), level="ERROR")
|
|
170
|
+
continue
|
|
171
|
+
|
|
172
|
+
if 'remote_directory_down' in endpoint_info:
|
|
173
|
+
# Process the endpoint and handle the files
|
|
174
|
+
MediLink_ConfigLoader.log("Processing endpoint: {}".format(endpoint_key))
|
|
175
|
+
consolidated_records, translated_files = process_endpoint(endpoint_key, endpoint_info, config)
|
|
176
|
+
|
|
177
|
+
# Accumulate the results for later processing
|
|
178
|
+
if consolidated_records:
|
|
179
|
+
all_consolidated_records.extend(consolidated_records)
|
|
180
|
+
if translated_files:
|
|
181
|
+
all_translated_files.extend(translated_files)
|
|
182
|
+
else:
|
|
183
|
+
MediLink_ConfigLoader.log("Skipping endpoint '{}'. 'remote_directory_down' not configured.".format(endpoint_info.get('name', 'Unknown')), level="WARNING")
|
|
184
|
+
|
|
185
|
+
# Step 3: After processing all endpoints, handle the accumulated results
|
|
186
|
+
if all_consolidated_records:
|
|
187
|
+
MediLink_Down.display_consolidated_records(all_consolidated_records) # Ensure this is called only once
|
|
188
|
+
MediLink_Down.prompt_csv_export(all_consolidated_records, config['MediLink_Config']['local_storage_path'])
|
|
158
189
|
else:
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
190
|
+
MediLink_ConfigLoader.log("No records to display after processing all endpoints.", level="WARNING")
|
|
191
|
+
print("No records to display after processing all endpoints.")
|
|
192
|
+
|
|
193
|
+
def process_endpoint(endpoint_key, endpoint_info, config):
|
|
194
|
+
"""
|
|
195
|
+
Helper function to process a single endpoint.
|
|
196
|
+
Downloads files from the endpoint, processes them, and returns the consolidated records and translated files.
|
|
197
|
+
"""
|
|
198
|
+
try:
|
|
199
|
+
# Process the files for the given endpoint
|
|
200
|
+
local_storage_path = config['MediLink_Config']['local_storage_path']
|
|
201
|
+
MediLink_ConfigLoader.log("[Process Endpoint] Local storage path set to {}".format(local_storage_path))
|
|
202
|
+
downloaded_files = MediLink_Down.operate_winscp("download", None, endpoint_info, local_storage_path, config)
|
|
168
203
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
for endpoint, path in processed_endpoints:
|
|
173
|
-
print("Endpoint: {}, ERA Path: {}".format(endpoint, path))
|
|
204
|
+
if downloaded_files:
|
|
205
|
+
MediLink_ConfigLoader.log("[Process Endpoint] WinSCP Downloaded the following files: \n{}".format(downloaded_files))
|
|
206
|
+
return MediLink_Down.handle_files(local_storage_path, downloaded_files)
|
|
174
207
|
else:
|
|
175
|
-
|
|
208
|
+
MediLink_ConfigLoader.log("[Process Endpoint]No files were downloaded for endpoint: {}.".format(endpoint_key), level="WARNING")
|
|
209
|
+
return [], []
|
|
210
|
+
|
|
211
|
+
except Exception as e:
|
|
212
|
+
# Handle any exceptions that occur during the processing
|
|
213
|
+
MediLink_ConfigLoader.log("Error processing endpoint {}: {}".format(endpoint_key, e), level="ERROR")
|
|
214
|
+
return [], []
|
|
176
215
|
|
|
177
|
-
def user_decision_on_suggestions(detailed_patient_data, config):
|
|
216
|
+
def user_decision_on_suggestions(detailed_patient_data, config, insurance_edited):
|
|
178
217
|
"""
|
|
179
218
|
Presents the user with all patient summaries and suggested endpoints,
|
|
180
219
|
then asks for confirmation to proceed with all or specify adjustments manually.
|
|
@@ -183,11 +222,11 @@ def user_decision_on_suggestions(detailed_patient_data, config):
|
|
|
183
222
|
although the user decision is persisting. Possibly consider making the current/suggested/confirmed endpoint
|
|
184
223
|
part of a class that the user can interact with via these menus? Probably better handling that way.
|
|
185
224
|
"""
|
|
186
|
-
|
|
187
|
-
|
|
225
|
+
if insurance_edited:
|
|
226
|
+
# Display summaries only if insurance types were edited
|
|
227
|
+
MediLink_UI.display_patient_summaries(detailed_patient_data)
|
|
188
228
|
|
|
189
|
-
|
|
190
|
-
proceed = MediLink_UI.ask_for_proceeding_with_endpoints()
|
|
229
|
+
proceed = input("Do you want to proceed with all suggested endpoints? (Y/N): ").strip().lower() == 'y'
|
|
191
230
|
|
|
192
231
|
# If the user agrees to proceed with all suggested endpoints, confirm them.
|
|
193
232
|
if proceed:
|
|
@@ -244,7 +283,9 @@ def main_menu():
|
|
|
244
283
|
config, crosswalk = MediLink_ConfigLoader.load_configuration()
|
|
245
284
|
|
|
246
285
|
# Check to make sure payer_id key is available in crosswalk, otherwise, go through that crosswalk initialization flow
|
|
247
|
-
|
|
286
|
+
if 'payer_id' not in crosswalk:
|
|
287
|
+
print("Crosswalk 'payer_id' not found. Please run MediBot_Preprocessor.py with the --update-crosswalk argument.")
|
|
288
|
+
exit() # BUG Halting the script execution here for now, should be handled in the preprocessor script.
|
|
248
289
|
|
|
249
290
|
# Check if the application is in test mode
|
|
250
291
|
if config.get("MediLink_Config", {}).get("TestMode", False):
|
|
@@ -287,7 +328,7 @@ def main_menu():
|
|
|
287
328
|
detailed_patient_data = collect_detailed_patient_data(selected_files, config, crosswalk)
|
|
288
329
|
|
|
289
330
|
# Process the claims submission.
|
|
290
|
-
handle_submission(detailed_patient_data, config)
|
|
331
|
+
handle_submission(detailed_patient_data, config, crosswalk)
|
|
291
332
|
elif choice == '3' or (choice == '2' and not all_files):
|
|
292
333
|
# Exit the program if the user chooses to exit or if no files are present.
|
|
293
334
|
MediLink_UI.display_exit_message()
|
|
@@ -296,16 +337,17 @@ def main_menu():
|
|
|
296
337
|
# Display an error message if the user's choice does not match any valid option.
|
|
297
338
|
MediLink_UI.display_invalid_choice()
|
|
298
339
|
|
|
299
|
-
def handle_submission(detailed_patient_data, config):
|
|
340
|
+
def handle_submission(detailed_patient_data, config, crosswalk):
|
|
300
341
|
"""
|
|
301
342
|
Handles the submission process for claims based on detailed patient data.
|
|
302
343
|
This function orchestrates the flow from user decision on endpoint suggestions to the actual submission of claims.
|
|
303
344
|
"""
|
|
304
|
-
|
|
305
|
-
|
|
345
|
+
insurance_edited = False # Flag to track if insurance types were edited
|
|
346
|
+
|
|
306
347
|
# Ask the user if they want to edit insurance types
|
|
307
348
|
edit_insurance = input("Do you want to edit insurance types? (y/n): ").strip().lower()
|
|
308
349
|
if edit_insurance in ['y', 'yes', '']:
|
|
350
|
+
insurance_edited = True # User chose to edit insurance types
|
|
309
351
|
while True:
|
|
310
352
|
# Bulk edit insurance types
|
|
311
353
|
MediLink_DataMgmt.bulk_edit_insurance_types(detailed_patient_data, insurance_options)
|
|
@@ -317,7 +359,7 @@ def handle_submission(detailed_patient_data, config):
|
|
|
317
359
|
print("Returning to bulk edit insurance types.")
|
|
318
360
|
|
|
319
361
|
# Initiate user interaction to confirm or adjust suggested endpoints.
|
|
320
|
-
adjusted_data = user_decision_on_suggestions(detailed_patient_data, config)
|
|
362
|
+
adjusted_data = user_decision_on_suggestions(detailed_patient_data, config, insurance_edited)
|
|
321
363
|
|
|
322
364
|
# Confirm all remaining suggested endpoints.
|
|
323
365
|
confirmed_data = MediLink_DataMgmt.confirm_all_suggested_endpoints(adjusted_data)
|
|
@@ -328,7 +370,7 @@ def handle_submission(detailed_patient_data, config):
|
|
|
328
370
|
if MediLink_Up.confirm_transmission(organized_data):
|
|
329
371
|
if MediLink_Up.check_internet_connection():
|
|
330
372
|
# Submit claims if internet connectivity is confirmed.
|
|
331
|
-
_ = MediLink_Up.submit_claims(organized_data, config)
|
|
373
|
+
_ = MediLink_Up.submit_claims(organized_data, config, crosswalk)
|
|
332
374
|
# TODO submit_claims will have a receipt return in the future.
|
|
333
375
|
else:
|
|
334
376
|
# Notify the user of an internet connection error.
|
|
@@ -1,13 +1,12 @@
|
|
|
1
|
-
|
|
1
|
+
# MediLink_837p_encoder.py
|
|
2
|
+
import re, argparse, os
|
|
2
3
|
from datetime import datetime
|
|
3
|
-
import argparse
|
|
4
|
-
import os
|
|
5
4
|
import MediLink_ConfigLoader
|
|
6
5
|
from MediLink_DataMgmt import parse_fixed_width_data, read_fixed_width_data
|
|
7
6
|
import MediLink_837p_encoder_library
|
|
8
7
|
#from tqdm import tqdm
|
|
9
8
|
|
|
10
|
-
def format_single_claim(patient_data, config, endpoint, transaction_set_control_number):
|
|
9
|
+
def format_single_claim(patient_data, config, endpoint, transaction_set_control_number, crosswalk, client):
|
|
11
10
|
"""
|
|
12
11
|
Formats a single claim into 837P segments based on the provided patient data and endpoint.
|
|
13
12
|
|
|
@@ -21,7 +20,7 @@ def format_single_claim(patient_data, config, endpoint, transaction_set_control_
|
|
|
21
20
|
- String representation of the formatted 837P claim.
|
|
22
21
|
"""
|
|
23
22
|
# Pre-resolve and enrich with Payer Name and ID for special case handling like Florida Blue.
|
|
24
|
-
patient_data = MediLink_837p_encoder_library.payer_id_to_payer_name(patient_data, config, endpoint)
|
|
23
|
+
patient_data = MediLink_837p_encoder_library.payer_id_to_payer_name(patient_data, config, endpoint, crosswalk, client)
|
|
25
24
|
|
|
26
25
|
segments = []
|
|
27
26
|
|
|
@@ -63,7 +62,7 @@ def format_single_claim(patient_data, config, endpoint, transaction_set_control_
|
|
|
63
62
|
segments.extend(MediLink_837p_encoder_library.create_nm1_rendering_provider_segment(config))
|
|
64
63
|
|
|
65
64
|
# Claim information 2300, 2310C Service Facility and 2400 loop segments
|
|
66
|
-
segments.extend(MediLink_837p_encoder_library.create_clm_and_related_segments(patient_data, config))
|
|
65
|
+
segments.extend(MediLink_837p_encoder_library.create_clm_and_related_segments(patient_data, config, crosswalk))
|
|
67
66
|
|
|
68
67
|
# Placeholder for the SE segment to be updated with actual segment count later
|
|
69
68
|
segments.append("SE**{transaction_set_control_number:04d}~")
|
|
@@ -124,7 +123,7 @@ def write_output_file(document_segments, output_directory, endpoint_key, input_f
|
|
|
124
123
|
MediLink_ConfigLoader.log("Error: Failed to write output file. {}".format(e), config, level="ERROR")
|
|
125
124
|
return None
|
|
126
125
|
|
|
127
|
-
def
|
|
126
|
+
def process_single_file(file_path, config, endpoint_key, transaction_set_control_number, crosswalk): # BUG Duplicate function name??
|
|
128
127
|
"""
|
|
129
128
|
Process the claim data from a file into the 837P format.
|
|
130
129
|
|
|
@@ -145,7 +144,7 @@ def process_file(file_path, config, endpoint_key, transaction_set_control_number
|
|
|
145
144
|
return None, transaction_set_control_number # Halt processing if the user chooses
|
|
146
145
|
|
|
147
146
|
# Process each valid claim
|
|
148
|
-
formatted_claims, transaction_set_control_number = format_claims(valid_claims, config, endpoint_key, transaction_set_control_number)
|
|
147
|
+
formatted_claims, transaction_set_control_number = format_claims(valid_claims, config, endpoint_key, transaction_set_control_number, crosswalk)
|
|
149
148
|
|
|
150
149
|
formatted_claims_str = '\n'.join(formatted_claims) # Join formatted claims into a single string
|
|
151
150
|
return formatted_claims_str, transaction_set_control_number
|
|
@@ -179,7 +178,7 @@ def read_and_validate_claims(file_path, config):
|
|
|
179
178
|
|
|
180
179
|
return valid_claims, validation_errors
|
|
181
180
|
|
|
182
|
-
def format_claims(parsed_data_list, config, endpoint, starting_transaction_set_control_number):
|
|
181
|
+
def format_claims(parsed_data_list, config, endpoint, starting_transaction_set_control_number, crosswalk):
|
|
183
182
|
"""
|
|
184
183
|
Formats a list of parsed claim data into 837P segments.
|
|
185
184
|
|
|
@@ -196,7 +195,7 @@ def format_claims(parsed_data_list, config, endpoint, starting_transaction_set_c
|
|
|
196
195
|
transaction_set_control_number = starting_transaction_set_control_number
|
|
197
196
|
|
|
198
197
|
for parsed_data in parsed_data_list:
|
|
199
|
-
formatted_claim = format_single_claim(parsed_data, config, endpoint, transaction_set_control_number)
|
|
198
|
+
formatted_claim = format_single_claim(parsed_data, config, endpoint, transaction_set_control_number, crosswalk)
|
|
200
199
|
formatted_claims.append(formatted_claim)
|
|
201
200
|
transaction_set_control_number += 1 # Increment for each successfully processed claim
|
|
202
201
|
|
|
@@ -274,7 +273,7 @@ def validate_claim_data(parsed_data, config, required_fields=[]):
|
|
|
274
273
|
|
|
275
274
|
return True, []
|
|
276
275
|
|
|
277
|
-
def process_and_write_file(file_path, config, endpoint, starting_tscn=1):
|
|
276
|
+
def process_and_write_file(file_path, config, endpoint, crosswalk, starting_tscn=1):
|
|
278
277
|
"""
|
|
279
278
|
Process a single file, create complete 837P document with headers and trailers, and write to output file.
|
|
280
279
|
|
|
@@ -286,7 +285,7 @@ def process_and_write_file(file_path, config, endpoint, starting_tscn=1):
|
|
|
286
285
|
"""
|
|
287
286
|
print("Processing: {}".format(file_path))
|
|
288
287
|
MediLink_ConfigLoader.log("Processing: {}".format(file_path))
|
|
289
|
-
formatted_data, transaction_set_control_number =
|
|
288
|
+
formatted_data, transaction_set_control_number = process_single_file(file_path, config, endpoint, starting_tscn, crosswalk)
|
|
290
289
|
isa_header, gs_header, ge_trailer, iea_trailer = MediLink_837p_encoder_library.create_interchange_elements(config, endpoint, transaction_set_control_number - 1)
|
|
291
290
|
|
|
292
291
|
# Combine everything into a single document
|
|
@@ -303,6 +302,7 @@ def process_and_write_file(file_path, config, endpoint, starting_tscn=1):
|
|
|
303
302
|
print("File processed. Output saved to: {}".format(output_file_path))
|
|
304
303
|
|
|
305
304
|
def main():
|
|
305
|
+
# BUG (MAJOR) THIS NEEDS THE API CLIENT TO BE PASSED INTO THE FUNCTION FOR SINGLE FILE PROCESSING.
|
|
306
306
|
"""
|
|
307
307
|
Converts fixed-width files to 837P format for health claim submissions.
|
|
308
308
|
|
|
@@ -329,7 +329,7 @@ def main():
|
|
|
329
329
|
parser.add_argument(
|
|
330
330
|
"-e", "--endpoint",
|
|
331
331
|
required=True,
|
|
332
|
-
choices=["AVAILITY", "OPTUMEDI", "PNT_DATA", "UHCAPI"],
|
|
332
|
+
choices=["AVAILITY", "OPTUMEDI", "PNT_DATA", "UHCAPI", "CLAIMSHUTTLE"], # This should read from the config?
|
|
333
333
|
help="Specify the endpoint for which the conversion is intended."
|
|
334
334
|
)
|
|
335
335
|
parser.add_argument(
|
|
@@ -346,13 +346,13 @@ def main():
|
|
|
346
346
|
|
|
347
347
|
print("Starting the conversion process for {}. Processing {} at '{}'.".format(args.endpoint, 'directory' if args.is_directory else 'file', args.path))
|
|
348
348
|
|
|
349
|
-
config,
|
|
349
|
+
config, crosswalk = MediLink_ConfigLoader.load_configuration()
|
|
350
350
|
config = config.get('MediLink_Config', config)
|
|
351
351
|
|
|
352
|
-
|
|
352
|
+
process_dat_files(args.path, config, args.endpoint, args.is_directory, crosswalk)
|
|
353
353
|
print("Conversion complete.")
|
|
354
354
|
|
|
355
|
-
def
|
|
355
|
+
def process_dat_files(path, config, endpoint, is_directory, crosswalk):
|
|
356
356
|
"""
|
|
357
357
|
Processes either a single file or all files within a directory.
|
|
358
358
|
|
|
@@ -370,10 +370,10 @@ def process_files(path, config, endpoint, is_directory):
|
|
|
370
370
|
for file_name in os.listdir(path):
|
|
371
371
|
if file_name.endswith(".DAT"):
|
|
372
372
|
file_path = os.path.join(path, file_name)
|
|
373
|
-
process_and_write_file(file_path, config, endpoint)
|
|
373
|
+
process_and_write_file(file_path, config, endpoint, crosswalk)
|
|
374
374
|
else:
|
|
375
375
|
MediLink_ConfigLoader.log("Processing the single file: {}".format(path))
|
|
376
|
-
process_and_write_file(path, config, endpoint)
|
|
376
|
+
process_and_write_file(path, config, endpoint, crosswalk)
|
|
377
377
|
|
|
378
378
|
if __name__ == "__main__":
|
|
379
379
|
main()
|
|
@@ -381,7 +381,7 @@ if __name__ == "__main__":
|
|
|
381
381
|
# The functions below are the ones that are used as non-main library by outside scripts.
|
|
382
382
|
#######################################################################################
|
|
383
383
|
|
|
384
|
-
def convert_files_for_submission(detailed_patient_data, config):
|
|
384
|
+
def convert_files_for_submission(detailed_patient_data, config, crosswalk, client):
|
|
385
385
|
"""
|
|
386
386
|
Processes detailed patient data for submission based on their confirmed endpoints,
|
|
387
387
|
generating separate 837P files for each endpoint according to the configured submission type.
|
|
@@ -424,18 +424,18 @@ def convert_files_for_submission(detailed_patient_data, config):
|
|
|
424
424
|
chart_number = patient_data.get('CHART', 'UNKNOWN')#[:5] truncation might cause collisions.
|
|
425
425
|
suffix = "_{}".format(chart_number)
|
|
426
426
|
# Process and convert each patient's data to a separate file
|
|
427
|
-
converted_path = process_claim(config, endpoint, [patient_data], suffix)
|
|
427
|
+
converted_path = process_claim(config, endpoint, [patient_data], crosswalk, client, suffix)
|
|
428
428
|
if converted_path:
|
|
429
429
|
converted_files_paths.append(converted_path)
|
|
430
430
|
else:
|
|
431
431
|
# Process all patient data together for batch submissions
|
|
432
|
-
converted_path = process_claim(config, endpoint, patient_data_list)
|
|
432
|
+
converted_path = process_claim(config, endpoint, patient_data_list, crosswalk, client)
|
|
433
433
|
if converted_path:
|
|
434
434
|
converted_files_paths.append(converted_path)
|
|
435
435
|
|
|
436
436
|
return converted_files_paths
|
|
437
437
|
|
|
438
|
-
def process_claim(config, endpoint, patient_data_list, suffix=""):
|
|
438
|
+
def process_claim(config, endpoint, patient_data_list, crosswalk, client, suffix=""):
|
|
439
439
|
"""
|
|
440
440
|
Processes patient data for a specified endpoint, converting it into the 837P format.
|
|
441
441
|
Can handle both batch and single-patient submissions.
|
|
@@ -465,7 +465,7 @@ def process_claim(config, endpoint, patient_data_list, suffix=""):
|
|
|
465
465
|
is_valid, validation_errors = validate_claim_data(patient_data, config)
|
|
466
466
|
if is_valid:
|
|
467
467
|
# Format the claim into 837P segments
|
|
468
|
-
formatted_claim = format_single_claim(patient_data, config, endpoint, transaction_set_control_number)
|
|
468
|
+
formatted_claim = format_single_claim(patient_data, config, endpoint, transaction_set_control_number, crosswalk, client)
|
|
469
469
|
document_segments.append(formatted_claim)
|
|
470
470
|
transaction_set_control_number += 1
|
|
471
471
|
else:
|