промежуточно. Работает все, неверно формируется вчерашний отчет (нет выхода).

This commit is contained in:
2026-07-28 16:41:26 +03:00
parent 522800d317
commit 2603627098
13 changed files with 297 additions and 283 deletions
+27 -75
View File
@@ -1,11 +1,5 @@
"""
Скрипт экспорта данных СКУД (Orion) в Excel.
Оптимизированная супер-быстрая версия (использует ROW_NUMBER() вместо подзапросов).
Запуск:
python scud_export.py
python scud_export.py -d
python scud_export.py --date 28.07.2026 -d
Модуль автоматического экспорта данных СКУД (Orion) из MS SQL Server в Excel.
"""
import argparse
@@ -19,13 +13,12 @@ import pandas as pd
import pyodbc
from openpyxl.utils import get_column_letter
from config import SCUD_DIR
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")
LOG_DIR = os.path.join(SCRIPT_DIR, "..", "logs")
os.makedirs(LOG_DIR, exist_ok=True)
TODAY_DATE_STR = datetime.now().strftime("%d.%m.%Y")
@@ -34,13 +27,14 @@ 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)
if not logger.handlers:
_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"):
@@ -56,21 +50,12 @@ def log(message: str, level: str = "INFO"):
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;
@@ -78,7 +63,6 @@ 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,
@@ -95,7 +79,6 @@ WITH DailyLogs AS (
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,
@@ -167,9 +150,6 @@ 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
@@ -196,7 +176,6 @@ def get_targets(input_date: str | None):
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)
@@ -211,16 +190,13 @@ def get_targets(input_date: str | None):
return targets
# ==============================================================================
# 5. Основная функция выгрузки
# ==============================================================================
def run_export(input_date: str | None, debug: bool = False):
def run_export(input_date: str | None = None, debug: bool = False):
if debug:
logger.setLevel(logging.DEBUG)
log("=== ВКЛЮЧЕН РЕЖИМ ОТЛАДКИ (DEBUG MODE) ===", "WARNING")
log("=== Запуск скрипта экспорта СКУД ===")
os.makedirs(FOLDER_PATH, exist_ok=True)
log("=== [ЭТАП 0] Выгрузка свежих данных СКУД напрямую из БД Орион ===")
os.makedirs(SCUD_DIR, exist_ok=True)
targets = get_targets(input_date)
@@ -234,6 +210,7 @@ def run_export(input_date: str | None, debug: bool = False):
f"Encrypt=no;"
)
success = True
for target in targets:
processing_date = target["date"]
processing_date_str = processing_date.strftime("%d.%m.%Y")
@@ -242,15 +219,13 @@ def run_export(input_date: str | None, debug: bool = False):
log(f"--- Обработка периода: {period_label} ({processing_date_str}) ---")
file_name = f"Сотрудники_{processing_date_str}.xlsx"
file_path = os.path.join(FOLDER_PATH, file_name)
file_path = os.path.join(SCUD_DIR, 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
log(f"ОШИБКА при удалении старого файла {file_name}: {e}", "ERROR")
sql_query = SQL_QUERY_TEMPLATE.format(target_date=processing_date.strftime("%Y-%m-%d"))
@@ -261,51 +236,28 @@ def run_export(input_date: str | None, debug: bool = False):
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")
log(f"[✓] Успешно экспортирован файл: data/scud/{file_name}", "SUCCESS")
else:
log(f"Запрос за {processing_date_str} вернул 0 строк.", "WARNING")
except Exception as e:
log(f"ОШИБКА при обработке даты {processing_date_str}: {e}", "ERROR")
log(f"🛑 ОШИБКА выгрузки СКУД за {processing_date_str}: {e}", "ERROR")
success = False
finally:
if connection is not None:
connection.close()
log("=== Завершение работы скрипта ===")
log("=== Выгрузка СКУД завершена ===\n")
return success
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="Запуск в режиме отладки (вывод детальной статистики)",
)
parser.add_argument("--date", dest="input_date", default=None)
parser.add_argument("-d", "--debug", action="store_true")
args = parser.parse_args()
run_export(args.input_date, debug=args.debug)