BUH_bot/bot.py
Игорь Надеин 5b6ab02e53 Initial commit: Core bot application
- Add main bot file with Telegram handlers
- Add configuration module
- Add Python dependencies
- Add modules: excel_generator, moysklad_client, xml_generator, user_manager
- Add .gitignore for Python, Docker, user data
2025-11-03 14:19:48 +03:00

1106 lines
40 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Telegram бот для работы с отгрузками МойСклад
Функции:
- Поиск отгрузок (по номеру заказа или отгрузки)
- Пагинация списка отгрузок
- Валидация данных отгрузки
- Генерация документов (XML/Excel)
- Выбор печатных форм из МойСклад
"""
import os
import sys
import logging
from pathlib import Path
from telegram import (
Update,
ReplyKeyboardMarkup,
InlineKeyboardButton,
InlineKeyboardMarkup
)
from telegram.ext import (
Application,
CommandHandler,
MessageHandler,
CallbackQueryHandler,
ContextTypes,
ConversationHandler,
filters
)
# Добавляем путь к modules
sys.path.insert(0, os.path.join(os.path.dirname(__file__), 'modules'))
from moysklad_client import MoySkladClient # noqa: E402
from validator import OrderValidator # noqa: E402
from xml_generator import XMLGenerator # noqa: E402
from user_manager import UserManager # noqa: E402
from config import settings # noqa: E402
# Настройка логирования
logging.basicConfig(
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
level=logging.INFO
)
logger = logging.getLogger(__name__)
# Состояния разговора
(
AUTH_LOGIN,
AUTH_PASSWORD,
MAIN_MENU,
SEARCH_ORDER,
SHOW_ORDERS,
SELECT_ORDER,
VALIDATE_ORDER,
SELECT_FORMAT,
SELECT_TEMPLATE,
DOWNLOAD_DOCUMENT
) = range(10)
# Менеджер пользователей
user_manager = UserManager()
def get_moysklad_client(user_id: int) -> MoySkladClient:
"""
Получает клиента МойСклад для конкретного пользователя
Args:
user_id: ID пользователя Telegram
Returns:
MoySkladClient с учетными данными пользователя
"""
credentials = user_manager.get_user_credentials(user_id)
if not credentials:
raise ValueError(f"Пользователь {user_id} не авторизован")
return MoySkladClient(
credentials['login'],
credentials['password'],
settings.moysklad_api_url
)
def get_main_keyboard():
"""Возвращает основную клавиатуру"""
keyboard = [
["🔍 Поиск отгрузки", "📋 Список отгрузок"],
["❓ Помощь"]
]
return ReplyKeyboardMarkup(keyboard, resize_keyboard=True)
async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработчик команды /start"""
user = update.effective_user
user_id = user.id
# Проверяем, авторизован ли пользователь
if not user_manager.is_user_registered(user_id):
await update.message.reply_text(
f"Привет, {user.first_name}! 👋\n\n"
f"Для начала работы необходимо авторизоваться в МойСклад.\n\n"
f"Пожалуйста, введите ваш <b>логин</b> от МойСклад:",
parse_mode='HTML'
)
return AUTH_LOGIN
# Пользователь авторизован - показываем главное меню
await update.message.reply_text(
f"С возвращением, {user.first_name}! 👋\n\n"
f"Выберите действие:",
reply_markup=get_main_keyboard()
)
# Очищаем данные пользователя
context.user_data.clear()
return MAIN_MENU
async def auth_login(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработчик ввода логина"""
login = update.message.text.strip()
# Сохраняем логин временно
context.user_data['temp_login'] = login
await update.message.reply_text(
"✅ Логин принят.\n\n"
"Теперь введите ваш <b>пароль</b> от МойСклад:\n\n"
"⚠️ Пароль будет зашифрован и сохранён безопасно.",
parse_mode='HTML'
)
return AUTH_PASSWORD
async def auth_password(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработчик ввода пароля и завершение авторизации"""
user = update.effective_user
user_id = user.id
password = update.message.text.strip()
login = context.user_data.get('temp_login')
# Удаляем сообщение с паролем для безопасности
try:
await update.message.delete()
except Exception:
pass
if not login:
await update.message.reply_text(
"❌ Ошибка: логин не найден. Начните заново с команды /start"
)
return ConversationHandler.END
# Проверяем учетные данные
await update.message.reply_text("⏳ Проверяю учетные данные...")
try:
# Создаём временный клиент для проверки
test_client = MoySkladClient(
login, password, settings.moysklad_api_url
)
# Пробуем получить данные для проверки авторизации
result = test_client.get_orders(limit=1)
if result and 'rows' in result:
# Авторизация успешна - сохраняем пользователя
success = user_manager.register_user(
user_id,
login,
password,
user.username
)
if success:
# Очищаем временные данные
context.user_data.pop('temp_login', None)
await update.message.reply_text(
"✅ <b>Авторизация успешна!</b>\n\n"
"Ваши учетные данные сохранены и зашифрованы.\n\n"
"Выберите действие:",
parse_mode='HTML',
reply_markup=get_main_keyboard()
)
logger.info(f"Пользователь {user_id} успешно авторизован")
return MAIN_MENU
else:
await update.message.reply_text(
"❌ Ошибка сохранения данных. "
"Попробуйте позже или обратитесь к администратору."
)
return ConversationHandler.END
else:
await update.message.reply_text(
"❌ <b>Ошибка авторизации</b>\n\n"
"Неверный логин или пароль. Попробуйте снова командой /start",
parse_mode='HTML'
)
context.user_data.pop('temp_login', None)
return ConversationHandler.END
except Exception as e:
logger.error(f"Ошибка авторизации пользователя {user_id}: {e}")
await update.message.reply_text(
"❌ <b>Ошибка авторизации</b>\n\n"
f"Не удалось подключиться к МойСклад.\n"
f"Проверьте правильность логина и пароля.\n\n"
f"Детали: {str(e)[:100]}",
parse_mode='HTML'
)
context.user_data.pop('temp_login', None)
return ConversationHandler.END
async def help_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Показывает помощь"""
help_text = (
"📖 <b>Инструкция по использованию</b>\n\n"
"🔍 <b>Поиск отгрузки</b>\n"
"Введите номер заказа или отгрузки для поиска.\n"
"Например: 951 или 00001\n\n"
"📋 <b>Список отгрузок</b>\n"
"Показывает последние отгрузки с пагинацией.\n"
"Используйте кнопки для навигации.\n\n"
"🔐 <b>Учетные данные</b>\n"
"/logout - Выйти из аккаунта МойСклад\n"
"/change_credentials - Сменить логин/пароль\n\n"
"✅ <b>Валидация</b>\n"
"После выбора отгрузки бот автоматически проверит:\n"
"- Заполненность обязательных полей\n"
"- Корректность ИНН/КПП\n"
"- Соответствие сумм\n\n"
"📄 <b>Генерация документов</b>\n"
"После успешной валидации можно:\n"
"- Скачать XML для ЭДО\n"
"- Сгенерировать Excel (УПД, ТН и др.)"
)
await update.message.reply_text(
help_text,
parse_mode='HTML',
reply_markup=get_main_keyboard()
)
async def logout_command(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Выход из аккаунта МойСклад"""
user_id = update.effective_user.id
if user_manager.delete_user(user_id):
await update.message.reply_text(
"✅ Вы вышли из аккаунта.\n\n"
"Ваши учетные данные удалены.\n\n"
"Используйте /start для новой авторизации."
)
logger.info(f"Пользователь {user_id} вышел из аккаунта")
else:
await update.message.reply_text(
"❌ Вы не были авторизованы."
)
return ConversationHandler.END
async def change_credentials_command(
update: Update,
context: ContextTypes.DEFAULT_TYPE
):
"""Смена учетных данных"""
user_id = update.effective_user.id
if not user_manager.is_user_registered(user_id):
await update.message.reply_text(
"❌ Вы не авторизованы. Используйте /start"
)
return ConversationHandler.END
# Удаляем старые данные
user_manager.delete_user(user_id)
await update.message.reply_text(
"🔄 <b>Смена учетных данных</b>\n\n"
"Введите новый <b>логин</b> от МойСклад:",
parse_mode='HTML'
)
return AUTH_LOGIN
return MAIN_MENU
async def search_order_prompt(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Запрашивает номер заказа для поиска"""
await update.message.reply_text(
"🔍 Введите номер заказа или отгрузки:\n\n"
"Например: 951, 00001 или полный UUID",
reply_markup=ReplyKeyboardMarkup(
[["⬅️ Назад"]],
resize_keyboard=True
)
)
return SEARCH_ORDER
async def search_order(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Ищет заказ по номеру"""
user_id = update.effective_user.id
query = update.message.text.strip()
if query == "⬅️ Назад":
return await start(update, context)
# Показываем индикатор загрузки
msg = await update.message.reply_text("🔄 Поиск...")
try:
# Получаем клиента для пользователя
moysklad_client = get_moysklad_client(user_id)
# Ищем через API
search_result = moysklad_client.search_orders(query, limit=10)
orders = search_result.get("rows", [])
if not orders:
await msg.edit_text(
"❌ Отгрузки не найдены.\n\n"
"Попробуйте другой запрос:"
)
await update.message.reply_text(
"Введите номер заказа или отгрузки:",
reply_markup=ReplyKeyboardMarkup(
[["⬅️ Назад"]],
resize_keyboard=True
)
)
return SEARCH_ORDER
# Сохраняем результаты
context.user_data['orders'] = orders
context.user_data['page'] = 0
# Показываем результаты
await msg.delete()
await show_orders_page(update, context)
return SELECT_ORDER
except Exception as e:
logger.error(f"Ошибка поиска: {e}")
await msg.edit_text(
f"❌ Ошибка поиска: {str(e)}\n\n"
f"Попробуйте снова:"
)
await update.message.reply_text(
"Введите номер заказа или отгрузки:",
reply_markup=ReplyKeyboardMarkup(
[["⬅️ Назад"]],
resize_keyboard=True
)
)
return SEARCH_ORDER
async def show_orders_list(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Показывает список последних отгрузок"""
user_id = update.effective_user.id
msg = await update.message.reply_text("🔄 Загрузка списка...")
try:
# Получаем клиента для пользователя
moysklad_client = get_moysklad_client(user_id)
# Получаем последние отгрузки
result = moysklad_client.get_orders(limit=25, offset=0)
orders = result.get("rows", [])
if not orders:
await msg.edit_text(
"❌ Отгрузки не найдены.",
reply_markup=get_main_keyboard()
)
return MAIN_MENU
# Сохраняем результаты
context.user_data['orders'] = orders
context.user_data['page'] = 0
context.user_data['total'] = result.get("meta", {}).get("size", len(orders))
# Показываем первую страницу
await msg.delete()
await show_orders_page(update, context)
return SELECT_ORDER
except Exception as e:
logger.error(f"Ошибка получения списка: {e}")
await msg.edit_text(
f"❌ Ошибка: {str(e)}",
reply_markup=get_main_keyboard()
)
return MAIN_MENU
async def show_orders_page(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Показывает страницу с отгрузками"""
orders = context.user_data.get('orders', [])
page = context.user_data.get('page', 0)
page_size = 5
start = page * page_size
end = start + page_size
page_orders = orders[start:end]
if not page_orders:
await update.message.reply_text(
"❌ Нет отгрузок на этой странице.",
reply_markup=get_main_keyboard()
)
return MAIN_MENU
# Формируем текст со списком
text = f"📋 <b>Отгрузки (стр. {page + 1})</b>\n\n"
# Создаём inline кнопки для каждой отгрузки
keyboard = []
for i, order in enumerate(page_orders, start=start+1):
order_name = order.get('name', 'N/A')
order_date = order.get('moment', '')[:10] # YYYY-MM-DD
sum_val = order.get('sum', 0) / 100 # Копейки в рубли
agent = order.get('agent', {})
agent_name = agent.get('name', 'N/A')
text += f"{i}. <b>№{order_name}</b> от {order_date}\n"
text += f" Контрагент: {agent_name}\n"
text += f" Сумма: {sum_val:,.2f} ₽\n\n"
# Кнопка выбора
order_id = order['meta']['href'].split('/')[-1].split('?')[0]
keyboard.append([
InlineKeyboardButton(
f"✅ Выбрать №{order_name}",
callback_data=f"select:{order_id}"
)
])
# Кнопки навигации
nav_buttons = []
if page > 0:
nav_buttons.append(
InlineKeyboardButton("⬅️ Назад", callback_data="page:prev")
)
if end < len(orders):
nav_buttons.append(
InlineKeyboardButton("➡️ Вперёд", callback_data="page:next")
)
if nav_buttons:
keyboard.append(nav_buttons)
# Кнопка отмены
keyboard.append([
InlineKeyboardButton("❌ Отмена", callback_data="cancel")
])
reply_markup = InlineKeyboardMarkup(keyboard)
# Отправляем или редактируем сообщение
if update.callback_query:
await update.callback_query.edit_message_text(
text,
parse_mode='HTML',
reply_markup=reply_markup
)
else:
await update.message.reply_text(
text,
parse_mode='HTML',
reply_markup=reply_markup
)
return SELECT_ORDER
async def handle_callback(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработчик inline кнопок"""
query = update.callback_query
await query.answer()
data = query.data
if data == "cancel":
await query.edit_message_text(
"❌ Отменено.",
reply_markup=None
)
await query.message.reply_text(
"Выберите действие:",
reply_markup=get_main_keyboard()
)
return MAIN_MENU
elif data.startswith("page:"):
# Пагинация
direction = data.split(":")[1]
page = context.user_data.get('page', 0)
if direction == "prev":
context.user_data['page'] = max(0, page - 1)
else: # next
context.user_data['page'] = page + 1
await show_orders_page(update, context)
return SELECT_ORDER
elif data.startswith("select:"):
# Выбрана отгрузка
order_id = data.split(":")[1]
context.user_data['selected_order_id'] = order_id
await query.edit_message_text(
"🔄 Загрузка данных отгрузки...",
reply_markup=None
)
# Переходим к валидации
return await validate_order(update, context)
elif data.startswith("format:"):
# Выбран формат документа
format_type = data.split(":")[1]
context.user_data['format'] = format_type
if format_type == "xml":
# XML - сразу генерируем
return await generate_xml(update, context)
else:
# Excel - сбрасываем флаг и показываем шаблоны
context.user_data['show_all_templates'] = False
return await show_templates(update, context)
elif data.startswith("template:"):
# Выбран шаблон
template_id = data.split(":")[1]
context.user_data['template_id'] = template_id
return await generate_excel(update, context)
elif data == "show_all_templates":
# Показать все шаблоны
context.user_data['show_all_templates'] = True
return await show_templates(update, context)
return SELECT_ORDER
async def validate_order(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Валидация отгрузки"""
user_id = update.effective_user.id
order_id = context.user_data.get('selected_order_id')
try:
# Получаем клиента для пользователя
moysklad_client = get_moysklad_client(user_id)
# Получаем полные данные
order_data = moysklad_client.get_full_order_data(order_id)
context.user_data['order_data'] = order_data
# Проводим валидацию
validator = OrderValidator()
# Собираем данные для валидации
validation_data = {
'order': order_data['order'],
'organization': order_data['organization'],
'counterparty': order_data['counterparty'],
'positions': order_data['positions']
}
# Валидируем заказ
validation_results = validator.validate_order(validation_data)
order = order_data['order']
org = order_data['organization']
counterparty = order_data['counterparty']
positions = order_data['positions']
# Проверяем наличие критичных ошибок
has_critical = any(
r.level.name == "CRITICAL"
for r in validation_results
)
has_warnings = any(
r.level.name == "WARNING"
for r in validation_results
)
# Формируем текст с результатами валидации
if has_critical:
status_icon = "❌"
status_text = "<b>Валидация не пройдена!</b>"
elif has_warnings:
status_icon = "⚠️"
status_text = "<b>Валидация выявила предупреждения</b>"
else:
status_icon = "✅"
status_text = "<b>Валидация пройдена успешно!</b>"
validation_text = (
f"{status_icon} {status_text}\n\n"
f"<b>Отгрузка №{order.get('name')}</b>\n"
f"Дата: {order.get('moment', '')[:10]}\n"
f"Сумма: {order.get('sum', 0) / 100:,.2f} ₽\n\n"
f"<b>Продавец:</b> {org.get('name')}\n"
f"ИНН: {org.get('inn')}\n\n"
f"<b>Покупатель:</b> {counterparty.get('name')}\n"
f"ИНН: {counterparty.get('inn')}\n\n"
)
# Добавляем информацию о грузополучателе если есть
# Если грузополучатель не указан явно, используем контрагента
consignee = order_data.get('consignee')
if not consignee:
consignee = counterparty # Используем контрагента
if consignee:
consignee_name = consignee.get('name', 'Не указан')
consignee_inn = consignee.get('inn', 'Не указан')
validation_text += (
f"<b>Грузополучатель:</b> {consignee_name}\n"
f"ИНН: {consignee_inn}\n\n"
)
validation_text += f"<b>Товаров:</b> {len(positions)} шт.\n"
# Добавляем результаты валидации
if validation_results:
validation_text += "\n<b>Результаты проверки:</b>\n"
for result in validation_results[:5]: # Показываем первые 5
level_icon = {
"VALID": "✅",
"WARNING": "⚠️",
"CRITICAL": "❌"
}.get(result.level.name, "ℹ️")
validation_text += f"{level_icon} {result.message}\n"
if len(validation_results) > 5:
remaining = len(validation_results) - 5
validation_text += f"\n... и ещё {remaining}\n"
validation_text += "\n<b>Выберите формат документа:</b>"
# Формируем клавиатуру - блокируем генерацию при критических ошибках
if has_critical:
keyboard = [
[InlineKeyboardButton(
"🔒 Генерация документов заблокирована",
callback_data="blocked"
)],
[InlineKeyboardButton("❌ Отмена", callback_data="cancel")]
]
else:
keyboard = [
[InlineKeyboardButton(
"🧾 XML",
callback_data="format:xml"
)],
[InlineKeyboardButton(
"📊 Excel",
callback_data="format:excel"
)],
[InlineKeyboardButton("❌ Отмена", callback_data="cancel")]
]
if update.callback_query:
await update.callback_query.edit_message_text(
validation_text,
parse_mode='HTML',
reply_markup=InlineKeyboardMarkup(keyboard)
)
else:
await update.message.reply_text(
validation_text,
parse_mode='HTML',
reply_markup=InlineKeyboardMarkup(keyboard)
)
return SELECT_FORMAT
except Exception as e:
logger.error(f"Ошибка валидации: {e}")
error_text = f"❌ Ошибка валидации:\n{str(e)}"
if update.callback_query:
await update.callback_query.edit_message_text(error_text)
else:
await update.message.reply_text(error_text)
await context.bot.send_message(
chat_id=update.effective_chat.id,
text="Выберите действие:",
reply_markup=get_main_keyboard()
)
return MAIN_MENU
async def generate_xml(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Генерация XML документа"""
user_id = update.effective_user.id
query = update.callback_query
await query.edit_message_text(
"🔄 Генерация XML документа...",
reply_markup=None
)
try:
order_id = context.user_data.get('selected_order_id')
if not order_id:
raise ValueError("ID отгрузки не найден")
# Получаем клиента для пользователя
moysklad_client = get_moysklad_client(user_id)
# Получаем полные данные отгрузки
order_data = moysklad_client.get_full_order_data(order_id)
# Создаём генератор XML
xml_gen = XMLGenerator()
# Создаём временную директорию
output_dir = Path("temp_documents")
output_dir.mkdir(exist_ok=True)
# Генерируем XML файл
filename = f"{order_data.get('name', order_id)}.xml"
output_path = str(output_dir / filename)
xml_gen.generate_upd(order_data, output_path)
# Отправляем файл
await query.message.reply_document(
document=open(output_path, 'rb'),
caption="✅ XML документ (УПД 5.03) успешно создан!",
reply_markup=get_main_keyboard()
)
# Удаляем временный файл
os.remove(output_path)
# Очищаем данные пользователя
context.user_data.clear()
await query.message.reply_text(
"Выберите действие:",
reply_markup=get_main_keyboard()
)
return MAIN_MENU
except Exception as e:
logger.error(f"Ошибка генерации XML: {e}")
await query.message.reply_text(
f"❌ Ошибка генерации XML: {str(e)}\n\n"
f"Выберите действие:",
reply_markup=get_main_keyboard()
)
context.user_data.clear()
return MAIN_MENU
async def show_templates(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Показывает рекомендованный шаблон или список всех шаблонов"""
user_id = update.effective_user.id
query = update.callback_query
# Проверяем, запрошены ли все шаблоны
show_all = context.user_data.get('show_all_templates', False)
await query.edit_message_text(
"🔄 Загрузка шаблонов...",
reply_markup=None
)
try:
# Получаем клиента для пользователя
moysklad_client = get_moysklad_client(user_id)
# Получаем данные заказа для определения типа клиента
order_data = context.user_data.get('order_data')
counterparty = order_data.get('counterparty') if order_data else None
# Определяем тип клиента
# ИП: ИНН 12 цифр + (есть КПП ИЛИ "ИП" в названии) → УПД
# Юр.лицо: ИНН 10 цифр → УПД
# Физ.лицо: ИНН 12 цифр без КПП и без "ИП" в названии → ТН
is_individual = True # По умолчанию физ.лицо (ТН)
if counterparty:
inn = counterparty.get('inn', '')
kpp = counterparty.get('kpp', '')
name = counterparty.get('name', '').upper()
if len(inn) == 10:
# Юр.лицо (ИНН 10 цифр) → УПД
is_individual = False
elif len(inn) == 12:
# ИНН 12 цифр: проверяем КПП или "ИП" в названии
if kpp or 'ИП ' in name or name.startswith('ИП'):
# ИП → УПД
is_individual = False
# Иначе: физ.лицо → ТН
# Получаем все шаблоны (customtemplate + embeddedtemplate)
custom_templates = moysklad_client.get_templates(
"demand", "customtemplate"
)
embedded_templates = moysklad_client.get_templates(
"demand", "embeddedtemplate"
)
templates = custom_templates + embedded_templates
if not templates:
await query.message.reply_text(
"❌ Шаблоны не найдены.\n\n"
"Выберите действие:",
reply_markup=get_main_keyboard()
)
context.user_data.clear()
return MAIN_MENU
# Определяем рекомендованный шаблон по конкретному ID
recommended_template = None
# ID шаблонов по умолчанию
TN_DEFAULT_ID = "92743bbb-c8c8-42e2-8686-350eace81edd" # ТН НДС
UPD_DEFAULT_ID = "be6d441a-5982-48fe-ac21-46754e051f8c" # УПД тест
# Выбираем нужный ID в зависимости от типа клиента
default_id = TN_DEFAULT_ID if is_individual else UPD_DEFAULT_ID
# Ищем шаблон с нужным ID
for tpl in templates:
if tpl.get('id') == default_id:
recommended_template = tpl
break
# Если не нашли по ID - берём первый
if not recommended_template and templates:
recommended_template = templates[0]
keyboard = []
if show_all:
# Показываем все шаблоны
text = "📄 <b>Все доступные шаблоны:</b>\n\n"
for i, tpl in enumerate(templates, 1):
tpl_name = tpl.get('name', 'N/A')
tpl_id = tpl.get('id')
# Отмечаем рекомендованный
if tpl == recommended_template:
text += f"{i}. ⭐ {tpl_name} (рекомендовано)\n"
else:
text += f"{i}. {tpl_name}\n"
keyboard.append([
InlineKeyboardButton(
(f"⭐ {tpl_name}"
if tpl == recommended_template
else tpl_name),
callback_data=f"template:{tpl_id}"
)
])
keyboard.append([
InlineKeyboardButton("❌ Отмена", callback_data="cancel")
])
else:
# Показываем только рекомендованный шаблон
client_type = "физ. лица" if is_individual else "юр. лица/ИП"
tpl_name = recommended_template.get('name', 'N/A')
tpl_id = recommended_template.get('id')
text = (
f"📄 <b>Рекомендованный шаблон</b>\n\n"
f"Тип клиента: {client_type}\n"
f"Шаблон: <b>{tpl_name}</b>\n\n"
f"Используйте рекомендованный шаблон или выберите другой:"
)
keyboard = [
[InlineKeyboardButton(
f"✅ {tpl_name}",
callback_data=f"template:{tpl_id}"
)],
[InlineKeyboardButton(
"📋 Показать все шаблоны",
callback_data="show_all_templates"
)],
[InlineKeyboardButton("❌ Отмена", callback_data="cancel")]
]
await query.edit_message_text(
text,
parse_mode='HTML',
reply_markup=InlineKeyboardMarkup(keyboard)
)
return SELECT_TEMPLATE
except Exception as e:
logger.error(f"Ошибка получения шаблонов: {e}")
await query.message.reply_text(
f"❌ Ошибка: {str(e)}\n\n"
f"Выберите действие:",
reply_markup=get_main_keyboard()
)
context.user_data.clear()
return MAIN_MENU
async def generate_excel(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Генерация Excel документа"""
user_id = update.effective_user.id
query = update.callback_query
await query.edit_message_text(
"🔄 Генерация документа...",
reply_markup=None
)
try:
# Получаем клиента для пользователя
moysklad_client = get_moysklad_client(user_id)
order_id = context.user_data.get('selected_order_id')
template_id = context.user_data.get('template_id')
template_type = context.user_data.get(
'template_type', 'customtemplate'
)
# Создаём временную директорию если нужна
output_dir = Path("temp_documents")
output_dir.mkdir(exist_ok=True)
# Генерируем документ
output_path = moysklad_client.print_document(
entity_type="demand",
entity_id=order_id,
template_id=template_id,
template_type=template_type,
extension="xls",
output_path=str(output_dir / f"{order_id}.xls")
)
# Отправляем файл
await query.message.reply_document(
document=open(output_path, 'rb'),
caption="✅ Документ успешно создан!",
reply_markup=get_main_keyboard()
)
# Удаляем временный файл
os.remove(output_path)
# Очищаем данные пользователя
context.user_data.clear()
await query.message.reply_text(
"Выберите действие:",
reply_markup=get_main_keyboard()
)
return MAIN_MENU
except Exception as e:
logger.error(f"Ошибка генерации документа: {e}")
await query.message.reply_text(
f"❌ Ошибка: {str(e)}\n\n"
f"Выберите действие:",
reply_markup=get_main_keyboard()
)
context.user_data.clear()
return MAIN_MENU
async def handle_text(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработчик текстовых сообщений"""
text = update.message.text
if text == "🔍 Поиск отгрузки":
return await search_order_prompt(update, context)
elif text == "📋 Список отгрузок":
return await show_orders_list(update, context)
elif text == "❓ Помощь":
return await help_command(update, context)
elif text == "⬅️ Назад":
return await start(update, context)
else:
await update.message.reply_text(
"❌ Неизвестная команда.\n"
"Используйте кнопки меню.",
reply_markup=get_main_keyboard()
)
return MAIN_MENU
def main():
"""Запуск бота"""
# Создаём приложение
application = Application.builder().token(
settings.telegram_bot_token
).build()
# Обработчики
application.add_handler(CommandHandler("start", start))
application.add_handler(CommandHandler("help", help_command))
application.add_handler(CommandHandler("logout", logout_command))
application.add_handler(
CommandHandler("change_credentials", change_credentials_command)
)
# Обработчик состояний
conv_handler = ConversationHandler(
entry_points=[
MessageHandler(
filters.Regex("^🔍 Поиск отгрузки$"),
search_order_prompt
),
MessageHandler(
filters.Regex("^📋 Список отгрузок$"),
show_orders_list
),
MessageHandler(filters.TEXT & ~filters.COMMAND, handle_text),
],
states={
AUTH_LOGIN: [
MessageHandler(
filters.TEXT & ~filters.COMMAND,
auth_login
),
],
AUTH_PASSWORD: [
MessageHandler(
filters.TEXT & ~filters.COMMAND,
auth_password
),
],
MAIN_MENU: [
MessageHandler(filters.TEXT & ~filters.COMMAND, handle_text),
],
SEARCH_ORDER: [
MessageHandler(
filters.TEXT & ~filters.COMMAND,
search_order
),
],
SELECT_ORDER: [
CallbackQueryHandler(handle_callback),
],
SELECT_FORMAT: [
CallbackQueryHandler(handle_callback),
],
SELECT_TEMPLATE: [
CallbackQueryHandler(handle_callback),
],
},
fallbacks=[
CommandHandler("start", start),
],
)
application.add_handler(conv_handler)
# Запускаем бота
logger.info("🤖 Бот запущен!")
application.run_polling(allowed_updates=Update.ALL_TYPES)
if __name__ == "__main__":
main()