diff --git a/bot/operations/statistics.py b/bot/operations/statistics.py index 10d54a82..cf421c10 100644 --- a/bot/operations/statistics.py +++ b/bot/operations/statistics.py @@ -1,4 +1,4 @@ -from datetime import datetime, timedelta +from datetime import datetime, timedelta, timezone from telegram import InlineKeyboardButton, InlineKeyboardMarkup, Update from telegram.ext import ContextTypes 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.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): """Вывод списка отелей для статистики.""" @@ -55,6 +55,7 @@ async def stats_select_period(update: Update, context: ContextTypes.DEFAULT_TYPE 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 @@ -66,47 +67,70 @@ async def generate_statistics(update: Update, context: ContextTypes.DEFAULT_TYPE return 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": - start_date = (now - timedelta(days=1)).date() # Вчерашняя дата - end_date = now.date() # Сегодняшняя дата + start_date = (now - timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0) + end_date = now.replace(hour=23, minute=59, second=59, microsecond=999999) elif period == "week": - start_date = (now - timedelta(days=7)).date() - end_date = now.date() + start_date = (now - timedelta(days=7)).replace(hour=0, minute=0, second=0, microsecond=0) + end_date = now.replace(hour=23, minute=59, second=59, microsecond=999999) elif period == "month": - start_date = (now - timedelta(days=30)).date() - end_date = now.date() - else: + start_date = (now - timedelta(days=30)).replace(hour=0, minute=0, second=0, microsecond=0) + end_date = now.replace(hour=23, minute=59, second=59, microsecond=999999) + else: # "all" start_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: reservations = await sync_to_async(list)( Reservation.objects.filter( hotel_id=hotel_id, - check_in__date__gte=start_date, - check_in__date__lte=end_date - ).prefetch_related('guests') + check_in__gte=start_date, + check_in__lte=end_date + ).select_related('hotel') ) - else: + else: # Без фильтра по дате 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: await query.edit_message_text("Нет данных для статистики за выбранный период.") return 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) + print(f'Generated file path: {file_path}') with open(file_path, "rb") as file: await query.message.reply_document(document=file, filename=f"{hotel.name}_report.pdf") - - async def stats_back(update: Update, context): """Возврат к выбору отеля.""" query = update.callback_query diff --git a/hotels/migrations/0004_alter_reservation_room_number.py b/hotels/migrations/0004_alter_reservation_room_number.py new file mode 100644 index 00000000..1aed70e5 --- /dev/null +++ b/hotels/migrations/0004_alter_reservation_room_number.py @@ -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), + ), + ] diff --git a/hotels/models.py b/hotels/models.py index 022f4aba..f066a8c5 100644 --- a/hotels/models.py +++ b/hotels/models.py @@ -88,7 +88,7 @@ class APIRequestLog(models.Model): class Reservation(models.Model): hotel = models.ForeignKey(Hotel, on_delete=models.CASCADE, verbose_name="Отель") 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="Тип комнаты") check_in = models.DateTimeField(verbose_name="Дата заезда") check_out = models.DateTimeField(verbose_name="Дата выезда") diff --git a/pms_integration/plugins/bnovo_pms.py b/pms_integration/plugins/bnovo_pms.py index 1d3f32e2..673664b5 100644 --- a/pms_integration/plugins/bnovo_pms.py +++ b/pms_integration/plugins/bnovo_pms.py @@ -3,7 +3,7 @@ import json from datetime import datetime, timedelta from .base_plugin import BasePMSPlugin from asgiref.sync import sync_to_async -from pms_integration.models import PMSConfiguration # Убедитесь, что модель существует +from pms_integration.models import PMSConfiguration class BnovoPMSPlugin(BasePMSPlugin): diff --git a/pms_integration/plugins/shelter_pms.py b/pms_integration/plugins/shelter_pms.py index 98c04aca..a6c04bf0 100644 --- a/pms_integration/plugins/shelter_pms.py +++ b/pms_integration/plugins/shelter_pms.py @@ -1,103 +1,182 @@ import requests import json -from datetime import datetime, timedelta -from asgiref.sync import sync_to_async -from .base_plugin import BasePMSPlugin -from hotels.models import Reservation -from hotels.models import Hotel +from datetime import datetime, timedelta, timezone +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): """ - Плагин для PMS Shelter Coud. + Плагин для интеграции с Shelter PMS. """ - def __init__(self, config): - super().__init__(config) - self.token = config.token + def __init__(self, pms_config): + super().__init__(pms_config) + self.api_url = pms_config.url + self.token = pms_config.token + self.pagination_count = 50 def get_default_parser_settings(self): """ - Возвращает настройки по умолчанию для обработки данных. + Возвращает настройки по умолчанию для разбора данных PMS Shelter. """ 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", - "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' - headers = { - 'accept': 'text/plain', - 'Authorization': f'Bearer {self.token}', - 'Content-Type': 'application/json', - } + try: + last_reservation = await sync_to_async( + Reservation.objects.filter(hotel__pms=self.pms_config).order_by('-check_in').first + )() + return last_reservation.check_in if last_reservation else None + except Exception as e: + print(f"[ERROR] Ошибка получения последнего сохраненного бронирования: {e}") + return None - from_index = 0 - count_per_request = 50 - total_count = None - all_items = [] - now = datetime.now() - start_date = (now - timedelta(days=60)).strftime('%Y-%m-%dT%H:%M:%SZ') - end_date = (now + timedelta(days=60)).strftime('%Y-%m-%dT%H:%M:%SZ') + async def _fetch_data(self): + """ + Получает данные о бронированиях из PMS Shelter. + """ + try: + now = datetime.utcnow() + 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: - data = { - "from": start_date, - "until": end_date, - "pagination": { - "from": from_index, - "count": count_per_request - } + headers = { + 'accept': 'text/plain', + 'Authorization': f'Bearer {self.token}', + 'Content-Type': 'application/json', } - response = requests.post(url, headers=headers, data=json.dumps(data)) - if response.status_code == 200: - response_data = response.json() - items = response_data.get("items", []) - all_items.extend(items) + print(f"[DEBUG] Start date: {start_date}, End date: {end_date}") - if total_count is None: - total_count = response_data.get("count", 0) + while total_count is None or from_index < total_count: + 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) - else: - raise ValueError(f'Shelter API Error: {response.status_code}') + print(f"[DEBUG] Updated from_index: {from_index}") - 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) - for item in data: - print(f"Данные для сохранения: {item}") + try: + print(f"[DEBUG] Fetching hotel for PMS: {self.pms_config}") + hotel = await sync_to_async(Hotel.objects.get)(pms=self.pms_config) + print(f"[DEBUG] Hotel found: {hotel.name}") - try: - reservation, created = await sync_to_async(Reservation.objects.update_or_create)( - reservation_id=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}") + # Учитываем формат даты без 'Z' + date_format = '%Y-%m-%dT%H:%M:%S' + print(f"[DEBUG] Parsing check-in and check-out dates for reservation {item['id']}") + check_in = datetime.strptime(item["from"], date_format).replace(tzinfo=timezone.utc) + check_out = datetime.strptime(item["until"], date_format).replace(tzinfo=timezone.utc) - \ No newline at end of file + # Проверяем 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}") diff --git a/reports/Golden Hills 3_report.pdf b/reports/Golden Hills 3_report.pdf index 958c51eb..459a0a8d 100644 Binary files a/reports/Golden Hills 3_report.pdf and b/reports/Golden Hills 3_report.pdf differ diff --git a/reports/GoldenHills 4_report.pdf b/reports/GoldenHills 4_report.pdf new file mode 100644 index 00000000..9a589418 Binary files /dev/null and b/reports/GoldenHills 4_report.pdf differ