Initial commit: TrueConf Chatbot КЛЕВЕР
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
# /opt/trueconf_bot/lk/documents_service.py
|
||||
import smtplib
|
||||
import logging
|
||||
from email.mime.text import MIMEText
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.base import MIMEBase
|
||||
from email import encoders
|
||||
from datetime import datetime
|
||||
|
||||
import config.config as config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def get_user_info_for_docs(cursor, card_id: str) -> dict:
|
||||
"""Извлекает кадровые данные сотрудника по CARD_ID для оформления бланков справок"""
|
||||
current_date = datetime.now().strftime('%Y-%m-%d')
|
||||
|
||||
query = """
|
||||
select e.TAB_N, a.NAME, d.NAME, hr.FULL_NAME, hr.EMAIL, f.SNAME, pr.EMAIL as workBookEmail
|
||||
from (select e.TAB_N, e.FIRM_ID, c.F_NAME, c.L_NAME, e.ID, c.M_NAME
|
||||
from UOV_SELFSERVICE_PR_EMP e
|
||||
join UOV_SELFSERVICE_PR_CARD c on c.ID=e.CARD_ID
|
||||
where c.ID = %s
|
||||
group by e.ID, e.FIRM_ID, c.F_NAME, c.L_NAME, c.M_NAME, e.TAB_N ) as e
|
||||
join UOV_SELFSERVICE_PR_TRANS t on t.EMP_ID = e.ID
|
||||
join UOV_SELFSERVICE_ST_APPOINT a on a.ID = t.APPOINT_ID
|
||||
join UOV_SELFSERVICE_HR_DEPT d on d.ID = t.DEPT_ID
|
||||
join UOV_SELFSERVICE_HR_S_USER_SL hr on hr.FIRM_ID = e.FIRM_ID
|
||||
join UOV_SELFSERVICE_HR_FIRM f on e.FIRM_ID=f.ID
|
||||
left join UOV_SELFSERVICE_HR_S_USER_PR pr on (pr.DEPT_ISOL_ID = d.DEPT_ISOL_ID or pr.FIRM_ID = f.ID)
|
||||
where ((t.D_FROM <= %s and t.D_TO >= %s)
|
||||
or (t.D_FROM <= %s and t.D_TO = '1999-12-31'))
|
||||
and e.FIRM_ID = f.ID
|
||||
"""
|
||||
cursor.execute(query, (card_id, current_date, current_date, current_date))
|
||||
row = cursor.fetchone()
|
||||
|
||||
if not row:
|
||||
raise Exception("Не удалось найти кадровые данные сотрудника для заказа справки.")
|
||||
|
||||
return {
|
||||
'tab_n': str(row[0]) if row[0] else "Не указан",
|
||||
'job': str(row[1]) if row[1] else "",
|
||||
'dept': str(row[2]) if row[2] else "",
|
||||
'hr_fio': str(row[3]) if row[3] else "",
|
||||
'hr_email': str(row[4]) if row[4] else "",
|
||||
'firm': str(row[5]) if row[5] else "",
|
||||
'wb_email': str(row[6]) if row[6] else ""
|
||||
}
|
||||
|
||||
def generate_mail_message(doc_name: str, quantity: int, user_info: dict, period: str = "", attachment_path: str = "") -> bool:
|
||||
"""Формирует и отправляет HTML-письмо заявки на справку в отдел кадров СибЦема"""
|
||||
|
||||
# Извлекаем оригинальные адреса кадровиков из структуры БД
|
||||
orig_to = user_info.get('hr_email', '').strip()
|
||||
# Копия уходит на ящик трудовых книжек только если заказывают пункт 7
|
||||
orig_cc = user_info.get('wb_email', '').strip() if doc_name == "Копия трудовой книжки" else ""
|
||||
|
||||
to_email = orig_to
|
||||
cc_list = [orig_cc] if orig_cc else []
|
||||
|
||||
# Если включен DOCUMENTS_TEST_MODE, жестко перехватываем поток писем
|
||||
if getattr(config, 'DOCUMENTS_TEST_MODE', False):
|
||||
test_email = getattr(config, 'DOCUMENTS_TARGET_EMAIL', 'mailonline@sibcem.ru')
|
||||
to_email = test_email
|
||||
cc_list = []
|
||||
|
||||
subject = f"Запрос документа {doc_name} из Личного кабинета"
|
||||
period_str = f" {period}" if period else ""
|
||||
|
||||
messageBody = (
|
||||
f"Необходимо оформить документ {doc_name} в количестве {quantity} шт. "
|
||||
f"на сотрудника предприятия {user_info['firm']} с табельным номером - "
|
||||
f"\"{user_info['tab_n']}\"{period_str}.<br/><br/>"
|
||||
f"Данное письмо отправлено автоматически, отвечать на него не стоит."
|
||||
)
|
||||
|
||||
message = MIMEMultipart()
|
||||
message['From'] = getattr(config, 'DEFAULT_EMAIL_FROM', 'mailonline@sibcem.ru')
|
||||
message['To'] = to_email
|
||||
message['Subject'] = subject
|
||||
|
||||
if cc_list:
|
||||
message['Cc'] = ", ".join(cc_list)
|
||||
|
||||
message.attach(MIMEText(messageBody, 'html', 'utf-8'))
|
||||
|
||||
if attachment_path:
|
||||
try:
|
||||
with open(attachment_path, 'rb') as attachment:
|
||||
part = MIMEBase("application", "octet-stream")
|
||||
part.set_payload(attachment.read())
|
||||
encoders.encode_base64(part)
|
||||
part.add_header("Content-Disposition", f"attachment; filename={attachment_path}")
|
||||
message.attach(part)
|
||||
except Exception as e:
|
||||
logger.error(f"Не удалось прикрепить файл к письму: {e}")
|
||||
|
||||
recipients = [to_email] + cc_list
|
||||
smtp_server = getattr(config, 'SMTP_SERVER', 'srvkem-mail.sibcem.ru')
|
||||
smtp_port = int(getattr(config, 'SMTP_PORT', 25))
|
||||
|
||||
try:
|
||||
server = smtplib.SMTP(smtp_server, smtp_port)
|
||||
server.starttls()
|
||||
# Если в конфигурации Passwork лежат логин/пароль для SMTP — авторизуемся
|
||||
if getattr(config, 'SMTP_USER', None):
|
||||
server.login(config.SMTP_USER, config.SMTP_PASSWORD)
|
||||
|
||||
server.sendmail(message['From'], recipients, message.as_string())
|
||||
server.quit()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка SMTP при отправке письма: {e}")
|
||||
return False
|
||||
+682
@@ -0,0 +1,682 @@
|
||||
from utils.texts import *
|
||||
# /opt/trueconf_bot/lk/handlers.py
|
||||
import re
|
||||
import os
|
||||
import csv
|
||||
import logging
|
||||
import asyncio
|
||||
import time
|
||||
import smtplib
|
||||
from datetime import datetime, timedelta
|
||||
from email.mime.multipart import MIMEMultipart
|
||||
from email.mime.text import MIMEText
|
||||
|
||||
from ldap3 import Server, Connection, ALL
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
from trueconf import Router, Message
|
||||
from trueconf.types import FSInputFile
|
||||
|
||||
import config.config as config
|
||||
import lk.payslip_service as ps
|
||||
import lk.documents_service as ds
|
||||
from utils.states import get_state, set_state, clear_state
|
||||
|
||||
# 🔌 Импортируем централизованную функцию сбора статистики из main
|
||||
from utils.stats_logger import log_menu_stats
|
||||
|
||||
# 🔌 Импортируем функции безопасности из изолированного утилитного модуля 2FA
|
||||
from utils.otp_service import generate_otp_code, send_otp_via_api, get_user_phone_from_ad
|
||||
|
||||
# 🔌 Импортируем вынесенную бизнес-логику запросов по CARD_ID
|
||||
from lk.info_service import (
|
||||
get_vacation_records,
|
||||
get_rewards_text,
|
||||
get_sick_leaves_text,
|
||||
get_tabel_report_text,
|
||||
get_lk_profile_by_card_id,
|
||||
get_missions_records,
|
||||
get_transfers_records
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# =========================================================
|
||||
# ГЛОБАЛЬНЫЙ МАРШРУТИЗАТОР ЦЕЛЬНЫХ VK-ЭМОДЗИ И СЕССИЙ ПАГИНАЦИИ
|
||||
# =========================================================
|
||||
EMOJI_DIGITS = {
|
||||
"0": "0⃣",
|
||||
"1": "1⃣",
|
||||
"2": "2⃣",
|
||||
"3": "3⃣",
|
||||
"4": "4⃣",
|
||||
"5": "5⃣",
|
||||
"6": "6⃣",
|
||||
"7": "7⃣",
|
||||
"8": "8⃣",
|
||||
"9": "9⃣",
|
||||
"10": "🔟"
|
||||
}
|
||||
|
||||
# Список из 7 справок строго по структуру портала
|
||||
DOC_TYPES = {
|
||||
"1": "Справка о доходах физического лица (2-НДФЛ)",
|
||||
"2": "Справка об удержаниях за ДМС",
|
||||
"3": "Справка о средней заработной плате",
|
||||
"4": "Справка о получении единовременного пособия по рождению ребенка",
|
||||
"5": "Справка о получении ежемесячного пособия и назначении отпуска по уходу за ребенком",
|
||||
"6": "Справка об удержаниях по исполнительным листам",
|
||||
"7": "Копия трудовой книжки"
|
||||
}
|
||||
|
||||
LK_DOCS_MENU_TEXT = (
|
||||
"📄 <b>Выберите тип документа:</b>\n\n" +
|
||||
"\n".join([f"{EMOJI_DIGITS[k]} — {v}" for k, v in DOC_TYPES.items()]) +
|
||||
f"\n\n<i>{EMOJI_DIGITS['9']} — Назад\n{EMOJI_DIGITS['0']} — В главное меню</i>"
|
||||
)
|
||||
|
||||
USER_COOLDOWN_SEC = 1.5
|
||||
EMAIL_COOLDOWN_SEC = 10.0
|
||||
|
||||
user_last_request = {}
|
||||
last_email_time = 0.0
|
||||
user_docs_session = {}
|
||||
user_page_sessions = {}
|
||||
|
||||
# 🔒 Репозиторий временных блокировок 2FA (хранит user_id: timestamp_разблокировки)
|
||||
user_payslip_blocks = {}
|
||||
|
||||
router = Router()
|
||||
|
||||
# =========================================================
|
||||
# СИСТЕМНЫЕ ФУНКЦИИ И ОШИБКИ
|
||||
# =========================================================
|
||||
def log_and_notify_error(user_id: str, action: str, error_details: str):
|
||||
global last_email_time
|
||||
log_dir = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "lk", "logs")
|
||||
os.makedirs(log_dir, exist_ok=True)
|
||||
log_file = os.path.join(log_dir, "lk_errors.log")
|
||||
current_time = time.time()
|
||||
timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
log_entry = f"[{timestamp}] User: {user_id} | Action: {action} | Error: {error_details}\n"
|
||||
|
||||
with open(log_file, "a", encoding="utf-8") as f:
|
||||
f.write(log_entry)
|
||||
|
||||
if current_time - last_email_time < EMAIL_COOLDOWN_SEC:
|
||||
return
|
||||
last_email_time = current_time
|
||||
|
||||
try:
|
||||
msg = MIMEMultipart()
|
||||
msg['From'] = str(getattr(config, 'DEFAULT_EMAIL_FROM', 'bot@noreply.com'))
|
||||
to_emails = getattr(config, 'ALERTS_SUPPORT_EMAILS', 'admin@example.com')
|
||||
msg['To'] = ", ".join(to_emails) if isinstance(to_emails, list) else str(to_emails)
|
||||
msg['Subject'] = f"TrueConf Bot Error: ЛК - {action}"
|
||||
body = f"Обнаружена ошибка в ЛК.\nВремя: {timestamp}\nПользователь: {user_id}\nДействие: {action}\nДетали:\n{error_details}"
|
||||
msg.attach(MIMEText(body, 'plain', 'utf-8'))
|
||||
import lk.info_service as info_service
|
||||
with smtplib.SMTP(getattr(config, 'SMTP_SERVER', 'localhost'), int(getattr(config, 'SMTP_PORT', 25))) as server:
|
||||
server.send_message(msg)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def handle_lk_error(msg: Message, user_id: str, action: str, error_details: str):
|
||||
await asyncio.to_thread(log_and_notify_error, user_id, action, error_details)
|
||||
error_msg = lk_error_handling(EMOJI_DIGITS)
|
||||
await msg.answer(error_msg, parse_mode="html")
|
||||
|
||||
# =========================================================
|
||||
# ИНТЕГРАЦИЯ С ACTIVE DIRECTORY ЧЕРЕЗ extensionAttribute2
|
||||
# =========================================================
|
||||
def get_card_id_from_ad(user_id: str) -> str:
|
||||
try:
|
||||
server = Server(config.AD_SERVER, get_info=ALL)
|
||||
conn = Connection(server, user=config.AD_USER, password=config.AD_PASSWORD, auto_bind=True)
|
||||
safe_user_id = escape_filter_chars(user_id)
|
||||
short_username = user_id.split('@')[0]
|
||||
search_filter = f"(&(objectClass=user)(|(mail={safe_user_id})(userPrincipalName={safe_user_id})(userPrincipalName={escape_filter_chars(f'{short_username}@sibcem.ru')})(sAMAccountName={escape_filter_chars(short_username)})))"
|
||||
|
||||
conn.search(config.AD_BASE, search_filter, attributes=['extensionAttribute2'])
|
||||
|
||||
if conn.entries and 'extensionAttribute2' in conn.entries[0] and conn.entries[0].extensionAttribute2.value:
|
||||
return str(conn.entries[0].extensionAttribute2.value).strip()
|
||||
raise Exception("Поле extensionAttribute2 не заполнено в AD.")
|
||||
except Exception as e: raise Exception(f"AD Card Query Error: {e}")
|
||||
|
||||
# =========================================================
|
||||
# МОДНОЕ УНИВЕРСАЛЬНОЕ ЯДРО ПАГИНАЦИИ СПИСКОВ ДЛЯ ЧАТА
|
||||
# =========================================================
|
||||
async def send_paginated_page(msg: Message, user_id: str):
|
||||
session = user_page_sessions.get(user_id)
|
||||
if not session: return
|
||||
|
||||
current_page = session["current_page"]
|
||||
per_page = session["per_page"]
|
||||
items = session["items"]
|
||||
|
||||
total_items = len(items)
|
||||
total_pages = (total_items + per_page - 1) // per_page if total_items > 0 else 1
|
||||
|
||||
start_idx = current_page * per_page
|
||||
end_idx = start_idx + per_page
|
||||
page_items = items[start_idx:end_idx]
|
||||
|
||||
if session["type"] == "vacation":
|
||||
body_text = session["static_header"] + "\n📜 <b>История отпусков (полный архив):</b>\n"
|
||||
body_text += "\n".join(page_items) + "\n" if page_items else "• <i>История отпусков пуста</i>\n"
|
||||
elif session["type"] == "missions":
|
||||
body_text = "🧳 <b>Информация о командировках (архив):</b>\n\n"
|
||||
body_text += "\n\n".join(page_items) + "\n" if page_items else "• <i>Записи о командировках отсутствуют</i>\n"
|
||||
elif session["type"] == "transfers":
|
||||
body_text = "🔄 <b>История кадровых перемещений:</b>\n\n"
|
||||
body_text += "\n\n".join(page_items) + "\n" if page_items else "• <i>Записи о перемещениях отсутствуют</i>\n"
|
||||
|
||||
body_text += f"\n📄 <b>Страница {current_page + 1} из {total_pages}</b>\n\n"
|
||||
|
||||
nav_lines = []
|
||||
if current_page > 0: nav_lines.append(f"{EMOJI_DIGITS['7']} — Предыдущая страница")
|
||||
if current_page < total_pages - 1: nav_lines.append(f"{EMOJI_DIGITS['8']} — Следующая страница")
|
||||
nav_lines.append(f"{EMOJI_DIGITS['9']} — Назад")
|
||||
nav_lines.append(f"{EMOJI_DIGITS['0']} — В главное меню")
|
||||
|
||||
body_text += "\n".join(nav_lines)
|
||||
await msg.answer(body_text, parse_mode="html")
|
||||
|
||||
# =========================================================
|
||||
# ДИНАМИЧЕСКАЯ СБОРКА ГЛАВНОГО МЕНЮ ЛК
|
||||
# =========================================================
|
||||
async def send_dynamic_lk_main_menu(msg: Message, user_id: str):
|
||||
try:
|
||||
card_id = await asyncio.to_thread(get_card_id_from_ad, user_id)
|
||||
profile = await asyncio.to_thread(get_lk_profile_by_card_id, card_id)
|
||||
|
||||
header = (
|
||||
f"👤 <b>Личный кабинет</b>\n\n"
|
||||
f"🏢 <b>Компания:</b> {profile['company']}\n"
|
||||
f"📂 <b>Отдел:</b> {profile['dept']}\n"
|
||||
f"💼 <b>Должность:</b> {profile['job']}\n"
|
||||
f"⏳ <b>Страховой стаж:</b> {profile['stag_o']}\n"
|
||||
f"📈 <b>Стаж на предприятии:</b> {profile['stag_p']}\n\n"
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Не удалось собрать динамический заголовок ЛК: {e}")
|
||||
header = "👤 <b>Личный кабинет</b>\n\n"
|
||||
|
||||
menu_text = lk_main_menu_text(header, EMOJI_DIGITS)
|
||||
await msg.answer(menu_text, parse_mode="html")
|
||||
|
||||
async def send_tabel_months_menu(msg: Message, user_id: str):
|
||||
now = datetime(2026, 6, 3)
|
||||
prev_1 = now.replace(day=1) - timedelta(days=1)
|
||||
prev_2 = prev_1.replace(day=1) - timedelta(days=1)
|
||||
months_ru = {1: "Январь", 2: "Февраль", 3: "Март", 4: "Апрель", 5: "Май", 6: "Июнь", 7: "Июль", 8: "Август", 9: "Сентябрь", 10: "Октябрь", 11: "Ноябрь", 12: "Декабрь"}
|
||||
|
||||
menu_text = lk_tabel_months_menu_text(prev_1, prev_2, months_ru, EMOJI_DIGITS)
|
||||
set_state(user_id, "LK_TABEL_MONTHS")
|
||||
await msg.answer(menu_text, parse_mode="html")
|
||||
|
||||
def wrapper_get_months(card_id: str) -> list:
|
||||
import lk.info_service as info_service
|
||||
conn = info_service.get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
try: return ps.get_latest_payslip_months(cursor, card_id)
|
||||
finally: conn.close()
|
||||
|
||||
def wrapper_get_payslip(card_id: str, target_cmonth: int):
|
||||
import lk.info_service as info_service
|
||||
conn = info_service.get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
try: return ps.build_payslip_data(cursor, card_id, target_cmonth)
|
||||
except Exception as e: raise Exception(f"Payslip Build Error: {e}")
|
||||
finally: conn.close()
|
||||
|
||||
async def send_payslip_months_menu(msg: Message, user_id: str, card_id: str):
|
||||
try:
|
||||
months_data = await asyncio.to_thread(wrapper_get_months, card_id)
|
||||
months_ru = {1: "Январь", 2: "Февраль", 3: "Март", 4: "Апрель", 5: "Май", 6: "Июнь", 7: "Июль", 8: "Август", 9: "Сентябрь", 10: "Октябрь", 11: "Ноябрь", 12: "Декабрь"}
|
||||
menu_text = lk_payslip_months_menu_text(months_data, months_ru, EMOJI_DIGITS)
|
||||
set_state(user_id, "LK_PAYSLIP_MONTHS")
|
||||
await msg.answer(menu_text, parse_mode="html")
|
||||
except Exception as e: raise Exception(str(e))
|
||||
|
||||
async def build_confirmation_text(user_id: str, doc: dict) -> str:
|
||||
confirm_txt = f"📝 <b>Подтверждение заказа:</b>\n\nДокумент: {doc['doc_name']}\nКоличество: {doc['quantity']} шт."
|
||||
if doc.get('period'): confirm_txt += f"\nУточнение: {doc['period']}"
|
||||
if getattr(config, 'DOCUMENTS_TEST_MODE', False):
|
||||
try:
|
||||
card_id = await asyncio.to_thread(get_card_id_from_ad, user_id)
|
||||
import lk.info_service as info_service
|
||||
with info_service.get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
user_info = await asyncio.to_thread(ds.get_user_info_for_docs, cursor, card_id)
|
||||
|
||||
orig_to = user_info.get('hr_email', '').strip()
|
||||
orig_cc = user_info.get('wb_email', '').strip() if doc['doc_id'] == "7" else ""
|
||||
requested_recipients = orig_to
|
||||
if orig_cc: requested_recipients += f" (Копия: {orig_cc})"
|
||||
test_email = getattr(config, 'DOCUMENTS_TARGET_EMAIL', 'mailonline@sibcem.ru')
|
||||
confirm_txt += f"\n\n⚠️ <b>Тест-режим.</b>\nКому: <code>{requested_recipients}</code>\nЗамена: <code>{test_email}</code>"
|
||||
except Exception as e: confirm_txt += f"\n\n⚠️ <b>Тест-режим.</b>\n(Ошибка БД: {e})"
|
||||
confirm_txt += f"\n\n<i>{EMOJI_DIGITS['1']} — Да, отправить\n{EMOJI_DIGITS['9']} — Назад\n{EMOJI_DIGITS['0']} — В главное меню</i>"
|
||||
return confirm_txt
|
||||
|
||||
# =========================================================
|
||||
# ОСНОВНОЙ ОБРАБОТЧИК МАРШРУТОВ ЛК
|
||||
# =========================================================
|
||||
@router.message()
|
||||
async def lk_router_handler(msg: Message):
|
||||
user_id = msg.from_user.id
|
||||
current_time = time.time()
|
||||
if current_time - user_last_request.get(user_id, 0) < USER_COOLDOWN_SEC: return
|
||||
user_last_request[user_id] = current_time
|
||||
|
||||
current_state = get_state(user_id)
|
||||
nav_text = lk_nav_text(EMOJI_DIGITS)
|
||||
|
||||
if not msg.text: return
|
||||
cmd = msg.text.strip().lower()
|
||||
|
||||
for raw_num, emoji_num in EMOJI_DIGITS.items():
|
||||
if cmd == emoji_num:
|
||||
cmd = raw_num
|
||||
break
|
||||
|
||||
if current_state is None:
|
||||
from utils.menu import MENU_MAP
|
||||
if MENU_MAP.get(cmd) == "LK":
|
||||
msg.handled = True
|
||||
set_state(user_id, "LK_MODE")
|
||||
await send_dynamic_lk_main_menu(msg, user_id)
|
||||
return
|
||||
return
|
||||
|
||||
if current_state == "LK_MODE":
|
||||
msg.handled = True
|
||||
if cmd == "0":
|
||||
log_menu_stats(user_id, "Личный кабинет", "Выход в главное меню")
|
||||
clear_state(user_id)
|
||||
from utils.menu import MENU_TEXT
|
||||
await msg.answer(MENU_TEXT, parse_mode="html")
|
||||
elif cmd == "1":
|
||||
block_expiry = user_payslip_blocks.get(user_id, 0)
|
||||
if time.time() < block_expiry:
|
||||
rem_minutes = int((block_expiry - time.time()) // 60) + 1
|
||||
await msg.answer(
|
||||
f"❌ <b>Доступ временно заблокирован.</b>\n"
|
||||
f"Вы трижды ввели неверный код подтверждения.\n"
|
||||
f"Доступ к расчетным листам ограничен на {int(getattr(config, 'OTP_BLOCK_DURATION_SEC', 1800) // 60)} минут.\n\n"
|
||||
f"⏱ Осталось подождать: <b>{rem_minutes} мин.</b>{nav_text}",
|
||||
parse_mode="html"
|
||||
)
|
||||
return
|
||||
|
||||
log_menu_stats(user_id, "Личный кабинет", "Расчётный лист")
|
||||
await msg.answer(lk_payslip_loading(EMOJI_DIGITS), parse_mode="html")
|
||||
try:
|
||||
card_id = await asyncio.to_thread(get_card_id_from_ad, user_id)
|
||||
await send_payslip_months_menu(msg, user_id, card_id)
|
||||
except Exception as e: await handle_lk_error(msg, user_id, "Меню расчетных листков", str(e))
|
||||
elif cmd == "2":
|
||||
log_menu_stats(user_id, "Личный кабинет", "Информация о днях отпуска")
|
||||
await msg.answer(lk_vacation_loading(EMOJI_DIGITS), parse_mode="html")
|
||||
try:
|
||||
card_id = await asyncio.to_thread(get_card_id_from_ad, user_id)
|
||||
static_header, history_lines = await asyncio.to_thread(get_vacation_records, card_id)
|
||||
user_page_sessions[user_id] = {"type": "vacation", "current_page": 0, "per_page": 5, "static_header": static_header, "items": history_lines}
|
||||
set_state(user_id, "LK_VACATION_VIEW")
|
||||
await send_paginated_page(msg, user_id)
|
||||
except Exception as e: await handle_lk_error(msg, user_id, "Справка по отпускам", str(e))
|
||||
elif cmd == "3":
|
||||
log_menu_stats(user_id, "Личный кабинет", "Больничные")
|
||||
await msg.answer(lk_sick_loading(EMOJI_DIGITS), parse_mode="html")
|
||||
try:
|
||||
card_id = await asyncio.to_thread(get_card_id_from_ad, user_id)
|
||||
sick_info = await asyncio.to_thread(get_sick_leaves_text, card_id)
|
||||
set_state(user_id, "LK_SICK_VIEW")
|
||||
await msg.answer(sick_info + nav_text, parse_mode="html")
|
||||
except Exception as e: await handle_lk_error(msg, user_id, "Просмотр больничных", str(e))
|
||||
elif cmd == "4":
|
||||
log_menu_stats(user_id, "Личный кабинет", "Командировки")
|
||||
await msg.answer(lk_missions_loading(EMOJI_DIGITS), parse_mode="html")
|
||||
try:
|
||||
card_id = await asyncio.to_thread(get_card_id_from_ad, user_id)
|
||||
mission_lines = await asyncio.to_thread(get_missions_records, card_id)
|
||||
user_page_sessions[user_id] = {"type": "missions", "current_page": 0, "per_page": 4, "items": mission_lines}
|
||||
set_state(user_id, "LK_MISSIONS_VIEW")
|
||||
await send_paginated_page(msg, user_id)
|
||||
except Exception as e: await handle_lk_error(msg, user_id, "Просмотр командировок", str(e))
|
||||
elif cmd == "5":
|
||||
log_menu_stats(user_id, "Личный кабинет", "Награды")
|
||||
await msg.answer(lk_rewards_loading(EMOJI_DIGITS), parse_mode="html")
|
||||
try:
|
||||
card_id = await asyncio.to_thread(get_card_id_from_ad, user_id)
|
||||
rewards_info = await asyncio.to_thread(get_rewards_text, card_id)
|
||||
set_state(user_id, "LK_REWARDS_VIEW")
|
||||
await msg.answer(rewards_info + nav_text, parse_mode="html")
|
||||
except Exception as e: await handle_lk_error(msg, user_id, "Запрос наград", str(e))
|
||||
elif cmd == "6":
|
||||
log_menu_stats(user_id, "Личный кабинет", "Переводы")
|
||||
await msg.answer(lk_transfers_loading(EMOJI_DIGITS), parse_mode="html")
|
||||
try:
|
||||
card_id = await asyncio.to_thread(get_card_id_from_ad, user_id)
|
||||
transfer_lines = await asyncio.to_thread(get_transfers_records, card_id)
|
||||
user_page_sessions[user_id] = {"type": "transfers", "current_page": 0, "per_page": 4, "items": transfer_lines}
|
||||
set_state(user_id, "LK_TRANSFERS_VIEW")
|
||||
await send_paginated_page(msg, user_id)
|
||||
except Exception as e: await handle_lk_error(msg, user_id, "Просмотр переводов", str(e))
|
||||
elif cmd == "7":
|
||||
log_menu_stats(user_id, "Личный кабинет", "Табель учёта рабочего времени")
|
||||
await send_tabel_months_menu(msg, user_id)
|
||||
elif cmd == "8":
|
||||
log_menu_stats(user_id, "Личный кабинет", "Справки")
|
||||
set_state(user_id, "LK_DOCS_MENU")
|
||||
await msg.answer(LK_DOCS_MENU_TEXT, parse_mode="html")
|
||||
else:
|
||||
# 🎯 ИСПРАВЛЕНО: Вместо спама копии меню ЛК выводим строгое предупреждение
|
||||
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
||||
|
||||
elif current_state == "LK_TABEL_MONTHS":
|
||||
msg.handled = True
|
||||
if cmd == "0":
|
||||
clear_state(user_id)
|
||||
from utils.menu import MENU_TEXT
|
||||
await msg.answer(MENU_TEXT, parse_mode="html")
|
||||
elif cmd == "9":
|
||||
set_state(user_id, "LK_MODE")
|
||||
await send_dynamic_lk_main_menu(msg, user_id)
|
||||
elif cmd in ["1", "2"]:
|
||||
log_menu_stats(user_id, "Личный кабинет", "Просмотр табеля за месяц")
|
||||
await msg.answer(lk_tabel_loading(EMOJI_DIGITS), parse_mode="html")
|
||||
try:
|
||||
card_id = await asyncio.to_thread(get_card_id_from_ad, user_id)
|
||||
now = datetime(2026, 6, 3)
|
||||
target_date = now.replace(day=1) - timedelta(days=1) if cmd == "1" else (now.replace(day=1) - timedelta(days=1)).replace(day=1) - timedelta(days=1)
|
||||
tabel_report = await asyncio.to_thread(get_tabel_report_text, card_id, target_date.year, target_date.month)
|
||||
set_state(user_id, "LK_TABEL_VIEW")
|
||||
await msg.answer(tabel_report + nav_text, parse_mode="html")
|
||||
except Exception as e: await handle_lk_error(msg, user_id, "Формирование табеля", str(e))
|
||||
else:
|
||||
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
||||
|
||||
elif current_state in ["LK_VACATION_VIEW", "LK_MISSIONS_VIEW", "LK_TRANSFERS_VIEW"]:
|
||||
msg.handled = True
|
||||
session = user_page_sessions.get(user_id)
|
||||
if cmd == "0":
|
||||
user_page_sessions.pop(user_id, None)
|
||||
clear_state(user_id)
|
||||
from utils.menu import MENU_TEXT
|
||||
await msg.answer(MENU_TEXT, parse_mode="html")
|
||||
elif cmd == "9":
|
||||
user_page_sessions.pop(user_id, None)
|
||||
set_state(user_id, "LK_MODE")
|
||||
await send_dynamic_lk_main_menu(msg, user_id)
|
||||
elif cmd == "7" and session and session["current_page"] > 0:
|
||||
session["current_page"] -= 1
|
||||
await send_paginated_page(msg, user_id)
|
||||
elif cmd == "8" and session:
|
||||
if session["current_page"] < (len(session["items"]) + session["per_page"] - 1) // session["per_page"] - 1:
|
||||
session["current_page"] += 1
|
||||
await send_paginated_page(msg, user_id)
|
||||
else:
|
||||
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
||||
|
||||
elif current_state == "LK_PAYSLIP_AUTH":
|
||||
msg.handled = True
|
||||
session = user_page_sessions.get(user_id)
|
||||
|
||||
if cmd == "0":
|
||||
user_page_sessions.pop(user_id, None)
|
||||
clear_state(user_id)
|
||||
from utils.menu import MENU_TEXT
|
||||
await msg.answer(MENU_TEXT, parse_mode="html")
|
||||
return
|
||||
elif cmd == "9":
|
||||
user_page_sessions.pop(user_id, None)
|
||||
set_state(user_id, "LK_PAYSLIP_MONTHS")
|
||||
try:
|
||||
card_id = await asyncio.to_thread(get_card_id_from_ad, user_id)
|
||||
await send_payslip_months_menu(msg, user_id, card_id)
|
||||
except Exception as e: await handle_lk_error(msg, user_id, "Возврат к меню месяцев", str(e))
|
||||
return
|
||||
|
||||
if not session:
|
||||
await msg.answer("⚠️ <i>Сессия авторизации не найдена. Начните сначала.</i>", parse_mode="html")
|
||||
set_state(user_id, "LK_MODE")
|
||||
await send_dynamic_lk_main_menu(msg, user_id)
|
||||
return
|
||||
|
||||
if time.time() > session.get("expires_at", 0):
|
||||
log_menu_stats(user_id, "Личный кабинет", "Ошибка 2FA: Истекло время жизни кода")
|
||||
user_page_sessions.pop(user_id, None)
|
||||
set_state(user_id, "LK_MODE")
|
||||
await msg.answer(
|
||||
f"⏱ <b>Время действия кода подтверждения изменилось или истекло.</b>\n"
|
||||
f"Пожалуйста, запросите расчетный лист заново.{nav_text}",
|
||||
parse_mode="html"
|
||||
)
|
||||
await send_dynamic_lk_main_menu(msg, user_id)
|
||||
return
|
||||
|
||||
if cmd == session.get("correct_code"):
|
||||
log_menu_stats(user_id, "Личный кабинет", "Успешная проверка 2FA расчетного листа")
|
||||
await msg.answer(lk_auth_success(EMOJI_DIGITS), parse_mode="html")
|
||||
|
||||
try:
|
||||
card_id = await asyncio.to_thread(get_card_id_from_ad, user_id)
|
||||
target_cmonth = session["target_cmonth"]
|
||||
|
||||
m_name = {1: "Январь", 2: "Февраль", 3: "Март", 4: "Апрель", 5: "Май", 6: "Июнь", 7: "Июль", 8: "Август", 9: "Сентябрь", 10: "Октябрь", 11: "Ноябрь", 12: "Декабрь"}
|
||||
tmonth = (target_cmonth - 1) % 12 + 1
|
||||
tyear = (target_cmonth - 1) // 12
|
||||
|
||||
payslip_info, pdf_path = await asyncio.to_thread(wrapper_get_payslip, card_id, target_cmonth)
|
||||
|
||||
set_state(user_id, "LK_PAYSLIP_VIEW")
|
||||
await msg.answer_document(
|
||||
FSInputFile(pdf_path, filename=f"Расчетный_лист_{m_name.get(tmonth, '00')}_{tyear}.pdf"),
|
||||
caption=payslip_info + f"\n\n<i>{EMOJI_DIGITS['9']} — Назад к выбору месяца\n{EMOJI_DIGITS['0']} — В главное меню</i>",
|
||||
parse_mode="html"
|
||||
)
|
||||
if os.path.exists(pdf_path): os.remove(pdf_path)
|
||||
except Exception as e:
|
||||
await handle_lk_error(msg, user_id, "Просмотр детализации после 2FA", str(e))
|
||||
finally:
|
||||
user_page_sessions.pop(user_id, None)
|
||||
else:
|
||||
session["attempts_left"] -= 1
|
||||
if session["attempts_left"] <= 0:
|
||||
log_menu_stats(user_id, "Личный кабинет", "Ошибка 2FA: Превышен лимит попыток ввода, бан")
|
||||
|
||||
block_duration = int(getattr(config, 'OTP_BLOCK_DURATION_SEC', 1800))
|
||||
user_payslip_blocks[user_id] = time.time() + block_duration
|
||||
|
||||
user_page_sessions.pop(user_id, None)
|
||||
set_state(user_id, "LK_MODE")
|
||||
await msg.answer(
|
||||
f"❌ <b>Превышено максимальное количество попыток ввода кода.</b>\n"
|
||||
f"Доступ к финансовым документам заблокирован на <b>{block_duration // 60} минут</b>.{nav_text}",
|
||||
parse_mode="html"
|
||||
)
|
||||
await send_dynamic_lk_main_menu(msg, user_id)
|
||||
else:
|
||||
await msg.answer(
|
||||
f"⚠️ <b>Неверный код подтверждения.</b>\n"
|
||||
f"Осталось попыток: {session['attempts_left']}. Попробуйте еще раз или вернитесь назад.{nav_text}",
|
||||
parse_mode="html"
|
||||
)
|
||||
|
||||
elif current_state in ["LK_REWARDS_VIEW", "LK_PAYSLIP_MONTHS", "LK_SICK_VIEW", "LK_TABEL_VIEW", "LK_STUB_VIEW"]:
|
||||
msg.handled = True
|
||||
if cmd == "0":
|
||||
clear_state(user_id)
|
||||
from utils.menu import MENU_TEXT
|
||||
await msg.answer(MENU_TEXT, parse_mode="html")
|
||||
elif cmd == "9":
|
||||
if current_state == "LK_TABEL_VIEW": await send_tabel_months_menu(msg, user_id)
|
||||
else: set_state(user_id, "LK_MODE"); await send_dynamic_lk_main_menu(msg, user_id)
|
||||
elif current_state == "LK_PAYSLIP_MONTHS" and cmd.isdigit():
|
||||
try:
|
||||
card_id = await asyncio.to_thread(get_card_id_from_ad, user_id)
|
||||
months_data = await asyncio.to_thread(wrapper_get_months, card_id)
|
||||
idx = int(cmd) - 1
|
||||
|
||||
if 0 <= idx < len(months_data):
|
||||
target_cmonth = months_data[idx]
|
||||
|
||||
await msg.answer(lk_auth_processing(EMOJI_DIGITS), parse_mode="html")
|
||||
user_phone = await asyncio.to_thread(get_user_phone_from_ad, user_id)
|
||||
|
||||
otp_code = generate_otp_code(4)
|
||||
sms_success, channel_info = await send_otp_via_api(user_phone, otp_code)
|
||||
|
||||
if sms_success:
|
||||
log_menu_stats(user_id, "Личный кабинет", f"Запрос кода 2FA для расчетного листа ({channel_info})")
|
||||
masked_phone = f"{user_phone[:5]}***{user_phone[-4:]}" if len(user_phone) > 8 else user_phone
|
||||
|
||||
otp_ttl = int(getattr(config, 'OTP_TTL_SEC', 180))
|
||||
max_attempts = int(getattr(config, 'OTP_MAX_ATTEMPTS', 3))
|
||||
|
||||
user_page_sessions[user_id] = {
|
||||
"correct_code": otp_code,
|
||||
"target_cmonth": target_cmonth,
|
||||
"expires_at": time.time() + otp_ttl,
|
||||
"attempts_left": max_attempts
|
||||
}
|
||||
|
||||
set_state(user_id, "LK_PAYSLIP_AUTH")
|
||||
await msg.answer(
|
||||
f"🔐 <b>Двухфакторная авторизация</b>\n\n"
|
||||
f"Код подтверждения отправлен <b>{channel_info}</b> на номер <code>{masked_phone}</code>.\n"
|
||||
f"Пожалуйста, введите 4-значный код из сообщения в ответ:\n\n"
|
||||
f"<i>Код действителен в течение {otp_ttl // 60} минут. У вас есть {max_attempts} попытки.</i>{nav_text}",
|
||||
parse_mode="html"
|
||||
)
|
||||
else:
|
||||
raise Exception(f"Внутренний каскадный шлюз недоступен: {channel_info}")
|
||||
else:
|
||||
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
||||
except Exception as e: await handle_lk_error(msg, user_id, "Инициализация 2FA расчетного листа", str(e))
|
||||
else:
|
||||
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
||||
|
||||
elif current_state == "LK_PAYSLIP_VIEW":
|
||||
msg.handled = True
|
||||
if cmd == "0": clear_state(user_id); from utils.menu import MENU_TEXT; await msg.answer(MENU_TEXT, parse_mode="html")
|
||||
elif cmd == "9":
|
||||
try: card_id = await asyncio.to_thread(get_card_id_from_ad, user_id); await send_payslip_months_menu(msg, user_id, card_id)
|
||||
except Exception as e: await handle_lk_error(msg, user_id, "Возврат в меню месяцев", str(e))
|
||||
else:
|
||||
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
||||
|
||||
elif current_state == "LK_DOCS_MENU":
|
||||
msg.handled = True
|
||||
if cmd == "0": user_docs_session.pop(user_id, None); clear_state(user_id); from utils.menu import MENU_TEXT; await msg.answer(MENU_TEXT, parse_mode="html")
|
||||
elif cmd == "9": user_docs_session.pop(user_id, None); set_state(user_id, "LK_MODE"); await send_dynamic_lk_main_menu(msg, user_id)
|
||||
elif cmd in DOC_TYPES:
|
||||
user_docs_session[user_id] = {'doc_id': cmd, 'doc_name': DOC_TYPES[cmd]}
|
||||
log_menu_stats(user_id, "Личный кабинет", f"Выбор справки: {DOC_TYPES[cmd]}")
|
||||
set_state(user_id, "LK_DOCS_QUANTITY")
|
||||
await msg.answer(lk_docs_quantity_prompt(EMOJI_DIGITS), parse_mode="html")
|
||||
else:
|
||||
# 🎯 ИСПРАВЛЕНО: Заменен спам списка документов на чистую ошибку ввода
|
||||
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
||||
|
||||
elif current_state == "LK_DOCS_QUANTITY":
|
||||
msg.handled = True
|
||||
if cmd == "0": user_docs_session.pop(user_id, None); clear_state(user_id); from utils.menu import MENU_TEXT; await msg.answer(MENU_TEXT, parse_mode="html")
|
||||
elif cmd == "9": set_state(user_id, "LK_DOCS_MENU"); await msg.answer(LK_DOCS_MENU_TEXT, parse_mode="html")
|
||||
elif cmd.isdigit() and 0 < int(cmd) <= 5:
|
||||
user_docs_session[user_id]['quantity'] = int(cmd)
|
||||
doc_id = user_docs_session[user_id]['doc_id']
|
||||
|
||||
if doc_id in ["1", "2", "6"]:
|
||||
set_state(user_id, "LK_DOCS_PERIOD")
|
||||
current_year = datetime.now().year
|
||||
years_text = lk_years_menu(current_year, EMOJI_DIGITS)
|
||||
await msg.answer(years_text, parse_mode="html")
|
||||
elif doc_id == "3":
|
||||
set_state(user_id, "LK_DOCS_PERIOD")
|
||||
await msg.answer(lk_docs_avg_salary_prompt(EMOJI_DIGITS), parse_mode="html")
|
||||
elif doc_id == "5":
|
||||
set_state(user_id, "LK_DOCS_PERIOD")
|
||||
await msg.answer(lk_docs_child_age_prompt(EMOJI_DIGITS), parse_mode="html")
|
||||
else:
|
||||
user_docs_session[user_id]['period'] = ""
|
||||
set_state(user_id, "LK_DOCS_CONFIRM")
|
||||
confirm_txt = await build_confirmation_text(user_id, user_docs_session[user_id])
|
||||
await msg.answer(confirm_txt, parse_mode="html")
|
||||
else:
|
||||
# 🎯 ИСПРАВЛЕНО: Вместо кастомной подсказки выводим единый стандарт
|
||||
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
||||
|
||||
elif current_state == "LK_DOCS_PERIOD":
|
||||
msg.handled = True
|
||||
if cmd == "0": user_docs_session.pop(user_id, None); clear_state(user_id); from utils.menu import MENU_TEXT; await msg.answer(MENU_TEXT, parse_mode="html")
|
||||
elif cmd == "9": set_state(user_id, "LK_DOCS_QUANTITY"); await msg.answer(lk_docs_quantity_prompt(EMOJI_DIGITS), parse_mode="html")
|
||||
else:
|
||||
doc_id = user_docs_session[user_id]['doc_id']
|
||||
if doc_id in ["1", "2", "6"]:
|
||||
if cmd in ["1", "2", "3", "4", "5", "6", "7", "8"]:
|
||||
user_docs_session[user_id]['period'] = f"за {datetime.now().year - (int(cmd) - 1)} год"
|
||||
else:
|
||||
# 🎯 ИСПРАВЛЕНО: Убран спам меню годов при абракадабре
|
||||
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
||||
return
|
||||
elif doc_id == "5":
|
||||
if cmd == "1": user_docs_session[user_id]['period'] = "до 1.5 лет"
|
||||
elif cmd == "2": user_docs_session[user_id]['period'] = "до 3 лет"
|
||||
else:
|
||||
# 🎯 ИСПРАВЛЕНО: Убран спам меню возраста при некорректном вводе
|
||||
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
||||
return
|
||||
elif doc_id == "3":
|
||||
user_docs_session[user_id]['period'] = f"за {cmd} мес."
|
||||
|
||||
set_state(user_id, "LK_DOCS_CONFIRM")
|
||||
confirm_txt = await build_confirmation_text(user_id, user_docs_session[user_id])
|
||||
await msg.answer(confirm_txt, parse_mode="html")
|
||||
|
||||
elif current_state == "LK_DOCS_CONFIRM":
|
||||
msg.handled = True
|
||||
if cmd == "0": user_docs_session.pop(user_id, None); clear_state(user_id); from utils.menu import MENU_TEXT; await msg.answer(MENU_TEXT, parse_mode="html")
|
||||
elif cmd == "9":
|
||||
doc_id = user_docs_session.get(user_id, {}).get('doc_id')
|
||||
if doc_id in ["1", "2", "3", "5", "6"]:
|
||||
set_state(user_id, "LK_DOCS_PERIOD")
|
||||
if doc_id in ["1", "2", "6"]:
|
||||
current_year = datetime.now().year
|
||||
years_text = lk_years_menu(current_year, EMOJI_DIGITS)
|
||||
await msg.answer(years_text, parse_mode="html")
|
||||
elif doc_id == "3": await msg.answer(lk_docs_avg_salary_prompt(EMOJI_DIGITS), parse_mode="html")
|
||||
elif doc_id == "5": await msg.answer(lk_docs_child_age_prompt(EMOJI_DIGITS), parse_mode="html")
|
||||
else: set_state(user_id, "LK_DOCS_QUANTITY"); await msg.answer(lk_docs_quantity_prompt(EMOJI_DIGITS), parse_mode="html")
|
||||
elif cmd == "1":
|
||||
await msg.answer("⏳ <i>Отправка заявки...</i>", parse_mode="html")
|
||||
try:
|
||||
doc = user_docs_session[user_id]
|
||||
card_id = await asyncio.to_thread(get_card_id_from_ad, user_id)
|
||||
|
||||
import lk.info_service as info_service
|
||||
with info_service.get_db_connection() as conn:
|
||||
cursor = conn.cursor()
|
||||
user_info = await asyncio.to_thread(ds.get_user_info_for_docs, cursor, card_id)
|
||||
|
||||
success = await asyncio.to_thread(ds.generate_mail_message, doc['doc_name'], doc['quantity'], user_info, doc.get('period', ''))
|
||||
if success:
|
||||
log_menu_stats(user_id, "Личный кабинет", f"Заказ справки: {doc['doc_name']}")
|
||||
await msg.answer(f"✅ <b>Ваша заявка успешно отправлена!</b>\nСпециалисты свяжутся с вами по готовности документа.\n\n<i>{EMOJI_DIGITS['9']} — Назад в список справок\n{EMOJI_DIGITS['0']} — В главное меню</i>", parse_mode="html")
|
||||
set_state(user_id, "LK_DOCS_SUCCESS")
|
||||
else: raise Exception("Внутренняя ошибка SMTP при отправке письма")
|
||||
except Exception as e: await handle_lk_error(msg, user_id, f"Заказ справки {user_docs_session.get(user_id, {}).get('doc_id')}", str(e)); set_state(user_id, "LK_MODE")
|
||||
finally: user_docs_session.pop(user_id, None)
|
||||
else:
|
||||
# 🎯 ИСПРАВЛЕНО: Удален спам-текст подтверждения
|
||||
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
||||
|
||||
elif current_state == "LK_DOCS_SUCCESS":
|
||||
msg.handled = True
|
||||
if cmd == "0": clear_state(user_id); from utils.menu import MENU_TEXT; await msg.answer(MENU_TEXT, parse_mode="html")
|
||||
elif cmd == "9": set_state(user_id, "LK_DOCS_MENU"); await msg.answer(LK_DOCS_MENU_TEXT, parse_mode="html")
|
||||
else:
|
||||
# 🎯 ИСПРАВЛЕНО: Унифицировано под общий шаблон ошибки
|
||||
await msg.answer(UNKNOWN_MAIN_CMD_TEXT, parse_mode="html")
|
||||
@@ -0,0 +1,378 @@
|
||||
# /opt/trueconf_bot/lk/info_service.py
|
||||
import re
|
||||
import logging
|
||||
import calendar
|
||||
import pymssql
|
||||
from datetime import datetime
|
||||
|
||||
import config.config as config
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
def get_db_connection():
|
||||
if not config.DB_USER or not config.DB_PASSWORD:
|
||||
raise Exception("Пароли БД не загружены из Passwork.")
|
||||
return pymssql.connect(
|
||||
server=config.SQL_SERVER,
|
||||
user=config.DB_USER,
|
||||
password=config.DB_PASSWORD,
|
||||
database=config.SQL_DB_NAME,
|
||||
charset='cp1251'
|
||||
)
|
||||
|
||||
def get_lk_profile_by_card_id(card_id: str) -> dict:
|
||||
"""Запрос 1: Получение информации о подразделении, должности и стаже по CARD_ID"""
|
||||
try:
|
||||
current_date = datetime.now().strftime('%Y-%m-%d')
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
query = """
|
||||
select f.SNAME [Компания], d.NAME [Отдел], appoint.NAME [Должность], rtrim(ltrim(e.STAGO)) [Страховой стаж], rtrim(ltrim(e.STAGP)) [Стаж на предприятии]
|
||||
from (select e.ID, e.FIRM_ID, st.STAGO, st.STAGP
|
||||
from UOV_SELFSERVICE_PR_EMP e
|
||||
join UOV_SELFSERVICE_PR_CARD c on c.ID=e.CARD_ID
|
||||
join UOV_SELFSERVICE_PR_STAG_CUR st on st.CARD_ID=c.ID
|
||||
where c.ID=%s
|
||||
group by e.ID , e.FIRM_ID, st.STAGO, st.STAGP) as e
|
||||
join UOV_SELFSERVICE_PR_TRANS t on t.EMP_ID = e.ID
|
||||
join UOV_SELFSERVICE_HR_FIRM f on f.ID = e.FIRM_ID
|
||||
join UOV_SELFSERVICE_HR_DEPT d on d.ID = t.DEPT_ID
|
||||
join UOV_SELFSERVICE_ST_APPOINT appoint on appoint.ID = t.APPOINT_ID
|
||||
where (t.D_FROM <= cast(%s as date)
|
||||
and (t.D_TO >= cast(%s as date)
|
||||
or t.D_TO = cast('1999-12-31' as date)))
|
||||
group by f.SNAME, d.NAME, appoint.NAME, rtrim(ltrim(e.STAGO)), rtrim(ltrim(e.STAGP))
|
||||
"""
|
||||
cursor.execute(query, (card_id, current_date, current_date))
|
||||
row = cursor.fetchone()
|
||||
conn.close()
|
||||
if not row: raise Exception(f"Запись сотрудника по CARD_ID {card_id} не найдена в БД.")
|
||||
return {
|
||||
'company': str(row[0]).strip() if row[0] else "",
|
||||
'dept': str(row[1]).strip() if row[1] else "",
|
||||
'job': str(row[2]).strip() if row[2] else "",
|
||||
'stag_o': str(row[3]).strip() if row[3] else "0",
|
||||
'stag_p': str(row[4]).strip() if row[4] else "0"
|
||||
}
|
||||
except Exception as e: raise Exception(f"SQL Profile Error: {e}")
|
||||
|
||||
def get_vacation_records(card_id: str) -> tuple:
|
||||
"""Возвращает статический заголовок (баланс + планы) и список строк истории отпусков"""
|
||||
try:
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("SELECT TOP 1 e.ID FROM UOV_SELFSERVICE_PR_EMP e WHERE e.CARD_ID = %s", (card_id,))
|
||||
row = cursor.fetchone()
|
||||
if not row:
|
||||
conn.close()
|
||||
return "🌴 <b>Информация об отпусках</b>\n\nСотрудник не найден в базе данных.", []
|
||||
emp_id = row[0]
|
||||
|
||||
# Балансы
|
||||
q_balance = """
|
||||
SELECT comp.LEAVE_NAME, comp.DAYS FROM UO_SELFSERVICE_LEAVE_COMP as comp
|
||||
INNER JOIN UOV_SELFSERVICE_PR_TRANS as trans ON trans.EMP_ID = comp.EMP_ID
|
||||
WHERE GETDATE() BETWEEN trans.D_FROM and trans.D_TO AND comp.EMP_ID = %s
|
||||
"""
|
||||
cursor.execute(q_balance, (emp_id,))
|
||||
balances = cursor.fetchall()
|
||||
|
||||
# Планы
|
||||
q_plans = """
|
||||
SELECT e.D_FROM, e.DAYS
|
||||
FROM UOV_SELFSERVICE_PR_LEAVE_PLAN e
|
||||
WHERE e.EMP_ID = %s AND e.D_FROM >= CAST(GETDATE() AS DATE)
|
||||
ORDER BY e.D_FROM ASC
|
||||
"""
|
||||
cursor.execute(q_plans, (emp_id,))
|
||||
plans = cursor.fetchall()
|
||||
|
||||
# Архив всей истории
|
||||
q_history = """
|
||||
SELECT e.D_FROM, e.D_TO, e.DAYS, CAST(e.NAME AS VARCHAR(250)), e.ADDITIONAL
|
||||
FROM UOV_SELFSERVICE_PR_LEAVE_ALL e
|
||||
WHERE e.EMP_ID = %s AND e.D_FROM < CAST(GETDATE() AS DATE)
|
||||
ORDER BY e.D_FROM DESC
|
||||
"""
|
||||
cursor.execute(q_history, (emp_id,))
|
||||
history = cursor.fetchall()
|
||||
conn.close()
|
||||
|
||||
# Формируем неизменяемую верхнюю часть (шапку) справки
|
||||
static_header = "🌴 <b>Информация об отпусках</b>\n\n🟢 <b>Доступный остаток:</b>\n"
|
||||
if balances:
|
||||
for b in balances:
|
||||
b_name = str(b[0]).strip() if b[0] else "Ежегодный оплачиваемый отпуск"
|
||||
static_header += f"• <b>{int(b[1]) if b[1] else 0} дн.</b> <i>({b_name})</i>\n"
|
||||
else:
|
||||
static_header += "• <b>0 дн.</b>\n"
|
||||
|
||||
static_header += "\n⏳ <b>Предстоящие отпуска:</b>\n"
|
||||
if plans:
|
||||
for p in plans:
|
||||
d_from = p[0].strftime('%d.%m.%Y') if hasattr(p[0], 'strftime') else str(p[0])[:10]
|
||||
static_header += f"• <b>{d_from}</b> — <b>{int(p[1]) if p[1] else 0} дн.</b>\n"
|
||||
else:
|
||||
static_header += "• <i>Нет запланированных отпусков</i>\n"
|
||||
|
||||
# Нарезаем строки истории в плоский массив для пагинатора
|
||||
history_lines = []
|
||||
if history:
|
||||
for h in history:
|
||||
h_from = h[0].strftime('%d.%m.%Y') if hasattr(h[0], 'strftime') else str(h[0])[:10]
|
||||
h_to = h[1].strftime('%d.%m.%Y') if hasattr(h[1], 'strftime') else str(h[1])[:10]
|
||||
h_days = int(h[2]) if h[2] else 0
|
||||
|
||||
if h[3] and str(h[3]).strip():
|
||||
type_str = str(h[3]).strip()
|
||||
else:
|
||||
type_str = "Дополнительный" if h[4] == 1 else "Основной"
|
||||
|
||||
history_lines.append(f"• {h_from} — {h_to} : 🔹 <b>{h_days} дн.</b> <i>({type_str})</i>")
|
||||
|
||||
return static_header, history_lines
|
||||
except Exception as e:
|
||||
raise Exception(f"SQL Vacation Records Error: {e}")
|
||||
|
||||
def get_sick_leaves_text(card_id: str) -> str:
|
||||
"""Запрашивает историю больничных листов строго по CARD_ID"""
|
||||
try:
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("SELECT e.ID FROM UOV_SELFSERVICE_PR_EMP e WHERE e.CARD_ID = %s", (card_id,))
|
||||
row = cursor.fetchone()
|
||||
if not row:
|
||||
conn.close()
|
||||
return "📑 <b>Ваши больничные листы:</b>\n\nСотрудник не найден в базе данных."
|
||||
emp_id = row[0]
|
||||
|
||||
query = """
|
||||
SELECT e.D_FROM, e.D_TO
|
||||
FROM UOV_SELFSERVICE_PR_ILL e
|
||||
WHERE e.EMP_ID = %s
|
||||
ORDER BY e.D_FROM DESC
|
||||
"""
|
||||
cursor.execute(query, (emp_id,))
|
||||
results = cursor.fetchall()
|
||||
conn.close()
|
||||
|
||||
if not results:
|
||||
return "📑 <b>Ваши больничные листы:</b>\n\nНичего не найдено."
|
||||
|
||||
response = "📑 <b>Ваши больничные листы:</b>\n\n"
|
||||
for r in results:
|
||||
d_from = r[0].strftime('%d.%m.%Y') if hasattr(r[0], 'strftime') else str(r[0])[:10]
|
||||
d_to = r[1].strftime('%d.%m.%Y') if hasattr(r[1], 'strftime') else str(r[1])[:10]
|
||||
|
||||
days_count = ""
|
||||
if hasattr(r[0], 'strftime') and hasattr(r[1], 'strftime'):
|
||||
delta_days = (r[1] - r[0]).days + 1
|
||||
days_count = f" — <b>{delta_days} дн.</b>"
|
||||
|
||||
response += f"🩺 <code>{d_from}</code> по <code>{d_to}</code>{days_count}\n"
|
||||
return response
|
||||
except Exception as e:
|
||||
raise Exception(f"SQL Sick Leaves Error: {e}")
|
||||
|
||||
def get_missions_records(card_id: str) -> list:
|
||||
"""Возвращает структурированный список pre-formatted текстовых блоков командировок"""
|
||||
try:
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
query = """
|
||||
SELECT mp.D_FROM, mp.D_TO, CAST(mp.NAME AS VARCHAR(250)), CAST(mp.CITY AS VARCHAR(250))
|
||||
FROM UOV_SELFSERVICE_PR_EMP e
|
||||
JOIN UOV_SELFSERVICE_PR_MISSION m ON e.ID = m.EMP_ID
|
||||
JOIN UOV_SELFSERVICE_PR_MISSION_P mp ON mp.MISSION_ID = m.ID
|
||||
WHERE e.CARD_ID = %s
|
||||
ORDER BY mp.D_FROM DESC
|
||||
"""
|
||||
cursor.execute(query, (card_id,))
|
||||
results = cursor.fetchall()
|
||||
conn.close()
|
||||
|
||||
mission_blocks = []
|
||||
for r in results:
|
||||
d_from = r[0].strftime('%d.%m.%Y') if hasattr(r[0], 'strftime') else str(r[0])[:10]
|
||||
d_to = r[1].strftime('%d.%m.%Y') if hasattr(r[1], 'strftime') else str(r[1])[:10]
|
||||
firm_name = str(r[2]).strip() if r[2] else "Организация не указана"
|
||||
city = str(r[3]).strip() if r[3] else "Город не указан"
|
||||
|
||||
block = f"• 📅 <code>{d_from}</code> — <code>{d_to}</code>\n 📍 г. {city}, {firm_name}"
|
||||
mission_blocks.append(block)
|
||||
|
||||
return mission_blocks
|
||||
except Exception as e:
|
||||
raise Exception(f"SQL Missions Records Error: {e}")
|
||||
|
||||
def get_transfers_records(card_id: str) -> list:
|
||||
"""Возвращает структурированный список кадровых переходов сотрудника по его CARD_ID"""
|
||||
try:
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
query = """
|
||||
SELECT
|
||||
tr.D_FROM,
|
||||
CASE
|
||||
WHEN tr.D_TO = CAST('1999-12-31' AS DATE) THEN CAST('2099-12-31' AS DATE)
|
||||
ELSE tr.D_TO
|
||||
END as D_TO,
|
||||
CAST(d.NAME AS VARCHAR(250)) as DEPT,
|
||||
CAST(a.NAME AS VARCHAR(250)) as APPOINT
|
||||
FROM UOV_SELFSERVICE_PR_EMP e
|
||||
JOIN UOV_SELFSERVICE_PR_TRANS tr ON tr.EMP_ID = e.ID
|
||||
JOIN UOV_SELFSERVICE_HR_DEPT d ON d.ID = tr.DEPT_ID
|
||||
JOIN UOV_SELFSERVICE_ST_APPOINT a ON a.ID = tr.APPOINT_ID
|
||||
WHERE e.CARD_ID = %s
|
||||
ORDER BY tr.D_FROM DESC
|
||||
"""
|
||||
cursor.execute(query, (card_id,))
|
||||
results = cursor.fetchall()
|
||||
conn.close()
|
||||
|
||||
transfer_blocks = []
|
||||
for r in results:
|
||||
d_from = r[0].strftime('%d.%m.%Y') if hasattr(r[0], 'strftime') else str(r[0])[:10]
|
||||
if r[1] and (r[1].year == 2099 or r[1].year == 1999):
|
||||
d_to = "наст. время"
|
||||
else:
|
||||
d_to = r[1].strftime('%d.%m.%Y') if hasattr(r[1], 'strftime') else str(r[1])[:10]
|
||||
|
||||
dept = str(r[2]).strip() if r[2] else "Не указан"
|
||||
appoint = str(r[3]).strip() if r[3] else "Не указана"
|
||||
|
||||
block = f"• ⏳ <b>{d_from} — {d_to}</b>\n 💼 <b>Должность:</b> {appoint}\n 📂 <b>Отдел:</b> {dept}"
|
||||
transfer_blocks.append(block)
|
||||
|
||||
return transfer_blocks
|
||||
except Exception as e:
|
||||
raise Exception(f"SQL Transfers Records Error: {e}")
|
||||
|
||||
def get_rewards_text(card_id: str) -> str:
|
||||
"""Запрашивает историю наград по CARD_ID"""
|
||||
try:
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
cursor.execute("""
|
||||
SELECT e.D, CAST(e.NAME AS VARCHAR(1000))
|
||||
FROM UOV_SELFSERVICE_PR_REW_ENC e
|
||||
WHERE e.CARD_ID = %s
|
||||
ORDER BY e.D
|
||||
""", (card_id,))
|
||||
results = cursor.fetchall()
|
||||
conn.close()
|
||||
|
||||
if not results:
|
||||
return "🏆 <b>Ваши награды и поощрения:</b>\n\nНичего не найдено."
|
||||
|
||||
response = "🏆 <b>Ваши награды и поощрения:</b>\n\n"
|
||||
for r in results:
|
||||
date_str = r[0].strftime('%d.%m.%Y') if hasattr(r[0], 'strftime') else str(r[0])[:10]
|
||||
response += f"🏅 <b>{date_str}</b> — {str(r[1]).strip()}\n"
|
||||
return response
|
||||
except Exception as e:
|
||||
raise Exception(f"SQL Rewards Error: {e}")
|
||||
|
||||
def get_tabel_report_text(card_id: str, year: int, month: int) -> str:
|
||||
"""Формирует отчет по табелю из UOV_SELFSERVICE_TB_TABEL на основе CARD_ID"""
|
||||
try:
|
||||
conn = get_db_connection()
|
||||
cursor = conn.cursor()
|
||||
|
||||
cursor.execute("SELECT e.ID FROM UOV_SELFSERVICE_PR_EMP e WHERE e.CARD_ID = %s", (card_id,))
|
||||
row = cursor.fetchone()
|
||||
if not row:
|
||||
conn.close()
|
||||
return "📋 <b>Рабочий табель:</b>\n\nСотрудник не найден в базе данных."
|
||||
emp_id = row[0]
|
||||
|
||||
query = """
|
||||
SELECT e.D, e.TDAY_ID, e.H
|
||||
FROM UOV_SELFSERVICE_TB_TABEL e
|
||||
WHERE e.EMP_ID = %s AND YEAR(e.D) = %s AND MONTH(e.D) = %s
|
||||
ORDER BY e.D ASC
|
||||
"""
|
||||
cursor.execute(query, (emp_id, year, month))
|
||||
records = cursor.fetchall()
|
||||
conn.close()
|
||||
|
||||
if not records:
|
||||
return f"📋 <b>Рабочий табель за {month:02d}.{year} г.</b>\n\nДанные отсутствуют."
|
||||
|
||||
tabel_map = {r[0].day if hasattr(r[0], 'day') else int(str(r[0])[-2:]): (str(r[1]).strip(), float(r[2]) if r[2] is not None else 0.0) for r in records}
|
||||
|
||||
total_days = 0
|
||||
total_hours = 0.0
|
||||
for code, hours in tabel_map.values():
|
||||
if code == 'Я':
|
||||
total_days += 1
|
||||
total_hours += hours
|
||||
|
||||
months_names = {1: "Январь", 2: "Февраль", 3: "Март", 4: "Апрель", 5: "Май", 6: "Июнь", 7: "Июль", 8: "Август", 9: "Сентябрь", 10: "Октябрь", 11: "Ноябрь", 12: "Декабрь"}
|
||||
|
||||
report = (
|
||||
f"📅 <b>Рабочий табель за {months_names.get(month, 'Указанный месяц')} {year} г.</b>\n\n"
|
||||
f"📊 <b>Итого за месяц:</b>\n"
|
||||
f"• Отработано дней: <b>{total_days}</b>\n"
|
||||
f"• Всего часов: <b>{total_hours:.1f} ч.</b>\n"
|
||||
f"• Переработки / Недоработки: <b>0 ч.</b>\n"
|
||||
)
|
||||
|
||||
days_in_month = calendar.monthrange(year, month)[1]
|
||||
weekday_ru = ["Пн", "Вт", "Ср", "Чт", "Пт", "Сб", "Вс"]
|
||||
current_week_days = []
|
||||
week_counter = 1
|
||||
|
||||
for day in range(1, days_in_month + 1):
|
||||
cur_date = datetime(year, month, day)
|
||||
wday_idx = cur_date.weekday()
|
||||
|
||||
code, hours = tabel_map.get(day, ('В', 0.0))
|
||||
current_week_days.append((day, weekday_ru[wday_idx], code, hours))
|
||||
|
||||
if wday_idx == 6 or day == days_in_month:
|
||||
w_start = f"{current_week_days[0][0]:02d}.{month:02d}"
|
||||
w_end = f"{current_week_days[-1][0]:02d}.{month:02d}"
|
||||
|
||||
report += f"\n🔹 <b>Неделя {week_counter} ({w_start} — {w_end}):</b>\n"
|
||||
|
||||
idx = 0
|
||||
while idx < len(current_week_days):
|
||||
d_num, d_name, c_code, c_hours = current_week_days[idx]
|
||||
|
||||
if c_code == 'Я' and c_hours == 8.2:
|
||||
run_start = d_name
|
||||
run_end = d_name
|
||||
count = 1
|
||||
while idx + 1 < len(current_week_days) and current_week_days[idx+1][2] == 'Я' and current_week_days[idx+1][3] == 8.2:
|
||||
idx += 1
|
||||
run_end = current_week_days[idx][1]
|
||||
count += 1
|
||||
range_str = f"{run_start} - {run_end}" if count > 1 else f"{run_start}"
|
||||
report += f"• {range_str}: 🛠 <b>Явка</b> ({count} дн. по 8.2 ч.)\n"
|
||||
|
||||
elif c_code == 'В' and c_hours == 0.0:
|
||||
run_start = d_name
|
||||
run_end = d_name
|
||||
count = 1
|
||||
while idx + 1 < len(current_week_days) and current_week_days[idx+1][2] == 'В' and current_week_days[idx+1][3] == 0.0:
|
||||
idx += 1
|
||||
run_end = current_week_days[idx][1]
|
||||
count += 1
|
||||
range_str = f"{run_start} - {run_end}" if count > 1 else f"{run_start}"
|
||||
report += f"• {range_str}: 🏖 <b>Выходные</b> (0 ч.)\n"
|
||||
|
||||
else:
|
||||
emoji = "🛠" if c_code == 'Я' else "⏱" if c_code in ['Б', 'БЛ'] else "🏖"
|
||||
label = "Явка" if c_code == 'Я' else "Больничный" if c_code in ['Б', 'БЛ'] else "Отпуск" if c_code == 'ОТ' else "Выходной"
|
||||
report += f"• {d_name}: {emoji} <b>{label}</b> ({c_hours} ч.)\n"
|
||||
|
||||
idx += 1
|
||||
|
||||
current_week_days = []
|
||||
week_counter += 1
|
||||
|
||||
return report
|
||||
except Exception as e:
|
||||
raise Exception(f"SQL Tabel Parser Error: {e}")
|
||||
@@ -0,0 +1 @@
|
||||
[2026-07-08 02:23:21] User: ds.krivochenko@tcs.sibcem.ru | Action: Инициализация 2FA расчетного листа | Error: Не удалось извлечь номер телефона: Не удалось извлечь номер телефона из БД BossCopy: (208, b"Invalid object name 'PR_CARD'.DB-Lib error message 20018, severity 16:\nGeneral SQL Server error: Check messages from the SQL Server\n")
|
||||
@@ -0,0 +1,322 @@
|
||||
# /opt/trueconf_bot/lk/payslip_service.py
|
||||
import calendar
|
||||
import tempfile
|
||||
from itertools import zip_longest
|
||||
from weasyprint import HTML
|
||||
import config.config as config
|
||||
|
||||
# Перевод английских системных месяцев в русские наименования для бланка PDF
|
||||
ENG_TO_RU_MONTHS = {
|
||||
"January": "Январь", "February": "Февраль", "March": "Март",
|
||||
"April": "Апрель", "May": "Май", "June": "Июнь",
|
||||
"July": "Июль", "August": "Август", "September": "Сентябрь",
|
||||
"October": "Октябрь", "November": "Ноябрь", "December": "Декабрь"
|
||||
}
|
||||
|
||||
def format_currency(summa):
|
||||
try:
|
||||
if summa is None:
|
||||
return "0,00"
|
||||
f_summa = float(summa)
|
||||
return f"{f_summa:,.2f}".replace(",", " ").replace(".", ",")
|
||||
except:
|
||||
return str(summa)
|
||||
|
||||
def translate_period(period_str):
|
||||
if not period_str:
|
||||
return ""
|
||||
for eng, ru in ENG_TO_RU_MONTHS.items():
|
||||
if eng in period_str:
|
||||
return period_str.replace(eng, ru)
|
||||
return period_str
|
||||
|
||||
def get_latest_payslip_months(cursor, card_id: str) -> list:
|
||||
query = """
|
||||
SELECT DISTINCT TOP 6 (t.TYEAR * 12 + t.TMONTH) as CMONTH
|
||||
FROM UOV_SELFSERVICE_PR_EMP e
|
||||
JOIN UOV_SELFSERVICE_PR_CARD c on c.ID = e.CARD_ID
|
||||
JOIN UOV_SELFSERVICE_SL_YTAX t on t.CARD_ID = e.CARD_ID
|
||||
JOIN UOV_SELFSERVICE_PR_TRANS tr on tr.EMP_ID = e.ID
|
||||
JOIN UOV_SELFSERVICE_ST_APPOINT a on a.ID = tr.APPOINT_ID
|
||||
JOIN UOV_SELFSERVICE_HR_FIRM f on f.ID = e.FIRM_ID
|
||||
WHERE c.ID = %s
|
||||
ORDER BY CMONTH DESC
|
||||
"""
|
||||
cursor.execute(query, (card_id,))
|
||||
months = cursor.fetchall()
|
||||
if not months:
|
||||
raise Exception("Отсутствуют расчетные периоды.")
|
||||
return [m[0] for m in months]
|
||||
|
||||
def create_payslip_pdf(month_name, target_tyear,
|
||||
header_sys, header_wage, header_deduct, header_base,
|
||||
earnings_rows, deductions_rows, noncash_rows, payout_rows,
|
||||
total_earned, total_withheld, total_noncash, total_payouts,
|
||||
debt_comp_start, debt_emp_start, debt_comp_end, debt_emp_end, net_pay):
|
||||
tbody_html = ""
|
||||
tbody_html += f"""
|
||||
<tr>
|
||||
<td colspan="6" class="section-title">1. Начислено</td>
|
||||
<td colspan="4" class="section-title">2. Удержано</td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
for earn, ded in zip_longest(earnings_rows, deductions_rows):
|
||||
if earn:
|
||||
earn_name = str(earn[0]).strip()
|
||||
earn_days = f"{earn[1]:g}".replace('.', ',') if earn[1] else ""
|
||||
earn_hours = f"{earn[2]:g}".replace('.', ',') if earn[2] else ""
|
||||
earn_prc = f"{earn[3]:g}" if earn[3] else ""
|
||||
earn_period = translate_period(earn[4] or "")
|
||||
earn_summa = format_currency(earn[5])
|
||||
earn_html = f'<td class="left-align">{earn_name}</td><td class="center-align">{earn_days}</td><td class="center-align">{earn_hours}</td><td class="center-align">{earn_prc}</td><td class="center-align">{earn_period}</td><td>{earn_summa}</td>'
|
||||
else:
|
||||
earn_html = '<td style="border-bottom: none; border-left: none;"></td><td style="border-bottom: none;"></td><td style="border-bottom: none;"></td><td style="border-bottom: none;"></td><td style="border-bottom: none;"></td><td style="border-bottom: none; border-right: none;"></td>'
|
||||
|
||||
if ded:
|
||||
ded_name = str(ded[0]).strip()
|
||||
ded_prc = f"{ded[1]:.0f}" if ded[1] else ""
|
||||
ded_period = translate_period(ded[2] or "")
|
||||
ded_summa = format_currency(ded[3])
|
||||
ded_html = f'<td class="left-align">{ded_name}</td><td class="center-align">{ded_prc}</td><td class="center-align">{ded_period}</td><td>{ded_summa}</td>'
|
||||
else:
|
||||
ded_html = '<td style="border-bottom: none; border-left: none; border-right: none;" colspan="4"></td>'
|
||||
|
||||
tbody_html += f"<tr>{earn_html}{ded_html}</tr>"
|
||||
|
||||
tbody_html += f"""
|
||||
<tr class="totals">
|
||||
<td class="left-align" colspan="5">Всего начислено</td><td>{format_currency(total_earned)}</td>
|
||||
<td class="left-align" colspan="3">Всего удержано</td><td>{format_currency(total_withheld)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="6" class="section-title">3. Доходы в неденежной форме</td>
|
||||
<td colspan="4" class="section-title">4. Выплачено</td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
for nc, pay in zip_longest(noncash_rows, payout_rows):
|
||||
if nc:
|
||||
nc_name = str(nc[0]).strip()
|
||||
nc_period = translate_period(nc[1] or "")
|
||||
nc_summa = format_currency(nc[2])
|
||||
nc_html = f'<td class="left-align" colspan="4">{nc_name}</td><td class="center-align">{nc_period}</td><td>{nc_summa}</td>'
|
||||
else:
|
||||
nc_html = '<td style="border-top: none; border-bottom: none; border-left: none;"></td><td style="border-top: none; border-bottom: none;"></td><td style="border-top: none; border-bottom: none;"></td><td style="border-top: none; border-bottom: none;"></td><td style="border-top: none; border-bottom: none;"></td><td style="border-top: none; border-bottom: none; border-right: none;"></td>'
|
||||
|
||||
if pay:
|
||||
pay_name = str(pay[0]).strip()
|
||||
pay_period = translate_period(pay[1] or "")
|
||||
pay_summa = format_currency(pay[2])
|
||||
pay_html = f'<td class="left-align" colspan="2">{pay_name}</td><td class="center-align">{pay_period}</td><td>{pay_summa}</td>'
|
||||
else:
|
||||
pay_html = '<td style="border-top: none; border-bottom: none; border-left: none; border-right: none;" colspan="4"></td>'
|
||||
|
||||
tbody_html += f"<tr>{nc_html}{pay_html}</tr>"
|
||||
|
||||
tbody_html += f"""
|
||||
<tr class="totals">
|
||||
<td class="left-align" colspan="5">Всего доходов в неденежной форме</td><td>{format_currency(total_noncash)}</td>
|
||||
<td class="left-align" colspan="3">Всего выплачено</td><td>{format_currency(total_payouts)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="left-align" colspan="5">Долг за предприятием на начало месяца</td><td>{format_currency(debt_comp_start)}</td>
|
||||
<td class="left-align" colspan="3">Долг за предприятием на конец месяца</td><td>{format_currency(debt_comp_end)}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="left-align" colspan="5">Долг за работником на начало месяца</td><td>{format_currency(debt_emp_start)}</td>
|
||||
<td class="left-align" colspan="3">Долг за работником на конец месяца</td><td>{format_currency(debt_emp_end)}</td>
|
||||
</tr>
|
||||
<tr class="totals">
|
||||
<td colspan="6" class="empty-cell"></td>
|
||||
<td class="left-align section-title" colspan="3"><b>5. Выплачено денежными средствами</b></td><td class="section-title"><b>{format_currency(net_pay)}</b></td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
html_template = f"""
|
||||
<!DOCTYPE html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<style>
|
||||
@page {{ size: A4 landscape; margin: 15mm; background-color: #ffffff; }}
|
||||
body {{ font-family: Arial, sans-serif; font-size: 11px; color: #333; margin: 0; padding: 0; }}
|
||||
h2 {{ text-align: center; margin-bottom: 15px; font-size: 16px; font-weight: normal; }}
|
||||
table {{ width: 100%; border-collapse: collapse; margin-bottom: 10px; }}
|
||||
th, td {{ border: 1px solid #a0a0a0; padding: 5px 8px; text-align: right; vertical-align: middle; }}
|
||||
th {{ background-color: #f2f2f2; text-align: center; font-weight: normal; color: #555; }}
|
||||
.header-table td {{ border: 1px solid #a0a0a0; text-align: left; background-color: #fcfcfc; }}
|
||||
.left-align {{ text-align: left; }}
|
||||
.center-align {{ text-align: center; }}
|
||||
.section-title {{ background-color: #fcfcfc; font-weight: bold; text-align: left; padding-top: 10px; padding-bottom: 10px; }}
|
||||
.totals {{ background-color: #fcfcfc; font-weight: bold; }}
|
||||
.empty-cell {{ border: none; }}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2><b>Расчетный лист за {month_name} {target_tyear} г.</b></h2>
|
||||
<table class="header-table" style="margin-bottom: 15px;">
|
||||
<tr><td style="width: 60%;">Система оплаты: {header_sys}</td><td style="width: 40%;">Оклад: {format_currency(header_wage)}</td></tr>
|
||||
<tr><td colspan="2">Стандартные вычеты за месяц: {format_currency(header_deduct)}</td></tr>
|
||||
<tr><td colspan="2">Облагаемая база по ставке 13%: {format_currency(header_base)}</td></tr>
|
||||
</table>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 20%;">Вид</th><th>Дни</th><th>Часы</th><th>%</th><th>Период</th><th>Сумма</th>
|
||||
<th style="width: 20%;">Вид</th><th>%</th><th>Период</th><th>Сумма</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{tbody_html}
|
||||
</tbody>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
temp_pdf = tempfile.NamedTemporaryFile(delete=False, suffix='.pdf')
|
||||
HTML(string=html_template).write_pdf(temp_pdf.name)
|
||||
return temp_pdf.name
|
||||
|
||||
def build_payslip_data(cursor, card_id: str, target_cmonth: int) -> tuple:
|
||||
tyear = (target_cmonth - 1) // 12
|
||||
tmonth = (target_cmonth - 1) % 12 + 1
|
||||
_, last_day = calendar.monthrange(tyear, tmonth)
|
||||
|
||||
date_start = f"{tyear}-{tmonth:02d}-01"
|
||||
date_end = f"{tyear}-{tmonth:02d}-{last_day:02d}"
|
||||
date_params = (date_start, date_start, date_end, date_end, date_end)
|
||||
|
||||
months_ru = {1: "Январь", 2: "Февраль", 3: "Март", 4: "Апрель", 5: "Май", 6: "Июнь",
|
||||
7: "Июль", 8: "Август", 9: "Сентябрь", 10: "Октябрь", 11: "Ноябрь", 12: "Декабрь"}
|
||||
month_name = months_ru.get(tmonth, f"{tmonth:02d}")
|
||||
|
||||
# FIRM_ID по c.ID
|
||||
q_firm = """
|
||||
select e.FIRM_ID, f.SNAME from (select e.id, e.FIRM_ID from UOV_SELFSERVICE_PR_EMP e
|
||||
join UOV_SELFSERVICE_PR_CARD c on c.ID=e.CARD_ID where c.ID = %s
|
||||
group by e.ID, e.FIRM_ID ) as e
|
||||
join UOV_SELFSERVICE_PR_TRANS t on t.EMP_ID = e.ID
|
||||
join UOV_SELFSERVICE_HR_FIRM f on f.ID = e.FIRM_ID
|
||||
where ((t.D_FROM <= %s and t.D_TO >= %s) or (t.D_FROM <= %s and t.D_TO >= %s) or (t.D_FROM <= %s and t.D_TO = '1999-12-31'))
|
||||
group by e.FIRM_ID, f.SNAME
|
||||
"""
|
||||
cursor.execute(q_firm, (card_id, *date_params))
|
||||
firm_row = cursor.fetchone()
|
||||
if not firm_row:
|
||||
raise Exception("Не удалось определить FIRM_ID для сотрудника в указанном периоде.")
|
||||
firm_id = firm_row[0]
|
||||
|
||||
# ШАПКА по c.ID
|
||||
q_header = """
|
||||
select s.NAME as [NAME], ROUND(t.WAGE, 2), isnull(sum(yt.u21 + yt.u30), 0), ROUND(isnull(sum(yt.n200 + yt.n5100 - yt.u21 - yt.u30 - yt.u42 - yt.u99), 0), 2)
|
||||
from (select e.ID, e.FIRM_ID, e.CARD_ID from UOV_SELFSERVICE_PR_EMP e join UOV_SELFSERVICE_PR_CARD c on c.ID=e.CARD_ID
|
||||
where c.ID = %s and e.FIRM_ID=%s group by e.ID, e.FIRM_ID, e.CARD_ID ) as e
|
||||
join UOV_SELFSERVICE_pr_trans t on t.EMP_ID = e.ID join UOV_SELFSERVICE_hr_syspay s on s.ID = t.SYSPAY_ID
|
||||
join UOV_SELFSERVICE_sl_ytax yt on yt.CARD_ID = e.CARD_ID and yt.FIRM_ID = e.FIRM_ID
|
||||
where e.FIRM_ID = %s and yt.tmonth = %s and yt.tyear = %s
|
||||
and ((t.D_FROM <= %s and t.D_TO >= %s) or (t.D_FROM <= %s and t.D_TO >= %s) or (t.D_FROM <= %s and t.D_TO = '1999-12-31'))
|
||||
and t.tp_abbr='CUR' group by s.NAME, t.WAGE, t.D_FROM order by t.D_FROM desc
|
||||
"""
|
||||
cursor.execute(q_header, (card_id, firm_id, firm_id, tmonth, tyear, *date_params))
|
||||
h_row = cursor.fetchone()
|
||||
header_sys = str(h_row[0]) if h_row else "Неизвестная система"
|
||||
header_wage = float(h_row[1]) if h_row and h_row[1] else 0.0
|
||||
header_deduct = float(h_row[2]) if h_row and h_row[2] else 0.0
|
||||
header_base = float(h_row[3]) if h_row and h_row[3] else 0.0
|
||||
|
||||
# НАЧИСЛЕНО по c.ID
|
||||
q_earn = """
|
||||
select p.NAME AS [NAME], ROUND(l.days, 1), ROUND(l.hours, 1), CAST(l.prc as INT), convert(varchar, i.D_FROM,104)+'-'+convert(varchar, i.D_TO,104), ROUND(l.summa, 2)
|
||||
from (select e.id, e.FIRM_ID from UOV_SELFSERVICE_PR_EMP e join UOV_SELFSERVICE_PR_CARD c on c.ID=e.CARD_ID where c.ID = %s and e.FIRM_ID=%s group by e.ID, FIRM_ID ) as e
|
||||
join UOV_SELFSERVICE_sl_lic l on l.EMP_ID = e.ID join UOV_SELFSERVICE_sl_pay p on p.ID = l.PAY_ID join UOV_SELFSERVICE_pr_ill i on i.ID=l.ID_MAIN
|
||||
join (select MAX(id) as id, EMP_ID from UOV_SELFSERVICE_pr_trans t where ((t.D_FROM <= %s and t.D_TO >= %s) or (t.D_FROM <= %s and t.D_TO >= %s) or (t.D_FROM <= %s and t.D_TO = '1999-12-31')) group by EMP_ID) tr on tr.EMP_ID = e.ID
|
||||
where l.t_pay = 1 and l.pay_id not in (264,301,262) and l.n_dirm = 6 and l.cmonth = %s
|
||||
UNION ALL
|
||||
select max(p.NAME) AS [NAME], sum(l.days), ROUND(sum(l.hours), 1), CAST(l.prc as INT), (DateName(month, DateAdd(month, l.TMONTH, 0) - 1) +' '+ cast(l.TYEAR as varchar)), ROUND(isnull(sum(l.summa),0), 2)
|
||||
from(select e.id, e.FIRM_ID from UOV_SELFSERVICE_PR_EMP e join UOV_SELFSERVICE_PR_CARD c on c.ID=e.CARD_ID where c.ID = %s and e.FIRM_ID=%s group by e.ID, FIRM_ID ) as e
|
||||
join (select MAX(id) as id, EMP_ID from UOV_SELFSERVICE_pr_trans t where ((t.D_FROM <= %s and t.D_TO >= %s) or (t.D_FROM <= %s and t.D_TO >= %s) or (t.D_FROM <= %s and t.D_TO = '1999-12-31')) group by EMP_ID) tr on tr.EMP_ID = e.ID
|
||||
join UOV_SELFSERVICE_sl_lic l on l.EMP_ID = e.ID join UOV_SELFSERVICE_sl_pay p on p.ID = l.PAY_ID
|
||||
where l.t_pay = 1 and l.pay_id not in (264,301,262) and l.n_dirm <> 6 and l.cmonth = %s
|
||||
group by l.emp_id,l.pay_id,l.tmonth,l.prc, case l.PAY_ID when 17 then l.id else 0 end, l.TYEAR order by 6 DESC
|
||||
"""
|
||||
cursor.execute(q_earn, (card_id, firm_id, *date_params, target_cmonth, card_id, firm_id, *date_params, target_cmonth))
|
||||
earnings_rows = cursor.fetchall()
|
||||
|
||||
# УДЕРЖАНО по c.ID
|
||||
q_deduct = """
|
||||
select max(p.NAME) AS [NAME], CAST(l.prc as INT), ISNULL(DATENAME(month, DATEADD(month, l.tmonth, 0) - 1) + ' ' + CAST(NULLIF(l.TYEAR, 0) AS varchar), ''), ROUND(isnull(sum(l.summa),0), 2)
|
||||
from (select e.id, e.FIRM_ID from UOV_SELFSERVICE_PR_EMP e join UOV_SELFSERVICE_PR_CARD c on c.ID=e.CARD_ID where c.ID = %s and e.FIRM_ID=%s group by e.ID, FIRM_ID ) as e
|
||||
JOIN UOV_SELFSERVICE_sl_lic l ON l.EMP_ID = e.ID JOIN UOV_SELFSERVICE_sl_pay p ON p.ID = l.PAY_ID
|
||||
join (select MAX(id) as id, EMP_ID from UOV_SELFSERVICE_pr_trans t where ((t.D_FROM <= %s and t.D_TO >= %s) or (t.D_FROM <= %s and t.D_TO >= %s) or (t.D_FROM <= %s and t.D_TO = '1999-12-31')) group by EMP_ID) tr on tr.EMP_ID = e.ID
|
||||
where l.t_pay=2 and l.pay_id not in (301,490) and l.pay_id not in (305, 308, 316, 328, 360, 364) and l.cmonth = %s
|
||||
group by l.emp_id,l.pay_id,l.tmonth,l.prc, l.id, l.TYEAR order by 4 DESC
|
||||
"""
|
||||
cursor.execute(q_deduct, (card_id, firm_id, *date_params, target_cmonth))
|
||||
deductions_rows = cursor.fetchall()
|
||||
|
||||
# ДОЛГИ по c.ID
|
||||
q_debts = """
|
||||
select l.pay_id, ROUND(isnull(sum(l.summa),0), 2) from (select e.ID, e.FIRM_ID from UOV_SELFSERVICE_PR_EMP e join UOV_SELFSERVICE_PR_CARD c on c.ID=e.CARD_ID where c.ID = %s and e.FIRM_ID=%s group by e.ID, FIRM_ID ) as e
|
||||
join UOV_SELFSERVICE_sl_lic l on l.EMP_ID = e.id join (select MAX(id) as id, EMP_ID from UOV_SELFSERVICE_pr_trans t where ((t.D_FROM <= %s and t.D_TO >= %s) or (t.D_FROM <= %s and t.D_TO >= %s) or (t.D_FROM <= %s and t.D_TO = '1999-12-31')) group by EMP_ID) tr on tr.EMP_ID = e.ID
|
||||
where l.cmonth = %s and l.pay_id in (264, 301, 490) group by l.pay_id
|
||||
"""
|
||||
cursor.execute(q_debts, (card_id, firm_id, *date_params, target_cmonth))
|
||||
debt_rows = cursor.fetchall()
|
||||
debt_comp_start, debt_emp_start, debt_emp_end = 0.0, 0.0, 0.0
|
||||
for r in debt_rows:
|
||||
if r[0] == 264: debt_comp_start = float(r[1])
|
||||
elif r[0] == 301: debt_emp_start = float(r[1])
|
||||
elif r[0] == 490: debt_emp_end = float(r[1])
|
||||
|
||||
# НЕ ДЕНЕЖНЫЕ по c.ID
|
||||
q_noncash = """
|
||||
select t.NAME AS [NAME], (DateName(month, CAST(%s as date)) +' '+ DateName(YEAR, CAST(%s as date))), ROUND(isnull(p.summa, 0), 2)
|
||||
from (select e.id, e.FIRM_ID from UOV_SELFSERVICE_PR_EMP e join UOV_SELFSERVICE_PR_CARD c on c.ID=e.CARD_ID where c.ID = %s and e.FIRM_ID=%s group by e.ID, FIRM_ID ) as e
|
||||
join UOV_SELFSERVICE_sl_profit p on p.EMP_ID = e.ID join UOV_SELFSERVICE_sl_tprofit t on t.ID = p.TPROFIT_ID
|
||||
join (select MAX(id) as id, EMP_ID from UOV_SELFSERVICE_pr_trans tr where ((tr.D_FROM <= %s and tr.D_TO >= %s) or (tr.D_FROM <= %s and tr.D_TO >= %s) or (tr.D_FROM <= %s and tr.D_TO = '1999-12-31')) group by EMP_ID) tr2 on tr2.EMP_ID = e.ID
|
||||
where p.cmonth = %s and t.id != 9999
|
||||
"""
|
||||
cursor.execute(q_noncash, (date_start, date_start, card_id, firm_id, *date_params, target_cmonth))
|
||||
noncash_rows = cursor.fetchall()
|
||||
|
||||
# ВЫПЛАЧЕНО по c.ID
|
||||
q_payout = """
|
||||
select p.NAME AS [NAME], (DateName(month, DateAdd(month, l.TMONTH, 0) - 1) +' '+ cast(l.TYEAR as varchar)), ROUND(sum(isnull(l.summa, 0)), 2)
|
||||
from (select e.id, e.FIRM_ID from UOV_SELFSERVICE_PR_EMP e join UOV_SELFSERVICE_PR_CARD c on c.ID=e.CARD_ID where c.ID = %s and e.FIRM_ID=%s group by e.ID, FIRM_ID ) as e
|
||||
join UOV_SELFSERVICE_sl_lic l on l.EMP_ID = e.ID join UOV_SELFSERVICE_sl_pay p on p.ID =l.PAY_ID
|
||||
join (select MAX(id) as id, EMP_ID from UOV_SELFSERVICE_pr_trans tr where ((tr.D_FROM <= %s and tr.D_TO >= %s) or (tr.D_FROM <= %s and tr.D_TO >= %s) or (tr.D_FROM <= %s and tr.D_TO = '1999-12-31')) group by EMP_ID) tr2 on tr2.EMP_ID = e.ID
|
||||
where l.t_pay = 2 and l.cmonth = %s and l.pay_id in (305, 308, 316, 328, 360, 364) group by p.NAME, l.TMONTH, l.TYEAR
|
||||
"""
|
||||
cursor.execute(q_payout, (card_id, firm_id, *date_params, target_cmonth))
|
||||
payout_rows = cursor.fetchall()
|
||||
|
||||
total_earned = sum(float(r[5]) for r in earnings_rows if r[5])
|
||||
total_withheld = sum(float(r[3]) for r in deductions_rows if r[3])
|
||||
total_noncash = sum(float(r[2]) for r in noncash_rows if r[2])
|
||||
total_payouts = sum(float(r[2]) for r in payout_rows if r[2])
|
||||
|
||||
# Расчет долга по формуле СибЦема
|
||||
debt_comp_end = total_earned - total_withheld - total_payouts
|
||||
net_pay = total_earned - total_withheld
|
||||
|
||||
response = f"💰 <b>Расчетный листок за {month_name} {tyear} г.</b>\n\n<b>1. Начислено</b>\n"
|
||||
for row in earnings_rows:
|
||||
time_spent = f" ({float(row[1]):g} дн. / {float(row[2]):g} ч.)" if (row[1] or row[2]) else ""
|
||||
response += f"🔹 {str(row[0]).strip()}{time_spent}: <b>{format_currency(row[5])} руб.</b>\n"
|
||||
response += f"<b>Всего начислено: {format_currency(total_earned)} руб.</b>\n\n<b>2. Удержано</b>\n"
|
||||
for row in deductions_rows:
|
||||
prc_str = f" ({int(row[1])}%)" if row[1] else ""
|
||||
response += f"🔹 {str(row[0]).strip()}{prc_str}: <b>{format_currency(row[3])} руб.</b>\n"
|
||||
response += f"<b>Всего удержано: {format_currency(total_withheld)} руб.</b>\n\n<b>3. Доходы в неденежной форме:</b> {format_currency(total_noncash)} руб.\n\n<b>4. Выплачено</b>\n"
|
||||
for row in payout_rows:
|
||||
response += f"🔹 {str(row[0]).strip()}: <b>{format_currency(row[2])} руб.</b>\n"
|
||||
response += f"<b>Всего выплачено: {format_currency(total_payouts)} руб.</b>\n\n<i>Долг за предприятием на конец месяца: {format_currency(debt_comp_end)} руб.</i>\n<i>Долг за работником на конец месяца: {format_currency(debt_emp_end)} руб.</i>\n\n<b>5. Выплачено денежными средствами: {format_currency(net_pay)} руб.</b>"
|
||||
|
||||
pdf_path = create_payslip_pdf(
|
||||
month_name, tyear, header_sys, header_wage, header_deduct, header_base,
|
||||
earnings_rows, deductions_rows, noncash_rows, payout_rows,
|
||||
total_earned, total_withheld, total_noncash, total_payouts,
|
||||
debt_comp_start, debt_emp_start, debt_comp_end, debt_emp_end, net_pay
|
||||
)
|
||||
return response, pdf_path
|
||||
@@ -0,0 +1,213 @@
|
||||
import re
|
||||
from datetime import datetime
|
||||
|
||||
import pymssql
|
||||
from ldap3 import Server, Connection, ALL
|
||||
from ldap3.utils.conv import escape_filter_chars
|
||||
|
||||
|
||||
# =========================================================
|
||||
# НАСТРОЙКИ
|
||||
# =========================================================
|
||||
|
||||
TEST_USER = "man.bogov@tcs.sibcem.ru"
|
||||
|
||||
AD_SERVER = "ldap://172.16.20.20"
|
||||
AD_BASE = "OU=-Пользователи,DC=sibcem,DC=ru"
|
||||
|
||||
AD_USER = "sdesk-mail"
|
||||
AD_PASSWORD = "fne?e!q.m8phcrGVAcqr"
|
||||
|
||||
SQL_SERVER = "SRVKEM-MOBILEIN.sibcem.ru"
|
||||
SQL_DB_NAME = "BossCopy"
|
||||
|
||||
DB_USER = "SIBCEM\bot_tcs"
|
||||
DB_PASSWORD = "Bav:fX#UwH8%atv4"
|
||||
|
||||
|
||||
# =========================================================
|
||||
# AD
|
||||
# =========================================================
|
||||
|
||||
def get_phone_from_ad(user_id: str) -> str:
|
||||
|
||||
server = Server(AD_SERVER, get_info=ALL)
|
||||
|
||||
conn = Connection(
|
||||
server,
|
||||
user=AD_USER,
|
||||
password=AD_PASSWORD,
|
||||
auto_bind=True
|
||||
)
|
||||
|
||||
safe_user_id = escape_filter_chars(user_id)
|
||||
|
||||
short_username = user_id.split("@")[0]
|
||||
|
||||
search_filter = (
|
||||
f"(&(objectClass=user)"
|
||||
f"(|(mail={safe_user_id})"
|
||||
f"(userPrincipalName={safe_user_id})"
|
||||
f"(userPrincipalName={escape_filter_chars(f'{short_username}@sibcem.ru')})"
|
||||
f"(sAMAccountName={escape_filter_chars(short_username)})))"
|
||||
)
|
||||
|
||||
conn.search(
|
||||
AD_BASE,
|
||||
search_filter,
|
||||
attributes=['mobile', 'telephoneNumber']
|
||||
)
|
||||
|
||||
if not conn.entries:
|
||||
raise Exception("Пользователь не найден в AD")
|
||||
|
||||
entry = conn.entries[0]
|
||||
|
||||
raw_phone = (
|
||||
str(entry.mobile.value)
|
||||
if 'mobile' in entry and entry.mobile.value
|
||||
else str(entry.telephoneNumber.value)
|
||||
if 'telephoneNumber' in entry and entry.telephoneNumber.value
|
||||
else None
|
||||
)
|
||||
|
||||
if not raw_phone:
|
||||
raise Exception("Телефон не найден")
|
||||
|
||||
clean_phone = re.sub(r'\D', '', raw_phone)
|
||||
|
||||
if clean_phone.startswith("8") and len(clean_phone) == 11:
|
||||
return "7" + clean_phone[1:]
|
||||
|
||||
if len(clean_phone) == 10:
|
||||
return "7" + clean_phone
|
||||
|
||||
return clean_phone
|
||||
|
||||
|
||||
# =========================================================
|
||||
# SQL
|
||||
# =========================================================
|
||||
|
||||
def get_db_connection():
|
||||
|
||||
return pymssql.connect(
|
||||
server=SQL_SERVER,
|
||||
user=DB_USER,
|
||||
password=DB_PASSWORD,
|
||||
database=SQL_DB_NAME,
|
||||
charset='cp1251'
|
||||
)
|
||||
|
||||
|
||||
# =========================================================
|
||||
# MAIN
|
||||
# =========================================================
|
||||
|
||||
def main():
|
||||
|
||||
print("=" * 80)
|
||||
print("ТЕСТ РАСЧЕТНЫХ ЛИСТКОВ")
|
||||
print("=" * 80)
|
||||
|
||||
date = datetime.now().date()
|
||||
|
||||
cmonth = date.year * 12 + date.month
|
||||
|
||||
print(f"Дата: {date}")
|
||||
print(f"CMONTH: {cmonth}")
|
||||
|
||||
# -----------------------------------------------------
|
||||
|
||||
print("\n[1] Получение телефона из AD")
|
||||
|
||||
phone_full = get_phone_from_ad(TEST_USER)
|
||||
|
||||
print(f"Телефон из AD: {phone_full}")
|
||||
|
||||
phone = phone_full[1:].strip()
|
||||
|
||||
print(f"Телефон для SQL: {phone}")
|
||||
|
||||
# -----------------------------------------------------
|
||||
|
||||
print("\n[2] Подключение к SQL")
|
||||
|
||||
conn = get_db_connection()
|
||||
|
||||
cursor = conn.cursor()
|
||||
|
||||
# -----------------------------------------------------
|
||||
|
||||
print("\n[3] Поиск PR_CARD")
|
||||
|
||||
query = """
|
||||
SELECT
|
||||
ID,
|
||||
PHONE
|
||||
FROM UOV_SELFSERVICE_PR_CARD
|
||||
WHERE PHONE = %s
|
||||
"""
|
||||
|
||||
print(query)
|
||||
|
||||
cursor.execute(query, (phone,))
|
||||
|
||||
rows = cursor.fetchall()
|
||||
|
||||
print(f"\nНайдено записей: {len(rows)}")
|
||||
|
||||
if rows:
|
||||
|
||||
for row in rows:
|
||||
|
||||
print("-" * 60)
|
||||
print(f"ID: {row[0]}")
|
||||
print(f"PHONE: {row[1]}")
|
||||
|
||||
else:
|
||||
|
||||
print("\n❌ Запись не найдена")
|
||||
|
||||
print("\n[4] Поиск похожих телефонов")
|
||||
|
||||
query2 = """
|
||||
SELECT TOP 20
|
||||
ID,
|
||||
PHONE
|
||||
FROM UOV_SELFSERVICE_PR_CARD
|
||||
WHERE
|
||||
PHONE LIKE %s
|
||||
OR RIGHT(RTRIM(PHONE), 10) = %s
|
||||
"""
|
||||
|
||||
phone_tail = phone[-10:]
|
||||
|
||||
cursor.execute(
|
||||
query2,
|
||||
(
|
||||
f"%{phone_tail}%",
|
||||
phone_tail
|
||||
)
|
||||
)
|
||||
|
||||
rows2 = cursor.fetchall()
|
||||
|
||||
print(f"\nПохожих записей: {len(rows2)}")
|
||||
|
||||
for row in rows2:
|
||||
|
||||
print("-" * 60)
|
||||
print(f"ID: {row[0]}")
|
||||
print(f"PHONE: {row[1]}")
|
||||
|
||||
conn.close()
|
||||
|
||||
print("\n")
|
||||
print("=" * 80)
|
||||
print("ГОТОВО")
|
||||
print("=" * 80)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python3
|
||||
# /opt/trueconf_bot/test_secure_connect.py
|
||||
|
||||
import sys
|
||||
import os
|
||||
import pymssql
|
||||
|
||||
# Добавляем путь к проекту для корректных импортов конфигурации
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
try:
|
||||
print("🔄 Загружаем конфигурацию и выгружаем секреты из Passwork...")
|
||||
import config.config as config
|
||||
except ImportError as e:
|
||||
print(f"❌ Ошибка импорта config.py: {e}")
|
||||
sys.exit(1)
|
||||
|
||||
def main():
|
||||
# Технические параметры целевого сервера
|
||||
SERVER = '172.16.20.203'
|
||||
DATABASE = 'iskmdax_model'
|
||||
|
||||
# Извлекаем данные из автоматически загруженного пула
|
||||
USER = getattr(config, 'DB_USER', None)
|
||||
PASSWORD = getattr(config, 'DB_PASSWORD', None)
|
||||
|
||||
if not USER or not PASSWORD:
|
||||
print("❌ Ошибка: Учетные данные для подключения к БД не найдены в Passwork!")
|
||||
sys.exit(1)
|
||||
|
||||
print(f"🔌 Устанавливаем ЗАЩИЩЕННОЕ соединение (encryption='require')...")
|
||||
print(f" Сервер: {SERVER}")
|
||||
print(f" База: {DATABASE}")
|
||||
print(f" Логин: [ДАННЫЕ СКРЫТЫ]")
|
||||
print("-" * 60)
|
||||
|
||||
try:
|
||||
conn = pymssql.connect(
|
||||
server=SERVER,
|
||||
user=USER,
|
||||
password=PASSWORD,
|
||||
database=DATABASE,
|
||||
charset='cp1251',
|
||||
encryption='require' # Строгое требование SSL/TLS
|
||||
)
|
||||
cursor = conn.cursor()
|
||||
print("✅ Защищенное TLS/SSL соединение с сервером успешно установлено!")
|
||||
|
||||
# Проверочный SQL-запрос
|
||||
query = "SELECT TOP(5) * FROM dbo.Layer;"
|
||||
print(f"🏃 Выполняем тестовый запрос: {query}\n")
|
||||
|
||||
cursor.execute(query)
|
||||
rows = cursor.fetchall()
|
||||
|
||||
if rows:
|
||||
print(f"🎯 Успешно получено строк: {len(rows)}")
|
||||
for idx, row in enumerate(rows, 1):
|
||||
# Печатаем содержимое строки как есть
|
||||
print(f" 🔹 Строка #{idx}: {row}")
|
||||
else:
|
||||
print("📭 Запрос выполнен успешно, но целевая таблица оказалась пустой.")
|
||||
|
||||
conn.close()
|
||||
print("\n🏁 Тест защищенного соединения завершен успешно!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ Ошибка подключения или выполнения запроса!")
|
||||
error_msg = str(e)
|
||||
if "Login failed" in error_msg or "18456" in error_msg:
|
||||
print("Детали: Ошибка аутентификации (проверьте корректность учетных данных в Passwork).")
|
||||
else:
|
||||
print(f"Детали: {error_msg}")
|
||||
sys.exit(1)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user