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.
Files changed (80) hide show
  1. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediBot/MediBot.py +74 -40
  2. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediBot/MediBot_Preprocessor_lib.py +128 -45
  3. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediBot/MediBot_UI.py +124 -37
  4. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediBot/__init__.py +1 -1
  5. medicafe-0.250818.0/MediBot/update_medicafe.py +245 -0
  6. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediCafe/MediLink_ConfigLoader.py +60 -13
  7. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediCafe/__init__.py +5 -2
  8. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediCafe/smart_import.py +12 -0
  9. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/__init__.py +2 -1
  10. {medicafe-0.250814.4 → medicafe-0.250818.0}/PKG-INFO +1 -1
  11. {medicafe-0.250814.4 → medicafe-0.250818.0}/medicafe.egg-info/PKG-INFO +1 -1
  12. {medicafe-0.250814.4 → medicafe-0.250818.0}/setup.py +1 -1
  13. medicafe-0.250814.4/MediBot/update_medicafe.py +0 -770
  14. {medicafe-0.250814.4 → medicafe-0.250818.0}/LICENSE +0 -0
  15. {medicafe-0.250814.4 → medicafe-0.250818.0}/MANIFEST.in +0 -0
  16. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediBot/MediBot.bat +0 -0
  17. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediBot/MediBot_Charges.py +0 -0
  18. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediBot/MediBot_Crosswalk_Library.py +0 -0
  19. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediBot/MediBot_Crosswalk_Utils.py +0 -0
  20. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediBot/MediBot_Post.py +0 -0
  21. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediBot/MediBot_Preprocessor.py +0 -0
  22. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediBot/MediBot_dataformat_library.py +0 -0
  23. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediBot/MediBot_docx_decoder.py +0 -0
  24. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediBot/MediBot_smart_import.py +0 -0
  25. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediBot/get_medicafe_version.py +0 -0
  26. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediBot/update_json.py +0 -0
  27. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediCafe/__main__.py +0 -0
  28. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediCafe/api_core.py +0 -0
  29. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediCafe/api_core_backup.py +0 -0
  30. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediCafe/api_factory.py +0 -0
  31. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediCafe/api_utils.py +0 -0
  32. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediCafe/core_utils.py +0 -0
  33. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediCafe/graphql_utils.py +0 -0
  34. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediCafe/logging_config.py +0 -0
  35. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediCafe/logging_demo.py +0 -0
  36. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediCafe/migration_helpers.py +0 -0
  37. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediCafe/submission_index.py +0 -0
  38. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/InsuranceTypeService.py +0 -0
  39. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_837p_cob_library.py +0 -0
  40. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_837p_encoder.py +0 -0
  41. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_837p_encoder_library.py +0 -0
  42. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_837p_utilities.py +0 -0
  43. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_API_Generator.py +0 -0
  44. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_Azure.py +0 -0
  45. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_ClaimStatus.py +0 -0
  46. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_DataMgmt.py +0 -0
  47. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_Decoder.py +0 -0
  48. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_Deductible.py +0 -0
  49. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_Deductible_Validator.py +0 -0
  50. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_Display_Utils.py +0 -0
  51. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_Down.py +0 -0
  52. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_Gmail.py +0 -0
  53. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_Mailer.py +0 -0
  54. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_Parser.py +0 -0
  55. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_PatientProcessor.py +0 -0
  56. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_Scan.py +0 -0
  57. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_Scheduler.py +0 -0
  58. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_UI.py +0 -0
  59. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_Up.py +0 -0
  60. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_insurance_utils.py +0 -0
  61. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_main.py +0 -0
  62. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/MediLink_smart_import.py +0 -0
  63. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/Soumit_api.py +0 -0
  64. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/gmail_http_utils.py +0 -0
  65. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/gmail_oauth_utils.py +0 -0
  66. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/insurance_type_integration_test.py +0 -0
  67. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/openssl.cnf +0 -0
  68. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/test.py +0 -0
  69. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/test_cob_library.py +0 -0
  70. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/test_timing.py +0 -0
  71. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/test_validation.py +0 -0
  72. {medicafe-0.250814.4 → medicafe-0.250818.0}/MediLink/webapp.html +0 -0
  73. {medicafe-0.250814.4 → medicafe-0.250818.0}/README.md +0 -0
  74. {medicafe-0.250814.4 → medicafe-0.250818.0}/medicafe.egg-info/SOURCES.txt +0 -0
  75. {medicafe-0.250814.4 → medicafe-0.250818.0}/medicafe.egg-info/dependency_links.txt +0 -0
  76. {medicafe-0.250814.4 → medicafe-0.250818.0}/medicafe.egg-info/entry_points.txt +0 -0
  77. {medicafe-0.250814.4 → medicafe-0.250818.0}/medicafe.egg-info/not-zip-safe +0 -0
  78. {medicafe-0.250814.4 → medicafe-0.250818.0}/medicafe.egg-info/requires.txt +0 -0
  79. {medicafe-0.250814.4 → medicafe-0.250818.0}/medicafe.egg-info/top_level.txt +0 -0
  80. {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
- # Get all surgery dates for this patient
699
- all_surgery_dates = patient_row.get('_all_surgery_dates', [patient_row.get('Surgery Date')])
700
- surgery_date_to_diagnosis = patient_row.get('_surgery_date_to_diagnosis', {})
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
- "NOTE: 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:",
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
- patient_id = row.get(reverse_mapping['Patient ID #2'])
737
- patient_name = row.get(reverse_mapping['Patient Name'])
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
- "NOTE: The following NEW patient(s) will be automatically entered into Medisoft:",
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 surgery_date > existing_date:
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
- patient_surgery_dates[patient_id].append(old_date)
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
- # Update the surgery dates list to reflect the new primary date
542
- patient_surgery_dates[patient_id] = [surgery_date] + [d for d in patient_surgery_dates[patient_id] if d != surgery_date]
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
- row['_all_surgery_dates'] = sorted(surgery_date_strings)
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
- csv_data[:] = sorted(unique_patients.values(), key=lambda x: (x['Surgery Date'], x.get('Patient Last', '').strip())) # TODO Does this need to be sorted twice? once before and once after?
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
- error_msg = "CRITICAL: MAINS file not found at: {}. This file is required for insurance name to Medisoft ID mapping.".format(mains_path)
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
- error_msg = "CRITICAL: MAINS file not found: {}. This file is required for insurance name to Medisoft ID mapping.".format(mains_path)
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
- sort_key = None
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
- sort_key = surgery_date
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
- # Use the raw string as sort key fallback
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
- sort_key = display_date
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
- sort_key = display_date
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((sort_key, display_date, str(patient_name or ''), str(patient_id or ''), display_diagnosis))
158
+ normalized_info.append((composite_sort_key, display_date, str(patient_name or ''), str(patient_id or ''), display_diagnosis))
95
159
 
96
- # Sort by normalized sort key then patient name
97
- normalized_info.sort(key=lambda x: (x[0], x[2]))
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
- print("{0:03d}: {3} (ID: {2}) {1} ".format(index+1, patient_name, patient_id, formatted_date))
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
- display_menu_header("MEDICARE Patient Selection for Today's Data Entry")
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
- display_menu_header("PRIVATE Patient Selection for Today's Data Entry")
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
- print("-" * 60)
302
- proceed = input("\nDo you want to proceed with the selected patients? (yes/no): ").lower().strip() in ['yes', 'y']
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
- display_menu_header("Patient Selection for Today's Data Entry")
306
- selected_indices, selected_patient_ids = display_patient_list(csv_data, reverse_mapping)
307
- print("-" * 60)
308
-
309
- while True:
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
- selection = input("\nEnter the number(s) of the patients you wish to proceed with\n(e.g., 1, 3, 5): ").strip()
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
- return proceed, selected_patient_ids, selected_indices, fixed_values
438
-
439
- return handle_user_interaction(interaction_mode, error_message)
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
@@ -19,7 +19,7 @@ Smart Import Integration:
19
19
  medibot_main = get_components('medibot_main')
20
20
  """
21
21
 
22
- __version__ = "0.250814.4"
22
+ __version__ = "0.250818.0"
23
23
  __author__ = "Daniel Vidaud"
24
24
  __email__ = "daniel@personalizedtransformation.com"
25
25