medicafe 0.240517.0__py3-none-any.whl → 0.240613.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of medicafe might be problematic. Click here for more details.
- MediBot/MediBot.bat +9 -1
- MediBot/MediBot.py +9 -36
- MediBot/MediBot_Charges.py +0 -28
- MediBot/MediBot_Crosswalk_Library.py +1 -0
- MediBot/MediBot_Post.py +0 -0
- MediBot/MediBot_Preprocessor.py +26 -63
- MediBot/MediBot_Preprocessor_lib.py +182 -43
- MediBot/MediBot_UI.py +2 -7
- MediBot/MediBot_dataformat_library.py +0 -9
- MediBot/MediBot_docx_decoder.py +275 -60
- MediLink/MediLink.py +102 -107
- MediLink/MediLink_837p_encoder.py +3 -28
- MediLink/MediLink_837p_encoder_library.py +14 -25
- MediLink/MediLink_APIs.py +2 -0
- MediLink/MediLink_DataMgmt.py +224 -68
- MediLink/MediLink_Decoder.py +63 -0
- MediLink/MediLink_Down.py +73 -79
- MediLink/MediLink_Gmail.py +453 -74
- MediLink/MediLink_Mailer.py +0 -7
- MediLink/MediLink_Parser.py +111 -0
- MediLink/MediLink_Scan.py +0 -0
- MediLink/MediLink_Scheduler.py +2 -172
- MediLink/MediLink_StatusCheck.py +0 -4
- MediLink/MediLink_UI.py +76 -18
- MediLink/MediLink_Up.py +4 -14
- {medicafe-0.240517.0.dist-info → medicafe-0.240613.0.dist-info}/METADATA +3 -1
- medicafe-0.240613.0.dist-info/RECORD +43 -0
- medicafe-0.240517.0.dist-info/RECORD +0 -39
- {medicafe-0.240517.0.dist-info → medicafe-0.240613.0.dist-info}/LICENSE +0 -0
- {medicafe-0.240517.0.dist-info → medicafe-0.240613.0.dist-info}/WHEEL +0 -0
- {medicafe-0.240517.0.dist-info → medicafe-0.240613.0.dist-info}/top_level.txt +0 -0
|
@@ -7,31 +7,6 @@ from MediLink_DataMgmt import parse_fixed_width_data, read_fixed_width_data
|
|
|
7
7
|
import MediLink_837p_encoder_library
|
|
8
8
|
#from tqdm import tqdm
|
|
9
9
|
|
|
10
|
-
"""
|
|
11
|
-
Single File Processing Flow:
|
|
12
|
-
|
|
13
|
-
This flow is triggered when the -d (directory) flag is not set. It handles the conversion of a single file specified by the -p flag.
|
|
14
|
-
It directly processes the single file specified, without the need to iterate over a directory.
|
|
15
|
-
The conversion initializes and processes this single file, appending the necessary EDI segments, and directly writes the output once processing is complete.
|
|
16
|
-
|
|
17
|
-
Batch Directory Processing Flow:
|
|
18
|
-
|
|
19
|
-
Activated when the -d flag is set, indicating that the -p flag points to a directory rather than a single file.
|
|
20
|
-
Iterates over all files in the specified directory, processing only those that end with the ".DAT" extension.
|
|
21
|
-
Each file is processed in sequence, with each undergoing a full cycle of reading, processing, and output file generation as in the single file flow.
|
|
22
|
-
|
|
23
|
-
Development Task List:
|
|
24
|
-
|
|
25
|
-
- [ ] 1. File Path Management: Enhance the handling of input paths to efficiently manage both individual files and directories, accommodating a range of file processing scenarios.
|
|
26
|
-
- [ ] 2. User Interface Improvement: Advance the CLI for intuitive user interaction, offering clear options for file processing and real-time progress updates.
|
|
27
|
-
- [ ] 3. Validation and Logging: Strengthen validation processes for input data, incorporating thorough checks against business rules and enhanced detailed logging for improved traceability and troubleshooting.
|
|
28
|
-
- [ ] 4. Batch Processing and Output Handling: Enhance output file management to support efficient batch operations, including systematic naming and organization for output files.
|
|
29
|
-
- [ ] 5. Comprehensive Documentation: Maintain up-to-date and detailed documentation within the codebase, ensuring all functions and complex logic are clearly explained.
|
|
30
|
-
- [ ] 6. De-persisting Intermediate Files.
|
|
31
|
-
- [ ] 7. Determination of Relationship to Patient for insurance holder. Can Compare Insured Name & closeness of DOB (usually spouse [2], child [3]).
|
|
32
|
-
- [ ] 8. Consolidation of certain functions needs to happen here.
|
|
33
|
-
"""
|
|
34
|
-
|
|
35
10
|
def format_single_claim(patient_data, config, endpoint, transaction_set_control_number):
|
|
36
11
|
"""
|
|
37
12
|
Formats a single claim into 837P segments based on the provided patient data and endpoint.
|
|
@@ -196,9 +171,9 @@ def read_and_validate_claims(file_path, config):
|
|
|
196
171
|
validation_errors = [] # List to store validation errors
|
|
197
172
|
|
|
198
173
|
# Iterate over data in the file
|
|
199
|
-
for personal_info, insurance_info, service_info in read_fixed_width_data(file_path):
|
|
174
|
+
for personal_info, insurance_info, service_info, service_info_2, service_info_3 in read_fixed_width_data(file_path):
|
|
200
175
|
# Parse data into a usable format
|
|
201
|
-
parsed_data = parse_fixed_width_data(personal_info, insurance_info, service_info, config.get('MediLink_Config', config))
|
|
176
|
+
parsed_data = parse_fixed_width_data(personal_info, insurance_info, service_info, service_info_2, service_info_3, config.get('MediLink_Config', config))
|
|
202
177
|
# Validate the parsed data
|
|
203
178
|
is_valid, errors = validate_claim_data(parsed_data, config)
|
|
204
179
|
if is_valid:
|
|
@@ -263,7 +238,7 @@ def validate_claim_data(parsed_data, config, required_fields=[]):
|
|
|
263
238
|
]
|
|
264
239
|
"""
|
|
265
240
|
errors = []
|
|
266
|
-
MediLink_ConfigLoader.log("Starting claim
|
|
241
|
+
MediLink_ConfigLoader.log("Starting claim data validation...")
|
|
267
242
|
if not required_fields:
|
|
268
243
|
# If no required fields are specified, assume validation is true
|
|
269
244
|
return True, []
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
from datetime import datetime
|
|
2
2
|
import sys
|
|
3
|
-
from MediLink import MediLink_ConfigLoader
|
|
3
|
+
from MediLink import MediLink_ConfigLoader, MediLink_UI
|
|
4
4
|
|
|
5
5
|
# Add parent directory of the project to the Python path
|
|
6
6
|
import sys
|
|
7
7
|
import os
|
|
8
|
+
|
|
8
9
|
project_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
|
|
9
10
|
sys.path.append(project_dir)
|
|
10
11
|
|
|
@@ -13,19 +14,6 @@ load_insurance_data_from_mains = MediBot_Preprocessor_lib.load_insurance_data_fr
|
|
|
13
14
|
from MediBot import MediBot_Crosswalk_Library
|
|
14
15
|
from MediLink_API_v2 import fetch_payer_name_from_api
|
|
15
16
|
|
|
16
|
-
"""
|
|
17
|
-
- [ ] 1. Code Refactoring: Increase modularity and clarity, particularly in segment creation functions (e.g., `create_st_segment`, `create_nm1_billing_provider_segment`), for better maintenance and readability.
|
|
18
|
-
- [ ] 2. Endpoint Support: Extend support within segment creation for additional endpoints with attention to their unique claim submission requirements.
|
|
19
|
-
- [ ] 3. Payer Identification Mechanism: Refine the mechanism for dynamically identifying payers, leveraging payer mappings and integrating with external APIs like Availity for precise payer information retrieval.
|
|
20
|
-
- [ ] 4. Adherence to Endpoint-Specific Standards: Implement and verify the compliance of claim data formatting and inclusion based on the specific demands of each target endpoint within the segment creation logic.
|
|
21
|
-
- [ ] 5. De-persisting Intermediate Files.
|
|
22
|
-
- [ ] 6. Get an API for Optum "Entered As".
|
|
23
|
-
- [ ] 7. (MED) Add Authorization Number
|
|
24
|
-
- [X] 8. (HIGH) Interchange number should be 000HHMMSS instead of fixed constant.
|
|
25
|
-
- [ ] 9. Upgrade Interchange formation to consolidate the batch processor and also de-risk batch
|
|
26
|
-
interchange number matching for multiple .DAT.
|
|
27
|
-
"""
|
|
28
|
-
|
|
29
17
|
# Converts date format from one format to another.
|
|
30
18
|
def convert_date_format(date_str):
|
|
31
19
|
# Parse the input date string into a datetime object using the input format
|
|
@@ -520,7 +508,17 @@ def insurance_type_selection(parsed_data):
|
|
|
520
508
|
- str: The insurance type code selected by the user.
|
|
521
509
|
|
|
522
510
|
"""
|
|
523
|
-
|
|
511
|
+
MediLink_ConfigLoader.log("insurance_type_selection(parsed_data): {}".format(parsed_data), level="DEBUG")
|
|
512
|
+
|
|
513
|
+
# Check if insurance type is already assigned and is valid
|
|
514
|
+
insurance_type_code = parsed_data.get('insurance_type')
|
|
515
|
+
if insurance_type_code and len(insurance_type_code) == 2 and insurance_type_code.isalnum():
|
|
516
|
+
MediLink_ConfigLoader.log("Insurance type already assigned: {}".format(insurance_type_code), level="DEBUG")
|
|
517
|
+
return insurance_type_code
|
|
518
|
+
elif insurance_type_code:
|
|
519
|
+
MediLink_ConfigLoader.log("Invalid insurance type: {}".format(insurance_type_code), level="WARNING")
|
|
520
|
+
|
|
521
|
+
print("\nInsurance Type Validation Error: Select the insurance type for patient {}: ".format(parsed_data['LAST']))
|
|
524
522
|
|
|
525
523
|
# Define insurance options with codes and descriptions
|
|
526
524
|
insurance_options = {
|
|
@@ -549,22 +547,13 @@ def insurance_type_selection(parsed_data):
|
|
|
549
547
|
"ZZ": "Mutually Defined"
|
|
550
548
|
}
|
|
551
549
|
|
|
552
|
-
# Function to display full list of insurance options
|
|
553
|
-
def display_insurance_options(options):
|
|
554
|
-
print("Insurance Type Options:")
|
|
555
|
-
# Sorting the dictionary keys to ensure consistent order
|
|
556
|
-
sorted_keys = sorted(options.keys())
|
|
557
|
-
for code in sorted_keys:
|
|
558
|
-
description = options[code]
|
|
559
|
-
print("{} - {}".format(code, description))
|
|
560
|
-
|
|
561
550
|
def prompt_display_insurance_options():
|
|
562
551
|
# Prompt to display full list
|
|
563
552
|
display_full_list = input("Do you want to see the full list of insurance options? (yes/no): ").strip().lower()
|
|
564
553
|
|
|
565
554
|
# Display full list if user confirms
|
|
566
555
|
if display_full_list in ['yes', 'y']:
|
|
567
|
-
display_insurance_options(insurance_options)
|
|
556
|
+
MediLink_UI.display_insurance_options(insurance_options)
|
|
568
557
|
|
|
569
558
|
# Horrible menu
|
|
570
559
|
prompt_display_insurance_options()
|
MediLink/MediLink_APIs.py
CHANGED
MediLink/MediLink_DataMgmt.py
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
|
+
# MediLink_DataMgmt.py
|
|
1
2
|
import csv
|
|
2
3
|
import os
|
|
3
4
|
from datetime import datetime, timedelta
|
|
5
|
+
import re
|
|
4
6
|
import subprocess
|
|
5
7
|
|
|
6
8
|
# Need this for running Medibot and MediLink
|
|
7
9
|
try:
|
|
8
10
|
import MediLink_ConfigLoader
|
|
11
|
+
import MediLink_UI
|
|
9
12
|
except ImportError:
|
|
10
13
|
from . import MediLink_ConfigLoader
|
|
14
|
+
from . import MediLink_UI
|
|
11
15
|
|
|
12
|
-
# Helper function to slice and strip values
|
|
13
|
-
def slice_data(data, slices):
|
|
16
|
+
# Helper function to slice and strip values with optional key suffix
|
|
17
|
+
def slice_data(data, slices, suffix=''):
|
|
14
18
|
# Convert slices list to a tuple for slicing operation
|
|
15
|
-
return {key: data[slice(*slices[key])].strip() for key in slices}
|
|
19
|
+
return {key + suffix: data[slice(*slices[key])].strip() for key in slices}
|
|
16
20
|
|
|
17
21
|
# Function to parse fixed-width Medisoft output and extract claim data
|
|
18
|
-
def parse_fixed_width_data(personal_info, insurance_info, service_info, config=None):
|
|
22
|
+
def parse_fixed_width_data(personal_info, insurance_info, service_info, service_info_2=None, service_info_3=None, config=None):
|
|
19
23
|
|
|
20
24
|
# Make sure we have the right config
|
|
21
25
|
if not config: # Checks if config is None or an empty dictionary
|
|
@@ -35,6 +39,12 @@ def parse_fixed_width_data(personal_info, insurance_info, service_info, config=N
|
|
|
35
39
|
parsed_data.update(slice_data(insurance_info, insurance_slices))
|
|
36
40
|
parsed_data.update(slice_data(service_info, service_slices))
|
|
37
41
|
|
|
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
|
+
|
|
38
48
|
MediLink_ConfigLoader.log("Successfully parsed data from segments", config, level="INFO")
|
|
39
49
|
|
|
40
50
|
return parsed_data
|
|
@@ -46,18 +56,32 @@ def read_fixed_width_data(file_path):
|
|
|
46
56
|
MediLink_ConfigLoader.log("Starting to read fixed width data...")
|
|
47
57
|
with open(file_path, 'r') as file:
|
|
48
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
|
+
|
|
49
69
|
for line in file:
|
|
50
70
|
stripped_line = line.strip()
|
|
51
|
-
if stripped_line:
|
|
71
|
+
if stripped_line:
|
|
52
72
|
lines_buffer.append(stripped_line)
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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)
|
|
61
85
|
|
|
62
86
|
# TODO (Refactor) Consider consolidating with the other read_fixed_with_data
|
|
63
87
|
def read_general_fixed_width_data(file_path, slices):
|
|
@@ -68,21 +92,34 @@ def read_general_fixed_width_data(file_path, slices):
|
|
|
68
92
|
insurance_name = {key: line[start:end].strip() for key, (start, end) in slices.items()}
|
|
69
93
|
yield insurance_name, line_number
|
|
70
94
|
|
|
71
|
-
def consolidate_csvs(source_directory):
|
|
95
|
+
def consolidate_csvs(source_directory, file_prefix="Consolidated", interactive=False):
|
|
72
96
|
"""
|
|
73
|
-
|
|
74
|
-
|
|
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.
|
|
75
106
|
"""
|
|
76
107
|
today = datetime.now()
|
|
77
|
-
consolidated_filename = today.strftime("
|
|
108
|
+
consolidated_filename = "{}_{}.csv".format(file_prefix, today.strftime("%m%d%y"))
|
|
78
109
|
consolidated_filepath = os.path.join(source_directory, consolidated_filename)
|
|
79
110
|
|
|
80
111
|
consolidated_data = []
|
|
81
112
|
header_saved = False
|
|
113
|
+
expected_header = None
|
|
82
114
|
|
|
83
115
|
# Check if the file already exists and log the action
|
|
84
116
|
if os.path.exists(consolidated_filepath):
|
|
85
|
-
MediLink_ConfigLoader.log("The file {} already exists. It will be overwritten.".format(consolidated_filename))
|
|
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
|
|
86
123
|
|
|
87
124
|
for filename in os.listdir(source_directory):
|
|
88
125
|
filepath = os.path.join(source_directory, filename)
|
|
@@ -94,26 +131,38 @@ def consolidate_csvs(source_directory):
|
|
|
94
131
|
if modification_time < today - timedelta(days=1):
|
|
95
132
|
continue # Skip files not modified in the last day
|
|
96
133
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
|
105
145
|
|
|
106
|
-
|
|
107
|
-
|
|
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
|
|
108
153
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
writer = csv.writer(csvfile)
|
|
112
|
-
writer.writerows(consolidated_data)
|
|
154
|
+
os.remove(filepath)
|
|
155
|
+
MediLink_ConfigLoader.log("Deleted source file after consolidation: {}".format(filepath), level="INFO")
|
|
113
156
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
117
166
|
|
|
118
167
|
def operate_winscp(operation_type, files, endpoint_config, local_storage_path, config):
|
|
119
168
|
"""
|
|
@@ -161,9 +210,20 @@ def operate_winscp(operation_type, files, endpoint_config, local_storage_path, c
|
|
|
161
210
|
winscp_log_path = os.path.join(local_storage_path, log_filename)
|
|
162
211
|
|
|
163
212
|
# Session and directory setup
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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 = ''
|
|
167
227
|
# Command building
|
|
168
228
|
command = [
|
|
169
229
|
winscp_path,
|
|
@@ -195,15 +255,18 @@ def operate_winscp(operation_type, files, endpoint_config, local_storage_path, c
|
|
|
195
255
|
# TestMode is enabled, do not execute the command
|
|
196
256
|
print("Test Mode is enabled! WinSCP Command not executed.")
|
|
197
257
|
MediLink_ConfigLoader.log("Test Mode is enabled! WinSCP Command not executed.")
|
|
198
|
-
MediLink_ConfigLoader.log("TEST MODE: Simulating WinSCP
|
|
258
|
+
MediLink_ConfigLoader.log("TEST MODE: Simulating WinSCP {} File List.".format(operation_type))
|
|
199
259
|
uploaded_files = []
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
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 []
|
|
207
270
|
else:
|
|
208
271
|
# TestMode is not enabled, execute the command
|
|
209
272
|
process = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=False)
|
|
@@ -233,26 +296,119 @@ def operate_winscp(operation_type, files, endpoint_config, local_storage_path, c
|
|
|
233
296
|
MediLink_ConfigLoader.log("Failed to {} files. Details: {}".format(operation_type, stderr.decode('utf-8')))
|
|
234
297
|
return [] # Return empty list to indicate failure. BUG check to make sure this doesn't break something else.
|
|
235
298
|
|
|
236
|
-
|
|
237
|
-
"""
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
|
242
310
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
"""
|
|
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')
|