medicafe 0.250728.8__py3-none-any.whl → 0.250805.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 +233 -19
- MediBot/MediBot.py +138 -46
- MediBot/MediBot_Crosswalk_Library.py +127 -623
- MediBot/MediBot_Crosswalk_Utils.py +618 -0
- MediBot/MediBot_Preprocessor.py +72 -17
- MediBot/MediBot_Preprocessor_lib.py +470 -76
- MediBot/MediBot_UI.py +32 -17
- MediBot/MediBot_dataformat_library.py +68 -20
- MediBot/MediBot_docx_decoder.py +120 -19
- MediBot/MediBot_smart_import.py +180 -0
- MediBot/__init__.py +89 -0
- MediBot/get_medicafe_version.py +25 -0
- MediBot/update_json.py +35 -6
- MediBot/update_medicafe.py +19 -1
- MediCafe/MediLink_ConfigLoader.py +160 -0
- MediCafe/__init__.py +171 -0
- MediCafe/__main__.py +222 -0
- MediCafe/api_core.py +1098 -0
- MediCafe/api_core_backup.py +427 -0
- MediCafe/api_factory.py +306 -0
- MediCafe/api_utils.py +356 -0
- MediCafe/core_utils.py +450 -0
- MediCafe/graphql_utils.py +445 -0
- MediCafe/logging_config.py +123 -0
- MediCafe/logging_demo.py +61 -0
- MediCafe/migration_helpers.py +463 -0
- MediCafe/smart_import.py +436 -0
- MediLink/MediLink.py +66 -26
- MediLink/MediLink_837p_cob_library.py +28 -28
- MediLink/MediLink_837p_encoder.py +33 -34
- MediLink/MediLink_837p_encoder_library.py +243 -151
- MediLink/MediLink_837p_utilities.py +129 -5
- MediLink/MediLink_API_Generator.py +83 -60
- MediLink/MediLink_API_v3.py +1 -1
- MediLink/MediLink_ClaimStatus.py +177 -31
- MediLink/MediLink_DataMgmt.py +405 -72
- MediLink/MediLink_Decoder.py +20 -1
- MediLink/MediLink_Deductible.py +155 -28
- MediLink/MediLink_Display_Utils.py +72 -0
- MediLink/MediLink_Down.py +127 -5
- MediLink/MediLink_Gmail.py +712 -653
- MediLink/MediLink_PatientProcessor.py +257 -0
- MediLink/MediLink_UI.py +85 -61
- MediLink/MediLink_Up.py +28 -4
- MediLink/MediLink_insurance_utils.py +227 -264
- MediLink/MediLink_main.py +248 -0
- MediLink/MediLink_smart_import.py +264 -0
- MediLink/__init__.py +93 -0
- MediLink/insurance_type_integration_test.py +66 -76
- MediLink/test.py +1 -1
- MediLink/test_timing.py +59 -0
- {medicafe-0.250728.8.dist-info → medicafe-0.250805.0.dist-info}/METADATA +1 -1
- medicafe-0.250805.0.dist-info/RECORD +81 -0
- medicafe-0.250805.0.dist-info/entry_points.txt +2 -0
- {medicafe-0.250728.8.dist-info → medicafe-0.250805.0.dist-info}/top_level.txt +1 -0
- medicafe-0.250728.8.dist-info/RECORD +0 -59
- {medicafe-0.250728.8.dist-info → medicafe-0.250805.0.dist-info}/LICENSE +0 -0
- {medicafe-0.250728.8.dist-info → medicafe-0.250805.0.dist-info}/WHEEL +0 -0
MediLink/MediLink_DataMgmt.py
CHANGED
|
@@ -1,13 +1,29 @@
|
|
|
1
1
|
import csv, os, re, subprocess, time
|
|
2
2
|
from datetime import datetime, timedelta
|
|
3
3
|
|
|
4
|
+
# Import centralized logging configuration
|
|
5
|
+
try:
|
|
6
|
+
from MediCafe.logging_config import PERFORMANCE_LOGGING
|
|
7
|
+
except ImportError:
|
|
8
|
+
# Fallback to local flag if centralized config is not available
|
|
9
|
+
PERFORMANCE_LOGGING = False
|
|
10
|
+
|
|
4
11
|
# Need this for running Medibot and MediLink
|
|
12
|
+
from MediCafe.core_utils import get_shared_config_loader
|
|
13
|
+
MediLink_ConfigLoader = get_shared_config_loader()
|
|
5
14
|
try:
|
|
6
|
-
import
|
|
7
|
-
import MediLink_UI
|
|
15
|
+
import MediLink_Display_Utils
|
|
8
16
|
except ImportError:
|
|
9
|
-
from
|
|
10
|
-
|
|
17
|
+
from MediLink import MediLink_Display_Utils
|
|
18
|
+
|
|
19
|
+
# MediBot imports will be done locally in functions to avoid circular imports
|
|
20
|
+
def _get_medibot_function(module_name, function_name):
|
|
21
|
+
"""Dynamically import MediBot functions when needed to avoid circular imports."""
|
|
22
|
+
try:
|
|
23
|
+
module = __import__('MediBot.{}'.format(module_name), fromlist=[function_name])
|
|
24
|
+
return getattr(module, function_name)
|
|
25
|
+
except (ImportError, AttributeError):
|
|
26
|
+
return None
|
|
11
27
|
|
|
12
28
|
# Helper function to slice and strip values with optional key suffix
|
|
13
29
|
def slice_data(data, slices, suffix=''):
|
|
@@ -29,16 +45,17 @@ def parse_fixed_width_data(personal_info, insurance_info, service_info, service_
|
|
|
29
45
|
insurance_slices = config['fixedWidthSlices']['insurance_slices']
|
|
30
46
|
service_slices = config['fixedWidthSlices']['service_slices']
|
|
31
47
|
|
|
32
|
-
# Parse each segment
|
|
48
|
+
# Parse each segment - core 3-line record structure
|
|
33
49
|
parsed_data = {}
|
|
34
|
-
parsed_data.update(slice_data(personal_info, personal_slices))
|
|
35
|
-
parsed_data.update(slice_data(insurance_info, insurance_slices))
|
|
36
|
-
parsed_data.update(slice_data(service_info, service_slices))
|
|
50
|
+
parsed_data.update(slice_data(personal_info, personal_slices)) # Line 1: Personal info
|
|
51
|
+
parsed_data.update(slice_data(insurance_info, insurance_slices)) # Line 2: Insurance info
|
|
52
|
+
parsed_data.update(slice_data(service_info, service_slices)) # Line 3: Service info
|
|
37
53
|
|
|
38
|
-
|
|
54
|
+
# Parse reserved expansion lines (future-ready design)
|
|
55
|
+
if service_info_2: # Line 4: Reserved for additional service data
|
|
39
56
|
parsed_data.update(slice_data(service_info_2, service_slices, suffix='_2'))
|
|
40
57
|
|
|
41
|
-
if service_info_3:
|
|
58
|
+
if service_info_3: # Line 5: Reserved for additional service data
|
|
42
59
|
parsed_data.update(slice_data(service_info_3, service_slices, suffix='_3'))
|
|
43
60
|
|
|
44
61
|
# Replace underscores with spaces in first and last names since this is downstream of MediSoft.
|
|
@@ -53,52 +70,174 @@ def parse_fixed_width_data(personal_info, insurance_info, service_info, service_
|
|
|
53
70
|
|
|
54
71
|
# Function to read fixed-width Medisoft output and extract claim data
|
|
55
72
|
def read_fixed_width_data(file_path):
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if stripped_line:
|
|
74
|
-
lines_buffer.append(stripped_line)
|
|
75
|
-
if 3 <= len(lines_buffer) <= 5:
|
|
76
|
-
next_line = file.readline().strip()
|
|
77
|
-
if not next_line:
|
|
78
|
-
yield yield_record(lines_buffer)
|
|
79
|
-
lines_buffer.clear()
|
|
80
|
-
else:
|
|
81
|
-
if len(lines_buffer) >= 3:
|
|
82
|
-
yield yield_record(lines_buffer)
|
|
83
|
-
lines_buffer.clear()
|
|
84
|
-
|
|
85
|
-
if lines_buffer: # Yield any remaining buffer if file ends without a blank line
|
|
86
|
-
yield yield_record(lines_buffer)
|
|
73
|
+
"""
|
|
74
|
+
Legacy function maintained for backward compatibility.
|
|
75
|
+
Reads fixed-width Medisoft data with RESERVED 5-line patient record format.
|
|
76
|
+
|
|
77
|
+
DESIGN NOTE: This implements a reserved record structure where each patient
|
|
78
|
+
record can contain 3-5 lines (currently using 3, with 2 reserved for future expansion).
|
|
79
|
+
The peek-ahead logic is intentional to handle variable-length records and maintain
|
|
80
|
+
proper spacing between patient records.
|
|
81
|
+
"""
|
|
82
|
+
# Use the consolidated function with Medisoft-specific configuration
|
|
83
|
+
medisoft_config = {
|
|
84
|
+
'mode': 'medisoft_records',
|
|
85
|
+
'min_lines': 3,
|
|
86
|
+
'max_lines': 5,
|
|
87
|
+
'skip_header': False
|
|
88
|
+
}
|
|
89
|
+
return read_consolidated_fixed_width_data(file_path, config=medisoft_config)
|
|
87
90
|
|
|
88
|
-
# TODO (Refactor) Consider consolidating with the other read_fixed_with_data
|
|
89
91
|
def read_general_fixed_width_data(file_path, slices):
|
|
90
|
-
|
|
92
|
+
"""
|
|
93
|
+
Legacy function maintained for backward compatibility.
|
|
94
|
+
Handles fixed-width data based on provided slice definitions.
|
|
95
|
+
"""
|
|
96
|
+
# Use the consolidated function with slice-based configuration
|
|
97
|
+
slice_config = {
|
|
98
|
+
'mode': 'slice_based',
|
|
99
|
+
'slices': slices,
|
|
100
|
+
'skip_header': True
|
|
101
|
+
}
|
|
102
|
+
return read_consolidated_fixed_width_data(file_path, config=slice_config)
|
|
103
|
+
|
|
104
|
+
# CONSOLIDATED IMPLEMENTATION - Replaces both read_fixed_width_data and read_general_fixed_width_data
|
|
105
|
+
def read_consolidated_fixed_width_data(file_path, config=None):
|
|
106
|
+
"""
|
|
107
|
+
Unified function for reading various fixed-width data formats.
|
|
108
|
+
|
|
109
|
+
Parameters:
|
|
110
|
+
- file_path: Path to the fixed-width data file
|
|
111
|
+
- config: Configuration dictionary with the following keys:
|
|
112
|
+
- mode: 'medisoft_records' (reserved 5-line format) or 'slice_based'
|
|
113
|
+
- slices: Dictionary of field slices (for slice_based mode)
|
|
114
|
+
- skip_header: Boolean, whether to skip first line
|
|
115
|
+
- min_lines: Minimum lines per record (for medisoft_records mode)
|
|
116
|
+
- max_lines: Maximum lines per record (for medisoft_records mode)
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
- Generator yielding parsed records based on the specified mode
|
|
120
|
+
"""
|
|
121
|
+
if config is None:
|
|
122
|
+
config = {'mode': 'medisoft_records', 'min_lines': 3, 'max_lines': 5, 'skip_header': False}
|
|
123
|
+
|
|
124
|
+
mode = config.get('mode', 'medisoft_records')
|
|
125
|
+
|
|
91
126
|
try:
|
|
92
127
|
with open(file_path, 'r', encoding='utf-8') as file:
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
128
|
+
# Skip header if configured
|
|
129
|
+
if config.get('skip_header', False):
|
|
130
|
+
next(file)
|
|
131
|
+
|
|
132
|
+
if mode == 'medisoft_records':
|
|
133
|
+
# Handle Medisoft reserved 5-line patient record format
|
|
134
|
+
yield from _process_medisoft_records(file, file_path, config)
|
|
135
|
+
elif mode == 'slice_based':
|
|
136
|
+
# Handle slice-based field extraction
|
|
137
|
+
yield from _process_slice_based_records(file, file_path, config)
|
|
138
|
+
else:
|
|
139
|
+
raise ValueError("Invalid mode '{}'. Use 'medisoft_records' or 'slice_based'".format(mode))
|
|
140
|
+
|
|
97
141
|
except FileNotFoundError:
|
|
98
142
|
print("File not found: {}".format(file_path))
|
|
99
143
|
MediLink_ConfigLoader.log("File not found: {}".format(file_path), level="ERROR")
|
|
100
144
|
return
|
|
101
145
|
|
|
146
|
+
def _process_medisoft_records(file, file_path, config):
|
|
147
|
+
"""Helper function to process Medisoft-style multi-line patient records
|
|
148
|
+
|
|
149
|
+
RECORD FORMAT DESIGN (RESERVED 5-LINE STRUCTURE):
|
|
150
|
+
════════════════════════════════════════════════════════════════
|
|
151
|
+
Each patient record is designed as a RESERVED 5-line block:
|
|
152
|
+
|
|
153
|
+
Line 1: Personal Info (REQUIRED - Always present)
|
|
154
|
+
Line 2: Insurance Info (REQUIRED - Always present)
|
|
155
|
+
Line 3: Service Info (REQUIRED - Always present)
|
|
156
|
+
Line 4: Service Info 2 (RESERVED - Future expansion)
|
|
157
|
+
Line 5: Service Info 3 (RESERVED - Future expansion)
|
|
158
|
+
[Blank line] (Record separator)
|
|
159
|
+
|
|
160
|
+
CURRENT PRODUCTION STATE:
|
|
161
|
+
────────────────────────────
|
|
162
|
+
- Currently using: 3 active lines per record
|
|
163
|
+
- Lines 4-5: Reserved for future service data expansion
|
|
164
|
+
- Blank lines: Maintain proper record separation
|
|
165
|
+
- Total: 5-line reserved format ready for expansion
|
|
166
|
+
|
|
167
|
+
PEEK-AHEAD LOGIC PURPOSE:
|
|
168
|
+
────────────────────────────
|
|
169
|
+
The peek-ahead logic serves to:
|
|
170
|
+
1. Detect when we've reached the end of a record (blank line)
|
|
171
|
+
2. Allow for future expansion to 4-5 lines without code changes
|
|
172
|
+
3. Maintain proper spacing between patient records
|
|
173
|
+
4. Handle variable-length records (3-5 lines) gracefully
|
|
174
|
+
|
|
175
|
+
FUTURE EXPANSION READY:
|
|
176
|
+
────────────────────────────
|
|
177
|
+
When additional service lines are needed:
|
|
178
|
+
- No code changes required in reader logic
|
|
179
|
+
- Parser automatically handles service_info_2 and service_info_3
|
|
180
|
+
- Slice definitions can be extended in configuration
|
|
181
|
+
- Maintains backward compatibility with 3-line records
|
|
182
|
+
════════════════════════════════════════════════════════════════
|
|
183
|
+
"""
|
|
184
|
+
MediLink_ConfigLoader.log("Starting to read fixed width data...")
|
|
185
|
+
lines_buffer = [] # Buffer to hold lines for current patient data
|
|
186
|
+
min_lines = config.get('min_lines', 3)
|
|
187
|
+
max_lines = config.get('max_lines', 5)
|
|
188
|
+
|
|
189
|
+
def yield_record(buffer):
|
|
190
|
+
personal_info = buffer[0] # Line 1: Always present (personal data)
|
|
191
|
+
insurance_info = buffer[1] # Line 2: Always present (insurance data)
|
|
192
|
+
service_info = buffer[2] # Line 3: Always present (primary service data)
|
|
193
|
+
service_info_2 = buffer[3] if len(buffer) > 3 else None # Line 4: Reserved (future expansion)
|
|
194
|
+
service_info_3 = buffer[4] if len(buffer) > 4 else None # Line 5: Reserved (future expansion)
|
|
195
|
+
MediLink_ConfigLoader.log("Successfully read data from file: {}".format(file_path), level="INFO")
|
|
196
|
+
return personal_info, insurance_info, service_info, service_info_2, service_info_3
|
|
197
|
+
|
|
198
|
+
# RESERVED RECORD FORMAT PROCESSING
|
|
199
|
+
# This logic handles the current 3-line + 2-reserved-line format
|
|
200
|
+
# and is ready for future expansion without modification
|
|
201
|
+
for line in file:
|
|
202
|
+
stripped_line = line.strip()
|
|
203
|
+
if stripped_line:
|
|
204
|
+
lines_buffer.append(stripped_line)
|
|
205
|
+
# Check if we're within the reserved record size (3-5 lines)
|
|
206
|
+
if min_lines <= len(lines_buffer) <= max_lines:
|
|
207
|
+
# Peek ahead to detect record boundary (intentional design)
|
|
208
|
+
next_line = file.readline().strip()
|
|
209
|
+
if not next_line:
|
|
210
|
+
# Found record separator (blank line) - yield complete record
|
|
211
|
+
yield yield_record(lines_buffer)
|
|
212
|
+
lines_buffer.clear()
|
|
213
|
+
# If next_line has content, continue building the record
|
|
214
|
+
# (This handles future expansion to 4-5 line records)
|
|
215
|
+
else:
|
|
216
|
+
# Blank line encountered - end of current record
|
|
217
|
+
if len(lines_buffer) >= min_lines:
|
|
218
|
+
yield yield_record(lines_buffer)
|
|
219
|
+
lines_buffer.clear()
|
|
220
|
+
|
|
221
|
+
if lines_buffer: # Yield any remaining buffer if file ends without a blank line
|
|
222
|
+
yield yield_record(lines_buffer)
|
|
223
|
+
|
|
224
|
+
def _process_slice_based_records(file, file_path, config):
|
|
225
|
+
"""Helper function to process slice-based field extraction"""
|
|
226
|
+
slices = config.get('slices', {})
|
|
227
|
+
for line_number, line in enumerate(file, start=1):
|
|
228
|
+
extracted_data = {key: line[start:end].strip() for key, (start, end) in slices.items()}
|
|
229
|
+
yield extracted_data, line_number
|
|
230
|
+
|
|
231
|
+
# TODO (Refactor) COMPLETED: Consolidated read_fixed_width_data and read_general_fixed_width_data
|
|
232
|
+
# into read_consolidated_fixed_width_data with unified configuration-based approach.
|
|
233
|
+
# Legacy functions maintained for backward compatibility.
|
|
234
|
+
#
|
|
235
|
+
# DESIGN NOTE: The reserved 5-line record format is intentional architecture:
|
|
236
|
+
# - Lines 1-3: Active data (personal, insurance, service)
|
|
237
|
+
# - Lines 4-5: Reserved for future service expansion
|
|
238
|
+
# - Peek-ahead logic maintains proper record spacing and handles variable-length records
|
|
239
|
+
# Next steps: Update all callers to use the new consolidated function directly.
|
|
240
|
+
|
|
102
241
|
def consolidate_csvs(source_directory, file_prefix="Consolidated", interactive=False):
|
|
103
242
|
"""
|
|
104
243
|
Consolidate CSV files in the source directory into a single CSV file.
|
|
@@ -411,9 +550,40 @@ def build_command(winscp_path, winscp_log_path, endpoint_config, remote_director
|
|
|
411
550
|
|
|
412
551
|
# Handle filemask input
|
|
413
552
|
if filemask:
|
|
414
|
-
# TODO
|
|
415
|
-
#
|
|
416
|
-
#
|
|
553
|
+
# TODO (MEDIUM PRIORITY - WinSCP Filemask Implementation):
|
|
554
|
+
# PROBLEM: Need to translate various filemask input formats into proper WinSCP syntax.
|
|
555
|
+
# Current implementation is incomplete and may not handle all edge cases.
|
|
556
|
+
#
|
|
557
|
+
# WINSCP FILEMASK SYNTAX REQUIREMENTS:
|
|
558
|
+
# - Multiple extensions: "*.ext1|*.ext2|*.ext3" (pipe-separated)
|
|
559
|
+
# - Single extension: "*.ext"
|
|
560
|
+
# - All files: "*" or "*.*"
|
|
561
|
+
# - Date patterns: "*YYYYMMDD*" for date-based filtering
|
|
562
|
+
# - Size patterns: ">100K" for files larger than 100KB
|
|
563
|
+
# - Combined: "*.csv|*.txt;>1K" (semicolon for AND conditions)
|
|
564
|
+
#
|
|
565
|
+
# INPUT FORMATS TO HANDLE:
|
|
566
|
+
# 1. List: ['csv', 'txt', 'pdf'] -> "*.csv|*.txt|*.pdf"
|
|
567
|
+
# 2. Dictionary: {'extensions': ['csv'], 'size': '>1K'} -> "*.csv;>1K"
|
|
568
|
+
# 3. String: "csv,txt" -> "*.csv|*.txt" (comma-separated to pipe-separated)
|
|
569
|
+
# 4. None/Empty: -> "*" (all files)
|
|
570
|
+
#
|
|
571
|
+
# IMPLEMENTATION STEPS:
|
|
572
|
+
# 1. Add helper function normalize_filemask(filemask) -> str
|
|
573
|
+
# 2. Handle list input: join with "|" prefix with "*."
|
|
574
|
+
# 3. Handle dict input: combine extensions with other filters using ";"
|
|
575
|
+
# 4. Handle string input: split on comma and normalize
|
|
576
|
+
# 5. Add validation for WinSCP-compatible patterns
|
|
577
|
+
# 6. Add logging for debugging filemask translations
|
|
578
|
+
#
|
|
579
|
+
# TESTING SCENARIOS:
|
|
580
|
+
# - filemask=['csv', 'txt'] should work for CSV and TXT files
|
|
581
|
+
# - filemask={'extensions': ['pdf'], 'min_size': '1MB'} for large PDFs
|
|
582
|
+
# - filemask="era,835" for claims response files
|
|
583
|
+
# - filemask=None should download all files
|
|
584
|
+
#
|
|
585
|
+
# FILES TO MODIFY: This file (execute_winscp_command function)
|
|
586
|
+
# REFERENCE: WinSCP documentation on file masks and filters
|
|
417
587
|
if isinstance(filemask, list):
|
|
418
588
|
filemask_str = '|'.join(['*.' + ext for ext in filemask])
|
|
419
589
|
elif isinstance(filemask, dict):
|
|
@@ -488,7 +658,32 @@ def execute_winscp_command(command, operation_type, files, local_storage_path):
|
|
|
488
658
|
MediLink_ConfigLoader.log("WinSCP {} operation completed successfully.".format(operation_type))
|
|
489
659
|
|
|
490
660
|
if operation_type == 'download':
|
|
491
|
-
downloaded_files = list_downloaded_files(local_storage_path)
|
|
661
|
+
downloaded_files = list_downloaded_files(local_storage_path)
|
|
662
|
+
# TODO (HIGH PRIORITY - WinSCP Path Configuration Issue):
|
|
663
|
+
# PROBLEM: WinSCP is not downloading files to the expected local_storage_path directory.
|
|
664
|
+
# The list_downloaded_files() function is checking the wrong location.
|
|
665
|
+
#
|
|
666
|
+
# INVESTIGATION STEPS:
|
|
667
|
+
# 1. Check WinSCP logs to determine actual download destination:
|
|
668
|
+
# - Look in config['MediLink_Config']['local_claims_path'] + "winscp_download.log"
|
|
669
|
+
# - Parse log entries for "file downloaded to:" or similar patterns
|
|
670
|
+
# 2. Compare actual WinSCP download path vs configured local_storage_path
|
|
671
|
+
# 3. Check if WinSCP uses different path conventions (forward/backward slashes)
|
|
672
|
+
#
|
|
673
|
+
# IMPLEMENTATION OPTIONS:
|
|
674
|
+
# Option A: Fix WinSCP command to use correct target directory
|
|
675
|
+
# - Update the lcd_command generation in execute_winscp_command()
|
|
676
|
+
# - Ensure local_storage_path is properly escaped for WinSCP
|
|
677
|
+
# Option B: Update list_downloaded_files() to check actual WinSCP location
|
|
678
|
+
# - Add function get_actual_winscp_download_path() that parses logs
|
|
679
|
+
# - Call list_downloaded_files(get_actual_winscp_download_path())
|
|
680
|
+
# Option C: Add configuration parameter for WinSCP-specific download path
|
|
681
|
+
# - Add 'winscp_download_path' to config
|
|
682
|
+
# - Default to local_storage_path if not specified
|
|
683
|
+
#
|
|
684
|
+
# RECOMMENDED: Option A (fix root cause) + Option C (explicit config)
|
|
685
|
+
# FILES TO MODIFY: This file (execute_winscp_command, list_downloaded_files functions)
|
|
686
|
+
# TESTING: Verify downloads work correctly after fix with various file types
|
|
492
687
|
MediLink_ConfigLoader.log("Files currently located in local_storage_path: {}".format(downloaded_files), level="DEBUG")
|
|
493
688
|
|
|
494
689
|
if not downloaded_files:
|
|
@@ -537,40 +732,58 @@ def detect_new_files(directory_path, file_extension='.DAT'):
|
|
|
537
732
|
:param file_extension: Extension of the files to detect.
|
|
538
733
|
: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.
|
|
539
734
|
"""
|
|
735
|
+
import time
|
|
736
|
+
detect_start = time.time()
|
|
737
|
+
if PERFORMANCE_LOGGING:
|
|
738
|
+
MediLink_ConfigLoader.log("File detection started for directory: {}".format(directory_path), level="INFO")
|
|
739
|
+
|
|
540
740
|
MediLink_ConfigLoader.log("Scanning directory: {}".format(directory_path), level="INFO")
|
|
541
741
|
detected_file_paths = []
|
|
542
742
|
file_flagged = False
|
|
543
743
|
|
|
544
744
|
try:
|
|
745
|
+
listdir_start = time.time()
|
|
545
746
|
filenames = os.listdir(directory_path)
|
|
546
|
-
|
|
747
|
+
listdir_end = time.time()
|
|
748
|
+
if PERFORMANCE_LOGGING:
|
|
749
|
+
print("Directory listing completed in {:.2f} seconds".format(listdir_end - listdir_start))
|
|
547
750
|
|
|
751
|
+
# Batch log the files found instead of logging each one individually
|
|
752
|
+
matching_files = []
|
|
753
|
+
for filename in filenames:
|
|
754
|
+
if filename.endswith(file_extension):
|
|
755
|
+
matching_files.append(filename)
|
|
756
|
+
|
|
757
|
+
if matching_files:
|
|
758
|
+
MediLink_ConfigLoader.log("Found {} files with extension {}: {}".format(
|
|
759
|
+
len(matching_files), file_extension, matching_files), level="INFO")
|
|
760
|
+
|
|
761
|
+
file_check_start = time.time()
|
|
548
762
|
for filename in filenames:
|
|
549
|
-
MediLink_ConfigLoader.log("Checking file: {}".format(filename), level="INFO")
|
|
550
763
|
if filename.endswith(file_extension):
|
|
551
|
-
MediLink_ConfigLoader.log("File matches extension: {}".format(file_extension), level="INFO")
|
|
552
764
|
name, ext = os.path.splitext(filename)
|
|
553
|
-
MediLink_ConfigLoader.log("File name: {}, File extension: {}".format(name, ext), level="INFO")
|
|
554
765
|
|
|
555
766
|
if not is_timestamped(name):
|
|
556
|
-
MediLink_ConfigLoader.log("File is not timestamped: {}".format(filename), level="INFO")
|
|
557
767
|
new_name = "{}_{}{}".format(name, datetime.now().strftime('%Y%m%d_%H%M%S'), ext)
|
|
558
768
|
os.rename(os.path.join(directory_path, filename), os.path.join(directory_path, new_name))
|
|
559
769
|
MediLink_ConfigLoader.log("Renamed file from {} to {}".format(filename, new_name), level="INFO")
|
|
560
770
|
file_flagged = True
|
|
561
771
|
filename = new_name
|
|
562
|
-
else:
|
|
563
|
-
MediLink_ConfigLoader.log("File is already timestamped: {}".format(filename), level="INFO")
|
|
564
772
|
|
|
565
773
|
file_path = os.path.join(directory_path, filename)
|
|
566
774
|
detected_file_paths.append(file_path)
|
|
567
|
-
|
|
775
|
+
|
|
776
|
+
file_check_end = time.time()
|
|
777
|
+
if PERFORMANCE_LOGGING:
|
|
778
|
+
print("File checking completed in {:.2f} seconds".format(file_check_end - file_check_start))
|
|
568
779
|
|
|
569
780
|
except Exception as e:
|
|
570
781
|
MediLink_ConfigLoader.log("Error occurred: {}".format(str(e)), level="INFO")
|
|
571
782
|
|
|
572
|
-
|
|
573
|
-
|
|
783
|
+
detect_end = time.time()
|
|
784
|
+
if PERFORMANCE_LOGGING:
|
|
785
|
+
print("File detection completed in {:.2f} seconds".format(detect_end - detect_start))
|
|
786
|
+
MediLink_ConfigLoader.log("Detected {} files, flagged: {}".format(len(detected_file_paths), file_flagged), level="INFO")
|
|
574
787
|
|
|
575
788
|
return detected_file_paths, file_flagged
|
|
576
789
|
|
|
@@ -616,25 +829,43 @@ def confirm_all_suggested_endpoints(detailed_patient_data):
|
|
|
616
829
|
return detailed_patient_data
|
|
617
830
|
|
|
618
831
|
def bulk_edit_insurance_types(detailed_patient_data, insurance_options):
|
|
619
|
-
|
|
620
|
-
print("
|
|
832
|
+
"""Allow user to edit insurance types in a table-like format with validation"""
|
|
833
|
+
print("\nEdit Insurance Type (Enter the code). Enter 'LIST' to display available insurance types.")
|
|
621
834
|
|
|
622
835
|
for data in detailed_patient_data:
|
|
623
|
-
|
|
836
|
+
patient_id = data.get('patient_id', 'Unknown')
|
|
837
|
+
patient_name = data.get('patient_name', 'Unknown')
|
|
838
|
+
current_insurance_type = data.get('insurance_type', '12')
|
|
624
839
|
current_insurance_description = insurance_options.get(current_insurance_type, "Unknown")
|
|
840
|
+
|
|
625
841
|
print("({}) {:<25} | Current Ins. Type: {} - {}".format(
|
|
626
|
-
|
|
842
|
+
patient_id, patient_name, current_insurance_type, current_insurance_description))
|
|
627
843
|
|
|
628
844
|
while True:
|
|
629
|
-
new_insurance_type = input("Enter new insurance type (or press Enter to keep current): ").upper()
|
|
845
|
+
new_insurance_type = input("Enter new insurance type (or press Enter to keep current): ").strip().upper()
|
|
846
|
+
|
|
630
847
|
if new_insurance_type == 'LIST':
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
848
|
+
MediLink_Display_Utils.display_insurance_options(insurance_options)
|
|
849
|
+
continue
|
|
850
|
+
|
|
851
|
+
elif not new_insurance_type:
|
|
852
|
+
# Keep current insurance type
|
|
853
|
+
break
|
|
854
|
+
|
|
855
|
+
elif new_insurance_type in insurance_options:
|
|
856
|
+
# Valid insurance type from config
|
|
857
|
+
data['insurance_type'] = new_insurance_type
|
|
635
858
|
break
|
|
859
|
+
|
|
636
860
|
else:
|
|
637
|
-
|
|
861
|
+
# User wants to use a code not in config - confirm with them
|
|
862
|
+
confirm = input("Code '{}' not found in configuration. Use it anyway? (y/n): ".format(new_insurance_type)).strip().lower()
|
|
863
|
+
if confirm in ['y', 'yes']:
|
|
864
|
+
data['insurance_type'] = new_insurance_type
|
|
865
|
+
break
|
|
866
|
+
else:
|
|
867
|
+
print("Invalid insurance type. Please enter a valid code or type 'LIST' to see options.")
|
|
868
|
+
continue
|
|
638
869
|
|
|
639
870
|
def review_and_confirm_changes(detailed_patient_data, insurance_options):
|
|
640
871
|
# Review and confirm changes
|
|
@@ -646,4 +877,106 @@ def review_and_confirm_changes(detailed_patient_data, insurance_options):
|
|
|
646
877
|
insurance_description = insurance_options.get(insurance_type, "Unknown")
|
|
647
878
|
print("{:<20} {:<10} {:<30}".format(data['patient_name'], insurance_type, insurance_description))
|
|
648
879
|
confirm = input("\nConfirm changes? (y/n): ").strip().lower()
|
|
649
|
-
return confirm in ['y', 'yes', '']
|
|
880
|
+
return confirm in ['y', 'yes', '']
|
|
881
|
+
|
|
882
|
+
|
|
883
|
+
def update_suggested_endpoint_with_user_preference(detailed_patient_data, patient_index, new_endpoint, config, crosswalk):
|
|
884
|
+
"""
|
|
885
|
+
Updates the suggested endpoint for a patient and optionally updates the crosswalk
|
|
886
|
+
for future patients with the same insurance.
|
|
887
|
+
|
|
888
|
+
:param detailed_patient_data: List of patient data dictionaries
|
|
889
|
+
:param patient_index: Index of the patient being updated
|
|
890
|
+
:param new_endpoint: The new endpoint selected by the user
|
|
891
|
+
:param config: Configuration settings
|
|
892
|
+
:param crosswalk: Crosswalk data for in-memory updates
|
|
893
|
+
:return: Updated crosswalk if changes were made, None otherwise
|
|
894
|
+
"""
|
|
895
|
+
# Note: load_insurance_data_from_mains is now imported at module level
|
|
896
|
+
|
|
897
|
+
data = detailed_patient_data[patient_index]
|
|
898
|
+
original_suggested = data.get('suggested_endpoint')
|
|
899
|
+
|
|
900
|
+
# Update the patient's endpoint preference
|
|
901
|
+
data['user_preferred_endpoint'] = new_endpoint
|
|
902
|
+
data['confirmed_endpoint'] = new_endpoint
|
|
903
|
+
|
|
904
|
+
# If user changed from the original suggestion, offer to update crosswalk
|
|
905
|
+
if original_suggested != new_endpoint:
|
|
906
|
+
primary_insurance = data.get('primary_insurance')
|
|
907
|
+
patient_name = data.get('patient_name')
|
|
908
|
+
|
|
909
|
+
print("\nYou changed the endpoint for {} from {} to {}.".format(patient_name, original_suggested, new_endpoint))
|
|
910
|
+
update_future = input("Would you like to use {} as the default endpoint for future patients with {}? (Y/N): ".format(new_endpoint, primary_insurance)).strip().lower()
|
|
911
|
+
|
|
912
|
+
if update_future in ['y', 'yes']:
|
|
913
|
+
# Find the payer ID associated with this insurance
|
|
914
|
+
load_insurance_data_from_mains = _get_medibot_function('MediBot_Preprocessor_lib', 'load_insurance_data_from_mains')
|
|
915
|
+
insurance_to_id = load_insurance_data_from_mains(config) if load_insurance_data_from_mains else {}
|
|
916
|
+
insurance_id = insurance_to_id.get(primary_insurance)
|
|
917
|
+
|
|
918
|
+
if insurance_id:
|
|
919
|
+
# Find the payer ID in crosswalk and update it
|
|
920
|
+
updated = False
|
|
921
|
+
for payer_id, payer_data in crosswalk.get('payer_id', {}).items():
|
|
922
|
+
medisoft_ids = [str(id) for id in payer_data.get('medisoft_id', [])]
|
|
923
|
+
if str(insurance_id) in medisoft_ids:
|
|
924
|
+
# Update the crosswalk in memory
|
|
925
|
+
crosswalk['payer_id'][payer_id]['endpoint'] = new_endpoint
|
|
926
|
+
MediLink_ConfigLoader.log("Updated crosswalk in memory: Payer ID {} ({}) now defaults to {}".format(payer_id, primary_insurance, new_endpoint), level="INFO")
|
|
927
|
+
|
|
928
|
+
# Update suggested_endpoint for other patients with same insurance in current batch
|
|
929
|
+
for other_data in detailed_patient_data:
|
|
930
|
+
if (other_data.get('primary_insurance') == primary_insurance and
|
|
931
|
+
'user_preferred_endpoint' not in other_data):
|
|
932
|
+
other_data['suggested_endpoint'] = new_endpoint
|
|
933
|
+
|
|
934
|
+
updated = True
|
|
935
|
+
break
|
|
936
|
+
|
|
937
|
+
if updated:
|
|
938
|
+
# Save the updated crosswalk to disk immediately using API bypass mode
|
|
939
|
+
if save_crosswalk_immediately(config, crosswalk):
|
|
940
|
+
print("Updated default endpoint for {} to {}".format(primary_insurance, new_endpoint))
|
|
941
|
+
else:
|
|
942
|
+
print("Updated endpoint preference (will be saved during next crosswalk update)")
|
|
943
|
+
return crosswalk
|
|
944
|
+
else:
|
|
945
|
+
MediLink_ConfigLoader.log("Could not find payer ID in crosswalk for insurance {}".format(primary_insurance), level="WARNING")
|
|
946
|
+
else:
|
|
947
|
+
MediLink_ConfigLoader.log("Could not find insurance ID for {} to update crosswalk".format(primary_insurance), level="WARNING")
|
|
948
|
+
|
|
949
|
+
return None
|
|
950
|
+
|
|
951
|
+
|
|
952
|
+
def save_crosswalk_immediately(config, crosswalk):
|
|
953
|
+
"""
|
|
954
|
+
Saves the crosswalk to disk immediately using API bypass mode.
|
|
955
|
+
|
|
956
|
+
:param config: Configuration settings
|
|
957
|
+
:param crosswalk: Crosswalk data to save
|
|
958
|
+
:return: True if saved successfully, False otherwise
|
|
959
|
+
"""
|
|
960
|
+
try:
|
|
961
|
+
# Note: save_crosswalk is imported at module level
|
|
962
|
+
|
|
963
|
+
# Save using API bypass mode (no client needed, skip API operations)
|
|
964
|
+
save_crosswalk = _get_medibot_function('MediBot_Crosswalk_Utils', 'save_crosswalk')
|
|
965
|
+
if not save_crosswalk:
|
|
966
|
+
save_crosswalk = _get_medibot_function('MediBot_Crosswalk_Library', 'save_crosswalk')
|
|
967
|
+
|
|
968
|
+
success = save_crosswalk(None, config, crosswalk, skip_api_operations=True) if save_crosswalk else False
|
|
969
|
+
|
|
970
|
+
if success:
|
|
971
|
+
MediLink_ConfigLoader.log("Successfully saved crosswalk with updated endpoint preferences", level="INFO")
|
|
972
|
+
else:
|
|
973
|
+
MediLink_ConfigLoader.log("Failed to save crosswalk - preferences will be saved during next crosswalk update", level="WARNING")
|
|
974
|
+
|
|
975
|
+
return success
|
|
976
|
+
|
|
977
|
+
except ImportError:
|
|
978
|
+
MediLink_ConfigLoader.log("Could not import MediBot_Crosswalk_Library for saving crosswalk", level="ERROR")
|
|
979
|
+
return False
|
|
980
|
+
except Exception as e:
|
|
981
|
+
MediLink_ConfigLoader.log("Error saving crosswalk: {}".format(e), level="ERROR")
|
|
982
|
+
return False
|
MediLink/MediLink_Decoder.py
CHANGED
|
@@ -1,6 +1,25 @@
|
|
|
1
1
|
# MediLink_Decoder.py
|
|
2
2
|
import os, sys, csv
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
# Add workspace directory to Python path for MediCafe imports
|
|
5
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
6
|
+
workspace_dir = os.path.dirname(current_dir)
|
|
7
|
+
if workspace_dir not in sys.path:
|
|
8
|
+
sys.path.insert(0, workspace_dir)
|
|
9
|
+
|
|
10
|
+
from MediCafe.core_utils import get_shared_config_loader
|
|
11
|
+
|
|
12
|
+
# Get shared config loader
|
|
13
|
+
MediLink_ConfigLoader = get_shared_config_loader()
|
|
14
|
+
if MediLink_ConfigLoader:
|
|
15
|
+
load_configuration = MediLink_ConfigLoader.load_configuration
|
|
16
|
+
log = MediLink_ConfigLoader.log
|
|
17
|
+
else:
|
|
18
|
+
# Fallback functions if config loader is not available
|
|
19
|
+
def load_configuration():
|
|
20
|
+
return {}, {}
|
|
21
|
+
def log(message, level="INFO"):
|
|
22
|
+
print("[{}] {}".format(level, message))
|
|
4
23
|
from MediLink_Parser import parse_era_content, parse_277_content, parse_277IBR_content, parse_277EBR_content, parse_dpt_content, parse_ebt_content, parse_ibt_content
|
|
5
24
|
|
|
6
25
|
# Define new_fieldnames globally
|