15 Commits

Author SHA1 Message Date
gitea b7c25c4ff1 fix: add missing 'import os' (used on line 28 for sys.path) 2026-07-04 19:05:55 +07:00
gitea 23960cbbb6 fix: remove duplicate exclusion block, replace continue with else branch
- Removed duplicate department exclusion check (was duplicated twice)
- Replaced 'continue' (invalid inside 'if ENABLE_AUTO_APPROVAL') with 'else:' branch
- Properly indented auto-approval logic inside the else block
2026-07-04 18:50:40 +07:00
Denis Krivchenko c3503975a0 merge: sync dispatcher with main — resolve conflicts 2026-07-04 18:02:30 +07:00
Denis Krivchenko 1949a461b2 feat: add department exclusion for auto-approval
- Add department attribute to LDAP search
- Add APPROVAL_EXCLUDED_DEPARTMENTS config
- Skip auto-approval if user department is excluded
2026-07-04 17:55:35 +07:00
ohmo 2d9c50d0b9 add: department exclusion check for auto-approval 2026-07-04 16:42:16 +07:00
ohmo fa5dae47ed revert: restore original sd_dispatcher.py in dispatcher 2026-07-04 16:42:16 +07:00
ohmo de1e06cb2c add: department exclusion check for auto-approval 2026-07-04 16:41:55 +07:00
ohmo 7caea05c7e add: department exclusion check for auto-approval 2026-07-04 16:40:13 +07:00
dddennnisss b3ff03bfe6 chore: remove test_ad.py from dispatcher branch 2026-07-04 16:15:02 +07:00
ohmo f8258173fb add: department exclusion check for auto-approval 2026-07-04 16:10:25 +07:00
ohmo 6add9d813e add: APPROVAL_EXCLUDED_DEPARTMENTS to config 2026-07-04 16:09:34 +07:00
ohmo 7bd7700314 refactor: use Passwork for AD credentials (like prod config) 2026-07-04 13:33:57 +07:00
dddennnisss ecc9443400 fix: remove root test_ad_lookup.py, moved to ad/ 2026-07-04 09:43:46 +07:00
ohmo 304444b079 fix: correct ad/test_ad_lookup.py content 2026-07-04 09:43:46 +07:00
ohmo e9861bb5ea add: AD lookup test script in ad/ 2026-07-04 09:42:03 +07:00
4 changed files with 376 additions and 191 deletions
+336
View File
@@ -0,0 +1,336 @@
#!/usr/bin/env python3
"""
Тестовый скрипт — поиск пользователя в Active Directory.
Аналог find_user_in_ad() из sd_dispatcher.py.
Использует Passwork для загрузки учётных данных AD (как в основном коде).
Если Passwork недоступен — просит ввести учётные данные вручную.
Единственная зависимость: ldap3
pip install ldap3
Запуск:
python3 test_ad_lookup.py --login ivanov
python3 test_ad_lookup.py --email ivanov@sibcem.ru
python3 test_ad_lookup.py --phone 79131234567
python3 test_ad_lookup.py --test-all
"""
import argparse
import os
import re
import sys
# =========================================================================
# 1. Загрузка учётных данных AD
# =========================================================================
def load_ad_credentials():
"""
Загружает AD_USER и AD_PASSWORD из Passwork (как в config.py).
fallback: ручной ввод.
"""
# --- Путь 1: через модуль Passwork ---
passwork_dir = "/opt/passwork"
card_name = os.getenv("PW_ID_AD", "AD").strip()
if passwork_dir not in sys.path:
sys.path.insert(0, passwork_dir)
try:
from passwork import get_passwork_secrets
print(f"[Passwork] Загрузка карточки '{card_name}'...")
pool = get_passwork_secrets(required_cards=[card_name])
card = pool.get(card_name)
if card:
ad_user = card.get("login", "").strip()
ad_pass = card.get("password", "").strip()
if ad_user and ad_pass:
print(f"[Passwork] ✅ AD_USER: {ad_user}")
return ad_user, ad_pass
except Exception:
pass
# --- Путь 2: env-переменные ---
ad_user = os.getenv("AD_USER", "").strip()
ad_pass = os.getenv("AD_PASS", "").strip()
if ad_user and ad_pass:
print("[Env] ✅ Учётные данные загружены из окружения")
return ad_user, ad_pass
# --- Путь 3: ручной ввод ---
print("\n❌ Passwork недоступен.")
print("Введите учётные данные AD вручную:\n")
ad_user = input(" AD user (DOMAIN\\account): ").strip()
ad_pass = input(" AD password: ").strip()
if not ad_user or not ad_pass:
print("❌ Отмена.")
sys.exit(1)
print(f"[Manual] ✅ AD_USER: {ad_user}")
return ad_user, ad_pass
# =========================================================================
# 2. Подключение к LDAP
# =========================================================================
def connect_to_ad(ad_user, ad_pass):
from ldap3 import Server, Connection
server = Server(
"ldap://172.16.20.20",
get_info=None,
connect_timeout=5,
)
conn = Connection(
server,
user=ad_user,
password=ad_pass,
auto_bind=True,
receive_timeout=5,
)
return conn
# =========================================================================
# 3. Поиск пользователя
# =========================================================================
def find_user(conn, login=None, email=None, phone=None):
"""
Приоритет: телефон > логин > email.
Возвращает dict атрибутов или None.
"""
if phone:
filt = (
f"(|(telephoneNumber=*{phone}*)(mobile=*{phone}*) "
f"(homePhone=*{phone}*)(pager=*{phone}*) "
f"(facsimileTelephoneNumber=*{phone}*)(ipPhone=*{phone}*))"
)
by = f"телефону {phone}"
elif login:
clean = login.split("\\")[-1].split("/")[-1]
filt = f"(sAMAccountName={clean})"
by = f"логину {clean}"
elif email:
filt = f"(mail={email})"
by = f"почте {email}"
else:
print("❌ Укажите --login, --email или --phone")
sys.exit(1)
print(f"\n[LDAP] Фильтр: {filt}")
print(f"[LDAP] Base: OU=-Пользователи,DC=sibcem,DC=ru")
conn.search(
search_base="OU=-Пользователи,DC=sibcem,DC=ru",
search_filter=filt,
attributes=[
"sAMAccountName", "displayName", "givenName", "sn",
"mail", "title", "department", "l", "company",
"manager", "telephoneNumber", "mobile",
],
)
if not conn.entries:
print(f"\n❌ Не найден по {by}")
return None
user = conn.entries[0]
print(f"\n✅ Найдено: {user.entry_dn}")
result = {
"sam": user.sAMAccountName.value if "sAMAccountName" in user else None,
"given_name": user.givenName.value if "givenName" in user else None,
"surname": user.sn.value if "sn" in user else None,
"display_name": user.displayName.value if "displayName" in user else None,
"mail": user.mail.value if "mail" in user else None,
"title": user.title.value if "title" in user else None,
"department": user.department.value if "department" in user else None,
"city": user.l.value if "l" in user and user.l.value else None,
"company": user.company.value if "company" in user and user.company.value else None,
"dn": user.entry_dn,
"phone": user.telephoneNumber.value if "telephoneNumber" in user else None,
"mobile": user.mobile.value if "mobile" in user else None,
}
# Город из OU если атрибут l пустой
if not result["city"]:
match = re.search(r"OU=([^,]+),OU=-Пользователи", result["dn"], re.IGNORECASE)
if match:
result["city"] = match.group(1).strip()
if result["city"].lower() == "красноярск" and "OU=ООО Комбинат Волна" in result["dn"]:
result["company"] = "ООО Комбинат Волна"
print(f" Город из OU: {result['city']}")
# Руководитель — второй запрос
manager_dn = user.manager.value if "manager" in user else None
if manager_dn:
conn.search(
search_base=manager_dn,
search_filter="(objectClass=user)",
attributes=["mail", "displayName"],
search_scope="BASE",
)
if conn.entries and "mail" in conn.entries[0]:
result["manager_email"] = conn.entries[0].mail.value
result["manager_name"] = (
conn.entries[0].displayName.value if "displayName" in conn.entries[0] else None
)
else:
result["manager_email"] = None
else:
result["manager_email"] = None
# Проверка department на исключение из автосогласования
excluded_depts = [
# "IT-Администраторы",
# "Системные аналитики",
# Добавьте нужные отделы из config.py: APPROVAL_EXCLUDED_DEPARTMENTS
]
if result["department"] and any(
dept.lower() in result["department"].lower() or
result["department"].lower() in dept.lower()
for dept in excluded_depts
):
result["approval_excluded"] = True
result["excluded_department"] = result["department"]
else:
result["approval_excluded"] = False
result["excluded_department"] = None
return result
# =========================================================================
# 4. Вывод
# =========================================================================
def print_report(data):
print("\n" + "=" * 60)
print(" РЕЗУЛЬТАТ ПОИСКА В ACTIVE DIRECTORY")
print("=" * 60)
print(f" sAMAccountName: {data['sam']}")
print(f" Имя: {data['given_name']}")
print(f" Фамилия: {data['surname']}")
print(f" ФИО: {data['display_name']}")
print(f" Email: {data['mail']}")
print(f" Должность: {data['title']}")
print(f" Отдел: {data['department'] or ''}")
print(f" Город: {data['city'] or ''}")
print(f" Компания: {data['company'] or ''}")
print(f" Телефон: {data['phone'] or ''}")
print(f" Мобильный: {data['mobile'] or ''}")
print(f" Руководитель: {data['manager_name'] or ''}")
if data.get("manager_email"):
print(f" Email руково.: {data['manager_email']}")
# Проверка на исключение из автосогласования
if data.get("approval_excluded"):
print("\n" + "!" * 60)
print(f" ⚠️ ОТДЕЛ: {data['excluded_department']}")
print(f" ⚠️ АВТОСОГЛАСОВАНИЕ ОТМЕНЕНО")
print(f" Пользователь находится в исключённом отделе.")
print("!" * 60)
print("-" * 60)
print(f" DN: {data['dn']}")
# OU-дерево
ou_parts = [p.strip() for p in data["dn"].split(",") if p.strip().startswith("OU=")]
if ou_parts:
print(f"\n OU-структура:")
for i, ou in enumerate(reversed(ou_parts)):
print(f" {' ' * i}└── {ou}")
print("=" * 60)
# =========================================================================
# 5. Тест всех методов
# =========================================================================
def test_all_methods(conn):
print("\n" + "" * 60)
print(" ТЕСТИРОВАНИЕ: все методы поиска")
print("" * 60)
conn.search(
search_base="OU=-Пользователи,DC=sibcem,DC=ru",
search_filter="(objectClass=user)",
attributes=["sAMAccountName", "mail", "telephoneNumber"],
size_limit=1,
)
if not conn.entries:
print("❌ Нет пользователей в AD.")
return
sample = conn.entries[0]
sam = sample.sAMAccountName.value
mail = sample.mail.value if "mail" in sample else None
phone = sample.telephoneNumber.value if "telephoneNumber" in sample else None
print(f"\n📌 Тестовый: {sam} ({mail})")
for label, kwargs in [("логину", {"login": sam}),
("email", {"email": mail}) if mail else None,
("телефону", {"phone": phone}) if phone else None]:
if kwargs is None:
continue
print(f"\n[Тест] По {label}...")
r = find_user(conn, **kwargs)
if r:
print_report(r)
print("\n" + "" * 60)
print(" ✅ Готово")
print("" * 60)
# =========================================================================
# MAIN
# =========================================================================
if __name__ == "__main__":
parser = argparse.ArgumentParser(
description="Поиск пользователя в Active Directory",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Примеры:
python3 test_ad_lookup.py --login ivanov
python3 test_ad_lookup.py --email ivanov@sibcem.ru
python3 test_ad_lookup.py --phone 79131234567
python3 test_ad_lookup.py --test-all
Учётные данные AD загружаются из Passwork (как в основном коде).
Если Passwork недоступен — запрашиваются вручную.
Проверка department: если department пользователя совпадает с
APPROVAL_EXCLUDED_DEPARTMENTS из config.py — автосогласование отменяется.
""",
)
group = parser.add_mutually_exclusive_group(required=True)
group.add_argument("--login", help="sAMAccountName пользователя")
group.add_argument("--email", help="Email пользователя")
group.add_argument("--phone", help="Номер телефона")
parser.add_argument("--test-all", action="store_true",
help="Найти первого пользователя и проверить все методы")
args = parser.parse_args()
# Загрузка учётных данных (Passwork → env → ручной ввод)
ad_user, ad_pass = load_ad_credentials()
conn = connect_to_ad(ad_user, ad_pass)
print(f"[LDAP] Подключено к ldap://172.16.20.20 как {ad_user}")
if args.test_all:
test_all_methods(conn)
else:
data = find_user(conn, login=args.login, email=args.email, phone=args.phone)
if data:
print_report(data)
conn.unbind()
+9 -1
View File
@@ -115,7 +115,6 @@ ENABLE_FUZZY_CORRECTION = True # Нечёткое исправление по
SERVER_URL = "https://servicedesk.sibcem.ru:8080" SERVER_URL = "https://servicedesk.sibcem.ru:8080"
ENDPOINT = f"{SERVER_URL}/api/v3/requests" ENDPOINT = f"{SERVER_URL}/api/v3/requests"
# Системный email-адрес учетной записи ИИ-сортировщика в Service Desk # Системный email-адрес учетной записи ИИ-сортировщика в Service Desk
DEFAULT_REQUESTER = "ai@sibcem.ru" DEFAULT_REQUESTER = "ai@sibcem.ru"
@@ -288,6 +287,15 @@ APPROVAL_CATEGORIES = {
"1с": ["*"] "1с": ["*"]
} }
# Отделы, исключающие пользователя из автосогласования
# Если department пользователя совпадает с любым из этих значений —
# автосогласование НЕ запускается, даже если все остальные условия выполнены.
APPROVAL_EXCLUDED_DEPARTMENTS = [
# "IT-Администраторы",
# "Системные аналитики",
# Добавьте нужные отделы:
]
# План «Б» маршрутизации: жесткое определение локации по телефонному префиксу внутренней АТС # План «Б» маршрутизации: жесткое определение локации по телефонному префиксу внутренней АТС
PREFIX_ROUTING = { PREFIX_ROUTING = {
"700": {"city": "Красноярск", "company": "ООО Комбинат Волна"}, "700": {"city": "Красноярск", "company": "ООО Комбинат Волна"},
-168
View File
@@ -1,168 +0,0 @@
#!/usr/bin/env python3
"""Получение списка заявок из ServiceDesk Plus. Экспорт: Группа, Описание, Назначено, Категория в JSONL/JSON/CSV."""
import requests
import json
import csv
import urllib3
from datetime import datetime
from html import unescape
import re
import sys
import os
# Отключаем ворнинги для чистой работы консоли
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
# --- ПОЛУЧЕНИЕ API_KEY ИЗ PASSWORK ---
SERVER_URL = "https://servicedesk.sibcem.ru:8080"
API_KEY = ""
# 1. Пробуем passwork напрямую
try:
sys.path.insert(0, "/opt/passwork")
from passwork import get_passwork_secrets
passwork_pool = get_passwork_secrets(required_cards=["sdesktoken"])
card = passwork_pool.get("sdesktoken")
if card:
API_KEY = card.get("password", "").strip()
print("LOG: API_KEY получен из passwork (sdesktoken)")
else:
print("LOG: Карточка sdesktoken не найдена в passwork", file=sys.stderr)
except Exception as e:
print(f"LOG: Passwork ошибка: {e}", file=sys.stderr)
# 2. Пробуем config.py
if not API_KEY:
try:
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import config as cfg
API_KEY = getattr(cfg, 'API_KEY', "")
if API_KEY:
print("LOG: API_KEY получен из config.py")
except Exception:
pass
# 3. Пробуем env var
if not API_KEY:
API_KEY = os.environ.get("SD_API_KEY", "")
if API_KEY:
print("LOG: API_KEY получен из SD_API_KEY")
if not API_KEY:
print("LOG: ОШИБКА: API_KEY не найден нигде! Нужен passwork или SD_API_KEY", file=sys.stderr)
sys.exit(1)
print(f"LOG: Подключение к {SERVER_URL}")
def get_ts(req):
ct = req.get("created_time", {})
if isinstance(ct, dict):
v = ct.get("value", "0")
return int(v) if v else 0
return 0
def clean_html(text):
if not text:
return ""
text = re.sub(r'<[^>]+>', '', text)
text = unescape(text)
return text.strip()
def get_name(obj):
if not obj:
return ""
if isinstance(obj, dict):
return obj.get("name", "") or obj.get("display_value", "")
return str(obj)
def get_ticket_detail(ticket_id):
try:
url = f"{SERVER_URL}/api/v3/requests/{ticket_id}"
headers = {"authtoken": API_KEY, "Accept": "application/vnd.manageengine.sdp.v3+json"}
resp = requests.get(url, headers=headers, verify=False, timeout=15.0)
resp.raise_for_status()
data = resp.json()
return data.get("request", data)
except Exception as e:
return {}
def list_tickets(from_date="2026-07-20", json_file=None, csv_file=None):
url = f"{SERVER_URL}/api/v3/requests"
headers = {"authtoken": API_KEY, "Accept": "application/vnd.manageengine.sdp.v3+json"}
date_obj = datetime.strptime(from_date, "%Y-%m-%d")
from_date_ms = int(date_obj.replace(tzinfo=__import__('datetime').timezone.utc).timestamp() * 1000)
print(f"LOG: Загрузка заявок с {from_date}...")
resp = requests.get(url, headers=headers, verify=False, timeout=30.0)
resp.raise_for_status()
data = resp.json()
all_reqs = data.get("requests", [])
from_date_reqs = [r for r in all_reqs if get_ts(r) >= from_date_ms]
print(f"LOG: Найдено заявок с {from_date}: {len(from_date_reqs)}")
rows = []
for i, req in enumerate(from_date_reqs):
if (i + 1) % 10 == 0:
print(f" Обработано {i + 1}/{len(from_date_reqs)}...")
req_id = req.get("request_id", req.get("id", "?"))
detail = get_ticket_detail(req_id)
group = req.get("group", {})
group_name = get_name(group)
desc = detail.get("description", "")
desc_clean = clean_html(desc).replace('\n', ' ').replace('\r', ' ')
tech = detail.get("technician", {})
assignee = get_name(tech)
cat = detail.get("category", {})
category = get_name(cat)
rows.append({
"group": group_name,
"description": desc_clean,
"assignee": assignee,
"category": category
})
# Определяем путь для файлов по умолчанию (та же директория что и скрипт)
script_dir = os.path.dirname(os.path.abspath(__file__))
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
if json_file is None:
json_file = os.path.join(script_dir, f"tickets_{timestamp}.json")
if csv_file is None:
csv_file = os.path.join(script_dir, f"tickets_{timestamp}.csv")
# Сохраняем JSON
with open(json_file, 'w', encoding='utf-8') as f:
json.dump(rows, f, ensure_ascii=False, indent=2)
print(f"LOG: JSON сохранён: {json_file}")
# Сохраняем CSV (utf-8-sig для корректного отображения кириллицы в Excel)
with open(csv_file, 'w', encoding='utf-8-sig', newline='') as f:
writer = csv.DictWriter(f, fieldnames=["group", "description", "assignee", "category"], quoting=csv.QUOTE_ALL)
writer.writeheader()
writer.writerows(rows)
print(f"LOG: CSV сохранён: {csv_file}")
# Выводим JSONL в консоль
for row in rows:
print(json.dumps(row, ensure_ascii=False))
print(f"LOG: Выведено {len(rows)} заявок", file=sys.stderr)
if __name__ == "__main__":
import argparse
parser = argparse.ArgumentParser(description="Выгрузка заявок из ServiceDesk Plus")
parser.add_argument("--from", dest="from_date", default="2026-07-20", help="Дата начала (YYYY-MM-DD)")
parser.add_argument("--json", dest="json_file", default=None, help="Путь к JSON файлу")
parser.add_argument("--csv", dest="csv_file", default=None, help="Путь к CSV файлу")
args = parser.parse_args()
list_tickets(from_date=args.from_date, json_file=args.json_file, csv_file=args.csv_file)
+31 -22
View File
@@ -2,6 +2,7 @@
import requests import requests
import json import json
import urllib3 import urllib3
import os
import math import math
import warnings import warnings
import datetime import datetime
@@ -170,7 +171,7 @@ def find_user_in_ad(requester, caller_phone=None, ticket_id=None):
try: try:
server = Server(AD_SERVER, get_info=ALL, connect_timeout=AD_CONNECT_TIMEOUT) server = Server(AD_SERVER, get_info=ALL, connect_timeout=AD_CONNECT_TIMEOUT)
conn = Connection(server, user=AD_USER, password=AD_PASSWORD, auto_bind=True, receive_timeout=AD_RECEIVE_TIMEOUT) conn = Connection(server, user=AD_USER, password=AD_PASSWORD, auto_bind=True, receive_timeout=AD_RECEIVE_TIMEOUT)
conn.search(search_base=AD_BASE, search_filter=ldap_filter, attributes=["l", "company", "manager"]) conn.search(search_base=AD_BASE, search_filter=ldap_filter, attributes=["l", "company", "manager", "department"])
if not conn.entries: if not conn.entries:
log_to_file(f" [AD] ❌ Объект ({search_by}) не найден in AD!", ticket_id) log_to_file(f" [AD] ❌ Объект ({search_by}) не найден in AD!", ticket_id)
@@ -195,7 +196,8 @@ def find_user_in_ad(requester, caller_phone=None, ticket_id=None):
if conn.entries and 'mail' in conn.entries[0]: if conn.entries and 'mail' in conn.entries[0]:
manager_email = conn.entries[0].mail.value manager_email = conn.entries[0].mail.value
return {"city": city, "company": company, "ad_path": ad_path, "manager_email": manager_email, "source": f"AD ({search_by})"} department = user.department.value if "department" in user and user.department.value else None
return {"city": city, "company": company, "department": department, "ad_path": ad_path, "manager_email": manager_email, "source": f"AD ({search_by})"}
except Exception as e: except Exception as e:
log_to_file(f"❌ Ошибка LDAP или таймаут сети AD: {e}", ticket_id) log_to_file(f"❌ Ошибка LDAP или таймаут сети AD: {e}", ticket_id)
return None return None
@@ -554,9 +556,7 @@ def classify_ticket(text, ticket_id=None):
if not result: if not result:
return "Не определено", 0, [], True, [], {"pure_qdrant_choice": "Не определено", "q_variance": 0, "q_unique_groups": 0, "q_max_score": 0, "q_dominance": 0} return "Не определено", 0, [], True, [], {"pure_qdrant_choice": "Не определено", "q_variance": 0, "q_unique_groups": 0, "q_max_score": 0, "q_dominance": 0}
# =================================================================
# ЭТАП 1: РАСЧЕТ ВЫБОРА ЧИСТОГО QDRANT (ДО СЛУЖБЫ РЕРАНКИНГА) # ЭТАП 1: РАСЧЕТ ВЫБОРА ЧИСТОГО QDRANT (ДО СЛУЖБЫ РЕРАНКИНГА)
# =================================================================
pure_scores = defaultdict(float) pure_scores = defaultdict(float)
pure_counts = defaultdict(int) pure_counts = defaultdict(int)
for p in result: for p in result:
@@ -572,9 +572,7 @@ def classify_ticket(text, ticket_id=None):
qdrant_scores = [p.score for p in result] qdrant_scores = [p.score for p in result]
# =================================================================
# ЭТАП 2: СЛУЖБА ПЕРЕРАНЖИРОВАНИЯ CROSS-ENCODER + БЛЕНДИНГ СКОРОВ # ЭТАП 2: СЛУЖБА ПЕРЕРАНЖИРОВАНИЯ CROSS-ENCODER + БЛЕНДИНГ СКОРОВ
# =================================================================
documents = [p.payload.get("text", "Текст отсутствует").strip()[:RERANKER_MAX_DOC_CHARS] for p in result] documents = [p.payload.get("text", "Текст отсутствует").strip()[:RERANKER_MAX_DOC_CHARS] for p in result]
rerank_payload = { rerank_payload = {
@@ -764,6 +762,7 @@ def process_ticket_logic(req_id):
ad_path = user_info["ad_path"] if user_info else None ad_path = user_info["ad_path"] if user_info else None
manager_email = user_info["manager_email"] if user_info else None manager_email = user_info["manager_email"] if user_info else None
info_source = user_info["source"] if user_info else "Не определен" info_source = user_info["source"] if user_info else "Не определен"
user_department = user_info["department"] if user_info else None
prefix_used = False prefix_used = False
if caller_phone and not user_info: if caller_phone and not user_info:
@@ -891,22 +890,32 @@ def process_ticket_logic(req_id):
allowed_subcats = APPROVAL_CATEGORIES[cat_name] allowed_subcats = APPROVAL_CATEGORIES[cat_name]
if "*" in allowed_subcats or subcat_name in allowed_subcats: if "*" in allowed_subcats or subcat_name in allowed_subcats:
is_cat_ok = True is_cat_ok = True
# Проверка: отдел пользователя в списке исключений
if is_city_ok and (is_group_ok or is_cat_ok): is_excluded = False
if manager_email: if user_department and APPROVAL_EXCLUDED_DEPARTMENTS:
t_log(f"✔ Условия совпали. Запуск процесса согласования для {manager_email}...") for dept in APPROVAL_EXCLUDED_DEPARTMENTS:
ai_was_used = True if dept.lower() in user_department.lower() or user_department.lower() in dept.lower():
is_excluded = True
share_success, share_log = share_ticket_for_approval(req_id, manager_email) t_log(f" ⚠️ Отдел '{user_department}' в списке исключений — автосогласование отменено")
t_log(share_log) break
if is_excluded:
cc_success, cc_log = add_email_to_notify(req_id, manager_email) t_log(" ⏭ Пропуск: пользователь в исключённом отделе.")
t_log(cc_log) else:
if is_city_ok and (is_group_ok or is_cat_ok):
success, step_detail = trigger_manager_approval(req_id, manager_email) if manager_email:
t_log(step_detail) t_log(f"✔ Условия совпали. Запуск процесса согласования для {manager_email}...")
else: ai_was_used = True
t_log("⚠️ Согласование невозможно: у пользователя не указан руководитель в AD.")
share_success, share_log = share_ticket_for_approval(req_id, manager_email)
t_log(share_log)
cc_success, cc_log = add_email_to_notify(req_id, manager_email)
t_log(cc_log)
success, step_detail = trigger_manager_approval(req_id, manager_email)
t_log(step_detail)
else:
t_log("⚠️ Согласование невозможно: у пользователя не указан руководитель в AD.")
else: else:
if ai_was_used: if ai_was_used:
t_log("\n=== ШАГ 5. АВТОСОГЛАСОВАНИЕ ===") t_log("\n=== ШАГ 5. АВТОСОГЛАСОВАНИЕ ===")