1399 lines
52 KiB
Python
1399 lines
52 KiB
Python
"""
|
||
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()
|
||
)
|
||
# Очищаем данные пользователя
|
||
context.user_data.clear()
|
||
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 - показываем выбор типа XML
|
||
return await show_xml_format_choice(update, context)
|
||
elif format_type == "package":
|
||
# Комплект документов - генерируем сразу
|
||
return await generate_package(update, context)
|
||
else:
|
||
# Excel - сбрасываем флаг и показываем шаблоны
|
||
context.user_data['show_all_templates'] = False
|
||
return await show_templates(update, context)
|
||
|
||
elif data.startswith("xml_format:"):
|
||
# Выбран тип XML
|
||
xml_format = data.split(":")[1]
|
||
context.user_data['xml_format'] = xml_format
|
||
return await generate_xml(update, context)
|
||
|
||
elif data == "generate_another":
|
||
# Генерировать другой файл - возвращаемся к выбору формата
|
||
order_id = context.user_data.get('selected_order_id')
|
||
logger.info(f"generate_another clicked, order_id: {order_id}")
|
||
if order_id:
|
||
# Убираем кнопки с предыдущего сообщения
|
||
await query.edit_message_reply_markup(reply_markup=None)
|
||
# Вызываем validate_order напрямую с query
|
||
# validate_order сам покажет данные валидации
|
||
return await validate_order(update, context)
|
||
else:
|
||
await query.answer("❌ Ошибка: отгрузка не найдена")
|
||
await query.message.reply_text(
|
||
"Выберите действие:",
|
||
reply_markup=get_main_keyboard()
|
||
)
|
||
context.user_data.clear()
|
||
return MAIN_MENU
|
||
|
||
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)
|
||
|
||
# Если callback не распознан - логируем и возвращаем текущее состояние
|
||
logger.warning(f"Неизвестный callback: {data}")
|
||
return SELECT_FORMAT
|
||
|
||
|
||
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(
|
||
"📦 Комплект (XML 1С + Excel)",
|
||
callback_data="format:package"
|
||
)],
|
||
[InlineKeyboardButton("❌ Отмена", callback_data="cancel")]
|
||
]
|
||
|
||
if update.callback_query:
|
||
# Проверяем, есть ли у сообщения reply_markup
|
||
# Если кнопки уже убраны (generate_another), отправляем новое
|
||
try:
|
||
await update.callback_query.edit_message_text(
|
||
validation_text,
|
||
parse_mode='HTML',
|
||
reply_markup=InlineKeyboardMarkup(keyboard)
|
||
)
|
||
except Exception as e:
|
||
# Если редактирование не удалось, отправляем новое сообщение
|
||
logger.warning(f"Не удалось отредактировать сообщение: {e}")
|
||
await update.callback_query.message.reply_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 show_xml_format_choice(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Показывает выбор формата XML"""
|
||
query = update.callback_query
|
||
|
||
text = (
|
||
"📄 <b>Выберите формат XML:</b>\n\n"
|
||
"🏛 <b>ВерсФорм 5.03 (УПД/ТН)</b>\n"
|
||
"Для ФНС и электронного документооборота\n"
|
||
"Стандартный формат для налоговой\n\n"
|
||
"💼 <b>1С EnterpriseData 1.20</b>\n"
|
||
"Для загрузки в программы 1С\n"
|
||
"Формат обмена с конфигурациями 1С"
|
||
)
|
||
|
||
keyboard = [
|
||
[InlineKeyboardButton(
|
||
"🏛 ВерсФорм 5.03 (УПД)",
|
||
callback_data="xml_format:versform"
|
||
)],
|
||
[InlineKeyboardButton(
|
||
"💼 1С EnterpriseData 1.20",
|
||
callback_data="xml_format:1c"
|
||
)],
|
||
[InlineKeyboardButton("❌ Отмена", callback_data="cancel")]
|
||
]
|
||
|
||
await query.edit_message_text(
|
||
text,
|
||
parse_mode='HTML',
|
||
reply_markup=InlineKeyboardMarkup(keyboard)
|
||
)
|
||
|
||
return SELECT_FORMAT
|
||
|
||
|
||
async def generate_xml(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Генерация XML документа"""
|
||
user_id = update.effective_user.id
|
||
query = update.callback_query
|
||
|
||
# Получаем выбранный формат XML
|
||
xml_format = context.user_data.get('xml_format', 'versform')
|
||
|
||
format_name = "ВерсФорм 5.03 (УПД)" if xml_format == 'versform' else "1С EnterpriseData 1.20"
|
||
|
||
await query.edit_message_text(
|
||
f"🔄 Генерация XML документа ({format_name})...",
|
||
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 в зависимости от выбранного формата
|
||
if xml_format == '1c':
|
||
from xml_generator import XML1CGenerator
|
||
xml_gen = XML1CGenerator()
|
||
generate_method = xml_gen.generate_realization
|
||
caption_format = "1С EnterpriseData 1.20"
|
||
else:
|
||
xml_gen = XMLGenerator()
|
||
generate_method = xml_gen.generate_upd
|
||
caption_format = "УПД ВерсФорм 5.03"
|
||
|
||
# Создаём временную директорию
|
||
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)
|
||
generate_method(order_data, output_path)
|
||
|
||
# Формируем информацию об отгрузке
|
||
order = order_data.get('order', {})
|
||
order_name = order.get('name', 'N/A')
|
||
order_moment = order.get('moment', '')[:10]
|
||
order_sum = order.get('sum', 0) / 100
|
||
counterparty = order_data.get('counterparty', {})
|
||
counterparty_name = counterparty.get('name', 'N/A')
|
||
|
||
caption = (
|
||
f"✅ <b>XML документ создан!</b>\n\n"
|
||
f"📄 Формат: {caption_format}\n"
|
||
f"📋 Отгрузка: №{order_name} от {order_moment}\n"
|
||
f"👤 Контрагент: {counterparty_name}\n"
|
||
f"💰 Сумма: {order_sum:,.2f} ₽"
|
||
)
|
||
|
||
# Кнопка для генерации другого файла
|
||
keyboard = [
|
||
[InlineKeyboardButton(
|
||
"📝 Сгенерировать другой файл",
|
||
callback_data="generate_another"
|
||
)],
|
||
[InlineKeyboardButton(
|
||
"🏠 Главное меню",
|
||
callback_data="cancel"
|
||
)]
|
||
]
|
||
|
||
# Отправляем файл с inline кнопками
|
||
await query.message.reply_document(
|
||
document=open(output_path, 'rb'),
|
||
caption=caption,
|
||
parse_mode='HTML',
|
||
reply_markup=InlineKeyboardMarkup(keyboard)
|
||
)
|
||
|
||
# Удаляем временный файл
|
||
os.remove(output_path)
|
||
|
||
# Показываем главное меню (ReplyKeyboard)
|
||
await context.bot.send_message(
|
||
chat_id=update.effective_chat.id,
|
||
text="Используйте кнопки ниже для продолжения работы:",
|
||
reply_markup=get_main_keyboard()
|
||
)
|
||
|
||
# НЕ очищаем user_data - нужны данные для "Сгенерировать другой"
|
||
return SELECT_FORMAT
|
||
|
||
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")
|
||
)
|
||
|
||
# Получаем информацию об отгрузке
|
||
order_data = context.user_data.get('order_data', {})
|
||
order = order_data.get('order', {})
|
||
order_name = order.get('name', 'N/A')
|
||
order_moment = order.get('moment', '')[:10]
|
||
order_sum = order.get('sum', 0) / 100
|
||
counterparty = order_data.get('counterparty', {})
|
||
counterparty_name = counterparty.get('name', 'N/A')
|
||
|
||
caption = (
|
||
f"✅ <b>Excel документ создан!</b>\n\n"
|
||
f"📋 Отгрузка: №{order_name} от {order_moment}\n"
|
||
f"👤 Контрагент: {counterparty_name}\n"
|
||
f"💰 Сумма: {order_sum:,.2f} ₽"
|
||
)
|
||
|
||
# Кнопка для генерации другого файла
|
||
keyboard = [
|
||
[InlineKeyboardButton(
|
||
"📝 Сгенерировать другой файл",
|
||
callback_data="generate_another"
|
||
)],
|
||
[InlineKeyboardButton(
|
||
"🏠 Главное меню",
|
||
callback_data="cancel"
|
||
)]
|
||
]
|
||
|
||
# Отправляем файл
|
||
await query.message.reply_document(
|
||
document=open(output_path, 'rb'),
|
||
caption=caption,
|
||
parse_mode='HTML',
|
||
reply_markup=InlineKeyboardMarkup(keyboard)
|
||
)
|
||
|
||
# Удаляем временный файл
|
||
os.remove(output_path)
|
||
|
||
# Показываем главное меню (ReplyKeyboard)
|
||
await context.bot.send_message(
|
||
chat_id=update.effective_chat.id,
|
||
text="Используйте кнопки ниже для продолжения работы:",
|
||
reply_markup=get_main_keyboard()
|
||
)
|
||
|
||
# НЕ очищаем user_data
|
||
return SELECT_FORMAT
|
||
|
||
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_package(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||
"""Генерация комплекта документов (XML 1С + рекомендованный Excel)"""
|
||
user_id = update.effective_user.id
|
||
query = update.callback_query
|
||
|
||
await query.edit_message_text(
|
||
"🔄 Генерация комплекта документов...\n"
|
||
"Это может занять несколько секунд",
|
||
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 = context.user_data.get('order_data')
|
||
if not order_data:
|
||
order_data = moysklad_client.get_full_order_data(order_id)
|
||
context.user_data['order_data'] = order_data
|
||
|
||
order = order_data.get('order', {})
|
||
counterparty = order_data.get('counterparty', {})
|
||
|
||
# Создаём временную директорию
|
||
output_dir = Path("temp_documents")
|
||
output_dir.mkdir(exist_ok=True)
|
||
|
||
# 1. Генерируем XML 1С
|
||
from xml_generator import XML1CGenerator
|
||
xml_gen = XML1CGenerator()
|
||
xml_filename = f"{order.get('name', order_id)}_1c.xml"
|
||
xml_path = str(output_dir / xml_filename)
|
||
xml_gen.generate_realization(order_data, xml_path)
|
||
|
||
# 2. Определяем рекомендованный Excel шаблон
|
||
inn = counterparty.get('inn', '')
|
||
kpp = counterparty.get('kpp', '')
|
||
name = counterparty.get('name', '').upper()
|
||
|
||
# Определяем тип клиента
|
||
is_individual = True
|
||
if len(inn) == 10:
|
||
is_individual = False
|
||
elif len(inn) == 12:
|
||
if kpp or 'ИП ' in name or name.startswith('ИП'):
|
||
is_individual = False
|
||
|
||
# ID рекомендованных шаблонов
|
||
TN_DEFAULT_ID = "92743bbb-c8c8-42e2-8686-350eace81edd"
|
||
UPD_DEFAULT_ID = "be6d441a-5982-48fe-ac21-46754e051f8c"
|
||
template_id = TN_DEFAULT_ID if is_individual else UPD_DEFAULT_ID
|
||
|
||
# Генерируем Excel
|
||
excel_path = moysklad_client.print_document(
|
||
entity_type="demand",
|
||
entity_id=order_id,
|
||
template_id=template_id,
|
||
template_type="customtemplate",
|
||
extension="xls",
|
||
output_path=str(output_dir / f"{order_id}.xls")
|
||
)
|
||
|
||
# Формируем информацию об отгрузке
|
||
order_name = order.get('name', 'N/A')
|
||
order_moment = order.get('moment', '')[:10]
|
||
order_sum = order.get('sum', 0) / 100
|
||
counterparty_name = counterparty.get('name', 'N/A')
|
||
|
||
caption = (
|
||
f"✅ <b>Комплект документов создан!</b>\n\n"
|
||
f"📦 Включено:\n"
|
||
f" • XML 1С EnterpriseData 1.20\n"
|
||
f" • Excel (рекомендованный шаблон)\n\n"
|
||
f"📋 Отгрузка: №{order_name} от {order_moment}\n"
|
||
f"👤 Контрагент: {counterparty_name}\n"
|
||
f"💰 Сумма: {order_sum:,.2f} ₽"
|
||
)
|
||
|
||
# Кнопка для генерации другого файла
|
||
keyboard = [
|
||
[InlineKeyboardButton(
|
||
"📝 Сгенерировать другой файл",
|
||
callback_data="generate_another"
|
||
)],
|
||
[InlineKeyboardButton(
|
||
"🏠 Главное меню",
|
||
callback_data="cancel"
|
||
)]
|
||
]
|
||
|
||
# Отправляем XML файл
|
||
await query.message.reply_document(
|
||
document=open(xml_path, 'rb'),
|
||
caption="📄 XML 1С EnterpriseData 1.20"
|
||
)
|
||
|
||
# Отправляем Excel файл с полной информацией
|
||
await query.message.reply_document(
|
||
document=open(excel_path, 'rb'),
|
||
caption=caption,
|
||
parse_mode='HTML',
|
||
reply_markup=InlineKeyboardMarkup(keyboard)
|
||
)
|
||
|
||
# Удаляем временные файлы
|
||
os.remove(xml_path)
|
||
os.remove(excel_path)
|
||
|
||
# Показываем главное меню (ReplyKeyboard)
|
||
await context.bot.send_message(
|
||
chat_id=update.effective_chat.id,
|
||
text="Используйте кнопки ниже для продолжения работы:",
|
||
reply_markup=get_main_keyboard()
|
||
)
|
||
|
||
# НЕ очищаем user_data
|
||
return SELECT_FORMAT
|
||
|
||
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()
|
||
|
||
# Обработчики команд (не входящих в ConversationHandler)
|
||
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=[
|
||
CommandHandler("start", start),
|
||
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),
|
||
MessageHandler(filters.TEXT & ~filters.COMMAND, handle_text),
|
||
],
|
||
SELECT_FORMAT: [
|
||
CallbackQueryHandler(handle_callback),
|
||
MessageHandler(filters.TEXT & ~filters.COMMAND, handle_text),
|
||
],
|
||
SELECT_TEMPLATE: [
|
||
CallbackQueryHandler(handle_callback),
|
||
MessageHandler(filters.TEXT & ~filters.COMMAND, handle_text),
|
||
],
|
||
},
|
||
fallbacks=[
|
||
CommandHandler("start", start),
|
||
],
|
||
)
|
||
|
||
application.add_handler(conv_handler)
|
||
|
||
# Запускаем бота
|
||
logger.info("🤖 Бот запущен!")
|
||
application.run_polling(allowed_updates=Update.ALL_TYPES)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|