This commit is contained in:
2024-12-12 07:05:26 +09:00
parent 6a3e6d5a65
commit ca82064e00
7 changed files with 212 additions and 91 deletions

View File

@@ -1,4 +1,4 @@
from datetime import datetime, timedelta from datetime import datetime, timedelta, timezone
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update
from telegram.ext import ContextTypes from telegram.ext import ContextTypes
from asgiref.sync import sync_to_async from asgiref.sync import sync_to_async
@@ -7,7 +7,7 @@ from users.models import User
from bot.utils.pdf_report import generate_pdf_report from bot.utils.pdf_report import generate_pdf_report
from bot.utils.database import get_hotels_for_user, get_hotel_by_name from bot.utils.database import get_hotels_for_user, get_hotel_by_name
from django.utils.timezone import make_aware
async def statistics(update: Update, context: ContextTypes.DEFAULT_TYPE): async def statistics(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Вывод списка отелей для статистики.""" """Вывод списка отелей для статистики."""
@@ -55,6 +55,7 @@ async def stats_select_period(update: Update, context: ContextTypes.DEFAULT_TYPE
reply_markup = InlineKeyboardMarkup(keyboard) reply_markup = InlineKeyboardMarkup(keyboard)
await query.edit_message_text("Выберите период времени:", reply_markup=reply_markup) await query.edit_message_text("Выберите период времени:", reply_markup=reply_markup)
async def generate_statistics(update: Update, context: ContextTypes.DEFAULT_TYPE): async def generate_statistics(update: Update, context: ContextTypes.DEFAULT_TYPE):
"""Генерация и отправка статистики.""" """Генерация и отправка статистики."""
query = update.callback_query query = update.callback_query
@@ -66,47 +67,70 @@ async def generate_statistics(update: Update, context: ContextTypes.DEFAULT_TYPE
return return
period = query.data.split("_")[2] period = query.data.split("_")[2]
print(f'Period raw: {query.data}')
print(f'Selected period: {period}')
now = datetime.utcnow().replace(tzinfo=timezone.utc) # Используем timezone.utc
now = datetime.now()
if period == "day": if period == "day":
start_date = (now - timedelta(days=1)).date() # Вчерашняя дата start_date = (now - timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0)
end_date = now.date() # Сегодняшняя дата end_date = now.replace(hour=23, minute=59, second=59, microsecond=999999)
elif period == "week": elif period == "week":
start_date = (now - timedelta(days=7)).date() start_date = (now - timedelta(days=7)).replace(hour=0, minute=0, second=0, microsecond=0)
end_date = now.date() end_date = now.replace(hour=23, minute=59, second=59, microsecond=999999)
elif period == "month": elif period == "month":
start_date = (now - timedelta(days=30)).date() start_date = (now - timedelta(days=30)).replace(hour=0, minute=0, second=0, microsecond=0)
end_date = now.date() end_date = now.replace(hour=23, minute=59, second=59, microsecond=999999)
else: else: # "all"
start_date = None start_date = None
end_date = None end_date = None
print(f'Raw start_date: {start_date}, Raw end_date: {end_date}')
# Убедитесь, что даты имеют временную зону UTC
if start_date:
start_date = make_aware(start_date) if start_date.tzinfo is None else start_date
if end_date:
end_date = make_aware(end_date) if end_date.tzinfo is None else end_date
print(f'Filtered start_date: {start_date}, Filtered end_date: {end_date}')
# Фильтрация по "дата заезда" # Фильтрация по "дата заезда"
if start_date and end_date: if start_date and end_date:
reservations = await sync_to_async(list)( reservations = await sync_to_async(list)(
Reservation.objects.filter( Reservation.objects.filter(
hotel_id=hotel_id, hotel_id=hotel_id,
check_in__date__gte=start_date, check_in__gte=start_date,
check_in__date__lte=end_date check_in__lte=end_date
).prefetch_related('guests') ).select_related('hotel')
) )
else: else: # Без фильтра по дате
reservations = await sync_to_async(list)( reservations = await sync_to_async(list)(
Reservation.objects.filter(hotel_id=hotel_id).prefetch_related('guests') Reservation.objects.filter(
hotel_id=hotel_id
).select_related('hotel')
) )
print(f'Filtered reservations count: {len(reservations)}')
if not reservations: if not reservations:
await query.edit_message_text("Нет данных для статистики за выбранный период.") await query.edit_message_text("Нет данных для статистики за выбранный период.")
return return
hotel = await sync_to_async(Hotel.objects.get)(id=hotel_id) hotel = await sync_to_async(Hotel.objects.get)(id=hotel_id)
print(f'Hotel: {hotel.name}')
for reservation in reservations:
print(f"Reservation ID: {reservation.reservation_id}, Hotel: {reservation.hotel.name}, "
f"Room number: {reservation.room_number}, Check-in: {reservation.check_in}, Check-out: {reservation.check_out}")
# Генерация PDF отчета (пример)
file_path = generate_pdf_report(hotel.name, reservations, start_date, end_date) file_path = generate_pdf_report(hotel.name, reservations, start_date, end_date)
print(f'Generated file path: {file_path}')
with open(file_path, "rb") as file: with open(file_path, "rb") as file:
await query.message.reply_document(document=file, filename=f"{hotel.name}_report.pdf") await query.message.reply_document(document=file, filename=f"{hotel.name}_report.pdf")
async def stats_back(update: Update, context): async def stats_back(update: Update, context):
"""Возврат к выбору отеля.""" """Возврат к выбору отеля."""
query = update.callback_query query = update.callback_query

View File

@@ -0,0 +1,18 @@
# Generated by Django 5.1.4 on 2024-12-11 10:52
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('hotels', '0003_initial'),
]
operations = [
migrations.AlterField(
model_name='reservation',
name='room_number',
field=models.CharField(blank=True, max_length=255, null=True),
),
]

View File

@@ -88,7 +88,7 @@ class APIRequestLog(models.Model):
class Reservation(models.Model): class Reservation(models.Model):
hotel = models.ForeignKey(Hotel, on_delete=models.CASCADE, verbose_name="Отель") hotel = models.ForeignKey(Hotel, on_delete=models.CASCADE, verbose_name="Отель")
reservation_id = models.BigIntegerField(unique=True, verbose_name="ID бронирования") reservation_id = models.BigIntegerField(unique=True, verbose_name="ID бронирования")
room_number = models.CharField(max_length=50, verbose_name="Номер комнаты") room_number = models.CharField(max_length=255, null=True, blank=True)
room_type = models.CharField(max_length=255, verbose_name="Тип комнаты") room_type = models.CharField(max_length=255, verbose_name="Тип комнаты")
check_in = models.DateTimeField(verbose_name="Дата заезда") check_in = models.DateTimeField(verbose_name="Дата заезда")
check_out = models.DateTimeField(verbose_name="Дата выезда") check_out = models.DateTimeField(verbose_name="Дата выезда")

View File

@@ -3,7 +3,7 @@ import json
from datetime import datetime, timedelta from datetime import datetime, timedelta
from .base_plugin import BasePMSPlugin from .base_plugin import BasePMSPlugin
from asgiref.sync import sync_to_async from asgiref.sync import sync_to_async
from pms_integration.models import PMSConfiguration # Убедитесь, что модель существует from pms_integration.models import PMSConfiguration
class BnovoPMSPlugin(BasePMSPlugin): class BnovoPMSPlugin(BasePMSPlugin):

View File

@@ -1,103 +1,182 @@
import requests import requests
import json import json
from datetime import datetime, timedelta from datetime import datetime, timedelta, timezone
from asgiref.sync import sync_to_async
from .base_plugin import BasePMSPlugin
from hotels.models import Reservation
from hotels.models import Hotel
from asgiref.sync import sync_to_async
from hotels.models import Reservation, Hotel
from .base_plugin import BasePMSPlugin
from pms_integration.models import PMSConfiguration
class Shelter(BasePMSPlugin): class Shelter(BasePMSPlugin):
""" """
Плагин для PMS Shelter Coud. Плагин для интеграции с Shelter PMS.
""" """
def __init__(self, config): def __init__(self, pms_config):
super().__init__(config) super().__init__(pms_config)
self.token = config.token self.api_url = pms_config.url
self.token = pms_config.token
self.pagination_count = 50
def get_default_parser_settings(self): def get_default_parser_settings(self):
""" """
Возвращает настройки по умолчанию для обработки данных. Возвращает настройки по умолчанию для разбора данных PMS Shelter.
""" """
return { return {
"fields_mapping": {
"reservation_id": "id",
"hotel_id": "hotelId",
"hotel_name": "hotelName",
"check_in": "from",
"check_out": "until",
"reservation_date": "date",
"room_type_id": "roomTypeId",
"room_id": "roomId",
"room_number": "roomNumber",
"room_type_name": "roomTypeName",
"check_in_status": "checkInStatus",
"is_annul": "isAnnul",
"tariff_id": "tariffId",
"reservation_price": "reservationPrice",
"discount": "discount",
"guests": "guests",
},
"date_format": "%Y-%m-%dT%H:%M:%S", "date_format": "%Y-%m-%dT%H:%M:%S",
"timezone": "UTC" "timezone": "UTC",
} }
def _fetch_data(self): async def _get_last_saved_date(self):
""" """
Выполняет запрос к API PMS для получения данных. Получает дату последнего сохраненного бронирования для отеля.
""" """
url = 'https://pms.frontdesk24.ru/sheltercloudapi/Reservations/ByFilter' try:
headers = { last_reservation = await sync_to_async(
'accept': 'text/plain', Reservation.objects.filter(hotel__pms=self.pms_config).order_by('-check_in').first
'Authorization': f'Bearer {self.token}', )()
'Content-Type': 'application/json', return last_reservation.check_in if last_reservation else None
} except Exception as e:
print(f"[ERROR] Ошибка получения последнего сохраненного бронирования: {e}")
return None
from_index = 0 async def _fetch_data(self):
count_per_request = 50 """
total_count = None Получает данные о бронированиях из PMS Shelter.
all_items = [] """
now = datetime.now() try:
start_date = (now - timedelta(days=60)).strftime('%Y-%m-%dT%H:%M:%SZ') now = datetime.utcnow()
end_date = (now + timedelta(days=60)).strftime('%Y-%m-%dT%H:%M:%SZ') start_date = await self._get_last_saved_date() or (now - timedelta(days=60))
end_date = now + timedelta(days=60)
total_count = None
from_index = 0
while total_count is None or from_index < total_count: headers = {
data = { 'accept': 'text/plain',
"from": start_date, 'Authorization': f'Bearer {self.token}',
"until": end_date, 'Content-Type': 'application/json',
"pagination": {
"from": from_index,
"count": count_per_request
}
} }
response = requests.post(url, headers=headers, data=json.dumps(data)) print(f"[DEBUG] Start date: {start_date}, End date: {end_date}")
if response.status_code == 200:
response_data = response.json()
items = response_data.get("items", [])
all_items.extend(items)
if total_count is None: while total_count is None or from_index < total_count:
total_count = response_data.get("count", 0) payload = {
"from": start_date.strftime('%Y-%m-%dT%H:%M:%SZ'),
"until": end_date.strftime('%Y-%m-%dT%H:%M:%SZ'),
"pagination": {
"from": from_index,
"count": self.pagination_count,
},
}
print(f"[DEBUG] Payload: {json.dumps(payload)}")
try:
response = await sync_to_async(requests.post)(self.api_url, headers=headers, data=json.dumps(payload))
except requests.RequestException as e:
print(f"[ERROR] Ошибка HTTP-запроса: {e}")
raise ValueError(f"Ошибка HTTP-запроса: {e}")
print(f"[DEBUG] Response status: {response.status_code}")
if response.status_code != 200:
print(f"[ERROR] Request error: {response.status_code}, {response.text}")
raise ValueError(f"Ошибка запроса: {response.status_code}, {response.text}")
try:
data = response.json()
except json.JSONDecodeError as e:
print(f"[ERROR] Ошибка декодирования JSON: {e}")
raise ValueError(f"Ошибка декодирования JSON: {e}")
# Проверяем, что ответ содержит ключи "count" и "items"
if not isinstance(data, dict) or "count" not in data or "items" not in data:
print(f"[ERROR] Неверный формат данных: {data}")
raise ValueError(f"Неверный формат данных: {data}")
total_count = data.get("count", 0)
items = data.get("items", [])
print(f"[DEBUG] Total count: {total_count}, Items retrieved: {len(items)}")
if not isinstance(items, list):
print(f"[ERROR] Неверный тип items: {type(items)}. Ожидался list.")
raise ValueError(f"Неверный тип items: {type(items)}. Ожидался list.")
for item in items:
if not isinstance(item, dict):
print(f"[ERROR] Неверный формат элемента items: {item}")
continue
try:
await self._save_to_db(item)
except Exception as e:
print(f"[ERROR] Ошибка сохранения бронирования {item.get('id')}: {e}")
from_index += len(items) from_index += len(items)
else: print(f"[DEBUG] Updated from_index: {from_index}")
raise ValueError(f'Shelter API Error: {response.status_code}')
return all_items except Exception as e:
print(f"[ERROR] Общая ошибка в методе _fetch_data: {e}")
async def _save_to_db(self, data, hotel_id):
async def _save_to_db(self, item):
""" """
Сохраняет данные о бронированиях в таблицу Reservation. Сохраняет данные о бронировании в БД.
:param data: Список данных о бронированиях.
:param hotel_id: ID отеля, к которому относятся бронирования.
""" """
hotel = await sync_to_async(Hotel.objects.get)(id=hotel_id) try:
for item in data: print(f"[DEBUG] Fetching hotel for PMS: {self.pms_config}")
print(f"Данные для сохранения: {item}") hotel = await sync_to_async(Hotel.objects.get)(pms=self.pms_config)
print(f"[DEBUG] Hotel found: {hotel.name}")
try: # Учитываем формат даты без 'Z'
reservation, created = await sync_to_async(Reservation.objects.update_or_create)( date_format = '%Y-%m-%dT%H:%M:%S'
reservation_id=item["id"], print(f"[DEBUG] Parsing check-in and check-out dates for reservation {item['id']}")
hotel=hotel,
defaults={
"room_number": item.get("roomNumber", ""), # Номер комнаты
"room_type": item.get("roomTypeName", ""), # Тип комнаты
"check_in": datetime.strptime(item["from"], '%Y-%m-%dT%H:%M:%S'), # Дата заезда
"check_out": datetime.strptime(item["until"], '%Y-%m-%dT%H:%M:%S'), # Дата выезда
"status": item.get("checkInStatus", ""), # Статус бронирования
"price": item.get("reservationPrice", 0), # Цена
"discount": item.get("discount", 0), # Скидка
}
)
if created:
print(f"Создана запись: {reservation}")
else:
print(f"Обновлена запись: {reservation}")
except Exception as e:
print(f"Ошибка при сохранении бронирования ID {item['id']}: {e}")
check_in = datetime.strptime(item["from"], date_format).replace(tzinfo=timezone.utc)
check_out = datetime.strptime(item["until"], date_format).replace(tzinfo=timezone.utc)
# Проверяем room_number и устанавливаем значение по умолчанию, если оно отсутствует
room_number = item.get("roomNumber", "") or "Unknown"
print(f"[DEBUG] Room number determined: {room_number}")
# Сохраняем бронирование
print(f"[DEBUG] Saving reservation {item['id']} to database")
reservation, created = await sync_to_async(Reservation.objects.update_or_create)(
reservation_id=item["id"],
hotel=hotel,
defaults={
"room_number": room_number,
"room_type": item.get("roomTypeName", ""),
"check_in": check_in,
"check_out": check_out,
"status": item.get("checkInStatus", ""),
"price": item.get("reservationPrice", 0),
"discount": item.get("discount", 0),
},
)
print(f"[DEBUG] {'Created' if created else 'Updated'} reservation {item['id']}")
except KeyError as ke:
print(f"[ERROR] Ошибка обработки ключей в элементе {item}: {ke}")
except ValueError as ve:
print(f"[ERROR] Ошибка обработки данных для бронирования {item['id']}: {ve}")
except Exception as e:
print(f"[ERROR] Общая ошибка сохранения бронирования {item.get('id', 'Unknown')}: {e}")

Binary file not shown.

Binary file not shown.