bot refactor

This commit is contained in:
2024-12-07 17:41:27 +09:00
parent 72cb7a4ef7
commit 626f378303
25 changed files with 1065 additions and 657 deletions

View File

@@ -1,37 +1,20 @@
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update from telegram import Update, InlineKeyboardMarkup, InlineKeyboardButton
from telegram.ext import ContextTypes from telegram.ext import ContextTypes
from users.models import User from bot.operations.hotels import manage_hotels, hotel_actions, delete_hotel, check_pms, setup_rooms
from hotels.models import Hotel, UserHotel, Reservation from bot.operations.statistics import statistics, stats_select_period, generate_statistics
from users.models import NotificationSettings from bot.operations.settings import settings_menu, toggle_telegram, toggle_email, set_notification_time, show_current_settings
from asgiref.sync import sync_to_async from bot.operations.users import show_users
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)()
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): async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработчик команды /start с проверкой chat_id""" """Обработчик команды /start."""
user_id = update.message.from_user.id user_id = (
update.message.from_user.id
if update.message
else update.callback_query.from_user.id
)
print(f"Пользователь {user_id} вызвал команду /start") print(f"Пользователь {user_id} вызвал команду /start")
user = await get_user_from_chat_id(user_id)
if user:
keyboard = [ keyboard = [
[InlineKeyboardButton("📊 Статистика", callback_data="stats")], [InlineKeyboardButton("📊 Статистика", callback_data="stats")],
[InlineKeyboardButton("🏨 Управление отелями", callback_data="manage_hotels")], [InlineKeyboardButton("🏨 Управление отелями", callback_data="manage_hotels")],
@@ -39,565 +22,74 @@ async def start(update: Update, context: ContextTypes.DEFAULT_TYPE):
[InlineKeyboardButton("⚙️ Настройки", callback_data="settings")], [InlineKeyboardButton("⚙️ Настройки", callback_data="settings")],
] ]
reply_markup = InlineKeyboardMarkup(keyboard) reply_markup = InlineKeyboardMarkup(keyboard)
if update.message:
await update.message.reply_text("Выберите действие:", reply_markup=reply_markup) await update.message.reply_text("Выберите действие:", reply_markup=reply_markup)
else: elif update.callback_query:
print(f"Пользователь {user_id} не зарегистрирован.") await update.callback_query.edit_message_text("Выберите действие:", reply_markup=reply_markup)
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
]
reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text("Выберите отель:", reply_markup=reply_markup)
# --- Обработчики кнопок ---
async def handle_button_click(update: Update, context: ContextTypes.DEFAULT_TYPE): async def handle_button_click(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработчик всех нажатий кнопок""" """Обработчик всех нажатий кнопок."""
query = update.callback_query query = update.callback_query
await query.answer() await query.answer()
if query.data == "stats": callback_data = query.data
await statistics(update, context) # Добавляем вызов функции для обработки статистики
elif query.data.startswith("stats_hotel_"): # Сохраняем предыдущее меню для кнопки "Назад"
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) await stats_select_period(update, context)
elif query.data.startswith("stats_period_"): elif callback_data.startswith("stats_period_"):
await generate_statistics(update, context) await generate_statistics(update, context)
elif query.data == "manage_hotels": elif callback_data == "manage_hotels":
await manage_hotels(update, context) 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) await hotel_actions(update, context)
elif query.data.startswith("delete_hotel_"): elif callback_data.startswith("delete_hotel_"):
await delete_hotel(update, context) await delete_hotel(update, context)
elif query.data.startswith("check_pms_"): elif callback_data.startswith("check_pms_"):
await check_pms(update, context) await check_pms(update, context)
elif query.data.startswith("setup_rooms_"): elif callback_data.startswith("setup_rooms_"):
await setup_rooms(update, context) await setup_rooms(update, context)
elif query.data == "settings": elif callback_data == "settings":
await settings_menu(update, context) await settings_menu(update, context)
elif query.data == "toggle_telegram": elif callback_data == "toggle_telegram":
await toggle_telegram(update, context) await toggle_telegram(update, context)
elif query.data == "toggle_email": elif callback_data == "toggle_email":
await toggle_email(update, context) await toggle_email(update, context)
elif query.data == "set_notification_time": elif callback_data == "set_notification_time":
await set_notification_time(update, context) await set_notification_time(update, context)
elif query.data == "current_settings": elif callback_data == "current_settings":
await show_current_settings(update, context) 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: else:
print(f"Неизвестный callback_data: {query.data}")
await query.edit_message_text("Команда не распознана.") await query.edit_message_text("Команда не распознана.")
async def hotel_actions(update: Update, context: ContextTypes.DEFAULT_TYPE): async def navigate_back(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Обработчик действий для выбранного отеля""" """Обработчик кнопки 'Назад'."""
query = update.callback_query previous_menu = context.user_data.get("previous_menu", "main_menu")
await query.answer() context.user_data["current_menu"] = previous_menu
hotel_id = int(query.data.split("_")[1]) if previous_menu == "main_menu":
print(f"Пользователь {query.from_user.id} выбрал отель с ID {hotel_id}") await start(update, context)
elif previous_menu == "stats":
hotel = await get_hotel_by_id(hotel_id) await statistics(update, context)
if not hotel: elif previous_menu == "manage_hotels":
print(f"Отель с ID {hotel_id} не найден.") await manage_hotels(update, context)
await query.edit_message_text("Отель не найден.") elif previous_menu == "manage_users":
return await show_users(update, context)
elif previous_menu.startswith("hotel_"):
keyboard = [ await hotel_actions(update, context)
[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} успешно удалён.")
else: else:
print(f"Отель с ID {hotel_id} не найден.") await update.callback_query.edit_message_text("Команда не распознана.")
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))

19
bot/keyboards.py Normal file
View File

@@ -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")],
])

View File

@@ -3,79 +3,14 @@ import django
import asyncio import asyncio
from apscheduler.schedulers.asyncio import AsyncIOScheduler from apscheduler.schedulers.asyncio import AsyncIOScheduler
from django.core.management.base import BaseCommand from django.core.management.base import BaseCommand
from telegram.ext import ( from telegram.ext import Application
Application, from bot.utils.bot_setup import setup_bot
CommandHandler, from bot.utils.scheduler import setup_scheduler
CallbackQueryHandler, from dotenv import load_dotenv
MessageHandler, from bot.operations.users import show_users
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,
)
# Настройка Django окружения # Загрузка переменных окружения
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'touchh.settings') load_dotenv()
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()
class Command(BaseCommand): class Command(BaseCommand):
@@ -84,20 +19,49 @@ class Command(BaseCommand):
def handle(self, *args, **options): def handle(self, *args, **options):
print("Запуск Telegram бота...") print("Запуск Telegram бота...")
# Получаем текущий цикл событий # Настройка Django окружения
loop = asyncio.get_event_loop() os.environ.setdefault("DJANGO_SETTINGS_MODULE", "touchh.settings")
django.setup()
# Создаём экземпляр приложения # Создание приложения Telegram
application = Application.builder().token("8125171867:AAGxDcSpQxJy3_pmq3TDBWtqaAVCj7b-F5k").build() 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: try:
loop.run_forever() print("Инициализация Telegram бота...")
except KeyboardInterrupt: 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 бота...") print("Остановка Telegram бота...")
loop.run_until_complete(application.stop()) await application.stop() # Завершаем приложение перед shutdown
scheduler = AsyncIOScheduler() print("Остановка планировщика...")
scheduler.shutdown(wait=False) scheduler.shutdown(wait=False)
print("Планировщик остановлен.")
try:
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

28
bot/operations.py Normal file
View File

@@ -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'))

View File

107
bot/operations/hotels.py Normal file
View File

@@ -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

View File

@@ -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

108
bot/operations/settings.py Normal file
View File

@@ -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} настраивает время уведомлений.")

View File

@@ -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)

163
bot/operations/users.py Normal file
View File

@@ -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,
)

0
bot/utils/__init__.py Normal file
View File

71
bot/utils/bot_setup.py Normal file
View File

@@ -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("Обработчики успешно зарегистрированы.")

26
bot/utils/database.py Normal file
View File

@@ -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'))

View File

@@ -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}")

67
bot/utils/pdf_report.py Normal file
View File

@@ -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

10
bot/utils/scheduler.py Normal file
View File

@@ -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

View File

@@ -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='Пользователь'),
),
]

View File

@@ -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='Пользователь'),
),
]

View File

@@ -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='Пользователь'),
),
]

View File

@@ -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='Пользователь'),
),
]

View File

@@ -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='Пользователь'),
),
]

View File

@@ -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='Пользователь'),
),
]

View File

@@ -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='Пользователь'),
),
]

View File

@@ -73,8 +73,12 @@ class PMSIntegrationLog(models.Model):
class UserHotel(models.Model): class UserHotel(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, verbose_name="Пользователь") user = models.ForeignKey(
hotel = models.ForeignKey(Hotel, on_delete=models.CASCADE, verbose_name="Отель") 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): def __str__(self):
return f"{self.user.username} - {self.hotel.name}" return f"{self.user.username} - {self.hotel.name}"
@@ -84,6 +88,8 @@ class UserHotel(models.Model):
verbose_name_plural = "Пользователи отелей" verbose_name_plural = "Пользователи отелей"
class APIRequestLog(models.Model): class APIRequestLog(models.Model):
api = models.ForeignKey(APIConfiguration, on_delete=models.CASCADE, verbose_name="API") api = models.ForeignKey(APIConfiguration, on_delete=models.CASCADE, verbose_name="API")
request_time = models.DateTimeField(auto_now_add=True, verbose_name="Время запроса") request_time = models.DateTimeField(auto_now_add=True, verbose_name="Время запроса")

Binary file not shown.