""" =============================================================================== FILE: main_etl.py PROJECT: SCUD Orion AI (Unified Architecture) ROLE: Главная точка входа ETL-конвейера СКУД ⟷ 1С:ЗУП. =============================================================================== """ import os import sys import argparse import logging import pandas as pd from datetime import datetime, timedelta from services.scud_etl.pipeline import load_best_snapshot_for_date, load_1c_files_for_date from services.scud_etl.merger import merge_scud_and_1c, calculate_summary_metrics from services.scud_etl.anomaly_detector import detect_registry_anomalies from services.scud_etl.svodka_generator import generate_svodka_service from services.scud_etl.otchet_generator import generate_otchet_service from services.text_reporter import generate_markdown_report from services.snapshots.retention import cleanup_old_intermediate_snapshots from services.scud_export import run_export from services.share_copier import copy_1c_files_from_share from services.excel_exporter import export_raw_scud logging.basicConfig(level=logging.INFO, format="[%(asctime)s] [%(levelname)s] %(message)s") def print_help(): print(""" =============================================================================== 🛠️ SCUD ORION AI — СИСТЕМА КОНТРОЛЛИНГА И СВОДНЫХ ОТЧЕТОВ =============================================================================== Использование: python main_etl.py [ОПЦИИ] Доступные аргументы: -h, --help, help Показать эту справку и выйти --date ДД.ММ.ГГГГ Дата расчета (по умолчанию: текущий рабочий день) --time ЧЧ:ММ Время среза для сводки (например: 14:30) Ищет ближайший срез (±20 мин) или запрашивает On-Demand экспорт --snapshot ID Точный ID снапшота для расчета (например: 20260827-002) --skip-export Пропустить выгрузку СКУД из MS SQL (работать только с SQLite) --export-only ТОЛЬКО сделать экспорт/снапшот СКУД в БД без построения отчетов -d, --debug Включить режим расширенной отладки Примеры использования: python main_etl.py 👉 Полный суточный цикл: экспорт -> отчет за вчера -> сводка за сегодня -> ИИ. python main_etl.py --export-only 👉 Почасовой тихий срез в БД (для cron) без генерации отчетов. python main_etl.py --date 27.08.2026 --time 12:15 --skip-export 👉 Построить сводку за 27.08 на 12:15 без запроса к внешнему MS SQL. python main_etl.py --snapshot 20260827-001 --skip-export 👉 Расчет отчетов строго по выбранному снапшоту из SQLite. =============================================================================== """) def main(): if len(sys.argv) > 1 and sys.argv[1] in ("-h", "--help", "help"): print_help() sys.exit(0) parser = argparse.ArgumentParser(add_help=False) parser.add_argument("-h", "--help", action="store_true") parser.add_argument("-d", "--debug", action="store_true") parser.add_argument("--skip-export", action="store_true") parser.add_argument("--export-only", action="store_true") parser.add_argument("--date", type=str, default=None) parser.add_argument("--time", type=str, default=None) parser.add_argument("--snapshot", type=str, default=None) args = parser.parse_args() if args.help: print_help() sys.exit(0) print("=" * 60) print(f"ЗАПУСК СИСТЕМЫ МОДУЛЬНОГО КОНТРОЛЛИНГА СКУД ⟷ 1С {'[DEBUG]' if args.debug else ''}") print("=" * 60) # Автоматическая ротация архивных почасовых срезов if not args.skip_export and not args.snapshot: deleted_count = cleanup_old_intermediate_snapshots(days_to_keep_all=2) if deleted_count > 0: print(f"[🧹] Ротация БД: очищено {deleted_count} строк промежуточных архивных срезов.") now = datetime.now() if args.date: today_str = args.date.replace('_', '.') dt_target = datetime.strptime(today_str, "%d.%m.%Y") days_back = 3 if dt_target.weekday() == 0 else 1 yesterday_str = (dt_target - timedelta(days=days_back)).strftime("%d.%m.%Y") else: today_str = now.strftime("%d.%m.%Y") if now.weekday() == 0: yesterday_str = (now - timedelta(days=3)).strftime("%d.%m.%Y") else: yesterday_str = (now - timedelta(days=1)).strftime("%d.%m.%Y") # [Этап 0] Выгрузка свежих данных СКУД if not args.skip_export and not args.snapshot: print(f"\n[0/5] Экспорт данных СКУД за {today_str} и {yesterday_str}...") run_export(input_date=args.date, debug=args.debug, save_xlsx=True) else: print("\n[0/5] Пропуск прямого экспорта СКУД из MS SQL (--skip-export)...") # Если запрошен режим тихого почасового среза — выходим без тяжелых генераций if args.export_only: print(f"\n[✓] Режим --export-only: срез зафиксирован в SQLite. Генерация отчетов пропущена.") sys.exit(0) # [Этап 0.5] Синхронизация файлов с шары 1С if not args.snapshot and not args.skip_export: print(f"\n[0.5/5] Проверка и копирование файлов 1С с шары...") copy_1c_files_from_share() else: print("\n[0.5/5] Пропуск синхронизации с шары (чтение локальных данных)...") # [Этап 1-2] Загрузка данных print(f"\n[1-2/5] Загрузка срезов: Сегодня = {today_str}, Накануне = {yesterday_str}...") df_scud_yesterday = load_best_snapshot_for_date(yesterday_str, prefer_final_y=True) df_scud_today = load_best_snapshot_for_date(today_str, prefer_final_y=False) df_staff_yesterday, df_abs_yesterday = load_1c_files_for_date(yesterday_str) df_staff_today, df_abs_today = load_1c_files_for_date(today_str) if df_scud_today is not None and not df_scud_today.empty: export_raw_scud(df_scud_today, filename=f"СКУД_Сырые_данные_{today_str}.xlsx") # [Этап 3] Детальный и Упрощенный отчеты за вчера print(f"\n[3/5] Обработка и построение детального и упрощенного отчетов за ВЧЕРА ({yesterday_str})...") res_otchet = generate_otchet_service(target_date=yesterday_str) if res_otchet.get("status") == "success": print(f"[✓] {res_otchet.get('message')}: {res_otchet.get('filepath')}") # Генерация Упрощенного отчета за вчера try: from services.reports.simplified_builder import generate_simplified_excel df_scud_y = load_best_snapshot_for_date(yesterday_str, prefer_final_y=True) df_staff_y, df_abs_y = load_1c_files_for_date(yesterday_str) df_merged_y = merge_scud_and_1c(df_scud_y, df_staff_y, df_abs_y) simplified_path = generate_simplified_excel(df_merged_y, date_str=yesterday_str) print(f"[✓] Упрощенный отчет за {yesterday_str} успешно сформирован: {simplified_path}") except Exception as e: print(f"[⚠️] Ошибка формирования упрощенного отчета: {e}") # [Этап 4] Сводка за сегодня через svodka_generator print(f"\n[4/5] Обработка и построение Ежедневной сводки за {today_str} {args.time or ''}...") res_svodka = generate_svodka_service( target_date=today_str, target_time=args.time, snapshot_id=args.snapshot ) if res_svodka.get("status") == "success": print(f"[✓] {res_svodka.get('message')}: {res_svodka.get('filepath')}") if res_svodka.get("note"): print(f" ℹ️ {res_svodka.get('note')}") # [Этап 5] Формирование Markdown-сводки через ИИ-аудитора (Ollama) print(f"\n[5/5] Формирование Markdown-сводки через ИИ-аудитора (Ollama)...") df_merged_today = merge_scud_and_1c(df_scud_today, df_staff_today, df_abs_today) anomalies_today = detect_registry_anomalies(df_merged_today, df_raw_scud=df_scud_today) absent_explained = df_merged_today[ (df_merged_today['Пришел'] == False) & (df_merged_today['Вид_отсутствия'].notna()) & (~df_merged_today['Вид_отсутствия'].astype(str).str.startswith('Исключение')) ] absent_unexplained = df_merged_today[ (df_merged_today['Пришел'] == False) & (df_merged_today['Вид_отсутствия'].isna() | (df_merged_today['Вид_отсутствия'].astype(str).str.strip().isin(['', 'nan', 'None']))) & (df_merged_today.get('is_excluded', False) == False) & (df_merged_today.get('not_hired_yet', False) == False) & (df_merged_today.get('no_scud_pass', False) == False) ] summary_md = generate_markdown_report( merged_df=df_merged_today[df_merged_today.get('is_excluded', False) == False], absent_explained=absent_explained, absent_unexplained=absent_unexplained, scud_present_but_absent_in_1c=pd.DataFrame(), anomalies_list=anomalies_today, raw_scud_df=df_scud_today, raw_staff_df=df_staff_today, raw_absent_df=df_abs_today, date_str=today_str ) os.makedirs("output", exist_ok=True) md_file_path = f"output/Сводка_контроллинга_{today_str}.md" with open(md_file_path, "w", encoding="utf-8") as f: f.write(summary_md) print(f"[✓] Текстовый отчет сохранен в: {md_file_path}") print("\n" + "=" * 60) print("ГОТОВАЯ ТЕКСТОВАЯ СВОДКА ИИ-АУДИТОРА:") print("=" * 60) print(summary_md) if __name__ == "__main__": main()