Files
scud_ai/main_etl.py
T

197 lines
9.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
===============================================================================
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] Детальный отчет за вчера через otchet_generator
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')}")
# [Этап 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() == '')) &
(df_merged_today.get('is_excluded', 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()