medicafe 0.250814.4__tar.gz → 0.250818.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediBot/MediBot.py +74 -40
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediBot/MediBot_Preprocessor_lib.py +128 -45
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediBot/MediBot_UI.py +124 -37
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediBot/__init__.py +1 -1
- medicafe-0.250818.0/MediBot/update_medicafe.py +245 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediCafe/MediLink_ConfigLoader.py +60 -13
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediCafe/__init__.py +5 -2
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediCafe/smart_import.py +12 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/__init__.py +2 -1
- {medicafe-0.250814.4 → medicafe-0.250818.0}/PKG-INFO +1 -1
- {medicafe-0.250814.4 → medicafe-0.250818.0}/medicafe.egg-info/PKG-INFO +1 -1
- {medicafe-0.250814.4 → medicafe-0.250818.0}/setup.py +1 -1
- medicafe-0.250814.4/MediBot/update_medicafe.py +0 -770
- {medicafe-0.250814.4 → medicafe-0.250818.0}/LICENSE +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MANIFEST.in +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediBot/MediBot.bat +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediBot/MediBot_Charges.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediBot/MediBot_Crosswalk_Library.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediBot/MediBot_Crosswalk_Utils.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediBot/MediBot_Post.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediBot/MediBot_Preprocessor.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediBot/MediBot_dataformat_library.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediBot/MediBot_docx_decoder.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediBot/MediBot_smart_import.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediBot/get_medicafe_version.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediBot/update_json.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediCafe/__main__.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediCafe/api_core.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediCafe/api_core_backup.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediCafe/api_factory.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediCafe/api_utils.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediCafe/core_utils.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediCafe/graphql_utils.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediCafe/logging_config.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediCafe/logging_demo.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediCafe/migration_helpers.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediCafe/submission_index.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/InsuranceTypeService.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_837p_cob_library.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_837p_encoder.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_837p_encoder_library.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_837p_utilities.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_API_Generator.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_Azure.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_ClaimStatus.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_DataMgmt.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_Decoder.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_Deductible.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_Deductible_Validator.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_Display_Utils.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_Down.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_Gmail.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_Mailer.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_Parser.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_PatientProcessor.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_Scan.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_Scheduler.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_UI.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_Up.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_insurance_utils.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_main.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_smart_import.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/Soumit_api.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/gmail_http_utils.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/gmail_oauth_utils.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/insurance_type_integration_test.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/openssl.cnf +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/test.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/test_cob_library.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/test_timing.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/test_validation.py +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/webapp.html +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/README.md +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/medicafe.egg-info/SOURCES.txt +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/medicafe.egg-info/dependency_links.txt +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/medicafe.egg-info/entry_points.txt +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/medicafe.egg-info/not-zip-safe +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/medicafe.egg-info/requires.txt +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/medicafe.egg-info/top_level.txt +0 -0
- {medicafe-0.250814.4 → medicafe-0.250818.0}/setup.cfg +0 -0
@@ -11,6 +11,7 @@ try:
|
|
11
11
|
except ImportError:
|
12
12
|
msvcrt = None # Not available on non-Windows systems
|
13
13
|
from collections import OrderedDict
|
14
|
+
from datetime import datetime # Added for primary surgery date logic
|
14
15
|
|
15
16
|
# ============================================================================
|
16
17
|
# MINIMAL PROTECTION: Import State Validation
|
@@ -171,6 +172,68 @@ def identify_field(header, field_mapping):
|
|
171
172
|
# Add this print to a function that is calling identify_field
|
172
173
|
#print("Warning: No matching field found for CSV header '{}'".format(header))
|
173
174
|
|
175
|
+
def create_patient_entries_from_row(row, reverse_mapping):
|
176
|
+
"""
|
177
|
+
Helper function to create patient entries from a row with surgery date handling.
|
178
|
+
|
179
|
+
Args:
|
180
|
+
row: The CSV row containing patient data
|
181
|
+
reverse_mapping: The reverse mapping for field lookups
|
182
|
+
|
183
|
+
Returns:
|
184
|
+
list: List of tuples (surgery_date, patient_name, patient_id, diagnosis_code, row)
|
185
|
+
"""
|
186
|
+
patient_id = row.get(reverse_mapping['Patient ID #2'])
|
187
|
+
patient_name = row.get(reverse_mapping['Patient Name'])
|
188
|
+
|
189
|
+
# Get all surgery dates for this patient
|
190
|
+
all_surgery_dates = row.get('_all_surgery_dates', [row.get('Surgery Date')])
|
191
|
+
surgery_date_to_diagnosis = row.get('_surgery_date_to_diagnosis', {})
|
192
|
+
|
193
|
+
patient_entries = []
|
194
|
+
|
195
|
+
# Sort surgery dates chronologically to ensure proper ordering
|
196
|
+
sorted_surgery_dates = []
|
197
|
+
for surgery_date in all_surgery_dates:
|
198
|
+
try:
|
199
|
+
if hasattr(surgery_date, 'strftime'):
|
200
|
+
# Already a datetime object
|
201
|
+
sorted_surgery_dates.append(surgery_date)
|
202
|
+
elif isinstance(surgery_date, str):
|
203
|
+
# Convert string to datetime for sorting
|
204
|
+
surgery_date_dt = datetime.strptime(surgery_date, '%m-%d-%Y')
|
205
|
+
sorted_surgery_dates.append(surgery_date_dt)
|
206
|
+
else:
|
207
|
+
# Fallback - use as is
|
208
|
+
sorted_surgery_dates.append(surgery_date)
|
209
|
+
except (ValueError, TypeError):
|
210
|
+
# If parsing fails, use the original value
|
211
|
+
sorted_surgery_dates.append(surgery_date)
|
212
|
+
|
213
|
+
# Sort the dates chronologically
|
214
|
+
sorted_surgery_dates.sort()
|
215
|
+
|
216
|
+
# Create entries for each surgery date in chronological order
|
217
|
+
# The enhanced table display will group by patient_id and show dashed lines for secondary dates
|
218
|
+
for surgery_date in sorted_surgery_dates:
|
219
|
+
try:
|
220
|
+
if hasattr(surgery_date, 'strftime'):
|
221
|
+
surgery_date_str = surgery_date.strftime('%m-%d-%Y')
|
222
|
+
elif isinstance(surgery_date, str):
|
223
|
+
surgery_date_str = surgery_date
|
224
|
+
else:
|
225
|
+
surgery_date_str = str(surgery_date)
|
226
|
+
except Exception:
|
227
|
+
surgery_date_str = str(surgery_date)
|
228
|
+
|
229
|
+
# Get the diagnosis code for this surgery date
|
230
|
+
diagnosis_code = surgery_date_to_diagnosis.get(surgery_date_str, 'N/A')
|
231
|
+
|
232
|
+
# Add entry for this surgery date
|
233
|
+
patient_entries.append((surgery_date, patient_name, patient_id, diagnosis_code, row))
|
234
|
+
|
235
|
+
return patient_entries
|
236
|
+
|
174
237
|
# Global flag to control AHK execution method - set to True to use optimized stdin method
|
175
238
|
USE_AHK_STDIN_OPTIMIZATION = True
|
176
239
|
|
@@ -672,7 +735,7 @@ if __name__ == "__main__":
|
|
672
735
|
print(msg)
|
673
736
|
print("-" * 60)
|
674
737
|
|
675
|
-
proceed, selected_patient_ids, selected_indices, fixed_values = user_interaction(csv_data, interaction_mode, error_message, reverse_mapping)
|
738
|
+
proceed, selected_patient_ids, selected_indices, fixed_values, is_medicare = user_interaction(csv_data, interaction_mode, error_message, reverse_mapping)
|
676
739
|
|
677
740
|
if proceed:
|
678
741
|
# Filter csv_data for selected patients from Triage mode
|
@@ -695,32 +758,19 @@ if __name__ == "__main__":
|
|
695
758
|
if patient_row is None:
|
696
759
|
raise ValueError("Patient row not found for patient ID: {}".format(patient_id))
|
697
760
|
|
698
|
-
#
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
# Create entries for each surgery date with its specific diagnosis code
|
703
|
-
for surgery_date in all_surgery_dates:
|
704
|
-
# Convert surgery_date to string format for consistent lookup (XP SP3 + Py3.4.4 compatible)
|
705
|
-
try:
|
706
|
-
if hasattr(surgery_date, 'strftime'):
|
707
|
-
surgery_date_str = surgery_date.strftime('%m-%d-%Y')
|
708
|
-
else:
|
709
|
-
surgery_date_str = str(surgery_date)
|
710
|
-
except Exception:
|
711
|
-
surgery_date_str = str(surgery_date)
|
712
|
-
|
713
|
-
diagnosis_code = surgery_date_to_diagnosis.get(surgery_date_str, 'N/A')
|
714
|
-
patient_info.append((surgery_date, patient_name, patient_id, diagnosis_code, patient_row))
|
761
|
+
# Use helper function to create patient entries
|
762
|
+
patient_entries = create_patient_entries_from_row(patient_row, reverse_mapping)
|
763
|
+
patient_info.extend(patient_entries)
|
715
764
|
|
716
765
|
except Exception as e:
|
717
766
|
MediLink_ConfigLoader.log("Warning: Error retrieving data for patient ID '{}': {}".format(patient_id, e), level="WARNING")
|
718
767
|
patient_info.append(('Unknown Date', patient_name, patient_id, 'N/A', None)) # Append with 'Unknown Date' if there's an error
|
719
768
|
|
720
769
|
# Display existing patients table using the enhanced display function
|
770
|
+
patient_type = "MEDICARE" if is_medicare else "PRIVATE"
|
721
771
|
MediBot_UI.display_enhanced_patient_table(
|
722
772
|
patient_info,
|
723
|
-
"
|
773
|
+
"{} PATIENTS - EXISTING: The following patient(s) already EXIST in the system but may have new dates of service.\n Their diagnosis codes may need to be updated manually by the user to the following list:".format(patient_type),
|
724
774
|
show_line_numbers=False
|
725
775
|
)
|
726
776
|
|
@@ -733,31 +783,15 @@ if __name__ == "__main__":
|
|
733
783
|
# Collect surgery dates and patient info for NEW patients
|
734
784
|
new_patient_info = []
|
735
785
|
for row in csv_data:
|
736
|
-
|
737
|
-
|
738
|
-
|
739
|
-
# Get all surgery dates for this patient
|
740
|
-
all_surgery_dates = row.get('_all_surgery_dates', [row.get('Surgery Date')])
|
741
|
-
surgery_date_to_diagnosis = row.get('_surgery_date_to_diagnosis', {})
|
742
|
-
|
743
|
-
# Create entries for each surgery date with its specific diagnosis code
|
744
|
-
for surgery_date in all_surgery_dates:
|
745
|
-
# Convert surgery_date to string format for consistent lookup (XP SP3 + Py3.4.4 compatible)
|
746
|
-
try:
|
747
|
-
if hasattr(surgery_date, 'strftime'):
|
748
|
-
surgery_date_str = surgery_date.strftime('%m-%d-%Y')
|
749
|
-
else:
|
750
|
-
surgery_date_str = str(surgery_date)
|
751
|
-
except Exception:
|
752
|
-
surgery_date_str = str(surgery_date)
|
753
|
-
|
754
|
-
diagnosis_code = surgery_date_to_diagnosis.get(surgery_date_str, 'N/A')
|
755
|
-
new_patient_info.append((surgery_date, patient_name, patient_id, diagnosis_code, row))
|
786
|
+
# Use helper function to create patient entries
|
787
|
+
patient_entries = create_patient_entries_from_row(row, reverse_mapping)
|
788
|
+
new_patient_info.extend(patient_entries)
|
756
789
|
|
757
790
|
# Display new patients table using the enhanced display function
|
791
|
+
patient_type = "MEDICARE" if is_medicare else "PRIVATE"
|
758
792
|
MediBot_UI.display_enhanced_patient_table(
|
759
793
|
new_patient_info,
|
760
|
-
"
|
794
|
+
"{} PATIENTS - NEW: The following patient(s) will be automatically entered into Medisoft:".format(patient_type),
|
761
795
|
show_line_numbers=True
|
762
796
|
)
|
763
797
|
|
@@ -531,17 +531,41 @@ def sort_and_deduplicate(csv_data):
|
|
531
531
|
existing_row = unique_patients[patient_id]
|
532
532
|
existing_date = existing_row['Surgery Date']
|
533
533
|
|
534
|
+
# Ensure both dates are comparable by converting to datetime objects
|
535
|
+
def normalize_date_for_comparison(date_value):
|
536
|
+
if isinstance(date_value, datetime):
|
537
|
+
return date_value
|
538
|
+
elif isinstance(date_value, str) and date_value.strip():
|
539
|
+
try:
|
540
|
+
# Try to parse the string as a date
|
541
|
+
return datetime.strptime(date_value, '%m/%d/%Y')
|
542
|
+
except ValueError:
|
543
|
+
try:
|
544
|
+
return datetime.strptime(date_value, '%m-%d-%Y')
|
545
|
+
except ValueError:
|
546
|
+
# If parsing fails, return minimum datetime
|
547
|
+
return datetime.min
|
548
|
+
else:
|
549
|
+
# Empty or invalid values get minimum datetime
|
550
|
+
return datetime.min
|
551
|
+
|
552
|
+
normalized_surgery_date = normalize_date_for_comparison(surgery_date)
|
553
|
+
normalized_existing_date = normalize_date_for_comparison(existing_date)
|
554
|
+
|
534
555
|
# Keep the most current demographic data (later surgery date takes precedence)
|
535
|
-
if
|
556
|
+
if normalized_surgery_date > normalized_existing_date:
|
536
557
|
# Store the old row's surgery date before replacing
|
537
558
|
old_date = existing_row['Surgery Date']
|
538
|
-
|
559
|
+
# Add the old date to the list if it's not already there
|
560
|
+
if old_date not in patient_surgery_dates[patient_id]:
|
561
|
+
patient_surgery_dates[patient_id].append(old_date)
|
539
562
|
# Replace with newer row (better demographics)
|
540
563
|
unique_patients[patient_id] = row
|
541
|
-
#
|
542
|
-
|
564
|
+
# Add the new surgery date to the list if it's not already there
|
565
|
+
if surgery_date not in patient_surgery_dates[patient_id]:
|
566
|
+
patient_surgery_dates[patient_id].append(surgery_date)
|
543
567
|
else:
|
544
|
-
# Add this surgery date to the list for this patient
|
568
|
+
# Add this surgery date to the list for this patient if it's not already there
|
545
569
|
if surgery_date not in patient_surgery_dates[patient_id]:
|
546
570
|
patient_surgery_dates[patient_id].append(surgery_date)
|
547
571
|
|
@@ -558,11 +582,77 @@ def sort_and_deduplicate(csv_data):
|
|
558
582
|
else:
|
559
583
|
surgery_date_strings.append(str(date) if date else 'MISSING')
|
560
584
|
|
561
|
-
|
585
|
+
# Remove duplicates and sort
|
586
|
+
unique_surgery_dates = list(set(surgery_date_strings))
|
587
|
+
sorted_surgery_dates = sorted(unique_surgery_dates, key=lambda x: datetime.strptime(x, '%m-%d-%Y') if x != 'MISSING' else datetime.min)
|
588
|
+
row['_all_surgery_dates'] = sorted_surgery_dates
|
562
589
|
row['_primary_surgery_date'] = row['Surgery Date'] # Keep track of which date has the demographics
|
590
|
+
# Compute and store earliest surgery date for emission sort
|
591
|
+
earliest_dt = None
|
592
|
+
earliest_str = None
|
593
|
+
for d in sorted_surgery_dates:
|
594
|
+
if d and d != 'MISSING':
|
595
|
+
try:
|
596
|
+
earliest_dt = datetime.strptime(d, '%m-%d-%Y')
|
597
|
+
earliest_str = d
|
598
|
+
break
|
599
|
+
except Exception:
|
600
|
+
pass
|
601
|
+
# Fallback to demographics date if earliest could not be determined
|
602
|
+
if earliest_str is None:
|
603
|
+
try:
|
604
|
+
sd = row.get('Surgery Date')
|
605
|
+
if isinstance(sd, datetime) and sd != datetime.min:
|
606
|
+
earliest_dt = sd
|
607
|
+
earliest_str = sd.strftime('%m-%d-%Y')
|
608
|
+
elif isinstance(sd, str) and sd.strip():
|
609
|
+
try:
|
610
|
+
earliest_dt = datetime.strptime(sd, '%m/%d/%Y')
|
611
|
+
except Exception:
|
612
|
+
try:
|
613
|
+
earliest_dt = datetime.strptime(sd, '%m-%d-%Y')
|
614
|
+
except Exception:
|
615
|
+
earliest_dt = None
|
616
|
+
earliest_str = sd
|
617
|
+
except Exception:
|
618
|
+
earliest_dt = None
|
619
|
+
earliest_str = None
|
620
|
+
row['_earliest_surgery_date'] = earliest_str
|
621
|
+
|
622
|
+
|
563
623
|
|
564
624
|
# Convert the unique_patients dictionary back to a list and sort it
|
565
|
-
|
625
|
+
# Use the same normalization function for consistent sorting
|
626
|
+
def sort_key(row):
|
627
|
+
# Prefer earliest surgery date across all known dates for the patient
|
628
|
+
earliest = row.get('_earliest_surgery_date')
|
629
|
+
if isinstance(earliest, str) and earliest and earliest != 'MISSING':
|
630
|
+
try:
|
631
|
+
normalized_date = datetime.strptime(earliest, '%m-%d-%Y')
|
632
|
+
except Exception:
|
633
|
+
normalized_date = datetime.min
|
634
|
+
else:
|
635
|
+
# Fallback to the single Surgery Date field
|
636
|
+
surgery_date = row.get('Surgery Date')
|
637
|
+
if isinstance(surgery_date, datetime):
|
638
|
+
normalized_date = surgery_date
|
639
|
+
elif isinstance(surgery_date, str) and surgery_date.strip():
|
640
|
+
try:
|
641
|
+
normalized_date = datetime.strptime(surgery_date, '%m/%d/%Y')
|
642
|
+
except ValueError:
|
643
|
+
try:
|
644
|
+
normalized_date = datetime.strptime(surgery_date, '%m-%d-%Y')
|
645
|
+
except ValueError:
|
646
|
+
normalized_date = datetime.min
|
647
|
+
else:
|
648
|
+
normalized_date = datetime.min
|
649
|
+
# Tie-break per requirement: last name (case-insensitive), then first name, then patient id
|
650
|
+
last_name = ((row.get('Patient Last') or '')).strip().upper()
|
651
|
+
first_name = ((row.get('Patient First') or '')).strip().upper()
|
652
|
+
patient_id_tiebreak = str(row.get('Patient ID') or '')
|
653
|
+
return (normalized_date, last_name, first_name, patient_id_tiebreak)
|
654
|
+
|
655
|
+
csv_data[:] = sorted(unique_patients.values(), key=sort_key) # TODO Does this need to be sorted twice? once before and once after?
|
566
656
|
|
567
657
|
# TODO: Consider adding an option in the config to sort based on Surgery Schedules when available.
|
568
658
|
# If no schedule is available, the current sorting strategy will be used.
|
@@ -1288,6 +1378,35 @@ def map_payer_ids_to_insurance_ids(patient_id_to_insurance_id, payer_id_to_patie
|
|
1288
1378
|
}
|
1289
1379
|
return payer_id_to_details
|
1290
1380
|
|
1381
|
+
def _display_mains_file_error(mains_path):
|
1382
|
+
"""
|
1383
|
+
Helper function to display the critical MAINS file error message.
|
1384
|
+
|
1385
|
+
Args:
|
1386
|
+
mains_path (str): The path where the MAINS file was expected to be found.
|
1387
|
+
"""
|
1388
|
+
error_msg = "CRITICAL: MAINS file not found at: {}. This file is required for insurance name to Medisoft ID mapping.".format(mains_path)
|
1389
|
+
if hasattr(MediLink_ConfigLoader, 'log'):
|
1390
|
+
MediLink_ConfigLoader.log(error_msg, level="CRITICAL")
|
1391
|
+
print("\n" + "="*80)
|
1392
|
+
print("CRITICAL ERROR: MAINS FILE MISSING")
|
1393
|
+
print("="*80)
|
1394
|
+
print("\nThe MAINS file is required for the following critical functions:")
|
1395
|
+
print("* Mapping insurance company names to Medisoft IDs")
|
1396
|
+
print("* Converting insurance names to payer IDs for claim submission")
|
1397
|
+
print("* Creating properly formatted 837p claim files")
|
1398
|
+
print("\nWithout this file, claim submission will fail because:")
|
1399
|
+
print("* Insurance names cannot be converted to payer IDs")
|
1400
|
+
print("* 837p claim files cannot be generated")
|
1401
|
+
print("* Claims cannot be submitted to insurance companies")
|
1402
|
+
print("\nTO FIX THIS:")
|
1403
|
+
print("1. Ensure the MAINS file exists at: {}".format(mains_path))
|
1404
|
+
print("2. If the file is missing, llamar a Dani")
|
1405
|
+
print("3. The file should contain insurance company data from your Medisoft system")
|
1406
|
+
print("="*80)
|
1407
|
+
time.sleep(3) # 3 second pause to allow user to read critical error message
|
1408
|
+
|
1409
|
+
|
1291
1410
|
def load_insurance_data_from_mains(config):
|
1292
1411
|
"""
|
1293
1412
|
Loads insurance data from MAINS and creates a mapping from insurance names to their respective IDs.
|
@@ -1331,25 +1450,7 @@ def load_insurance_data_from_mains(config):
|
|
1331
1450
|
try:
|
1332
1451
|
# Check if MAINS file exists before attempting to read
|
1333
1452
|
if not os.path.exists(mains_path):
|
1334
|
-
|
1335
|
-
if hasattr(MediLink_ConfigLoader, 'log'):
|
1336
|
-
MediLink_ConfigLoader.log(error_msg, level="CRITICAL")
|
1337
|
-
print("\n" + "="*80)
|
1338
|
-
print("CRITICAL ERROR: MAINS FILE MISSING")
|
1339
|
-
print("="*80)
|
1340
|
-
print("\nThe MAINS file is required for the following critical functions:")
|
1341
|
-
print("* Mapping insurance company names to Medisoft IDs")
|
1342
|
-
print("* Converting insurance names to payer IDs for claim submission")
|
1343
|
-
print("* Creating properly formatted 837p claim files")
|
1344
|
-
print("\nWithout this file, claim submission will fail because:")
|
1345
|
-
print("* Insurance names cannot be converted to payer IDs")
|
1346
|
-
print("* 837p claim files cannot be generated")
|
1347
|
-
print("* Claims cannot be submitted to insurance companies")
|
1348
|
-
print("\nTO FIX THIS:")
|
1349
|
-
print("1. Ensure the MAINS file exists at: {}".format(mains_path))
|
1350
|
-
print("2. If the file is missing, llamar a Dani")
|
1351
|
-
print("3. The file should contain insurance company data from your Medisoft system")
|
1352
|
-
print("="*80)
|
1453
|
+
_display_mains_file_error(mains_path)
|
1353
1454
|
return insurance_to_id
|
1354
1455
|
|
1355
1456
|
# XP Compatibility: Check if MediLink_DataMgmt has the required function
|
@@ -1369,25 +1470,7 @@ def load_insurance_data_from_mains(config):
|
|
1369
1470
|
print("Successfully loaded {} insurance records from MAINS".format(len(insurance_to_id)))
|
1370
1471
|
|
1371
1472
|
except FileNotFoundError:
|
1372
|
-
|
1373
|
-
if hasattr(MediLink_ConfigLoader, 'log'):
|
1374
|
-
MediLink_ConfigLoader.log(error_msg, level="CRITICAL")
|
1375
|
-
print("\n" + "="*80)
|
1376
|
-
print("CRITICAL ERROR: MAINS FILE MISSING")
|
1377
|
-
print("="*80)
|
1378
|
-
print("\nThe MAINS file is required for the following critical functions:")
|
1379
|
-
print("* Mapping insurance company names to Medisoft IDs")
|
1380
|
-
print("* Converting insurance names to payer IDs for claim submission")
|
1381
|
-
print("* Creating properly formatted 837p claim files")
|
1382
|
-
print("\nWithout this file, claim submission will fail because:")
|
1383
|
-
print("* Insurance names cannot be converted to payer IDs")
|
1384
|
-
print("* 837p claim files cannot be generated")
|
1385
|
-
print("* Claims cannot be submitted to insurance companies")
|
1386
|
-
print("\nTO FIX THIS:")
|
1387
|
-
print("1. Ensure the MAINS file exists at: {}".format(mains_path))
|
1388
|
-
print("2. If the file is missing, llamar a Dani")
|
1389
|
-
print("3. The file should contain insurance company data from your Medisoft system")
|
1390
|
-
print("="*80)
|
1473
|
+
_display_mains_file_error(mains_path)
|
1391
1474
|
except Exception as e:
|
1392
1475
|
error_msg = "Error loading MAINS data: {}. Continuing without MAINS data.".format(str(e))
|
1393
1476
|
if hasattr(MediLink_ConfigLoader, 'log'):
|
@@ -48,26 +48,29 @@ def display_enhanced_patient_table(patient_info, title, show_line_numbers=True):
|
|
48
48
|
for surgery_date, patient_name, patient_id, diagnosis_code, patient_row in patient_info:
|
49
49
|
# Normalize date into comparable key and display string
|
50
50
|
display_date = None
|
51
|
-
|
51
|
+
current_date_dt = None
|
52
52
|
try:
|
53
53
|
if hasattr(surgery_date, 'strftime'):
|
54
54
|
display_date = surgery_date.strftime('%m-%d')
|
55
|
-
|
55
|
+
current_date_dt = surgery_date
|
56
56
|
elif isinstance(surgery_date, str):
|
57
57
|
# Date strings may be MM-DD-YYYY or already MM-DD
|
58
58
|
parts = surgery_date.split('-') if surgery_date else []
|
59
59
|
if len(parts) == 3 and all(parts):
|
60
60
|
display_date = "{}-{}".format(parts[0], parts[1])
|
61
|
+
try:
|
62
|
+
current_date_dt = datetime.strptime(surgery_date, '%m-%d-%Y')
|
63
|
+
except Exception:
|
64
|
+
current_date_dt = None
|
61
65
|
else:
|
62
66
|
display_date = surgery_date or 'Unknown Date'
|
63
|
-
|
64
|
-
sort_key = surgery_date or ''
|
67
|
+
current_date_dt = None
|
65
68
|
else:
|
66
69
|
display_date = str(surgery_date) if surgery_date is not None else 'Unknown Date'
|
67
|
-
|
70
|
+
current_date_dt = None
|
68
71
|
except Exception:
|
69
72
|
display_date = str(surgery_date) if surgery_date is not None else 'Unknown Date'
|
70
|
-
|
73
|
+
current_date_dt = None
|
71
74
|
|
72
75
|
# Normalize diagnosis display: only show "-Not Found-" when explicitly flagged as N/A
|
73
76
|
# XP SP3 + Py3.4.4 compatible error handling
|
@@ -90,11 +93,72 @@ def display_enhanced_patient_table(patient_info, title, show_line_numbers=True):
|
|
90
93
|
# Fallback logging if string formatting fails
|
91
94
|
MediLink_ConfigLoader.log("Error converting diagnosis code to string", level="WARNING")
|
92
95
|
display_diagnosis = "-Not Found-"
|
96
|
+
|
97
|
+
# Grouping: place all dates for a patient together under their earliest date
|
98
|
+
primary_date_dt = None
|
99
|
+
within_index = 0
|
100
|
+
last_name_key = ''
|
101
|
+
first_name_key = ''
|
102
|
+
try:
|
103
|
+
all_dates = []
|
104
|
+
if patient_row is not None:
|
105
|
+
raw_dates = patient_row.get('_all_surgery_dates', [])
|
106
|
+
# Convert to datetime list and find primary
|
107
|
+
for d in raw_dates:
|
108
|
+
try:
|
109
|
+
if hasattr(d, 'strftime'):
|
110
|
+
all_dates.append(d)
|
111
|
+
elif isinstance(d, str):
|
112
|
+
all_dates.append(datetime.strptime(d, '%m-%d-%Y'))
|
113
|
+
except Exception:
|
114
|
+
pass
|
115
|
+
if all_dates:
|
116
|
+
all_dates.sort()
|
117
|
+
primary_date_dt = all_dates[0]
|
118
|
+
# Determine within-patient index of current date
|
119
|
+
if current_date_dt is not None:
|
120
|
+
# Find matching index by exact date
|
121
|
+
for idx, ad in enumerate(all_dates):
|
122
|
+
if current_date_dt == ad:
|
123
|
+
within_index = idx
|
124
|
+
break
|
125
|
+
# Prefer explicit last/first from row for sorting
|
126
|
+
try:
|
127
|
+
ln = patient_row.get('Patient Last')
|
128
|
+
fn = patient_row.get('Patient First')
|
129
|
+
if isinstance(ln, str):
|
130
|
+
last_name_key = ln.strip().upper()
|
131
|
+
if isinstance(fn, str):
|
132
|
+
first_name_key = fn.strip().upper()
|
133
|
+
except Exception:
|
134
|
+
pass
|
135
|
+
except Exception:
|
136
|
+
primary_date_dt = None
|
137
|
+
within_index = 0
|
138
|
+
|
139
|
+
# Fallbacks if parsing failed
|
140
|
+
if primary_date_dt is None:
|
141
|
+
primary_date_dt = current_date_dt
|
142
|
+
# If last/first not available from row, parse from display name "LAST, FIRST ..."
|
143
|
+
if not last_name_key and isinstance(patient_name, str):
|
144
|
+
try:
|
145
|
+
parts = [p.strip() for p in patient_name.split(',')]
|
146
|
+
if len(parts) >= 1:
|
147
|
+
last_name_key = parts[0].upper()
|
148
|
+
if len(parts) >= 2:
|
149
|
+
first_name_key = parts[1].split()[0].upper() if parts[1] else ''
|
150
|
+
except Exception:
|
151
|
+
last_name_key = ''
|
152
|
+
first_name_key = ''
|
153
|
+
|
154
|
+
# Build composite sort key per requirement: by earliest date, then last name within date,
|
155
|
+
# while keeping same patient's additional dates directly under the first line
|
156
|
+
composite_sort_key = (primary_date_dt, last_name_key, first_name_key, str(patient_id or ''), within_index)
|
93
157
|
|
94
|
-
normalized_info.append((
|
158
|
+
normalized_info.append((composite_sort_key, display_date, str(patient_name or ''), str(patient_id or ''), display_diagnosis))
|
95
159
|
|
96
|
-
# Sort
|
97
|
-
normalized_info.sort(key=lambda x:
|
160
|
+
# Sort so that all entries for a patient are grouped under their earliest date
|
161
|
+
normalized_info.sort(key=lambda x: x[0])
|
98
162
|
|
99
163
|
# Calculate column widths for proper alignment
|
100
164
|
max_patient_id_len = max(len(pid) for _, _, _, pid, _ in normalized_info)
|
@@ -252,6 +316,10 @@ def display_patient_selection_menu(csv_data, reverse_mapping, proceed_as_medicar
|
|
252
316
|
selected_patient_ids = []
|
253
317
|
selected_indices = []
|
254
318
|
|
319
|
+
# TODO: Future enhancement - make this configurable via config file
|
320
|
+
# Example: config.get('silent_initial_selection', True)
|
321
|
+
SILENT_INITIAL_SELECTION = True # Set to False to restore original interactive behavior
|
322
|
+
|
255
323
|
def display_menu_header(title):
|
256
324
|
print("\n" + "-" * 60)
|
257
325
|
print(title)
|
@@ -284,7 +352,10 @@ def display_patient_selection_menu(csv_data, reverse_mapping, proceed_as_medicar
|
|
284
352
|
formatted_date = surgery_date.strftime('%m-%d')
|
285
353
|
except Exception:
|
286
354
|
formatted_date = str(surgery_date)
|
287
|
-
|
355
|
+
|
356
|
+
# Only display if not in silent mode
|
357
|
+
if not SILENT_INITIAL_SELECTION:
|
358
|
+
print("{0:03d}: {3} (ID: {2}) {1} ".format(index+1, patient_name, patient_id, formatted_date))
|
288
359
|
|
289
360
|
displayed_indices.append(index)
|
290
361
|
displayed_patient_ids.append(patient_id)
|
@@ -292,23 +363,44 @@ def display_patient_selection_menu(csv_data, reverse_mapping, proceed_as_medicar
|
|
292
363
|
return displayed_indices, displayed_patient_ids
|
293
364
|
|
294
365
|
if proceed_as_medicare:
|
295
|
-
|
366
|
+
if not SILENT_INITIAL_SELECTION:
|
367
|
+
display_menu_header("MEDICARE Patient Selection for Today's Data Entry")
|
296
368
|
selected_indices, selected_patient_ids = display_patient_list(csv_data, reverse_mapping, medicare_filter=True)
|
297
369
|
else:
|
298
|
-
|
370
|
+
if not SILENT_INITIAL_SELECTION:
|
371
|
+
display_menu_header("PRIVATE Patient Selection for Today's Data Entry")
|
299
372
|
selected_indices, selected_patient_ids = display_patient_list(csv_data, reverse_mapping, exclude_medicare=True)
|
300
373
|
|
301
|
-
|
302
|
-
|
374
|
+
if not SILENT_INITIAL_SELECTION:
|
375
|
+
print("-" * 60)
|
376
|
+
proceed = input("\nDo you want to proceed with the selected patients? (yes/no): ").lower().strip() in ['yes', 'y']
|
377
|
+
else:
|
378
|
+
# Auto-confirm in silent mode
|
379
|
+
proceed = True
|
303
380
|
|
304
381
|
if not proceed:
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
382
|
+
if not SILENT_INITIAL_SELECTION:
|
383
|
+
display_menu_header("Patient Selection for Today's Data Entry")
|
384
|
+
selected_indices, selected_patient_ids = display_patient_list(csv_data, reverse_mapping)
|
385
|
+
print("-" * 60)
|
386
|
+
|
310
387
|
while True:
|
311
|
-
|
388
|
+
while True:
|
389
|
+
selection = input("\nEnter the number(s) of the patients you wish to proceed with\n(e.g., 1, 3, 5): ").strip()
|
390
|
+
if not selection:
|
391
|
+
print("Invalid entry. Please provide at least one number.")
|
392
|
+
continue
|
393
|
+
|
394
|
+
selection = selection.replace('.', ',') # Replace '.' with ',' in the user input just in case
|
395
|
+
selected_indices = [int(x.strip()) - 1 for x in selection.split(',') if x.strip().isdigit()]
|
396
|
+
|
397
|
+
if not selected_indices:
|
398
|
+
print("Invalid entry. Please provide at least one integer.")
|
399
|
+
continue
|
400
|
+
|
401
|
+
proceed = True
|
402
|
+
break
|
403
|
+
|
312
404
|
if not selection:
|
313
405
|
print("Invalid entry. Please provide at least one number.")
|
314
406
|
continue
|
@@ -322,20 +414,6 @@ def display_patient_selection_menu(csv_data, reverse_mapping, proceed_as_medicar
|
|
322
414
|
|
323
415
|
proceed = True
|
324
416
|
break
|
325
|
-
|
326
|
-
if not selection:
|
327
|
-
print("Invalid entry. Please provide at least one number.")
|
328
|
-
continue
|
329
|
-
|
330
|
-
selection = selection.replace('.', ',') # Replace '.' with ',' in the user input just in case
|
331
|
-
selected_indices = [int(x.strip()) - 1 for x in selection.split(',') if x.strip().isdigit()]
|
332
|
-
|
333
|
-
if not selected_indices:
|
334
|
-
print("Invalid entry. Please provide at least one integer.")
|
335
|
-
continue
|
336
|
-
|
337
|
-
proceed = True
|
338
|
-
break
|
339
417
|
|
340
418
|
patient_id_header = reverse_mapping['Patient ID #2']
|
341
419
|
selected_patient_ids = [csv_data[i][patient_id_header] for i in selected_indices if i < len(csv_data)]
|
@@ -434,6 +512,15 @@ def user_interaction(csv_data, interaction_mode, error_message, reverse_mapping)
|
|
434
512
|
fixed_values.update(medicare_added_fixed_values) # Add any medicare-specific fixed values from config
|
435
513
|
|
436
514
|
proceed, selected_patient_ids, selected_indices = display_patient_selection_menu(csv_data, reverse_mapping, response in ['yes', 'y'])
|
437
|
-
|
438
|
-
|
439
|
-
|
515
|
+
is_medicare = response in ['yes', 'y']
|
516
|
+
return proceed, selected_patient_ids, selected_indices, fixed_values, is_medicare
|
517
|
+
|
518
|
+
# For non-triage modes (error, normal), return a compatible structure
|
519
|
+
# The is_medicare value is not relevant in these modes, so we'll use a default
|
520
|
+
result = handle_user_interaction(interaction_mode, error_message)
|
521
|
+
if isinstance(result, int):
|
522
|
+
# This is a control value (-1, 1, -2), return with default values
|
523
|
+
return False, [], [], {}, False # proceed=False, empty lists, empty dict, is_medicare=False
|
524
|
+
else:
|
525
|
+
# Unexpected return type, handle gracefully
|
526
|
+
return False, [], [], {}, False
|