medicafe 0.240419.2__py3-none-any.whl → 0.240613.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of medicafe might be problematic. Click here for more details.
- MediBot/MediBot.bat +174 -38
- MediBot/MediBot.py +80 -77
- MediBot/MediBot_Charges.py +0 -28
- MediBot/MediBot_Crosswalk_Library.py +281 -0
- MediBot/MediBot_Post.py +0 -0
- MediBot/MediBot_Preprocessor.py +138 -211
- MediBot/MediBot_Preprocessor_lib.py +496 -0
- MediBot/MediBot_UI.py +80 -35
- MediBot/MediBot_dataformat_library.py +79 -35
- MediBot/MediBot_docx_decoder.py +295 -0
- MediBot/update_medicafe.py +46 -8
- MediLink/MediLink.py +207 -108
- MediLink/MediLink_837p_encoder.py +299 -214
- MediLink/MediLink_837p_encoder_library.py +445 -245
- MediLink/MediLink_API_v2.py +174 -0
- MediLink/MediLink_APIs.py +139 -0
- MediLink/MediLink_ConfigLoader.py +44 -32
- MediLink/MediLink_DataMgmt.py +297 -89
- MediLink/MediLink_Decoder.py +63 -0
- MediLink/MediLink_Down.py +73 -102
- MediLink/MediLink_ERA_decoder.py +4 -4
- MediLink/MediLink_Gmail.py +479 -4
- MediLink/MediLink_Mailer.py +0 -0
- MediLink/MediLink_Parser.py +111 -0
- MediLink/MediLink_Scan.py +0 -0
- MediLink/MediLink_Scheduler.py +2 -131
- MediLink/MediLink_StatusCheck.py +0 -4
- MediLink/MediLink_UI.py +87 -27
- MediLink/MediLink_Up.py +301 -45
- MediLink/MediLink_batch.bat +1 -1
- MediLink/test.py +74 -0
- medicafe-0.240613.0.dist-info/METADATA +55 -0
- medicafe-0.240613.0.dist-info/RECORD +43 -0
- {medicafe-0.240419.2.dist-info → medicafe-0.240613.0.dist-info}/WHEEL +5 -5
- medicafe-0.240419.2.dist-info/METADATA +0 -19
- medicafe-0.240419.2.dist-info/RECORD +0 -32
- {medicafe-0.240419.2.dist-info → medicafe-0.240613.0.dist-info}/LICENSE +0 -0
- {medicafe-0.240419.2.dist-info → medicafe-0.240613.0.dist-info}/top_level.txt +0 -0
MediLink/MediLink_DataMgmt.py
CHANGED
|
@@ -1,18 +1,33 @@
|
|
|
1
|
+
# MediLink_DataMgmt.py
|
|
1
2
|
import csv
|
|
2
3
|
import os
|
|
3
4
|
from datetime import datetime, timedelta
|
|
4
|
-
import
|
|
5
|
-
import
|
|
6
|
-
import subprocess # BUG Currently disabled for testing.
|
|
7
|
-
import logging
|
|
5
|
+
import re
|
|
6
|
+
import subprocess
|
|
8
7
|
|
|
9
|
-
#
|
|
10
|
-
|
|
8
|
+
# Need this for running Medibot and MediLink
|
|
9
|
+
try:
|
|
10
|
+
import MediLink_ConfigLoader
|
|
11
|
+
import MediLink_UI
|
|
12
|
+
except ImportError:
|
|
13
|
+
from . import MediLink_ConfigLoader
|
|
14
|
+
from . import MediLink_UI
|
|
15
|
+
|
|
16
|
+
# Helper function to slice and strip values with optional key suffix
|
|
17
|
+
def slice_data(data, slices, suffix=''):
|
|
11
18
|
# Convert slices list to a tuple for slicing operation
|
|
12
|
-
return {key: data[slice(*slices[key])].strip() for key in slices}
|
|
19
|
+
return {key + suffix: data[slice(*slices[key])].strip() for key in slices}
|
|
13
20
|
|
|
14
21
|
# Function to parse fixed-width Medisoft output and extract claim data
|
|
15
|
-
def parse_fixed_width_data(personal_info, insurance_info, service_info, config):
|
|
22
|
+
def parse_fixed_width_data(personal_info, insurance_info, service_info, service_info_2=None, service_info_3=None, config=None):
|
|
23
|
+
|
|
24
|
+
# Make sure we have the right config
|
|
25
|
+
if not config: # Checks if config is None or an empty dictionary
|
|
26
|
+
MediLink_ConfigLoader.log("No config passed to parse_fixed_width_data. Re-loading config...", level="WARNING")
|
|
27
|
+
config, _ = MediLink_ConfigLoader.load_configuration()
|
|
28
|
+
|
|
29
|
+
config = config.get('MediLink_Config', config) # Safest config call.
|
|
30
|
+
|
|
16
31
|
# Load slice definitions from config within the MediLink_Config section
|
|
17
32
|
personal_slices = config['fixedWidthSlices']['personal_slices']
|
|
18
33
|
insurance_slices = config['fixedWidthSlices']['insurance_slices']
|
|
@@ -24,44 +39,87 @@ def parse_fixed_width_data(personal_info, insurance_info, service_info, config):
|
|
|
24
39
|
parsed_data.update(slice_data(insurance_info, insurance_slices))
|
|
25
40
|
parsed_data.update(slice_data(service_info, service_slices))
|
|
26
41
|
|
|
27
|
-
|
|
42
|
+
if service_info_2:
|
|
43
|
+
parsed_data.update(slice_data(service_info_2, service_slices, suffix='_2'))
|
|
44
|
+
|
|
45
|
+
if service_info_3:
|
|
46
|
+
parsed_data.update(slice_data(service_info_3, service_slices, suffix='_3'))
|
|
47
|
+
|
|
48
|
+
MediLink_ConfigLoader.log("Successfully parsed data from segments", config, level="INFO")
|
|
28
49
|
|
|
29
50
|
return parsed_data
|
|
30
51
|
|
|
31
52
|
# Function to read fixed-width Medisoft output and extract claim data
|
|
32
|
-
def read_fixed_width_data(file_path
|
|
53
|
+
def read_fixed_width_data(file_path):
|
|
33
54
|
# Reads the fixed width data from the file and yields each patient's
|
|
34
55
|
# personal, insurance, and service information.
|
|
56
|
+
MediLink_ConfigLoader.log("Starting to read fixed width data...")
|
|
35
57
|
with open(file_path, 'r') as file:
|
|
36
58
|
lines_buffer = [] # Buffer to hold lines for current patient data
|
|
59
|
+
|
|
60
|
+
def yield_record(buffer):
|
|
61
|
+
personal_info = buffer[0]
|
|
62
|
+
insurance_info = buffer[1]
|
|
63
|
+
service_info = buffer[2]
|
|
64
|
+
service_info_2 = buffer[3] if len(buffer) > 3 else None
|
|
65
|
+
service_info_3 = buffer[4] if len(buffer) > 4 else None
|
|
66
|
+
MediLink_ConfigLoader.log("Successfully read data from file: {}".format(file_path), level="INFO")
|
|
67
|
+
return personal_info, insurance_info, service_info, service_info_2, service_info_3
|
|
68
|
+
|
|
37
69
|
for line in file:
|
|
38
70
|
stripped_line = line.strip()
|
|
39
|
-
if stripped_line:
|
|
71
|
+
if stripped_line:
|
|
40
72
|
lines_buffer.append(stripped_line)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
73
|
+
if 3 <= len(lines_buffer) <= 5:
|
|
74
|
+
next_line = file.readline().strip()
|
|
75
|
+
if not next_line:
|
|
76
|
+
yield yield_record(lines_buffer)
|
|
77
|
+
lines_buffer.clear()
|
|
78
|
+
else:
|
|
79
|
+
if len(lines_buffer) >= 3:
|
|
80
|
+
yield yield_record(lines_buffer)
|
|
81
|
+
lines_buffer.clear()
|
|
82
|
+
|
|
83
|
+
if lines_buffer: # Yield any remaining buffer if file ends without a blank line
|
|
84
|
+
yield yield_record(lines_buffer)
|
|
85
|
+
|
|
86
|
+
# TODO (Refactor) Consider consolidating with the other read_fixed_with_data
|
|
87
|
+
def read_general_fixed_width_data(file_path, slices):
|
|
88
|
+
# handle any fixed-width data based on provided slice definitions
|
|
89
|
+
with open(file_path, 'r', encoding='utf-8') as file:
|
|
90
|
+
next(file) # Skip the header
|
|
91
|
+
for line_number, line in enumerate(file, start=1):
|
|
92
|
+
insurance_name = {key: line[start:end].strip() for key, (start, end) in slices.items()}
|
|
93
|
+
yield insurance_name, line_number
|
|
94
|
+
|
|
95
|
+
def consolidate_csvs(source_directory, file_prefix="Consolidated", interactive=False):
|
|
51
96
|
"""
|
|
52
|
-
|
|
53
|
-
|
|
97
|
+
Consolidate CSV files in the source directory into a single CSV file.
|
|
98
|
+
|
|
99
|
+
Parameters:
|
|
100
|
+
source_directory (str): The directory containing the CSV files to consolidate.
|
|
101
|
+
file_prefix (str): The prefix for the consolidated file's name.
|
|
102
|
+
interactive (bool): If True, prompt the user for confirmation before overwriting existing files.
|
|
103
|
+
|
|
104
|
+
Returns:
|
|
105
|
+
str: The filepath of the consolidated CSV file, or None if no files were consolidated.
|
|
54
106
|
"""
|
|
55
107
|
today = datetime.now()
|
|
56
|
-
consolidated_filename = today.strftime("
|
|
108
|
+
consolidated_filename = "{}_{}.csv".format(file_prefix, today.strftime("%m%d%y"))
|
|
57
109
|
consolidated_filepath = os.path.join(source_directory, consolidated_filename)
|
|
58
110
|
|
|
59
111
|
consolidated_data = []
|
|
60
112
|
header_saved = False
|
|
113
|
+
expected_header = None
|
|
61
114
|
|
|
62
115
|
# Check if the file already exists and log the action
|
|
63
116
|
if os.path.exists(consolidated_filepath):
|
|
64
|
-
|
|
117
|
+
MediLink_ConfigLoader.log("The file {} already exists. It will be overwritten.".format(consolidated_filename), level="INFO")
|
|
118
|
+
if interactive:
|
|
119
|
+
overwrite = input("The file {} already exists. Do you want to overwrite it? (y/n): ".format(consolidated_filename)).strip().lower()
|
|
120
|
+
if overwrite != 'y':
|
|
121
|
+
MediLink_ConfigLoader.log("User opted not to overwrite the file {}.".format(consolidated_filename), level="INFO")
|
|
122
|
+
return None
|
|
65
123
|
|
|
66
124
|
for filename in os.listdir(source_directory):
|
|
67
125
|
filepath = os.path.join(source_directory, filename)
|
|
@@ -73,28 +131,40 @@ def consolidate_csvs(source_directory):
|
|
|
73
131
|
if modification_time < today - timedelta(days=1):
|
|
74
132
|
continue # Skip files not modified in the last day
|
|
75
133
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
134
|
+
try:
|
|
135
|
+
with open(filepath, 'r') as csvfile:
|
|
136
|
+
reader = csv.reader(csvfile)
|
|
137
|
+
header = next(reader) # Read the header
|
|
138
|
+
if not header_saved:
|
|
139
|
+
expected_header = header
|
|
140
|
+
consolidated_data.append(header)
|
|
141
|
+
header_saved = True
|
|
142
|
+
elif header != expected_header:
|
|
143
|
+
MediLink_ConfigLoader.log("Header mismatch in file {}. Skipping file.".format(filepath), level="WARNING")
|
|
144
|
+
continue
|
|
84
145
|
|
|
85
|
-
|
|
86
|
-
|
|
146
|
+
consolidated_data.extend(row for row in reader)
|
|
147
|
+
except StopIteration:
|
|
148
|
+
MediLink_ConfigLoader.log("File {} is empty or contains only header. Skipping file.".format(filepath), level="WARNING")
|
|
149
|
+
continue
|
|
150
|
+
except Exception as e:
|
|
151
|
+
MediLink_ConfigLoader.log("Error processing file {}: {}".format(filepath, e), level="ERROR")
|
|
152
|
+
continue
|
|
87
153
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
writer = csv.writer(csvfile)
|
|
91
|
-
writer.writerows(consolidated_data)
|
|
154
|
+
os.remove(filepath)
|
|
155
|
+
MediLink_ConfigLoader.log("Deleted source file after consolidation: {}".format(filepath), level="INFO")
|
|
92
156
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
157
|
+
if consolidated_data:
|
|
158
|
+
with open(consolidated_filepath, 'w') as csvfile:
|
|
159
|
+
writer = csv.writer(csvfile)
|
|
160
|
+
writer.writerows(consolidated_data)
|
|
161
|
+
MediLink_ConfigLoader.log("Consolidated CSVs into {}".format(consolidated_filepath), level="INFO")
|
|
162
|
+
return consolidated_filepath
|
|
163
|
+
else:
|
|
164
|
+
MediLink_ConfigLoader.log("No valid CSV files were found for consolidation.", level="INFO")
|
|
165
|
+
return None
|
|
96
166
|
|
|
97
|
-
def operate_winscp(operation_type, files, endpoint_config, local_storage_path):
|
|
167
|
+
def operate_winscp(operation_type, files, endpoint_config, local_storage_path, config):
|
|
98
168
|
"""
|
|
99
169
|
General function to operate WinSCP for uploading or downloading files.
|
|
100
170
|
|
|
@@ -110,7 +180,7 @@ def operate_winscp(operation_type, files, endpoint_config, local_storage_path):
|
|
|
110
180
|
'remote_directory_up': '/remote/upload/path'
|
|
111
181
|
}
|
|
112
182
|
|
|
113
|
-
operate_winscp('upload', upload_files, upload_config, 'path/to/local/storage')
|
|
183
|
+
operate_winscp('upload', upload_files, upload_config, 'path/to/local/storage', config)
|
|
114
184
|
|
|
115
185
|
# Example of how to call this function for downloads
|
|
116
186
|
download_config = {
|
|
@@ -118,30 +188,42 @@ def operate_winscp(operation_type, files, endpoint_config, local_storage_path):
|
|
|
118
188
|
'remote_directory_down': '/remote/download/path'
|
|
119
189
|
}
|
|
120
190
|
|
|
121
|
-
operate_winscp('download', None, download_config, 'path/to/local/storage')
|
|
191
|
+
operate_winscp('download', None, download_config, 'path/to/local/storage', config)
|
|
122
192
|
"""
|
|
123
193
|
# Setup paths
|
|
124
194
|
try:
|
|
125
|
-
|
|
195
|
+
# TODO (Easy / Config) Get this updated. ??
|
|
196
|
+
winscp_path = config['winscp_path']
|
|
126
197
|
except KeyError:
|
|
127
198
|
winscp_path = os.path.join(os.getcwd(), "Installers", "WinSCP-Portable", "WinSCP.com")
|
|
128
199
|
except Exception as e:
|
|
129
200
|
# Handle any other exceptions here
|
|
130
|
-
print("An error occurred:", e)
|
|
201
|
+
print("An error occurred while running WinSCP:", e)
|
|
131
202
|
winscp_path = None
|
|
132
203
|
|
|
133
204
|
if not os.path.isfile(winscp_path):
|
|
134
|
-
|
|
135
|
-
return
|
|
205
|
+
MediLink_ConfigLoader.log("WinSCP.com not found at {}".format(winscp_path))
|
|
206
|
+
return []
|
|
136
207
|
|
|
137
208
|
# Setup logging
|
|
138
209
|
log_filename = "winscp_upload.log" if operation_type == "upload" else "winscp_download.log"
|
|
139
210
|
winscp_log_path = os.path.join(local_storage_path, log_filename)
|
|
140
211
|
|
|
141
212
|
# Session and directory setup
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
213
|
+
try:
|
|
214
|
+
session_name = endpoint_config.get('session_name', '')
|
|
215
|
+
if operation_type == "upload":
|
|
216
|
+
remote_directory = endpoint_config['remote_directory_up']
|
|
217
|
+
else:
|
|
218
|
+
remote_directory = endpoint_config['remote_directory_down']
|
|
219
|
+
except KeyError as e:
|
|
220
|
+
# Log the missing key information
|
|
221
|
+
missing_key = str(e)
|
|
222
|
+
message = "KeyError: Endpoint config is missing key: {}".format(missing_key)
|
|
223
|
+
MediLink_ConfigLoader.log(message)
|
|
224
|
+
# Set default values or handle the situation accordingly
|
|
225
|
+
session_name = ''
|
|
226
|
+
remote_directory = ''
|
|
145
227
|
# Command building
|
|
146
228
|
command = [
|
|
147
229
|
winscp_path,
|
|
@@ -154,7 +236,7 @@ def operate_winscp(operation_type, files, endpoint_config, local_storage_path):
|
|
|
154
236
|
]
|
|
155
237
|
|
|
156
238
|
# Add commands to WinSCP script
|
|
157
|
-
# BUG We really need to fix this path situation.
|
|
239
|
+
# BUG (Low SFTP) We really need to fix this path situation.
|
|
158
240
|
# Unfortunately, this just needs to be a non-spaced path because WinSCP can't
|
|
159
241
|
# handle the spaces. Also, Windows won't let me use shutil to move the files out of G:\ into C:\ and it it wants an administrator security
|
|
160
242
|
# check or verification thing for me to even move the file by hand so that doesn't work either.
|
|
@@ -162,45 +244,171 @@ def operate_winscp(operation_type, files, endpoint_config, local_storage_path):
|
|
|
162
244
|
if operation_type == "upload":
|
|
163
245
|
for file_path in files:
|
|
164
246
|
normalized_path = os.path.normpath(file_path)
|
|
165
|
-
command.append("put
|
|
247
|
+
command.append("put {}".format(normalized_path))
|
|
166
248
|
else:
|
|
167
249
|
command.append('get *') # Adjust pattern as needed
|
|
168
250
|
|
|
169
251
|
command += ['close', 'exit']
|
|
170
252
|
|
|
171
|
-
#
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
253
|
+
# Check if TestMode is enabled in the configuration
|
|
254
|
+
if config.get("MediLink_Config", {}).get("TestMode", True):
|
|
255
|
+
# TestMode is enabled, do not execute the command
|
|
256
|
+
print("Test Mode is enabled! WinSCP Command not executed.")
|
|
257
|
+
MediLink_ConfigLoader.log("Test Mode is enabled! WinSCP Command not executed.")
|
|
258
|
+
MediLink_ConfigLoader.log("TEST MODE: Simulating WinSCP {} File List.".format(operation_type))
|
|
259
|
+
uploaded_files = []
|
|
260
|
+
if files is not None: # Check if files is not None
|
|
261
|
+
for file_path in files:
|
|
262
|
+
normalized_path = os.path.normpath(file_path)
|
|
263
|
+
if os.path.exists(normalized_path): # Check if the file exists before appending
|
|
264
|
+
uploaded_files.append(normalized_path)
|
|
265
|
+
else:
|
|
266
|
+
MediLink_ConfigLoader.log("TEST MODE: Failed to {} file: {} does not exist.".format(operation_type, normalized_path))
|
|
267
|
+
else:
|
|
268
|
+
MediLink_ConfigLoader.log("TEST MODE: No files to upload.")
|
|
269
|
+
return uploaded_files if files is not None else []
|
|
182
270
|
else:
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
#
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
271
|
+
# TestMode is not enabled, execute the command
|
|
272
|
+
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False)
|
|
273
|
+
stdout, stderr = process.communicate()
|
|
274
|
+
|
|
275
|
+
if process.returncode == 0: # BUG Does this work as intended?
|
|
276
|
+
MediLink_ConfigLoader.log("WinSCP {} attempted.".format(operation_type))
|
|
277
|
+
# Construct a list of downloaded files if operation_type is 'download'
|
|
278
|
+
if operation_type == 'download':
|
|
279
|
+
downloaded_files = []
|
|
280
|
+
for root, dirs, files in os.walk(local_storage_path):
|
|
281
|
+
for file in files:
|
|
282
|
+
downloaded_files.append(os.path.join(root, file))
|
|
283
|
+
return downloaded_files
|
|
284
|
+
|
|
285
|
+
if operation_type == 'upload':
|
|
286
|
+
# Return a list of uploaded files
|
|
287
|
+
uploaded_files = []
|
|
288
|
+
for file_path in files:
|
|
289
|
+
normalized_path = os.path.normpath(file_path)
|
|
290
|
+
if os.path.exists(normalized_path): # Check if the file exists before appending
|
|
291
|
+
uploaded_files.append(normalized_path)
|
|
292
|
+
else:
|
|
293
|
+
MediLink_ConfigLoader.log("Failed to upload file: {} does not exist.".format(normalized_path))
|
|
294
|
+
return uploaded_files
|
|
295
|
+
else:
|
|
296
|
+
MediLink_ConfigLoader.log("Failed to {} files. Details: {}".format(operation_type, stderr.decode('utf-8')))
|
|
297
|
+
return [] # Return empty list to indicate failure. BUG check to make sure this doesn't break something else.
|
|
298
|
+
|
|
299
|
+
def detect_new_files(directory_path, file_extension='.DAT'):
|
|
300
|
+
"""
|
|
301
|
+
Scans the specified directory for new files with a given extension and adds a timestamp if needed.
|
|
302
|
+
|
|
303
|
+
:param directory_path: Path to the directory containing files to be detected.
|
|
304
|
+
:param file_extension: Extension of the files to detect.
|
|
305
|
+
:return: A tuple containing a list of paths to new files detected in the directory and a flag indicating if a new file was just renamed.
|
|
306
|
+
"""
|
|
307
|
+
MediLink_ConfigLoader.log("Scanning directory: {}".format(directory_path), level="INFO")
|
|
308
|
+
detected_file_paths = []
|
|
309
|
+
file_flagged = False
|
|
310
|
+
|
|
311
|
+
try:
|
|
312
|
+
filenames = os.listdir(directory_path)
|
|
313
|
+
MediLink_ConfigLoader.log("Files in directory: {}".format(filenames), level="INFO")
|
|
314
|
+
|
|
315
|
+
for filename in filenames:
|
|
316
|
+
MediLink_ConfigLoader.log("Checking file: {}".format(filename), level="INFO")
|
|
317
|
+
if filename.endswith(file_extension):
|
|
318
|
+
MediLink_ConfigLoader.log("File matches extension: {}".format(file_extension), level="INFO")
|
|
319
|
+
name, ext = os.path.splitext(filename)
|
|
320
|
+
MediLink_ConfigLoader.log("File name: {}, File extension: {}".format(name, ext), level="INFO")
|
|
321
|
+
|
|
322
|
+
if not is_timestamped(name):
|
|
323
|
+
MediLink_ConfigLoader.log("File is not timestamped: {}".format(filename), level="INFO")
|
|
324
|
+
new_name = "{}_{}{}".format(name, datetime.now().strftime('%Y%m%d_%H%M%S'), ext)
|
|
325
|
+
os.rename(os.path.join(directory_path, filename), os.path.join(directory_path, new_name))
|
|
326
|
+
MediLink_ConfigLoader.log("Renamed file from {} to {}".format(filename, new_name), level="INFO")
|
|
327
|
+
file_flagged = True
|
|
328
|
+
filename = new_name
|
|
329
|
+
else:
|
|
330
|
+
MediLink_ConfigLoader.log("File is already timestamped: {}".format(filename), level="INFO")
|
|
331
|
+
|
|
332
|
+
file_path = os.path.join(directory_path, filename)
|
|
333
|
+
detected_file_paths.append(file_path)
|
|
334
|
+
MediLink_ConfigLoader.log("Detected file path: {}".format(file_path), level="INFO")
|
|
335
|
+
|
|
336
|
+
except Exception as e:
|
|
337
|
+
MediLink_ConfigLoader.log("Error occurred: {}".format(str(e)), level="INFO")
|
|
338
|
+
|
|
339
|
+
MediLink_ConfigLoader.log("Detected files: {}".format(detected_file_paths), level="INFO")
|
|
340
|
+
MediLink_ConfigLoader.log("File flagged status: {}".format(file_flagged), level="INFO")
|
|
341
|
+
|
|
342
|
+
return detected_file_paths, file_flagged
|
|
343
|
+
|
|
344
|
+
def is_timestamped(name):
|
|
345
|
+
"""
|
|
346
|
+
Checks if the given filename has a timestamp in the expected format.
|
|
347
|
+
|
|
348
|
+
:param name: The name of the file without extension.
|
|
349
|
+
:return: True if the filename includes a timestamp, False otherwise.
|
|
350
|
+
"""
|
|
351
|
+
# Regular expression to match timestamps in the format YYYYMMDD_HHMMSS
|
|
352
|
+
timestamp_pattern = re.compile(r'.*_\d{8}_\d{6}$')
|
|
353
|
+
return bool(timestamp_pattern.match(name))
|
|
354
|
+
|
|
355
|
+
def organize_patient_data_by_endpoint(detailed_patient_data):
|
|
356
|
+
"""
|
|
357
|
+
Organizes detailed patient data by their confirmed endpoints.
|
|
358
|
+
This simplifies processing and conversion per endpoint basis, ensuring that claims are generated and submitted
|
|
359
|
+
according to the endpoint-specific requirements.
|
|
360
|
+
|
|
361
|
+
:param detailed_patient_data: A list of dictionaries, each containing detailed patient data including confirmed endpoint.
|
|
362
|
+
:return: A dictionary with endpoints as keys and lists of detailed patient data as values for processing.
|
|
363
|
+
"""
|
|
364
|
+
organized = {}
|
|
365
|
+
for data in detailed_patient_data:
|
|
366
|
+
# Retrieve confirmed endpoint from each patient's data
|
|
367
|
+
endpoint = data['confirmed_endpoint'] if 'confirmed_endpoint' in data else data['suggested_endpoint']
|
|
368
|
+
# Initialize a list for the endpoint if it doesn't exist
|
|
369
|
+
if endpoint not in organized:
|
|
370
|
+
organized[endpoint] = []
|
|
371
|
+
organized[endpoint].append(data)
|
|
372
|
+
return organized
|
|
373
|
+
|
|
374
|
+
def confirm_all_suggested_endpoints(detailed_patient_data):
|
|
375
|
+
"""
|
|
376
|
+
Confirms all suggested endpoints for each patient's detailed data.
|
|
377
|
+
"""
|
|
378
|
+
for data in detailed_patient_data:
|
|
379
|
+
if 'confirmed_endpoint' not in data:
|
|
380
|
+
data['confirmed_endpoint'] = data['suggested_endpoint']
|
|
381
|
+
return detailed_patient_data
|
|
382
|
+
|
|
383
|
+
def bulk_edit_insurance_types(detailed_patient_data, insurance_options):
|
|
384
|
+
# Allow user to edit insurance types in a table-like format with validation
|
|
385
|
+
print("Edit Insurance Type (Enter the 2-character code). Enter 'LIST' to display available insurance types.")
|
|
386
|
+
|
|
387
|
+
for data in detailed_patient_data:
|
|
388
|
+
current_insurance_type = data['insurance_type']
|
|
389
|
+
current_insurance_description = insurance_options.get(current_insurance_type, "Unknown")
|
|
390
|
+
print("({}) {:<25} | Current Ins. Type: {} - {}".format(
|
|
391
|
+
data['patient_id'], data['patient_name'], current_insurance_type, current_insurance_description))
|
|
392
|
+
|
|
393
|
+
while True:
|
|
394
|
+
new_insurance_type = input("Enter new insurance type (or press Enter to keep current): ").upper()
|
|
395
|
+
if new_insurance_type == 'LIST':
|
|
396
|
+
MediLink_UI.display_insurance_options(insurance_options)
|
|
397
|
+
elif not new_insurance_type or new_insurance_type in insurance_options:
|
|
398
|
+
if new_insurance_type:
|
|
399
|
+
data['insurance_type'] = new_insurance_type
|
|
400
|
+
break
|
|
401
|
+
else:
|
|
402
|
+
print("Invalid insurance type. Please enter a valid 2-character code or type 'LIST' to see options.")
|
|
403
|
+
|
|
404
|
+
def review_and_confirm_changes(detailed_patient_data, insurance_options):
|
|
405
|
+
# Review and confirm changes
|
|
406
|
+
print("\nReview changes:")
|
|
407
|
+
print("{:<20} {:<10} {:<30}".format("Patient Name", "Ins. Type", "Description"))
|
|
408
|
+
print("="*65)
|
|
409
|
+
for data in detailed_patient_data:
|
|
410
|
+
insurance_type = data['insurance_type']
|
|
411
|
+
insurance_description = insurance_options.get(insurance_type, "Unknown")
|
|
412
|
+
print("{:<20} {:<10} {:<30}".format(data['patient_name'], insurance_type, insurance_description))
|
|
413
|
+
confirm = input("\nConfirm changes? (y/n): ").strip().lower()
|
|
414
|
+
return confirm in ['y', 'yes', '']
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# MediLink_Decoder.py
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import csv
|
|
5
|
+
from MediLink_ConfigLoader import load_configuration, log
|
|
6
|
+
from MediLink_Parser import parse_era_content, parse_277_content
|
|
7
|
+
|
|
8
|
+
def process_file(file_path, output_directory):
|
|
9
|
+
if not os.path.exists(output_directory):
|
|
10
|
+
os.makedirs(output_directory)
|
|
11
|
+
|
|
12
|
+
file_type = determine_file_type(file_path)
|
|
13
|
+
content = read_file(file_path)
|
|
14
|
+
|
|
15
|
+
if file_type == 'ERA':
|
|
16
|
+
records = parse_era_content(content)
|
|
17
|
+
fieldnames = ['Date of Service', 'Check EFT', 'Chart Number', 'Payer Address', 'Amount Paid',
|
|
18
|
+
'Adjustment Amount', 'Allowed Amount', 'Write Off', 'Patient Responsibility', 'Charge']
|
|
19
|
+
elif file_type == '277':
|
|
20
|
+
records = parse_277_content(content)
|
|
21
|
+
fieldnames = ['Clearing House', 'Received Date', 'Claim Status Tracking #', 'Billed Amt', 'Date of Service',
|
|
22
|
+
'Last', 'First', 'Acknowledged Amt', 'Status']
|
|
23
|
+
else:
|
|
24
|
+
raise ValueError("Unsupported file type: {}".format(file_type))
|
|
25
|
+
|
|
26
|
+
output_file_path = os.path.join(output_directory, os.path.basename(file_path) + '_decoded.csv')
|
|
27
|
+
write_records_to_csv(records, output_file_path, fieldnames)
|
|
28
|
+
print("Decoded data written to {}".format(output_file_path))
|
|
29
|
+
|
|
30
|
+
def determine_file_type(file_path):
|
|
31
|
+
if file_path.endswith('.era'):
|
|
32
|
+
return 'ERA'
|
|
33
|
+
elif file_path.endswith('.277'):
|
|
34
|
+
return '277'
|
|
35
|
+
else:
|
|
36
|
+
raise ValueError("Unsupported file type for file: {}".format(file_path))
|
|
37
|
+
|
|
38
|
+
def read_file(file_path):
|
|
39
|
+
with open(file_path, 'r') as file:
|
|
40
|
+
content = file.read().replace('\n', '')
|
|
41
|
+
return content
|
|
42
|
+
|
|
43
|
+
def write_records_to_csv(records, output_file_path, fieldnames):
|
|
44
|
+
with open(output_file_path, 'w', newline='') as csvfile:
|
|
45
|
+
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
|
46
|
+
writer.writeheader()
|
|
47
|
+
for record in records:
|
|
48
|
+
writer.writerow(record)
|
|
49
|
+
|
|
50
|
+
if __name__ == "__main__":
|
|
51
|
+
config = load_configuration()
|
|
52
|
+
|
|
53
|
+
files = sys.argv[1:]
|
|
54
|
+
if not files:
|
|
55
|
+
log("No files provided as arguments.", 'error')
|
|
56
|
+
sys.exit(1)
|
|
57
|
+
|
|
58
|
+
output_directory = config['output_directory']
|
|
59
|
+
for file_path in files:
|
|
60
|
+
try:
|
|
61
|
+
process_file(file_path, output_directory)
|
|
62
|
+
except Exception as e:
|
|
63
|
+
log("Failed to process {}: {}".format(file_path, e), 'error')
|