diff --git a/modules/logs/export_28.07.2026.log b/modules/logs/export_28.07.2026.log new file mode 100644 index 0000000..134753c --- /dev/null +++ b/modules/logs/export_28.07.2026.log @@ -0,0 +1,62 @@ +[2026-07-28 14:45:26] [INFO] === Запуск скрипта экспорта СКУД === +[2026-07-28 14:45:26] [INFO] --- Обработка периода: Вчера (27.07.2026) --- +[2026-07-28 14:45:26] [INFO] Файл Сотрудники_27.07.2026.xlsx уже существует. Удаляем для перезаписи... +[2026-07-28 14:45:26] [ERROR] ОШИБКА при обработке даты 27.07.2026: ('IM002', '[IM002] [Microsoft][Диспетчер драйверов ODBC] Источник данных не найден и не указан драйвер, используемый по умолчанию (0) (SQLDriverConnect)') +[2026-07-28 14:45:26] [INFO] --- Обработка периода: Сегодня (28.07.2026) --- +[2026-07-28 14:45:26] [INFO] Файл Сотрудники_28.07.2026.xlsx уже существует. Удаляем для перезаписи... +[2026-07-28 14:45:26] [ERROR] ОШИБКА при обработке даты 28.07.2026: ('IM002', '[IM002] [Microsoft][Диспетчер драйверов ODBC] Источник данных не найден и не указан драйвер, используемый по умолчанию (0) (SQLDriverConnect)') +[2026-07-28 14:45:26] [INFO] === Завершение работы скрипта === +[2026-07-28 14:47:46] [INFO] === Запуск скрипта экспорта СКУД === +[2026-07-28 14:47:46] [INFO] --- Обработка периода: Вчера (27.07.2026) --- +[2026-07-28 14:48:17] [INFO] База вернула 272 строк за 27.07.2026 +[2026-07-28 14:48:17] [INFO] [SUCCESS] Успешно создан файл C:\scud_exchsnge\Сотрудники_27.07.2026.xlsx +[2026-07-28 14:48:17] [INFO] --- Обработка периода: Сегодня (28.07.2026) --- +[2026-07-28 14:48:44] [INFO] База вернула 272 строк за 28.07.2026 +[2026-07-28 14:48:44] [INFO] [SUCCESS] Успешно создан файл C:\scud_exchsnge\Сотрудники_28.07.2026.xlsx +[2026-07-28 14:48:44] [INFO] === Завершение работы скрипта === +[2026-07-28 14:49:55] [INFO] === Запуск скрипта экспорта СКУД === +[2026-07-28 14:49:55] [INFO] --- Обработка периода: Вчера (27.07.2026) --- +[2026-07-28 14:49:55] [INFO] Файл Сотрудники_27.07.2026.xlsx уже существует. Удаляем для перезаписи... +[2026-07-28 14:50:17] [INFO] База вернула 272 строк за 27.07.2026 +[2026-07-28 14:50:18] [INFO] [SUCCESS] Успешно создан файл C:\scud_exchsnge\Сотрудники_27.07.2026.xlsx +[2026-07-28 14:50:18] [INFO] --- Обработка периода: Сегодня (28.07.2026) --- +[2026-07-28 14:50:18] [INFO] Файл Сотрудники_28.07.2026.xlsx уже существует. Удаляем для перезаписи... +[2026-07-28 14:50:50] [INFO] База вернула 272 строк за 28.07.2026 +[2026-07-28 14:50:50] [INFO] [SUCCESS] Успешно создан файл C:\scud_exchsnge\Сотрудники_28.07.2026.xlsx +[2026-07-28 14:50:50] [INFO] === Завершение работы скрипта === +[2026-07-28 14:53:08] [INFO] === Запуск скрипта экспорта СКУД === +[2026-07-28 14:53:08] [INFO] --- Обработка периода: Вчера (27.07.2026) --- +[2026-07-28 14:53:08] [INFO] Файл Сотрудники_27.07.2026.xlsx уже существует. Удаляем для перезаписи... +[2026-07-28 14:53:30] [INFO] База вернула 272 строк за 27.07.2026 +[2026-07-28 14:53:30] [INFO] [SUCCESS] Успешно создан файл C:\scud_exchsnge\Сотрудники_27.07.2026.xlsx +[2026-07-28 14:53:30] [INFO] --- Обработка периода: Сегодня (28.07.2026) --- +[2026-07-28 14:53:30] [INFO] Файл Сотрудники_28.07.2026.xlsx уже существует. Удаляем для перезаписи... +[2026-07-28 14:53:52] [INFO] База вернула 272 строк за 28.07.2026 +[2026-07-28 14:53:52] [INFO] [SUCCESS] Успешно создан файл C:\scud_exchsnge\Сотрудники_28.07.2026.xlsx +[2026-07-28 14:53:52] [INFO] === Завершение работы скрипта === +[2026-07-28 15:12:05] [INFO] === Запуск скрипта экспорта СКУД === +[2026-07-28 15:12:05] [INFO] --- Обработка периода: Вчера (27.07.2026) --- +[2026-07-28 15:12:05] [INFO] Файл Сотрудники_27.07.2026.xlsx существует. Перезаписываем... +[2026-07-28 15:12:05] [ERROR] ОШИБКА при обработке даты 27.07.2026: ('42000', '[42000] [Microsoft][SQL Server Native Client 10.0][SQL Server]Неправильный синтаксис около конструкции "me". (102) (SQLExecDirectW)') +[2026-07-28 15:12:05] [INFO] --- Обработка периода: Сегодня (28.07.2026) --- +[2026-07-28 15:12:05] [INFO] Файл Сотрудники_28.07.2026.xlsx существует. Перезаписываем... +[2026-07-28 15:12:05] [ERROR] ОШИБКА при обработке даты 28.07.2026: ('42000', '[42000] [Microsoft][SQL Server Native Client 10.0][SQL Server]Неправильный синтаксис около конструкции "me". (102) (SQLExecDirectW)') +[2026-07-28 15:12:05] [INFO] === Завершение работы скрипта === +[2026-07-28 15:13:31] [INFO] === Запуск скрипта экспорта СКУД === +[2026-07-28 15:13:31] [INFO] --- Обработка периода: Вчера (27.07.2026) --- +[2026-07-28 15:15:02] [INFO] База вернула 272 строк за 27.07.2026 +[2026-07-28 15:15:03] [INFO] [SUCCESS] Успешно создан файл C:\scud_exchsnge\Сотрудники_27.07.2026.xlsx +[2026-07-28 15:15:03] [INFO] --- Обработка периода: Сегодня (28.07.2026) --- +[2026-07-28 15:16:20] [INFO] База вернула 272 строк за 28.07.2026 +[2026-07-28 15:16:21] [INFO] [SUCCESS] Успешно создан файл C:\scud_exchsnge\Сотрудники_28.07.2026.xlsx +[2026-07-28 15:16:21] [INFO] === Завершение работы скрипта === +[2026-07-28 15:17:22] [INFO] === Запуск скрипта экспорта СКУД === +[2026-07-28 15:17:22] [INFO] --- Обработка периода: Вчера (27.07.2026) --- +[2026-07-28 15:17:22] [INFO] Файл Сотрудники_27.07.2026.xlsx существует. Перезаписываем... +[2026-07-28 15:17:46] [INFO] База вернула 272 строк за 27.07.2026 +[2026-07-28 15:17:47] [INFO] [SUCCESS] Успешно создан файл C:\scud_exchsnge\Сотрудники_27.07.2026.xlsx +[2026-07-28 15:17:47] [INFO] --- Обработка периода: Сегодня (28.07.2026) --- +[2026-07-28 15:17:47] [INFO] Файл Сотрудники_28.07.2026.xlsx существует. Перезаписываем... +[2026-07-28 15:18:11] [INFO] База вернула 272 строк за 28.07.2026 +[2026-07-28 15:18:12] [INFO] [SUCCESS] Успешно создан файл C:\scud_exchsnge\Сотрудники_28.07.2026.xlsx +[2026-07-28 15:18:12] [INFO] === Завершение работы скрипта === diff --git a/modules/scud_export.py b/modules/scud_export.py new file mode 100644 index 0000000..4d1842e --- /dev/null +++ b/modules/scud_export.py @@ -0,0 +1,311 @@ +""" +Скрипт экспорта данных СКУД (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) \ No newline at end of file