feat: обновлен config.py с параметрами подключения к MS SQL ACCOUNT-01

This commit is contained in:
2026-08-04 12:55:35 +03:00
parent e2a3c4f0a2
commit d667b2d846
5 changed files with 168 additions and 9 deletions
+2 -1
View File
@@ -9,7 +9,8 @@ SCUD_DIR = os.path.join(DATA_DIR, "scud")
ZUP_1C_DIR = os.path.join(DATA_DIR, "1c")
OUTPUT_DIR = os.path.join(BASE_DIR, "output")
SHARE_1C_DIR = r"\\storage\SCUD\Обмен\Штат"
# Путь к намонтированной сетевой шаре 1С в Linux (вместо \\storage\SCUD\Обмен\Штат)
SHARE_1C_DIR = "/mnt/scud_share"
for folder in [DATA_DIR, SCUD_DIR, ZUP_1C_DIR, OUTPUT_DIR]:
os.makedirs(folder, exist_ok=True)
Binary file not shown.
+3 -7
View File
@@ -106,7 +106,6 @@ def main():
args = parser.parse_args()
DEBUG = args.debug
SAVE_XLSX = args.with_xlsx
print("=" * 60)
print(f"ЗАПУСК СИСТЕМЫ МОДУЛЬНОГО КОНТРОЛЛИНГА СКУД ⟷ 1С {'[DEBUG MODE]' if DEBUG else ''}")
@@ -117,16 +116,16 @@ def main():
kb_data = load_knowledge_base()
kb_rules = kb_data.get("rules", [])
# 0.1 Выгрузка из БД СКУД напрямую в SQLite
# 0.1 Выгрузка из БД СКУД напрямую в SQLite и Excel (save_xlsx=True требуется для data_loader)
if not args.skip_export:
try:
run_export(save_xlsx=SAVE_XLSX, debug=DEBUG)
run_export(save_xlsx=True, debug=DEBUG)
except Exception as e:
print(f"[⚠️] Ошибка автоэкспорта из БД: {e}. Используем имеющиеся записи в SQLite.")
else:
print("[0/5] Пропуск прямого экспорта из БД (--skip-export)...")
# 0.5 Копирование файлов 1С с сетевой шары \\storage\SCUD\Обмен\Штат
# 0.5 Копирование файлов 1С с сетевой шары /mnt/scud_share
copy_1c_files_from_share()
# 1. Валидация наличия файлов и данных
@@ -296,9 +295,6 @@ def main():
f.write(report_text)
print(f"\n[✓] Текстовый отчет сохранен в: {md_report_path}")
# 🎯 ОПРОС ОПЕРАТОРА (HUMAN-IN-THE-LOOP):
# Доказанные нарушения (приход во время отпуска/больничного в 1С) записываются автоматически.
# Запрос оператору выводится ТОЛЬКО при спорных/нетипичных аномалиях.
suspicious_cases = []
for a in anomalies_list:
if "ОФИЦИАЛЬНОМ ОТСУТСТВИИ" not in a.get('type', ''):
+2 -1
View File
@@ -1,6 +1,7 @@
"""
Модуль автоматического экспорта данных СКУД (Orion) из MS SQL Server в SQLite и Excel.
Исправлена логика определения направления проходов (Mode = 2 -> OUT).
Адаптирован для Linux (Ubuntu 24.04).
"""
import argparse
@@ -56,7 +57,7 @@ SERVER_NAME = r"172.16.31.221\SQL"
DATABASE_NAME = "Orion-14.01.21-1"
SQL_USER = "sa"
SQL_PASSWORD = "123456"
ODBC_DRIVER = "SQL Server Native Client 10.0"
ODBC_DRIVER = "ODBC Driver 18 for SQL Server"
SQL_QUERY_TEMPLATE = r"""
DECLARE @InputDate DATE = '{target_date}';
+161
View File
@@ -0,0 +1,161 @@
import os
import logging
from datetime import date
import pyodbc
import pandas as pd
from config import DATA_DIR, normalize_fio
logger = logging.getLogger(__name__)
# Конфигурация MS SQL Server 1С:ЗУП 3.1
ZUP_SQL_CONFIG = {
"driver": "{ODBC Driver 18 for SQL Server}",
"server": os.getenv("ZUP_MS_SQL_SERVER", "192.168.1.10"), # Укажите IP MS SQL 1С
"database": os.getenv("ZUP_MS_SQL_DB", "zup_db"),
"user": os.getenv("ZUP_MS_SQL_USER", "sa"),
"password": os.getenv("ZUP_MS_SQL_PASS", "your_password"),
}
def get_zup_connection_string() -> str:
"""Формирует строку подключения pyodbc к MS SQL Server."""
return (
f"DRIVER={ZUP_SQL_CONFIG['driver']};"
f"SERVER={ZUP_SQL_CONFIG['server']};"
f"DATABASE={ZUP_SQL_CONFIG['database']};"
f"UID={ZUP_SQL_CONFIG['user']};"
f"PWD={ZUP_SQL_CONFIG['password']};"
f"TrustServerCertificate=yes;"
f"Encrypt=no;"
)
def fetch_zup_absences_from_sql(target_date: date) -> pd.DataFrame:
"""
Извлекает оперативные отсутствия и действующие декреты из MS SQL 1С:ЗУП 3.1
на указанную дату.
"""
query = """
DECLARE @TargetDate DATE = ?;
-- 1. Оперативные отсутствия (Отпуска, Командировки, Больничные, Отгулы)
SELECT
LTRIM(RTRIM(ref_emp._Description)) AS [ФИО],
CASE state._Fld16925RRef
WHEN 0x9C10B2452D414FDF4A90E2B2AB81D3F7 THEN N'Отпуск основной'
WHEN 0xBA63FCF94B4AD0664ED369D2E6505D67 THEN N'Командировка'
WHEN 0x8C3B61F23954155A40EB0108FC0932DB THEN N'Болезнь'
WHEN 0x853001C18D0965EE4B2702405C94054A THEN N'Отпуск неоплачиваемый по разрешению работодателя'
WHEN 0xB7335AEFD8708C3E462861FC59489A38 THEN N'Отпуск по беременности и родам'
ELSE N'Другое отсутствие'
END AS [Вид_отсутствия]
FROM dbo._InfoRg16921 state WITH (NOLOCK)
INNER JOIN dbo._Reference299 ref_emp WITH (NOLOCK)
ON state._Fld16922RRef = ref_emp._IDRRef
WHERE @TargetDate BETWEEN CAST(CASE WHEN YEAR(state._Fld16926) > 3000 THEN DATEADD(YEAR, -2000, state._Fld16926) ELSE state._Fld16926 END AS DATE)
AND CAST(CASE WHEN YEAR(state._Fld16927) > 3000 THEN DATEADD(YEAR, -2000, state._Fld16927) ELSE state._Fld16927 END AS DATE)
UNION ALL
-- 2. Динамический выбор ДЕЙСТВУЮЩИХ декретниц по уходу за ребенком
SELECT
active_state.fio AS [ФИО],
N'Отпуск по уходу за ребенком' AS [Вид_отсутствия]
FROM (
SELECT
LTRIM(RTRIM(ref_emp._Description)) AS fio,
all_states._Fld16925RRef AS state_guid,
CAST(CASE WHEN YEAR(all_states._Fld16926) > 3000 THEN DATEADD(YEAR, -2000, all_states._Fld16926) ELSE all_states._Fld16926 END AS DATE) AS date_start,
ROW_NUMBER() OVER (
PARTITION BY all_states._Fld16922RRef
ORDER BY all_states._Fld16926 DESC
) AS rn
FROM dbo._InfoRg16921 all_states WITH (NOLOCK)
INNER JOIN dbo._Reference299 ref_emp WITH (NOLOCK)
ON all_states._Fld16922RRef = ref_emp._IDRRef
WHERE CAST(CASE WHEN YEAR(all_states._Fld16926) > 3000 THEN DATEADD(YEAR, -2000, all_states._Fld16926) ELSE all_states._Fld16926 END AS DATE) <= @TargetDate
) active_state
WHERE active_state.rn = 1
AND active_state.state_guid = 0xA4FBA038663B3C2A48DA151C262855E1
AND active_state.date_start >= DATEADD(YEAR, -3, @TargetDate)
ORDER BY [ФИО] ASC;
"""
conn_str = get_zup_connection_string()
with pyodbc.connect(conn_str) as conn:
df = pd.read_sql(query, conn, params=[target_date])
if not df.empty:
df['fio_clean'] = df['ФИО'].apply(normalize_fio)
return df
def fetch_zup_staff_from_sql() -> pd.DataFrame:
"""Извлекает весь актуальный штат из MS SQL 1С:ЗУП."""
query = """
SELECT DISTINCT
LTRIM(RTRIM(ref_emp._Description)) AS [ФИО],
N'Организация' AS [Подразделение],
N'Сотрудник' AS [Должность]
FROM dbo._Reference299 ref_emp WITH (NOLOCK)
WHERE ref_emp._Description <> ''
AND ref_emp._Marked = 0x00
ORDER BY [ФИО] ASC;
"""
conn_str = get_zup_connection_string()
with pyodbc.connect(conn_str) as conn:
df = pd.read_sql(query, conn)
if not df.empty:
df['fio_clean'] = df['ФИО'].apply(normalize_fio)
return df
def sync_zup_to_excel(target_date: date) -> bool:
"""
Извлекает данные из MS SQL и автоматически создает локальные Excel-файлы 1С:
data/1c/Штат_DD_MM_YYYY.xlsx
data/1c/Отсутствия_DD_MM_YYYY.xlsx
"""
try:
date_str_file = target_date.strftime("%d_%m_%Y")
c_1c_dir = os.path.join(DATA_DIR, "1c")
os.makedirs(c_1c_dir, exist_ok=True)
df_staff = fetch_zup_staff_from_sql()
df_absences = fetch_zup_absences_from_sql(target_date)
logger.info(f"[1С Direct SQL] Выгрузка за {target_date.strftime('%d.%m.%Y')}: Штат ({len(df_staff)} чел.), Отсутствия ({len(df_absences)} чел.)")
# Сохранение в формате, который ожидает data_loader.py
staff_excel_path = os.path.join(c_1c_dir, f"Штат_{date_str_file}.xlsx")
absences_excel_path = os.path.join(c_1c_dir, f"Отсутствия_{date_str_file}.xlsx")
# Формируем структуру Excel с нужным количеством пропущенных строк (skiprows)
with pd.ExcelWriter(staff_excel_path, engine='openpyxl') as writer:
# Пустой заголовок (8 строк) для эмуляции отчета 1С
dummy_headers = pd.DataFrame([[""] * 13] * 8)
dummy_headers.to_excel(writer, index=False, header=False)
# Данные
df_staff[['ФИО', 'Подразделение', 'Должность']].to_excel(writer, startrow=8, index=False)
with pd.ExcelWriter(absences_excel_path, engine='openpyxl') as writer:
# Пустой заголовок (3 строки) для эмуляции отчета 1С
dummy_headers = pd.DataFrame([[""] * 3] * 3)
dummy_headers.to_excel(writer, index=False, header=False)
# Данные
df_absences[['ФИО', 'Вид_отсутствия']].to_excel(writer, startrow=3, index=False)
logger.info(f"[✓] Успешно созданы файлы 1С за {target_date.strftime('%d.%m.%Y')}: {staff_excel_path}, {absences_excel_path}")
return True
except Exception as e:
logger.error(f"[❌] Ошибка прямого импорта из MS SQL 1С: {e}")
return False