bot refactor
This commit is contained in:
620
bot/handlers.py
620
bot/handlers.py
@@ -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
19
bot/keyboards.py
Normal 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")],
|
||||||
|
])
|
||||||
@@ -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
28
bot/operations.py
Normal 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'))
|
||||||
0
bot/operations/__init__.py
Normal file
0
bot/operations/__init__.py
Normal file
107
bot/operations/hotels.py
Normal file
107
bot/operations/hotels.py
Normal 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
|
||||||
74
bot/operations/notifications.py
Normal file
74
bot/operations/notifications.py
Normal 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
108
bot/operations/settings.py
Normal 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} настраивает время уведомлений.")
|
||||||
81
bot/operations/statistics.py
Normal file
81
bot/operations/statistics.py
Normal 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
163
bot/operations/users.py
Normal 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
0
bot/utils/__init__.py
Normal file
71
bot/utils/bot_setup.py
Normal file
71
bot/utils/bot_setup.py
Normal 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
26
bot/utils/database.py
Normal 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'))
|
||||||
28
bot/utils/notifications.py
Normal file
28
bot/utils/notifications.py
Normal 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
67
bot/utils/pdf_report.py
Normal 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
10
bot/utils/scheduler.py
Normal 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
|
||||||
@@ -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='Пользователь'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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='Пользователь'),
|
||||||
|
),
|
||||||
|
]
|
||||||
20
hotels/migrations/0014_alter_userhotel_user.py
Normal file
20
hotels/migrations/0014_alter_userhotel_user.py
Normal 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='Пользователь'),
|
||||||
|
),
|
||||||
|
]
|
||||||
20
hotels/migrations/0015_alter_userhotel_user.py
Normal file
20
hotels/migrations/0015_alter_userhotel_user.py
Normal 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='Пользователь'),
|
||||||
|
),
|
||||||
|
]
|
||||||
20
hotels/migrations/0016_alter_userhotel_user.py
Normal file
20
hotels/migrations/0016_alter_userhotel_user.py
Normal 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='Пользователь'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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='Пользователь'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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='Пользователь'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -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.
Reference in New Issue
Block a user