diff --git a/bot/handlers.py b/bot/handlers.py index 893f9a50..99f78f82 100644 --- a/bot/handlers.py +++ b/bot/handlers.py @@ -1,603 +1,95 @@ -from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update +from telegram import Update, InlineKeyboardMarkup, InlineKeyboardButton from telegram.ext import ContextTypes -from users.models import User -from hotels.models import Hotel, UserHotel, Reservation -from users.models import NotificationSettings -from asgiref.sync import sync_to_async -import smtplib -from hotels.models import PMSIntegrationLog -import requests -from email.mime.text import MIMEText -from django.core.mail import send_mail -from datetime import datetime, timedelta -from fpdf import FPDF -import os - -# --- Вспомогательные функции --- -async def get_user_from_chat_id(chat_id): - """Получение пользователя из базы по chat_id.""" - return await sync_to_async(User.objects.filter(chat_id=chat_id).first)() +from bot.operations.hotels import manage_hotels, hotel_actions, delete_hotel, check_pms, setup_rooms +from bot.operations.statistics import statistics, stats_select_period, generate_statistics +from bot.operations.settings import settings_menu, toggle_telegram, toggle_email, set_notification_time, show_current_settings +from bot.operations.users import show_users -async def get_hotel_by_id(hotel_id): - """Получение отеля по ID.""" - return await sync_to_async(Hotel.objects.filter(id=hotel_id).first)() - - -# --- Обработчики команд --- async def start(update: Update, context: ContextTypes.DEFAULT_TYPE): - """Обработчик команды /start с проверкой chat_id""" - user_id = update.message.from_user.id + """Обработчик команды /start.""" + user_id = ( + update.message.from_user.id + if update.message + else update.callback_query.from_user.id + ) print(f"Пользователь {user_id} вызвал команду /start") - user = await get_user_from_chat_id(user_id) - if user: - keyboard = [ - [InlineKeyboardButton("📊 Статистика", callback_data="stats")], - [InlineKeyboardButton("🏨 Управление отелями", callback_data="manage_hotels")], - [InlineKeyboardButton("👤 Пользователи", callback_data="manage_users")], - [InlineKeyboardButton("⚙️ Настройки", callback_data="settings")], - ] - reply_markup = InlineKeyboardMarkup(keyboard) - await update.message.reply_text("Выберите действие:", reply_markup=reply_markup) - else: - print(f"Пользователь {user_id} не зарегистрирован.") - await update.message.reply_text("Вы не зарегистрированы в системе. Обратитесь к администратору.") - - -async def manage_hotels(update: Update, context: ContextTypes.DEFAULT_TYPE): - """Вывод списка отелей, связанных с пользователем""" - query = update.callback_query - await query.answer() - - user_id = query.from_user.id - print(f"Пользователь {user_id} выбрал 'Управление отелями'") - - user = await get_user_from_chat_id(user_id) - if not user: - print(f"Пользователь {user_id} не зарегистрирован.") - await query.edit_message_text("Вы не зарегистрированы в системе.") - return - - user_hotels = await sync_to_async(list)( - UserHotel.objects.filter(user=user).select_related("hotel") - ) - - if not user_hotels: - print(f"У пользователя {user_id} нет связанных отелей.") - await query.edit_message_text("У вас нет связанных отелей.") - return - keyboard = [ - [InlineKeyboardButton(f"🏨 {hotel.hotel.name}", callback_data=f"hotel_{hotel.hotel.id}")] - for hotel in user_hotels + [InlineKeyboardButton("📊 Статистика", callback_data="stats")], + [InlineKeyboardButton("🏨 Управление отелями", callback_data="manage_hotels")], + [InlineKeyboardButton("👤 Пользователи", callback_data="manage_users")], + [InlineKeyboardButton("⚙️ Настройки", callback_data="settings")], ] reply_markup = InlineKeyboardMarkup(keyboard) - await query.edit_message_text("Выберите отель:", reply_markup=reply_markup) + if update.message: + await update.message.reply_text("Выберите действие:", reply_markup=reply_markup) + elif update.callback_query: + await update.callback_query.edit_message_text("Выберите действие:", reply_markup=reply_markup) + -# --- Обработчики кнопок --- async def handle_button_click(update: Update, context: ContextTypes.DEFAULT_TYPE): - """Обработчик всех нажатий кнопок""" + """Обработчик всех нажатий кнопок.""" query = update.callback_query await query.answer() - if query.data == "stats": - await statistics(update, context) # Добавляем вызов функции для обработки статистики - elif query.data.startswith("stats_hotel_"): + callback_data = query.data + + # Сохраняем предыдущее меню для кнопки "Назад" + context.user_data["previous_menu"] = context.user_data.get("current_menu", "main_menu") + context.user_data["current_menu"] = callback_data + + if callback_data == "stats": + await statistics(update, context) + elif callback_data.startswith("stats_hotel_"): await stats_select_period(update, context) - elif query.data.startswith("stats_period_"): + elif callback_data.startswith("stats_period_"): await generate_statistics(update, context) - elif query.data == "manage_hotels": + elif callback_data == "manage_hotels": await manage_hotels(update, context) - elif query.data.startswith("hotel_"): + elif callback_data == "manage_users": + await show_users(update, context) + elif callback_data.startswith("hotel_"): await hotel_actions(update, context) - elif query.data.startswith("delete_hotel_"): + elif callback_data.startswith("delete_hotel_"): await delete_hotel(update, context) - elif query.data.startswith("check_pms_"): + elif callback_data.startswith("check_pms_"): await check_pms(update, context) - elif query.data.startswith("setup_rooms_"): + elif callback_data.startswith("setup_rooms_"): await setup_rooms(update, context) - elif query.data == "settings": + elif callback_data == "settings": await settings_menu(update, context) - elif query.data == "toggle_telegram": + elif callback_data == "toggle_telegram": await toggle_telegram(update, context) - elif query.data == "toggle_email": + elif callback_data == "toggle_email": await toggle_email(update, context) - elif query.data == "set_notification_time": + elif callback_data == "set_notification_time": await set_notification_time(update, context) - elif query.data == "current_settings": + elif callback_data == "current_settings": await show_current_settings(update, context) + elif callback_data == "main_menu": + await start(update, context) + elif callback_data == "back": + await navigate_back(update, context) else: - print(f"Неизвестный callback_data: {query.data}") await query.edit_message_text("Команда не распознана.") -async def hotel_actions(update: Update, context: ContextTypes.DEFAULT_TYPE): - """Обработчик действий для выбранного отеля""" - query = update.callback_query - await query.answer() +async def navigate_back(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Обработчик кнопки 'Назад'.""" + previous_menu = context.user_data.get("previous_menu", "main_menu") + context.user_data["current_menu"] = previous_menu - hotel_id = int(query.data.split("_")[1]) - print(f"Пользователь {query.from_user.id} выбрал отель с ID {hotel_id}") - - hotel = await get_hotel_by_id(hotel_id) - if not hotel: - print(f"Отель с ID {hotel_id} не найден.") - await query.edit_message_text("Отель не найден.") - return - - keyboard = [ - [InlineKeyboardButton("🗑️ Удалить отель", callback_data=f"delete_hotel_{hotel_id}")], - [InlineKeyboardButton("🔗 Проверить интеграцию с PMS", callback_data=f"check_pms_{hotel_id}")], - [InlineKeyboardButton("🛏️ Настроить номера", callback_data=f"setup_rooms_{hotel_id}")], - ] - reply_markup = InlineKeyboardMarkup(keyboard) - await query.edit_message_text(f"Управление отелем: {hotel.name}", reply_markup=reply_markup) - - -async def delete_hotel(update: Update, context: ContextTypes.DEFAULT_TYPE): - """Удаление отеля""" - query = update.callback_query - await query.answer() - - hotel_id = int(query.data.split("_")[2]) - print(f"Пользователь {query.from_user.id} выбрал удаление отеля с ID {hotel_id}") - - hotel = await get_hotel_by_id(hotel_id) - if hotel: - hotel_name = hotel.name - await sync_to_async(hotel.delete)() - print(f"Отель {hotel_name} удалён.") - await query.edit_message_text(f"Отель {hotel_name} успешно удалён.") + if previous_menu == "main_menu": + await start(update, context) + elif previous_menu == "stats": + await statistics(update, context) + elif previous_menu == "manage_hotels": + await manage_hotels(update, context) + elif previous_menu == "manage_users": + await show_users(update, context) + elif previous_menu.startswith("hotel_"): + await hotel_actions(update, context) else: - print(f"Отель с ID {hotel_id} не найден.") - await query.edit_message_text("Отель не найден.") - - -async def check_pms(update: Update, context: ContextTypes.DEFAULT_TYPE): - """Проверить интеграцию с PMS""" - query = update.callback_query - await query.answer() - - hotel_id = int(query.data.split("_")[2]) - print(f"Пользователь {query.from_user.id} проверяет интеграцию PMS для отеля с ID {hotel_id}") - - # Асинхронно получаем отель - hotel = await sync_to_async(Hotel.objects.select_related('api', 'pms').get)(id=hotel_id) - if not hotel: - print(f"Отель с ID {hotel_id} не найден.") - await query.edit_message_text("Отель не найден.") - return - - # Асинхронно извлекаем связанные данные - api_name = hotel.api.name if hotel.api else "Не настроен" - pms_name = hotel.pms.name if hotel.pms else "Не указана" - - # Формируем сообщение - status_message = ( - f"Отель: {hotel.name}\n" - f"PMS система: {pms_name}\n" - f"API: {api_name}" - ) - - await query.edit_message_text(status_message) -async def setup_rooms(update: Update, context: ContextTypes.DEFAULT_TYPE): - """Настроить номера отеля""" - query = update.callback_query - await query.answer() - - hotel_id = int(query.data.split("_")[2]) - print(f"Пользователь {query.from_user.id} настраивает номера для отеля с ID {hotel_id}") - - hotel = await get_hotel_by_id(hotel_id) - if not hotel: - print(f"Отель с ID {hotel_id} не найден.") - await query.edit_message_text("Отель не найден.") - return - - await query.edit_message_text(f"Настройка номеров для отеля: {hotel.name}") - - -async def settings_menu(update: Update, context: ContextTypes.DEFAULT_TYPE): - """Меню настроек уведомлений.""" - query = update.callback_query - await query.answer() - - user_id = query.from_user.id - user = await get_user_from_chat_id(user_id) - if not user: - await query.edit_message_text("Вы не зарегистрированы.") - return - - settings, _ = await sync_to_async(NotificationSettings.objects.get_or_create)(user=user) - - telegram_status = "✅" if settings.telegram_enabled else "❌" - email_status = "✅" if settings.email_enabled else "❌" - - keyboard = [ - [InlineKeyboardButton(f"{telegram_status} Уведомления в Telegram", callback_data="toggle_telegram")], - [InlineKeyboardButton(f"{email_status} Уведомления по Email", callback_data="toggle_email")], - [InlineKeyboardButton("🕒 Настроить время уведомлений", callback_data="set_notification_time")], - [InlineKeyboardButton("📋 Показать текущие настройки", callback_data="current_settings")], - ] - reply_markup = InlineKeyboardMarkup(keyboard) - await query.edit_message_text("Настройки уведомлений:", reply_markup=reply_markup) - -async def toggle_telegram(update: Update, context: ContextTypes.DEFAULT_TYPE): - """Переключение состояния Telegram-уведомлений.""" - query = update.callback_query - await query.answer() - - user_id = query.from_user.id - user = await get_user_from_chat_id(user_id) - if not user: - await query.edit_message_text("Вы не зарегистрированы.") - return - - settings, _ = await sync_to_async(NotificationSettings.objects.get_or_create)(user=user) - settings.telegram_enabled = not settings.telegram_enabled - await sync_to_async(settings.save)() - - print(f"Пользователь {user_id} переключил Telegram-уведомления: {settings.telegram_enabled}") - await settings_menu(update, context) - - -async def toggle_email(update: Update, context: ContextTypes.DEFAULT_TYPE): - """Переключение состояния Email-уведомлений.""" - query = update.callback_query - await query.answer() - - user_id = query.from_user.id - user = await get_user_from_chat_id(user_id) - if not user: - await query.edit_message_text("Вы не зарегистрированы.") - return - - settings, _ = await sync_to_async(NotificationSettings.objects.get_or_create)(user=user) - settings.email_enabled = not settings.email_enabled - await sync_to_async(settings.save)() - - print(f"Пользователь {user_id} переключил Email-уведомления: {settings.email_enabled}") - await settings_menu(update, context) - - -async def set_notification_time(update: Update, context: ContextTypes.DEFAULT_TYPE): - """Настройка времени уведомлений.""" - query = update.callback_query - await query.answer() - - user_id = query.from_user.id - user = await get_user_from_chat_id(user_id) - if not user: - await query.edit_message_text("Вы не зарегистрированы.") - return - - await query.edit_message_text("Введите новое время для уведомлений в формате HH:MM (например, 08:30):") - context.user_data["set_time"] = True - print(f"Пользователь {user_id} настраивает время уведомлений.") - - -async def handle_notification_time(update: Update, context: ContextTypes.DEFAULT_TYPE): - """Обработка ввода времени уведомлений.""" - if context.user_data.get("set_time"): - user_id = update.message.from_user.id - new_time = update.message.text - - try: - hour, minute = map(int, new_time.split(":")) - user = await get_user_from_chat_id(user_id) - if user: - settings, _ = await sync_to_async(NotificationSettings.objects.get_or_create)(user=user) - settings.notification_time = f"{hour:02}:{minute:02}" - await sync_to_async(settings.save)() - print(f"Пользователь {user_id} установил новое время уведомлений: {new_time}") - await update.message.reply_text(f"Время уведомлений обновлено на {new_time}.") - except ValueError: - print(f"Пользователь {user_id} ввёл некорректное время: {new_time}") - await update.message.reply_text("Неверный формат. Введите время в формате HH:MM.") - finally: - context.user_data["set_time"] = False - - -async def settings_menu(update: Update, context: ContextTypes.DEFAULT_TYPE): - """Меню настроек уведомлений.""" - query = update.callback_query - await query.answer() - - user_id = query.from_user.id - user = await get_user_from_chat_id(user_id) - if not user: - await query.edit_message_text("Вы не зарегистрированы.") - return - - settings, _ = await sync_to_async(NotificationSettings.objects.get_or_create)(user=user) - - telegram_status = "✅" if settings.telegram_enabled else "❌" - email_status = "✅" if settings.email_enabled else "❌" - - keyboard = [ - [InlineKeyboardButton(f"{telegram_status} Уведомления в Telegram", callback_data="toggle_telegram")], - [InlineKeyboardButton(f"{email_status} Уведомления по Email", callback_data="toggle_email")], - [InlineKeyboardButton("🕒 Настроить время уведомлений", callback_data="set_notification_time")], - ] - reply_markup = InlineKeyboardMarkup(keyboard) - await query.edit_message_text("Настройки уведомлений:", reply_markup=reply_markup) - -async def send_telegram_notification(user, message): - """Отправка уведомления через Telegram.""" - - # bot = Bot(token="ВАШ_ТОКЕН") - if user.chat_id: - try: - await bot.send_message(chat_id=user.chat_id, text=message) - print(f"Telegram-уведомление отправлено пользователю {user.chat_id}: {message}") - except Exception as e: - print(f"Ошибка отправки Telegram-уведомления пользователю {user.chat_id}: {e}") - - -def send_email_notification(user, message): - """Отправка уведомления через Email.""" - if user.email: - try: - send_mail( - subject="Уведомление от системы", - message=message, - from_email="noreply@yourdomain.com", - recipient_list=[user.email], - fail_silently=False, - ) - print(f"Email-уведомление отправлено на {user.email}: {message}") - except Exception as e: - print(f"Ошибка отправки Email-уведомления пользователю {user.email}: {e}") - - -async def schedule_notifications(): - """Планировщик уведомлений.""" - print("Запуск планировщика уведомлений...") - now = datetime.now().strftime("%H:%M") - - users = await sync_to_async(list)(User.objects.all()) - for user in users: - settings, _ = await sync_to_async(NotificationSettings.objects.get_or_create)(user=user) - - if settings.notification_time == now: - message = "Это ваше уведомление от системы." - if settings.telegram_enabled: - await send_telegram_notification(user, message) - if settings.email_enabled: - send_email_notification(user, message) - - -async def show_current_settings(update: Update, context: ContextTypes.DEFAULT_TYPE): - """Отображение текущих настроек уведомлений.""" - query = update.callback_query - await query.answer() - - user_id = query.from_user.id - user = await get_user_from_chat_id(user_id) - if not user: - await query.edit_message_text("Вы не зарегистрированы.") - return - - settings, _ = await sync_to_async(NotificationSettings.objects.get_or_create)(user=user) - telegram_status = "✅ Включены" if settings.telegram_enabled else "❌ Выключены" - email_status = "✅ Включены" if settings.email_enabled else "❌ Выключены" - notification_time = settings.notification_time or "Не установлено" - - message = ( - f"📋 Ваши настройки уведомлений:\n" - f"🔔 Telegram: {telegram_status}\n" - f"📧 Email: {email_status}\n" - f"🕒 Время: {notification_time}" - ) - await query.edit_message_text(message) - - - -async def check_pms_integration(update: Update, context: ContextTypes.DEFAULT_TYPE): - query = update.callback_query - await query.answer() - - hotel_id = int(query.data.split("_")[2]) - hotel = await sync_to_async(Hotel.objects.get)(id=hotel_id) - pms_settings = hotel.pms.parser_settings # Настройки из связанной PMSConfiguration - - try: - # Выполняем запрос к PMS - response = requests.post( - url=pms_settings["url"], - headers={ - "Authorization": f"Bearer {hotel.api.api_key}", - "Content-Type": "application/json", - }, - json={ - "from": "2024-01-01T00:00:00Z", - "until": "2024-01-10T00:00:00Z", - "pagination": {"from": 0, "count": 10}, - }, - ) - - # Проверяем результат - if response.status_code == 200: - await sync_to_async(PMSIntegrationLog.objects.create)( - hotel=hotel, - status="success", - message="Интеграция успешно проверена.", - ) - await query.edit_message_text(f"Интеграция с PMS для отеля '{hotel.name}' успешна.") - else: - await sync_to_async(PMSIntegrationLog.objects.create)( - hotel=hotel, - status="error", - message=f"Ошибка: {response.status_code}", - ) - await query.edit_message_text(f"Ошибка интеграции с PMS для отеля '{hotel.name}': {response.status_code}") - except Exception as e: - await sync_to_async(PMSIntegrationLog.objects.create)( - hotel=hotel, - status="error", - message=str(e), - ) - await query.edit_message_text(f"Произошла ошибка: {str(e)}") - -async def get_hotels_for_user(user_id): - """Получение отелей, связанных с пользователем.""" - user = await sync_to_async(User.objects.filter(chat_id=user_id).first)() - if not user: - return [] - return await sync_to_async(list)( - Hotel.objects.filter(userhotel__user=user).distinct() - ) - - -async def get_reservations(hotel_id, start_date=None, end_date=None): - """Получение статистики бронирований по отелю с гостями.""" - query = Reservation.objects.filter(hotel_id=hotel_id) - if start_date: - query = query.filter(check_in__gte=start_date) - if end_date: - query = query.filter(check_out__lte=end_date) - - reservations = await sync_to_async(list)(query.prefetch_related('guests')) - return reservations - -def generate_pdf_report(hotel_name, reservations, start_date, end_date): - """Генерация PDF отчета.""" - pdf = FPDF() - pdf.add_page() - - # Укажите путь к шрифту - font_path = os.path.join("bot", "fonts", "OpenSans-Regular.ttf") - if not os.path.exists(font_path): - raise FileNotFoundError(f"Шрифт {font_path} не найден. Убедитесь, что он находится в указанной папке.") - - pdf.add_font("OpenSans", "", font_path, uni=True) - pdf.set_font("OpenSans", size=12) - - # Заголовки - title = f"Отчет по заселениям: {hotel_name}" - period = ( - f"Период: {start_date.strftime('%d.%m.%Y')} - {end_date.strftime('%d.%m.%Y')}" - if start_date and end_date - else "Период: Все время" - ) - - pdf.cell(0, 10, txt=title, ln=True, align="C") - pdf.cell(0, 10, txt=period, ln=True, align="C") - pdf.ln(10) - - # Ширины колонок - page_width = pdf.w - 20 # Учитываем отступы - col_widths = [page_width * 0.2, # Дата заезда - page_width * 0.2, # Дата выезда - page_width * 0.15, # Номер - page_width * 0.25, # Тип комнаты - page_width * 0.1, # Цена - page_width * 0.1] # Скидка - - # Заголовки таблицы - pdf.set_font("OpenSans", size=10) - headers = ["Дата заезда", "Дата выезда", "Номер", "Тип комнаты", "Цена", "Скидка"] - for width, header in zip(col_widths, headers): - pdf.cell(width, 10, header, border=1, align="C") - pdf.ln() - - # Инициализация сумм - total_price = 0 - total_discount = 0 - - # Добавление данных - for res in reservations: - guests = ", ".join([guest.name for guest in res.guests.all()]) - price = res.price or 0 - discount = res.discount or 0 - total_price += price - total_discount += discount - - pdf.cell(col_widths[0], 10, res.check_in.strftime('%d.%m.%Y'), border=1) - pdf.cell(col_widths[1], 10, res.check_out.strftime('%d.%m.%Y'), border=1) - pdf.cell(col_widths[2], 10, res.room_number, border=1) - pdf.cell(col_widths[3], 10, res.room_type, border=1) - pdf.cell(col_widths[4], 10, f"{price:.2f} ₽", border=1, align="R") - pdf.cell(col_widths[5], 10, f"{discount:.2f} ₽", border=1, align="R") - pdf.ln() - - pdf.set_font("OpenSans", size=8) - # pdf.multi_cell(page_width, 5, f"Гости: {guests}", border=0) - pdf.set_font("OpenSans", size=10) - - # Итоги - pdf.ln(5) - pdf.set_font("OpenSans", size=12) - pdf.cell(0, 10, "Итоги:", ln=True) - pdf.cell(0, 10, f"Общая сумма цен: {total_price:.2f} ₽", ln=True) - pdf.cell(0, 10, f"Общая сумма скидок: {total_discount:.2f} ₽", ln=True) - - # Сохранение файла - file_path = os.path.join("reports", f"{hotel_name}_report.pdf") - os.makedirs(os.path.dirname(file_path), exist_ok=True) - pdf.output(file_path) - return file_path - - -# --- Обработчики --- -async def statistics(update: Update, context: ContextTypes.DEFAULT_TYPE): - """Вывод списка отелей для статистики.""" - query = update.callback_query - user_id = query.from_user.id - await query.answer() - - hotels = await get_hotels_for_user(user_id) - if not hotels: - await query.edit_message_text("У вас нет доступных отелей для статистики.") - return - - keyboard = [[InlineKeyboardButton(hotel.name, callback_data=f"stats_hotel_{hotel.id}")] for hotel in hotels] - reply_markup = InlineKeyboardMarkup(keyboard) - await query.edit_message_text("Выберите отель:", reply_markup=reply_markup) - - -async def stats_select_period(update: Update, context: ContextTypes.DEFAULT_TYPE): - """Выбор периода времени для статистики.""" - query = update.callback_query - await query.answer() - - hotel_id = int(query.data.split("_")[2]) - context.user_data["selected_hotel"] = hotel_id - - keyboard = [ - [InlineKeyboardButton("Неделя", callback_data="stats_period_week")], - [InlineKeyboardButton("Месяц", callback_data="stats_period_month")], - [InlineKeyboardButton("Все время", callback_data="stats_period_all")], - ] - reply_markup = InlineKeyboardMarkup(keyboard) - await query.edit_message_text("Выберите период времени:", reply_markup=reply_markup) - - -async def generate_statistics(update: Update, context: ContextTypes.DEFAULT_TYPE): - """Генерация и отправка статистики.""" - query = update.callback_query - await query.answer() - - hotel_id = context.user_data["selected_hotel"] - period = query.data.split("_")[2] - - now = datetime.now() - if period == "week": - start_date = now - timedelta(days=7) - end_date = now - elif period == "month": - start_date = now - timedelta(days=30) - end_date = now - else: - start_date = None - end_date = None - - reservations = await get_reservations(hotel_id, start_date, end_date) - hotel = await sync_to_async(Hotel.objects.get)(id=hotel_id) - file_path = generate_pdf_report(hotel.name, reservations, start_date, end_date) - - # Отправляем PDF файл пользователю - with open(file_path, "rb") as file: - await query.message.reply_document(document=file, filename=os.path.basename(file_path)) + await update.callback_query.edit_message_text("Команда не распознана.") diff --git a/bot/keyboards.py b/bot/keyboards.py new file mode 100644 index 00000000..5c89e9a9 --- /dev/null +++ b/bot/keyboards.py @@ -0,0 +1,19 @@ +from telegram import InlineKeyboardButton, InlineKeyboardMarkup + +def main_menu_keyboard(): + """Главное меню клавиатуры.""" + return InlineKeyboardMarkup([ + [InlineKeyboardButton("📊 Статистика", callback_data="stats")], + [InlineKeyboardButton("🏨 Управление отелями", callback_data="manage_hotels")], + [InlineKeyboardButton("👤 Пользователи", callback_data="manage_users")], + [InlineKeyboardButton("⚙️ Настройки", callback_data="settings")] + ]) + +def stats_period_keyboard(): + """Клавиатура для выбора периода статистики.""" + return InlineKeyboardMarkup([ + [InlineKeyboardButton("Неделя", callback_data="stats_period_week")], + [InlineKeyboardButton("Месяц", callback_data="stats_period_month")], + [InlineKeyboardButton("Все время", callback_data="stats_period_all")], + [InlineKeyboardButton("🔙 Назад", callback_data="back")], + ]) diff --git a/bot/management/commands/run_bot.py b/bot/management/commands/run_bot.py index cbf5eedc..b79d0bea 100644 --- a/bot/management/commands/run_bot.py +++ b/bot/management/commands/run_bot.py @@ -3,79 +3,14 @@ import django import asyncio from apscheduler.schedulers.asyncio import AsyncIOScheduler from django.core.management.base import BaseCommand -from telegram.ext import ( - Application, - CommandHandler, - CallbackQueryHandler, - MessageHandler, - filters, -) -from bot.handlers import ( - start, - handle_button_click, - manage_hotels, - hotel_actions, - delete_hotel, - check_pms, - setup_rooms, - settings_menu, - toggle_telegram, - toggle_email, - set_notification_time, - handle_notification_time, - schedule_notifications, - show_current_settings, - statistics, - generate_statistics, - stats_select_period, -) +from telegram.ext import Application +from bot.utils.bot_setup import setup_bot +from bot.utils.scheduler import setup_scheduler +from dotenv import load_dotenv +from bot.operations.users import show_users -# Настройка Django окружения -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'touchh.settings') -django.setup() - - -async def start_bot(application): - """Настройка и запуск Telegram бота.""" - print("Настройка Telegram приложения...") - - # Регистрация обработчиков команд - print("Регистрация обработчиков команд...") - application.add_handler(CommandHandler("start", start)) - - # Регистрация обработчиков кнопок - print("Регистрация обработчиков кнопок...") - application.add_handler(CallbackQueryHandler(handle_button_click)) - application.add_handler(CallbackQueryHandler(manage_hotels, pattern="^manage_hotels$")) - application.add_handler(CallbackQueryHandler(settings_menu, pattern="^settings$")) - application.add_handler(CallbackQueryHandler(toggle_telegram, pattern="^toggle_telegram$")) - application.add_handler(CallbackQueryHandler(toggle_email, pattern="^toggle_email$")) - application.add_handler(CallbackQueryHandler(set_notification_time, pattern="^set_notification_time$")) - application.add_handler(CallbackQueryHandler(show_current_settings, pattern="^current_settings$")) - application.add_handler(CallbackQueryHandler(hotel_actions, pattern="^hotel_")) - application.add_handler(CallbackQueryHandler(delete_hotel, pattern="^delete_hotel_")) - application.add_handler(CallbackQueryHandler(check_pms, pattern="^check_pms_")) - application.add_handler(CallbackQueryHandler(setup_rooms, pattern="^setup_rooms_")) - application.add_handler(CallbackQueryHandler(statistics, pattern="^stats$")) - application.add_handler(CallbackQueryHandler(stats_select_period, pattern="^stats_hotel_")) - application.add_handler(CallbackQueryHandler(generate_statistics, pattern="^stats_period_")) - - # Регистрация обработчиков текстовых сообщений - print("Регистрация обработчиков текстовых сообщений...") - application.add_handler(MessageHandler(filters.TEXT & filters.ChatType.PRIVATE, handle_notification_time)) - - # Настройка планировщика - print("Настройка планировщика уведомлений...") - scheduler = AsyncIOScheduler() - scheduler.add_job(schedule_notifications, "cron", minute="*") - scheduler.start() - - # Запуск бота - print("Запуск Telegram бота...") - await application.initialize() - await application.start() - print("Бот успешно запущен. Ожидание событий...") - await application.updater.start_polling() +# Загрузка переменных окружения +load_dotenv() class Command(BaseCommand): @@ -84,20 +19,49 @@ class Command(BaseCommand): def handle(self, *args, **options): print("Запуск Telegram бота...") - # Получаем текущий цикл событий - loop = asyncio.get_event_loop() + # Настройка Django окружения + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "touchh.settings") + django.setup() - # Создаём экземпляр приложения - application = Application.builder().token("8125171867:AAGxDcSpQxJy3_pmq3TDBWtqaAVCj7b-F5k").build() + # Создание приложения Telegram + bot_token = os.getenv("TELEGRAM_BOT_TOKEN") + if not bot_token: + raise ValueError("Токен бота не найден в переменных окружения.") + application = Application.builder().token(bot_token).build() - # Добавляем задачу для запуска бота - loop.create_task(start_bot(application)) + # Настройка бота и обработчиков + setup_bot(application) + + async def main(): + print("Настройка планировщика...") + scheduler = setup_scheduler() + scheduler.start() + + try: + print("Инициализация Telegram бота...") + await application.initialize() # Инициализация приложения + print("Бот запущен. Ожидание сообщений...") + await application.start() # Запуск приложения + await application.updater.start_polling() # Запуск обработки сообщений + + # Бесконечный цикл для удержания приложения активным + while True: + await asyncio.sleep(3600) # Ожидание 1 час + except Exception as e: + print(f"Ошибка во время работы бота: {e}") + finally: + print("Остановка Telegram бота...") + await application.stop() # Завершаем приложение перед shutdown + print("Остановка планировщика...") + scheduler.shutdown(wait=False) + print("Планировщик остановлен.") - # Запускаем цикл событий try: - loop.run_forever() - except KeyboardInterrupt: - print("Остановка Telegram бота...") - loop.run_until_complete(application.stop()) - scheduler = AsyncIOScheduler() - scheduler.shutdown(wait=False) + asyncio.run(main()) + except RuntimeError as e: + if str(e) == "This event loop is already running": + print("Цикл событий уже запущен. Используем другой подход для запуска.") + loop = asyncio.get_event_loop() + loop.run_until_complete(main()) + else: + raise diff --git a/bot/operations.py b/bot/operations.py new file mode 100644 index 00000000..87faecc8 --- /dev/null +++ b/bot/operations.py @@ -0,0 +1,28 @@ +from asgiref.sync import sync_to_async +from hotels.models import UserHotel, Hotel, Reservation +from users.models import User + + +async def get_user_from_chat_id(chat_id): + """Получение пользователя из базы по chat_id.""" + return await sync_to_async(User.objects.filter(chat_id=chat_id).first)() + + +async def get_hotels_for_user(user_id): + """Получение отелей, связанных с пользователем.""" + user = await sync_to_async(User.objects.filter(chat_id=user_id).first)() + if not user: + return [] + return await sync_to_async(list)( + Hotel.objects.filter(userhotel__user=user).distinct() + ) + + +async def get_reservations(hotel_id, start_date=None, end_date=None): + """Получение статистики бронирований по отелю с гостями.""" + query = Reservation.objects.filter(hotel_id=hotel_id) + if start_date: + query = query.filter(check_in__gte=start_date) + if end_date: + query = query.filter(check_out__lte=end_date) + return await sync_to_async(list)(query.prefetch_related('guests')) diff --git a/bot/operations/__init__.py b/bot/operations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bot/operations/hotels.py b/bot/operations/hotels.py new file mode 100644 index 00000000..3e7fa2f1 --- /dev/null +++ b/bot/operations/hotels.py @@ -0,0 +1,107 @@ +from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update +from asgiref.sync import sync_to_async +from hotels.models import Hotel, UserHotel +from users.models import User + +async def manage_hotels(update: Update, context): + """Отображение списка отелей, связанных с пользователем.""" + query = update.callback_query + await query.answer() + + user_id = query.from_user.id + user = await sync_to_async(User.objects.filter(chat_id=user_id).first)() + if not user: + await query.edit_message_text("Вы не зарегистрированы в системе.") + return + + user_hotels = await sync_to_async(list)( + UserHotel.objects.filter(user=user).select_related("hotel") + ) + + if not user_hotels: + await query.edit_message_text("У вас нет связанных отелей.") + return + + keyboard = [ + [InlineKeyboardButton(f"🏨 {hotel.hotel.name}", callback_data=f"hotel_{hotel.hotel.id}")] + for hotel in user_hotels + ] + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text("Выберите отель:", reply_markup=reply_markup) + + +async def hotel_actions(update: Update, context): + """Обработчик действий для выбранного отеля.""" + query = update.callback_query + await query.answer() + + hotel_id = int(query.data.split("_")[1]) + hotel = await sync_to_async(Hotel.objects.filter(id=hotel_id).first)() + if not hotel: + await query.edit_message_text("Отель не найден.") + return + + keyboard = [ + [InlineKeyboardButton("🗑️ Удалить отель", callback_data=f"delete_hotel_{hotel_id}")], + [InlineKeyboardButton("🔗 Проверить интеграцию с PMS", callback_data=f"check_pms_{hotel_id}")], + [InlineKeyboardButton("🛏️ Настроить номера", callback_data=f"setup_rooms_{hotel_id}")], + [InlineKeyboardButton("🏠 Главная", callback_data="main_menu")], + [InlineKeyboardButton("🔙 Назад", callback_data="back")], + ] + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text(f"Управление отелем: {hotel.name}", reply_markup=reply_markup) + + +async def delete_hotel(update: Update, context): + """Удаление отеля.""" + query = update.callback_query + await query.answer() + + hotel_id = int(query.data.split("_")[2]) + hotel = await sync_to_async(Hotel.objects.filter(id=hotel_id).first)() + if hotel: + hotel_name = hotel.name + await sync_to_async(hotel.delete)() + await query.edit_message_text(f"Отель {hotel_name} успешно удалён.") + else: + await query.edit_message_text("Отель не найден.") + + +async def check_pms(update: Update, context): + """Проверить интеграцию с PMS.""" + query = update.callback_query + await query.answer() + + hotel_id = int(query.data.split("_")[2]) + hotel = await sync_to_async(Hotel.objects.select_related('api', 'pms').get)(id=hotel_id) + if not hotel: + await query.edit_message_text("Отель не найден.") + return + + api_name = hotel.api.name if hotel.api else "Не настроен" + pms_name = hotel.pms.name if hotel.pms else "Не указана" + + status_message = f"Отель: {hotel.name}\nPMS система: {pms_name}\nAPI: {api_name}" + await query.edit_message_text(status_message) + + +async def setup_rooms(update: Update, context): + """Настроить номера отеля.""" + query = update.callback_query + await query.answer() + + hotel_id = int(query.data.split("_")[2]) + hotel = await sync_to_async(Hotel.objects.filter(id=hotel_id).first)() + if not hotel: + await query.edit_message_text("Отель не найден.") + return + + await query.edit_message_text(f"Настройка номеров для отеля: {hotel.name}") + + +async def get_users_for_hotel(hotel_id): + """Получение пользователей, зарегистрированных в отеле с правами управления через бота.""" + users = await sync_to_async(list)( + User.objects.filter(user_hotels__hotel_id=hotel_id, user_hotels__role__in=["admin", "manager"]).distinct() + ) + return users diff --git a/bot/operations/notifications.py b/bot/operations/notifications.py new file mode 100644 index 00000000..68fdb3ae --- /dev/null +++ b/bot/operations/notifications.py @@ -0,0 +1,74 @@ +from telegram import Bot +from django.core.mail import send_mail +from datetime import datetime +from asgiref.sync import sync_to_async +from users.models import User, NotificationSettings + + +async def send_telegram_notification(user, message): + """Отправка уведомления через Telegram.""" + if user.chat_id: + try: + bot = Bot(token="ВАШ_ТОКЕН") + await bot.send_message(chat_id=user.chat_id, text=message) + print(f"Telegram-уведомление отправлено пользователю {user.chat_id}: {message}") + except Exception as e: + print(f"Ошибка отправки Telegram-уведомления пользователю {user.chat_id}: {e}") + + +def send_email_notification(user, message): + """Отправка уведомления через Email.""" + if user.email: + try: + send_mail( + subject="Уведомление от системы", + message=message, + from_email="noreply@yourdomain.com", + recipient_list=[user.email], + fail_silently=False, + ) + print(f"Email-уведомление отправлено на {user.email}: {message}") + except Exception as e: + print(f"Ошибка отправки Email-уведомления пользователю {user.email}: {e}") + +async def schedule_notifications(): + """Планировщик уведомлений.""" + print("Запуск планировщика уведомлений...") + now = datetime.now().strftime("%H:%M") + + # Получение всех пользователей + users = await sync_to_async(list)(User.objects.all()) + for user in users: + # Получение настроек уведомлений для каждого пользователя + settings, _ = await sync_to_async(NotificationSettings.objects.get_or_create)(user=user) + + # Проверка времени уведомления + if settings.notification_time == now: + message = "Это ваше уведомление от системы." + if settings.telegram_enabled: + await send_telegram_notification(user, message) + if settings.email_enabled: + send_email_notification(user, message) + +async def handle_notification_time(update, context): + """Обработка ввода времени уведомлений.""" + if context.user_data.get("set_time"): + user_id = update.message.from_user.id + new_time = update.message.text + + try: + # Проверяем правильность формата времени + hour, minute = map(int, new_time.split(":")) + user = await sync_to_async(User.objects.filter(chat_id=user_id).first)() + if user: + # Обновляем настройки уведомлений + settings, _ = await sync_to_async(NotificationSettings.objects.get_or_create)(user=user) + settings.notification_time = f"{hour:02}:{minute:02}" + await sync_to_async(settings.save)() + print(f"Пользователь {user_id} установил новое время уведомлений: {new_time}") + await update.message.reply_text(f"Время уведомлений обновлено на {new_time}.") + except ValueError: + print(f"Пользователь {user_id} ввёл некорректное время: {new_time}") + await update.message.reply_text("Неверный формат. Введите время в формате HH:MM.") + finally: + context.user_data["set_time"] = False \ No newline at end of file diff --git a/bot/operations/settings.py b/bot/operations/settings.py new file mode 100644 index 00000000..238db23d --- /dev/null +++ b/bot/operations/settings.py @@ -0,0 +1,108 @@ +from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update +from telegram.ext import ContextTypes +from asgiref.sync import sync_to_async +from users.models import User, NotificationSettings + +async def settings_menu(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Меню настроек уведомлений.""" + query = update.callback_query + await query.answer() + + user_id = query.from_user.id + user = await sync_to_async(User.objects.filter(chat_id=user_id).first)() + if not user: + await query.edit_message_text("Вы не зарегистрированы.") + return + + settings, _ = await sync_to_async(NotificationSettings.objects.get_or_create)(user=user) + + telegram_status = "✅" if settings.telegram_enabled else "❌" + email_status = "✅" if settings.email_enabled else "❌" + + keyboard = [ + [InlineKeyboardButton(f"{telegram_status} Уведомления в Telegram", callback_data="toggle_telegram")], + [InlineKeyboardButton(f"{email_status} Уведомления по Email", callback_data="toggle_email")], + [InlineKeyboardButton("🕒 Настроить время уведомлений", callback_data="set_notification_time")], + [InlineKeyboardButton("📋 Показать текущие настройки", callback_data="current_settings")], + [InlineKeyboardButton("🏠 Главная", callback_data="main_menu")], + [InlineKeyboardButton("🔙 Назад", callback_data="back")], + ] + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text("Настройки уведомлений:", reply_markup=reply_markup) + +async def toggle_telegram(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Переключение состояния Telegram-уведомлений.""" + query = update.callback_query + await query.answer() + + user_id = query.from_user.id + user = await sync_to_async(User.objects.filter(chat_id=user_id).first)() + if not user: + await query.edit_message_text("Вы не зарегистрированы.") + return + + settings, _ = await sync_to_async(NotificationSettings.objects.get_or_create)(user=user) + settings.telegram_enabled = not settings.telegram_enabled + await sync_to_async(settings.save)() + + print(f"Пользователь {user_id} переключил Telegram-уведомления: {settings.telegram_enabled}") + await settings_menu(update, context) + + +async def toggle_email(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Переключение состояния Email-уведомлений.""" + query = update.callback_query + await query.answer() + + user_id = query.from_user.id + user = await sync_to_async(User.objects.filter(chat_id=user_id).first)() + if not user: + await query.edit_message_text("Вы не зарегистрированы.") + return + + settings, _ = await sync_to_async(NotificationSettings.objects.get_or_create)(user=user) + settings.email_enabled = not settings.email_enabled + await sync_to_async(settings.save)() + + print(f"Пользователь {user_id} переключил Email-уведомления: {settings.email_enabled}") + await settings_menu(update, context) + + +async def show_current_settings(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Отображение текущих настроек уведомлений.""" + query = update.callback_query + await query.answer() + + user_id = query.from_user.id + user = await sync_to_async(User.objects.filter(chat_id=user_id).first)() + if not user: + await query.edit_message_text("Вы не зарегистрированы.") + return + + settings, _ = await sync_to_async(NotificationSettings.objects.get_or_create)(user=user) + telegram_status = "✅ Включены" if settings.telegram_enabled else "❌ Выключены" + email_status = "✅ Включены" if settings.email_enabled else "❌ Выключены" + notification_time = settings.notification_time or "Не установлено" + + message = ( + f"📋 Ваши настройки уведомлений:\n" + f"🔔 Telegram: {telegram_status}\n" + f"📧 Email: {email_status}\n" + f"🕒 Время: {notification_time}" + ) + await query.edit_message_text(message) + +async def set_notification_time(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Настройка времени уведомлений.""" + query = update.callback_query + await query.answer() + + user_id = query.from_user.id + user = await sync_to_async(User.objects.filter(chat_id=user_id).first)() + if not user: + await query.edit_message_text("Вы не зарегистрированы.") + return + + await query.edit_message_text("Введите новое время для уведомлений в формате HH:MM (например, 08:30):") + context.user_data["set_time"] = True + print(f"Пользователь {user_id} настраивает время уведомлений.") \ No newline at end of file diff --git a/bot/operations/statistics.py b/bot/operations/statistics.py new file mode 100644 index 00000000..5a8453f5 --- /dev/null +++ b/bot/operations/statistics.py @@ -0,0 +1,81 @@ +from datetime import datetime, timedelta +from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update +from telegram.ext import ContextTypes +from asgiref.sync import sync_to_async +from hotels.models import Reservation, Hotel +from users.models import User + +from bot.utils.pdf_report import generate_pdf_report +from bot.utils.database import get_hotels_for_user + +async def statistics(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Вывод списка отелей для статистики.""" + query = update.callback_query + user_id = query.from_user.id + await query.answer() + + # Получаем пользователя + user = await sync_to_async(User.objects.filter(chat_id=user_id).first)() + if not user: + await query.edit_message_text("Вы не зарегистрированы в системе.") + return + + # Получаем отели, связанные с пользователем + hotels = await get_hotels_for_user(user) + if not hotels: + await query.edit_message_text("У вас нет доступных отелей для статистики.") + return + + # Формируем кнопки для выбора отеля + keyboard = [[InlineKeyboardButton(hotel.name, callback_data=f"stats_hotel_{hotel.id}")] for hotel in hotels] + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text("Выберите отель:", reply_markup=reply_markup) + + + +async def stats_select_period(update: Update, context): + """Выбор периода времени для статистики.""" + query = update.callback_query + await query.answer() + + hotel_id = int(query.data.split("_")[2]) + context.user_data["selected_hotel"] = hotel_id + + keyboard = [ + [InlineKeyboardButton("Неделя", callback_data="stats_period_week")], + [InlineKeyboardButton("Месяц", callback_data="stats_period_month")], + [InlineKeyboardButton("Все время", callback_data="stats_period_all")], + [InlineKeyboardButton("🏠 Главная", callback_data="main_menu")], + [InlineKeyboardButton("🔙 Назад", callback_data="back")], + ] + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text("Выберите период времени:", reply_markup=reply_markup) + + +async def generate_statistics(update: Update, context): + """Генерация и отправка статистики.""" + query = update.callback_query + await query.answer() + + hotel_id = context.user_data["selected_hotel"] + period = query.data.split("_")[2] + + now = datetime.now() + if period == "week": + start_date = now - timedelta(days=7) + end_date = now + elif period == "month": + start_date = now - timedelta(days=30) + end_date = now + else: + start_date = None + end_date = None + + reservations = await sync_to_async(list)( + Reservation.objects.filter(hotel_id=hotel_id).prefetch_related('guests') + ) + hotel = await sync_to_async(Hotel.objects.get)(id=hotel_id) + file_path = generate_pdf_report(hotel.name, reservations, start_date, end_date) + + with open(file_path, "rb") as file: + await query.message.reply_document(document=file, filename=file_path) diff --git a/bot/operations/users.py b/bot/operations/users.py new file mode 100644 index 00000000..15a817c4 --- /dev/null +++ b/bot/operations/users.py @@ -0,0 +1,163 @@ +from asgiref.sync import sync_to_async +from telegram import Update +from telegram.ext import ContextTypes +from telegram.ext import CallbackContext +from telegram import InlineKeyboardButton, InlineKeyboardMarkup +from telegram import KeyboardButton, ReplyKeyboardMarkup + +from hotels.models import Hotel, UserHotel +from users.models import User + + +async def edit_user(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Изменение имени пользователя.""" + query = update.callback_query + await query.answer() + + user_id = int(query.data.split("_")[2]) + context.user_data["edit_user_id"] = user_id + + await query.edit_message_text("Введите новое имя пользователя:") + +async def delete_user(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Удаление пользователя.""" + query = update.callback_query + await query.answer() + + user_id = int(query.data.split("_")[2]) + user = await sync_to_async(User.objects.get)(id=user_id) + await sync_to_async(user.delete)() + + await query.edit_message_text("Пользователь успешно удален.") + +async def get_users_for_hotel(hotel_id): + """ + Получение пользователей, зарегистрированных в отеле с правами управления через бота. + """ + users = await sync_to_async(list)( + User.objects.filter(user_hotels__hotel_id=hotel_id, user_hotels__role__in=["admin", "manager"]).distinct() + ) + return users + + +async def show_users(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Показать пользователей, зарегистрированных в отеле.""" + query = update.callback_query + await query.answer() + + # Если callback_data не содержит ID отеля, отображаем список отелей + if not query.data.startswith("manage_users_hotel_"): + user_id = query.from_user.id + hotels = await get_hotels_for_user(user_id) + + if not hotels: + await query.edit_message_text("У вас нет доступных отелей.") + return + + keyboard = [ + [InlineKeyboardButton(hotel.name, callback_data=f"manage_users_hotel_{hotel.id}")] + for hotel in hotels + ] + keyboard.append([InlineKeyboardButton("🏠 Главная", callback_data="main_menu")]) + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text("Выберите отель:", reply_markup=reply_markup) + return + + # Обработка пользователей отеля + hotel_id = int(query.data.split("_")[-1]) + users = await sync_to_async(list)( + User.objects.filter(user_hotel__hotel_id=hotel_id) + ) + + if not users: + await query.edit_message_text("В этом отеле нет пользователей.") + return + + keyboard = [ + [InlineKeyboardButton(f"{user.username}", callback_data=f"edit_user_{user.id}")] + for user in users + ] + keyboard.append([ + InlineKeyboardButton("🏠 Главная", callback_data="main_menu"), + InlineKeyboardButton("🔙 Назад", callback_data="manage_users"), + [InlineKeyboardButton("🏠 Главная", callback_data="main_menu")], + [InlineKeyboardButton("🔙 Назад", callback_data="back")], + ]) + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text("Выберите пользователя:", reply_markup=reply_markup) + + +async def get_hotels_for_user(user): + """Получение отелей, связанных с пользователем.""" + return await sync_to_async(list)( + Hotel.objects.filter(hotel_users__user=user).distinct() + ) + +async def show_user_hotels(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Показ списка отелей пользователя.""" + query = update.callback_query + await query.answer() + + user_id = query.from_user.id + user_hotels = await get_hotels_for_user(user_id) + + if not user_hotels: + await query.edit_message_text("У вас нет связанных отелей.") + return + + keyboard = [ + [InlineKeyboardButton(hotel.name, callback_data=f"users_hotel_{hotel.id}")] + for hotel in user_hotels + ] + keyboard.append([InlineKeyboardButton("🔙 Назад", callback_data="main_menu")]) + + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text("Выберите отель:", reply_markup=reply_markup) + +async def show_users_in_hotel(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Показ пользователей в выбранном отеле.""" + query = update.callback_query + await query.answer() + + hotel_id = int(query.data.split("_")[2]) + users = await get_users_for_hotel(hotel_id) + + if not users: + await query.edit_message_text("В этом отеле нет пользователей.") + return + + keyboard = [ + [InlineKeyboardButton(f"{user.first_name} {user.last_name}", callback_data=f"user_action_{user.id}")] + for user in users + ] + keyboard.append([ + InlineKeyboardButton("🏠 Главная", callback_data="main_menu"), + InlineKeyboardButton("🔙 Назад", callback_data="manage_users"), + ]) + + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text("Пользователи отеля:", reply_markup=reply_markup) + +async def user_action_menu(update: Update, context: ContextTypes.DEFAULT_TYPE): + """Меню действий для пользователя.""" + query = update.callback_query + await query.answer() + + user_id = int(query.data.split("_")[2]) + user = await sync_to_async(User.objects.get)(id=user_id) + + keyboard = [ + [InlineKeyboardButton("✏️ Изменить имя", callback_data=f"edit_user_{user_id}")], + [InlineKeyboardButton("🗑️ Удалить", callback_data=f"delete_user_{user_id}")], + [ + InlineKeyboardButton("🏠 Главная", callback_data="main_menu"), + InlineKeyboardButton("🔙 Назад", callback_data="users_hotel"), + ], + ] + + reply_markup = InlineKeyboardMarkup(keyboard) + await query.edit_message_text( + f"Пользователь: {user.first_name} {user.last_name}\nВыберите действие:", + reply_markup=reply_markup, + ) + diff --git a/bot/utils/__init__.py b/bot/utils/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/bot/utils/bot_setup.py b/bot/utils/bot_setup.py new file mode 100644 index 00000000..ac609edf --- /dev/null +++ b/bot/utils/bot_setup.py @@ -0,0 +1,71 @@ +from telegram.ext import ( + Application, + CommandHandler, + CallbackQueryHandler, + MessageHandler, + filters, +) +from bot.handlers import ( + start, + handle_button_click, + manage_hotels, + hotel_actions, + delete_hotel, + check_pms, + setup_rooms, + settings_menu, + toggle_telegram, + toggle_email, + show_current_settings, + statistics, + generate_statistics, + stats_select_period, +) + +from bot.operations.settings import ( + settings_menu, + toggle_telegram, + toggle_email, + show_current_settings, + set_notification_time, + +) + +from bot.operations.notifications import ( + handle_notification_time, +) + +from bot.operations.users import ( + + show_users, + +) + +def setup_bot(application: Application): + """Настройка Telegram бота: регистрация обработчиков.""" + print("Настройка Telegram приложения...") + + # Регистрация обработчиков команд + application.add_handler(CommandHandler("start", start)) + + # Регистрация обработчиков кнопок + application.add_handler(CallbackQueryHandler(handle_button_click)) + application.add_handler(CallbackQueryHandler(manage_hotels, pattern="^manage_hotels$")) + application.add_handler(CallbackQueryHandler(show_users, pattern="^manage_users$")) + application.add_handler(CallbackQueryHandler(settings_menu, pattern="^settings$")) + application.add_handler(CallbackQueryHandler(toggle_telegram, pattern="^toggle_telegram$")) + application.add_handler(CallbackQueryHandler(toggle_email, pattern="^toggle_email$")) + application.add_handler(CallbackQueryHandler(set_notification_time, pattern="^set_notification_time$")) + application.add_handler(CallbackQueryHandler(show_current_settings, pattern="^current_settings$")) + application.add_handler(CallbackQueryHandler(hotel_actions, pattern="^hotel_")) + application.add_handler(CallbackQueryHandler(delete_hotel, pattern="^delete_hotel_")) + application.add_handler(CallbackQueryHandler(check_pms, pattern="^check_pms_")) + application.add_handler(CallbackQueryHandler(setup_rooms, pattern="^setup_rooms_")) + application.add_handler(CallbackQueryHandler(statistics, pattern="^stats$")) + application.add_handler(CallbackQueryHandler(stats_select_period, pattern="^stats_hotel_")) + application.add_handler(CallbackQueryHandler(generate_statistics, pattern="^stats_period_")) + + # Регистрация обработчиков текстовых сообщений + application.add_handler(MessageHandler(filters.TEXT & filters.ChatType.PRIVATE, handle_notification_time)) + + print("Обработчики успешно зарегистрированы.") diff --git a/bot/utils/database.py b/bot/utils/database.py new file mode 100644 index 00000000..7f1409bf --- /dev/null +++ b/bot/utils/database.py @@ -0,0 +1,26 @@ +from users.models import User +from hotels.models import Hotel, Reservation +from asgiref.sync import sync_to_async + +async def get_user_from_chat_id(chat_id): + return await sync_to_async(User.objects.filter(chat_id=chat_id).first)() + +async def get_hotel_by_id(hotel_id): + return await sync_to_async(Hotel.objects.get)(id=hotel_id) + +async def get_hotels_for_user(user): + """Получение отелей, связанных с пользователем.""" + # Проверяем, является ли пользователь сотрудником какого-либо отеля + user_hotels = await sync_to_async(list)( + Hotel.objects.filter(user_hotel__user=user).distinct() + ) + print(user_hotels) + return user_hotels + +async def get_reservations(hotel_id, start_date=None, end_date=None): + query = Reservation.objects.filter(hotel_id=hotel_id) + if start_date: + query = query.filter(check_in__gte=start_date) + if end_date: + query = query.filter(check_out__lte=end_date) + return await sync_to_async(list)(query.prefetch_related('guests')) diff --git a/bot/utils/notifications.py b/bot/utils/notifications.py new file mode 100644 index 00000000..b822682d --- /dev/null +++ b/bot/utils/notifications.py @@ -0,0 +1,28 @@ +from telegram import Bot +from django.core.mail import send_mail + +async def send_telegram_notification(user, message): + """Отправка уведомления через Telegram.""" + if user.chat_id: + try: + bot = Bot(token="ВАШ_ТОКЕН") + await bot.send_message(chat_id=user.chat_id, text=message) + print(f"Telegram-уведомление отправлено пользователю {user.chat_id}: {message}") + except Exception as e: + print(f"Ошибка отправки Telegram-уведомления пользователю {user.chat_id}: {e}") + + +def send_email_notification(user, message): + """Отправка уведомления через Email.""" + if user.email: + try: + send_mail( + subject="Уведомление от системы", + message=message, + from_email="noreply@yourdomain.com", + recipient_list=[user.email], + fail_silently=False, + ) + print(f"Email-уведомление отправлено на {user.email}: {message}") + except Exception as e: + print(f"Ошибка отправки Email-уведомления пользователю {user.email}: {e}") diff --git a/bot/utils/pdf_report.py b/bot/utils/pdf_report.py new file mode 100644 index 00000000..ac25ce51 --- /dev/null +++ b/bot/utils/pdf_report.py @@ -0,0 +1,67 @@ +from fpdf import FPDF +import os + + +def generate_pdf_report(hotel_name, reservations, start_date, end_date): + """Генерация PDF отчета.""" + pdf = FPDF() + pdf.add_page() + + # Укажите путь к шрифту + font_path = os.path.join("bot", "fonts", "OpenSans-Regular.ttf") + if not os.path.exists(font_path): + raise FileNotFoundError(f"Шрифт {font_path} не найден. Убедитесь, что он находится в указанной папке.") + + pdf.add_font("OpenSans", "", font_path, uni=True) + pdf.set_font("OpenSans", size=12) + + # Заголовки + title = f"Отчет по заселениям: {hotel_name}" + period = ( + f"Период: {start_date.strftime('%d.%m.%Y')} - {end_date.strftime('%d.%m.%Y')}" + if start_date and end_date + else "Период: Все время" + ) + + pdf.cell(0, 10, txt=title, ln=True, align="C") + pdf.cell(0, 10, txt=period, ln=True, align="C") + pdf.ln(10) + + # Ширины колонок + page_width = pdf.w - 20 + col_widths = [page_width * 0.2, page_width * 0.2, page_width * 0.15, page_width * 0.25, page_width * 0.1, page_width * 0.1] + + # Заголовки таблицы + pdf.set_font("OpenSans", size=10) + headers = ["Дата заезда", "Дата выезда", "Номер", "Тип комнаты", "Цена", "Скидка"] + for width, header in zip(col_widths, headers): + pdf.cell(width, 10, header, border=1, align="C") + pdf.ln() + + total_price = 0 + total_discount = 0 + + for res in reservations: + price = res.price or 0 + discount = res.discount or 0 + total_price += price + total_discount += discount + + pdf.cell(col_widths[0], 10, res.check_in.strftime('%d.%m.%Y'), border=1) + pdf.cell(col_widths[1], 10, res.check_out.strftime('%d.%m.%Y'), border=1) + pdf.cell(col_widths[2], 10, res.room_number, border=1) + pdf.cell(col_widths[3], 10, res.room_type, border=1) + pdf.cell(col_widths[4], 10, f"{price:.2f} ₽", border=1, align="R") + pdf.cell(col_widths[5], 10, f"{discount:.2f} ₽", border=1, align="R") + pdf.ln() + + pdf.ln(5) + pdf.set_font("OpenSans", size=12) + pdf.cell(0, 10, "Итоги:", ln=True) + pdf.cell(0, 10, f"Общая сумма цен: {total_price:.2f} ₽", ln=True) + pdf.cell(0, 10, f"Общая сумма скидок: {total_discount:.2f} ₽", ln=True) + + file_path = os.path.join("reports", f"{hotel_name}_report.pdf") + os.makedirs(os.path.dirname(file_path), exist_ok=True) + pdf.output(file_path) + return file_path diff --git a/bot/utils/scheduler.py b/bot/utils/scheduler.py new file mode 100644 index 00000000..7075b3a8 --- /dev/null +++ b/bot/utils/scheduler.py @@ -0,0 +1,10 @@ +from apscheduler.schedulers.asyncio import AsyncIOScheduler +from bot.operations.notifications import schedule_notifications + + +def setup_scheduler(): + """Настройка планировщика уведомлений.""" + print("Настройка планировщика...") + scheduler = AsyncIOScheduler() + scheduler.add_job(schedule_notifications, "cron", minute="*") + return scheduler diff --git a/hotels/migrations/0012_userhotel_role_alter_userhotel_hotel_and_more.py b/hotels/migrations/0012_userhotel_role_alter_userhotel_hotel_and_more.py new file mode 100644 index 00000000..29d97fac --- /dev/null +++ b/hotels/migrations/0012_userhotel_role_alter_userhotel_hotel_and_more.py @@ -0,0 +1,30 @@ +# Generated by Django 5.1.4 on 2024-12-07 06:28 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hotels', '0011_reservation_guest'), + ('users', '0005_notificationsettings'), + ] + + operations = [ + migrations.AddField( + model_name='userhotel', + name='role', + field=models.CharField(choices=[('admin', 'Admin'), ('manager', 'Manager')], default='manager', max_length=50, verbose_name='Роль'), + ), + migrations.AlterField( + model_name='userhotel', + name='hotel', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='hotel_users', to='hotels.hotel', verbose_name='Отель'), + ), + migrations.AlterField( + model_name='userhotel', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_hotels', to='users.user', verbose_name='Пользователь'), + ), + ] diff --git a/hotels/migrations/0013_remove_userhotel_role_alter_userhotel_user.py b/hotels/migrations/0013_remove_userhotel_role_alter_userhotel_user.py new file mode 100644 index 00000000..7655c612 --- /dev/null +++ b/hotels/migrations/0013_remove_userhotel_role_alter_userhotel_user.py @@ -0,0 +1,24 @@ +# Generated by Django 5.1.4 on 2024-12-07 07:04 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hotels', '0012_userhotel_role_alter_userhotel_hotel_and_more'), + ('users', '0005_notificationsettings'), + ] + + operations = [ + migrations.RemoveField( + model_name='userhotel', + name='role', + ), + migrations.AlterField( + model_name='userhotel', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.user', verbose_name='Пользователь'), + ), + ] diff --git a/hotels/migrations/0014_alter_userhotel_user.py b/hotels/migrations/0014_alter_userhotel_user.py new file mode 100644 index 00000000..e7396f30 --- /dev/null +++ b/hotels/migrations/0014_alter_userhotel_user.py @@ -0,0 +1,20 @@ +# Generated by Django 5.1.4 on 2024-12-07 08:24 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hotels', '0013_remove_userhotel_role_alter_userhotel_user'), + ('users', '0005_notificationsettings'), + ] + + operations = [ + migrations.AlterField( + model_name='userhotel', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_hotels', to='users.user', verbose_name='Пользователь'), + ), + ] diff --git a/hotels/migrations/0015_alter_userhotel_user.py b/hotels/migrations/0015_alter_userhotel_user.py new file mode 100644 index 00000000..3c6a4a5b --- /dev/null +++ b/hotels/migrations/0015_alter_userhotel_user.py @@ -0,0 +1,20 @@ +# Generated by Django 5.1.4 on 2024-12-07 08:29 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hotels', '0014_alter_userhotel_user'), + ('users', '0005_notificationsettings'), + ] + + operations = [ + migrations.AlterField( + model_name='userhotel', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='userhotels', to='users.user', verbose_name='Пользователь'), + ), + ] diff --git a/hotels/migrations/0016_alter_userhotel_user.py b/hotels/migrations/0016_alter_userhotel_user.py new file mode 100644 index 00000000..e14c8459 --- /dev/null +++ b/hotels/migrations/0016_alter_userhotel_user.py @@ -0,0 +1,20 @@ +# Generated by Django 5.1.4 on 2024-12-07 08:30 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hotels', '0015_alter_userhotel_user'), + ('users', '0005_notificationsettings'), + ] + + operations = [ + migrations.AlterField( + model_name='userhotel', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_hotels', to='users.user', verbose_name='Пользователь'), + ), + ] diff --git a/hotels/migrations/0017_alter_userhotel_hotel_alter_userhotel_user.py b/hotels/migrations/0017_alter_userhotel_hotel_alter_userhotel_user.py new file mode 100644 index 00000000..b420cb7a --- /dev/null +++ b/hotels/migrations/0017_alter_userhotel_hotel_alter_userhotel_user.py @@ -0,0 +1,25 @@ +# Generated by Django 5.1.4 on 2024-12-07 08:31 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hotels', '0016_alter_userhotel_user'), + ('users', '0005_notificationsettings'), + ] + + operations = [ + migrations.AlterField( + model_name='userhotel', + name='hotel', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='hotelusers', to='hotels.hotel', verbose_name='Отель'), + ), + migrations.AlterField( + model_name='userhotel', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='userhotel', to='users.user', verbose_name='Пользователь'), + ), + ] diff --git a/hotels/migrations/0018_alter_userhotel_hotel_alter_userhotel_user.py b/hotels/migrations/0018_alter_userhotel_hotel_alter_userhotel_user.py new file mode 100644 index 00000000..57d4e89c --- /dev/null +++ b/hotels/migrations/0018_alter_userhotel_hotel_alter_userhotel_user.py @@ -0,0 +1,25 @@ +# Generated by Django 5.1.4 on 2024-12-07 08:36 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hotels', '0017_alter_userhotel_hotel_alter_userhotel_user'), + ('users', '0005_notificationsettings'), + ] + + operations = [ + migrations.AlterField( + model_name='userhotel', + name='hotel', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='hotel_users', to='hotels.hotel', verbose_name='Отель'), + ), + migrations.AlterField( + model_name='userhotel', + name='user', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_hotel', to='users.user', verbose_name='Пользователь'), + ), + ] diff --git a/hotels/models.py b/hotels/models.py index eddaf943..a33a8aa3 100644 --- a/hotels/models.py +++ b/hotels/models.py @@ -73,8 +73,12 @@ class PMSIntegrationLog(models.Model): class UserHotel(models.Model): - user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Пользователь") - hotel = models.ForeignKey(Hotel, on_delete=models.CASCADE, verbose_name="Отель") + user = models.ForeignKey( + User, on_delete=models.CASCADE, related_name="user_hotel", verbose_name="Пользователь" + ) + hotel = models.ForeignKey( + Hotel, on_delete=models.CASCADE, related_name="hotel_users", verbose_name="Отель" + ) def __str__(self): return f"{self.user.username} - {self.hotel.name}" @@ -84,6 +88,8 @@ class UserHotel(models.Model): verbose_name_plural = "Пользователи отелей" + + class APIRequestLog(models.Model): api = models.ForeignKey(APIConfiguration, on_delete=models.CASCADE, verbose_name="API") request_time = models.DateTimeField(auto_now_add=True, verbose_name="Время запроса") diff --git a/reports/Golden Hills 4*_report.pdf b/reports/Golden Hills 4*_report.pdf index 02379a3c..eb1d69dd 100644 Binary files a/reports/Golden Hills 4*_report.pdf and b/reports/Golden Hills 4*_report.pdf differ