Files
scud_orion_ai_v2/modules/scud_export.py
T

311 lines
13 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.
"""
Скрипт экспорта данных СКУД (Orion) в Excel.
Оптимизированная супер-быстрая версия (использует ROW_NUMBER() вместо подзапросов).
Запуск:
python scud_export.py
python scud_export.py -d
python scud_export.py --date 28.07.2026 -d
"""
import argparse
import logging
import os
import sys
import warnings
from datetime import datetime, timedelta
import pandas as pd
import pyodbc
from openpyxl.utils import get_column_letter
warnings.filterwarnings("ignore", message="pandas only supports SQLAlchemy connectable")
# ==============================================================================
# 1. Настройки путей и логов
# ==============================================================================
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
LOG_DIR = os.path.join(SCRIPT_DIR, "logs")
os.makedirs(LOG_DIR, exist_ok=True)
TODAY_DATE_STR = datetime.now().strftime("%d.%m.%Y")
LOG_FILE = os.path.join(LOG_DIR, f"export_{TODAY_DATE_STR}.log")
logger = logging.getLogger("scud_export")
logger.setLevel(logging.INFO)
_file_handler = logging.FileHandler(LOG_FILE, encoding="utf-8")
_console_handler = logging.StreamHandler(sys.stdout)
_formatter = logging.Formatter("[%(asctime)s] [%(levelname)s] %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
_file_handler.setFormatter(_formatter)
_console_handler.setFormatter(_formatter)
logger.addHandler(_file_handler)
logger.addHandler(_console_handler)
def log(message: str, level: str = "INFO"):
level_map = {
"INFO": logging.INFO,
"ERROR": logging.ERROR,
"WARNING": logging.WARNING,
"SUCCESS": logging.INFO,
}
if level == "SUCCESS":
logger.info(f"[SUCCESS] {message}")
else:
logger.log(level_map.get(level, logging.INFO), message)
# ==============================================================================
# 2. Настройки базы данных и папки
# ==============================================================================
SERVER_NAME = r"192.168.1.253\SQL"
DATABASE_NAME = "Orion-14.01.21-1"
SQL_USER = "sa"
SQL_PASSWORD = "123456"
FOLDER_PATH = r"C:\scud_exchsnge"
ODBC_DRIVER = "SQL Server Native Client 10.0"
# ==============================================================================
# 3. СУПЕР-ОПТИМИЗИРОВАННЫЙ SQL-ЗАПРОС (Мгновенное выполнение)
# ==============================================================================
SQL_QUERY_TEMPLATE = r"""
DECLARE @InputDate DATE = '{target_date}';
DECLARE @TargetDate DATE = @InputDate;
DECLARE @StartDate DATETIME = CAST(@TargetDate AS DATETIME);
DECLARE @EndDate DATETIME = DATEADD(SECOND, -1, DATEADD(DAY, 1, @StartDate));
-- 1. Фильтруем логи только за нужные 24 часа за один проход
WITH DailyLogs AS (
SELECT
log.HozOrgan AS EmployeeID,
log.TimeVal,
log.Event,
log.Mode,
CASE WHEN log.Event IN (28, 26, 32) OR log.Mode = 1 THEN 'IN'
WHEN log.Event IN (29, 27, 33) OR log.Mode = 2 THEN 'OUT'
ELSE 'OTHER' END AS Direction,
ROW_NUMBER() OVER (PARTITION BY log.HozOrgan ORDER BY log.TimeVal DESC) AS RowNumDesc
FROM pLogData log WITH (NOLOCK)
WHERE log.TimeVal BETWEEN @StartDate AND @EndDate
AND log.HozOrgan IS NOT NULL
AND log.HozOrgan > 0
AND (log.Event IN (28, 29, 26, 27, 32, 33) OR log.Mode IN (1, 2))
),
-- 2. Агрегируем времена входа/выхода и определяем последнее событие
Passages AS (
SELECT
EmployeeID,
MIN(CASE WHEN Direction = 'IN' THEN TimeVal END) AS FirstIn,
MAX(CASE WHEN Direction = 'OUT' THEN TimeVal END) AS LastOut,
MAX(CASE WHEN RowNumDesc = 1 THEN Direction END) AS LastEventType
FROM DailyLogs
GROUP BY EmployeeID
)
SELECT
N'ЛЕНМОРНИИПРОЕКТ' AS [Фирма],
ISNULL(CAST(div.Name AS NVARCHAR(255)), N'Без подразделения') AS [Подразделение],
LTRIM(RTRIM(
ISNULL(CAST(p.Name AS NVARCHAR(255)), N'') +
CASE WHEN p.FirstName IS NOT NULL AND CAST(p.FirstName AS NVARCHAR(255)) <> ''
THEN N' ' + CAST(p.FirstName AS NVARCHAR(255)) ELSE N'' END +
CASE WHEN p.MidName IS NOT NULL AND CAST(p.MidName AS NVARCHAR(255)) <> ''
THEN N' ' + CAST(p.MidName AS NVARCHAR(255)) ELSE N'' END
)) AS [Сотрудник],
ISNULL(CAST(post.Name AS NVARCHAR(255)), N'—') AS [Должность],
ISNULL(CAST(p.TabNumber AS NVARCHAR(50)), N'—') AS [Таб_№],
CONVERT(VARCHAR(10), @TargetDate, 104) AS [Дата],
ISNULL(CAST(CONVERT(VARCHAR(8), pass.FirstIn, 108) AS NVARCHAR(20)), N'Нет входа') AS [Начало_дня],
CASE
WHEN @TargetDate = CAST(GETDATE() AS DATE) AND (pass.LastEventType = 'IN' OR pass.LastOut IS NULL OR pass.LastOut <= pass.FirstIn)
THEN N'Нет выхода'
WHEN pass.LastOut IS NOT NULL AND pass.LastOut > pass.FirstIn
THEN CAST(CONVERT(VARCHAR(8), pass.LastOut, 108) AS NVARCHAR(20))
ELSE N'Нет выхода'
END AS [Конец_дня],
CASE
WHEN pass.EmployeeID IS NOT NULL AND pass.FirstIn IS NOT NULL THEN
RIGHT('0' + CAST(DATEDIFF(MINUTE,
pass.FirstIn,
CASE
WHEN @TargetDate = CAST(GETDATE() AS DATE) AND (pass.LastEventType = 'IN' OR pass.LastOut IS NULL OR pass.LastOut <= pass.FirstIn) THEN GETDATE()
ELSE ISNULL(pass.LastOut, pass.FirstIn)
END) / 60 AS VARCHAR), 2) + ':' +
RIGHT('0' + CAST(DATEDIFF(MINUTE,
pass.FirstIn,
CASE
WHEN @TargetDate = CAST(GETDATE() AS DATE) AND (pass.LastEventType = 'IN' OR pass.LastOut IS NULL OR pass.LastOut <= pass.FirstIn) THEN GETDATE()
ELSE ISNULL(pass.LastOut, pass.FirstIn)
END) % 60 AS VARCHAR), 2)
ELSE N'00:00'
END AS [Находился_в_здании],
CASE
WHEN pass.EmployeeID IS NOT NULL THEN N'Присутствовал'
ELSE N'Отсутствовал (Нет событий)'
END AS [Статус]
FROM pList p WITH (NOLOCK)
LEFT JOIN PDivision div WITH (NOLOCK) ON p.Section = div.ID
LEFT JOIN PPost post WITH (NOLOCK) ON p.Post = post.ID
LEFT JOIN Passages pass ON p.ID = pass.EmployeeID
WHERE
ISNULL(p.StatusRecord, 0) = 0
AND p.DateTimeInArchive IS NULL
AND ISNULL(CAST(div.Name AS NVARCHAR(255)), N'') NOT LIKE N'Аренд%'
AND ISNULL(CAST(div.Name AS NVARCHAR(255)), N'') NOT IN (N'Без подразделения', N'')
AND p.Name NOT LIKE N'бр.%'
AND p.Name NOT LIKE N'Гость%'
AND ISNULL(CAST(div.Name AS NVARCHAR(255)), N'') NOT IN (N'БГИ', N'КНР')
AND ISNULL(CAST(div.Name AS NVARCHAR(255)), N'') NOT LIKE N'Рабоч%'
AND ISNULL(CAST(div.Name AS NVARCHAR(255)), N'') NOT LIKE N'Врем%'
AND ISNULL(CAST(div.Name AS NVARCHAR(255)), N'') NOT LIKE N'Практика%'
AND ISNULL(CAST(div.Name AS NVARCHAR(255)), N'') NOT LIKE N'тест%'
AND ISNULL(CAST(post.Name AS NVARCHAR(255)), N'') NOT LIKE N'Практикант%'
ORDER BY p.Name ASC;
"""
# ==============================================================================
# 4. Вспомогательные функции
# ==============================================================================
def auto_fit_columns(file_path: str, sheet_name: str, padding: float = 2.0, min_width: float = 8.0, max_width: float = 60.0):
from openpyxl import load_workbook
wb = load_workbook(file_path)
ws = wb[sheet_name]
for col_cells in ws.columns:
max_len = 0
col_letter = get_column_letter(col_cells[0].column)
for cell in col_cells:
if cell.value is not None:
cell_len = len(str(cell.value))
if cell_len > max_len:
max_len = cell_len
width = max(min_width, min(max_len + padding, max_width))
ws.column_dimensions[col_letter].width = width
wb.save(file_path)
def get_targets(input_date: str | None):
targets = []
if input_date:
try:
parsed = datetime.strptime(input_date, "%d.%m.%Y").date()
targets.append({"name": "Указанная дата", "date": parsed})
log(f"Запущен ручной режим для даты: {input_date}")
except ValueError:
log(f"ОШИБКА: Неверный формат даты '{input_date}'. Используйте ДД.ММ.ГГГГ", "ERROR")
sys.exit(1)
else:
now = datetime.now()
yesterday = (now - timedelta(days=3 if now.weekday() == 0 else 1)).date()
today = now.date()
targets.append({"name": "Вчера", "date": yesterday})
targets.append({"name": "Сегодня", "date": today})
return targets
# ==============================================================================
# 5. Основная функция выгрузки
# ==============================================================================
def run_export(input_date: str | None, debug: bool = False):
if debug:
logger.setLevel(logging.DEBUG)
log("=== ВКЛЮЧЕН РЕЖИМ ОТЛАДКИ (DEBUG MODE) ===", "WARNING")
log("=== Запуск скрипта экспорта СКУД ===")
os.makedirs(FOLDER_PATH, exist_ok=True)
targets = get_targets(input_date)
conn_str = (
f"DRIVER={{{ODBC_DRIVER}}};"
f"SERVER={SERVER_NAME};"
f"DATABASE={DATABASE_NAME};"
f"UID={SQL_USER};"
f"PWD={SQL_PASSWORD};"
f"TrustServerCertificate=yes;"
f"Encrypt=no;"
)
for target in targets:
processing_date = target["date"]
processing_date_str = processing_date.strftime("%d.%m.%Y")
period_label = target["name"]
log(f"--- Обработка периода: {period_label} ({processing_date_str}) ---")
file_name = f"Сотрудники_{processing_date_str}.xlsx"
file_path = os.path.join(FOLDER_PATH, file_name)
if os.path.exists(file_path):
log(f"Файл {file_name} существует. Перезаписываем...")
try:
os.remove(file_path)
except OSError as e:
log(f"ОШИБКА при удалении файла {file_name}: {e}", "ERROR")
continue
sql_query = SQL_QUERY_TEMPLATE.format(target_date=processing_date.strftime("%Y-%m-%d"))
connection = None
try:
connection = pyodbc.connect(conn_str, timeout=120)
df = pd.read_sql(sql_query, connection)
log(f"База вернула {len(df)} строк за {processing_date_str}")
if debug:
present_df = df[df["Начало_дня"] != "Нет входа"]
still_inside_df = df[(df["Начало_дня"] != "Нет входа") & (df["Конец_дня"] == "Нет выхода")]
left_df = df[(df["Начало_дня"] != "Нет входа") & (df["Конец_дня"] != "Нет выхода")]
print("\n" + "=" * 50)
print(f"[DEBUG STATS] Дата: {processing_date_str}")
print(f" Всего активных сотрудников в БД: {len(df)}")
print(f" Присутствуют / были сегодня: {len(present_df)}")
print(f" Физически находятся в здании: {len(still_inside_df)} (Конец_дня = 'Нет выхода')")
print(f" Завершили рабочий день (Ушли): {len(left_df)}")
if not still_inside_df.empty:
print("\n Первые 5 сотрудников, находящихся сейчас в здании:")
for _, r in still_inside_df.head(5).iterrows():
print(f" • {r['Сотрудник']} | Вход: {r['Начало_дня']} | В здании: {r['Находился_в_здании']}")
print("=" * 50 + "\n")
if len(df) > 0:
df.to_excel(file_path, sheet_name="Отчет", index=False, engine="openpyxl")
auto_fit_columns(file_path, sheet_name="Отчет")
log(f"Успешно создан файл {file_path}", "SUCCESS")
except Exception as e:
log(f"ОШИБКА при обработке даты {processing_date_str}: {e}", "ERROR")
finally:
if connection is not None:
connection.close()
log("=== Завершение работы скрипта ===")
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Экспорт данных СКУД в Excel")
parser.add_argument(
"--date",
dest="input_date",
default=None,
help='Дата в формате ДД.ММ.ГГГГ (например, "28.07.2026")',
)
parser.add_argument(
"-d", "--debug",
action="store_true",
help="Запуск в режиме отладки (вывод детальной статистики)",
)
args = parser.parse_args()
run_export(args.input_date, debug=args.debug)