197 lines
9.7 KiB
Python
197 lines
9.7 KiB
Python
"""
|
||
===============================================================================
|
||
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() |