diff --git a/bot/operations/hotels.py b/bot/operations/hotels.py index 21c8f0cd..5598ea09 100644 --- a/bot/operations/hotels.py +++ b/bot/operations/hotels.py @@ -151,7 +151,7 @@ async def check_pms(update, context): if hasattr(pms_manager.plugin, 'fetch_data') and callable(pms_manager.plugin.fetch_data): # Плагин поддерживает метод fetch_data report = await pms_manager.plugin._fetch_data() - logger.debug(f"REPORT: {report}, TYPE: {type(report)}") + logger.debug(f"TYPE: {type(report)}") else: await query.edit_message_text("Подходящий способ интеграции с PMS не найден.") return diff --git a/pms_integration/manager.py b/pms_integration/manager.py index b1251aab..d4052093 100644 --- a/pms_integration/manager.py +++ b/pms_integration/manager.py @@ -51,12 +51,18 @@ class PMSIntegrationManager: def load_plugin(self): """ - Загружает плагин для PMS на основе конфигурации. + Загружает плагин для PMS на основе конфигурации отеля. """ plugins = PluginLoader.load_plugins() - if self.pms_config.plugin_name not in plugins: - raise ValueError(f"Плагин для PMS {self.pms_config.plugin_name} не найден.") - self.plugin = plugins[self.pms_config.plugin_name](self.pms_config) + if not self.hotel: + raise ValueError("Отель не загружен. Пожалуйста, вызовите load_hotel перед загрузкой плагина.") + if not self.hotel.pms: + raise ValueError(f"Отель {self.hotel.name} не связан с PMS конфигурацией.") + if self.hotel.pms.plugin_name not in plugins: + raise ValueError(f"Плагин для PMS {self.hotel.pms.plugin_name} не найден.") + + # Передача объекта Hotel в плагин + self.plugin = plugins[self.hotel.pms.plugin_name](self.hotel) def fetch_data(self): """ diff --git a/pms_integration/models.py b/pms_integration/models.py index 8f37b9db..acf7bcee 100644 --- a/pms_integration/models.py +++ b/pms_integration/models.py @@ -14,11 +14,6 @@ class PMSConfiguration(models.Model): password = models.CharField(max_length=255, blank=True, null=True, verbose_name="Пароль") plugin_name = models.CharField(max_length=255, blank=True, null=True, verbose_name="Плагин") created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания") - hotels = models.ManyToManyField( - 'hotels.Hotel', - related_name='pms_configurations', - verbose_name="Отели" - ) def __str__(self): return self.name diff --git a/pms_integration/plugins/bnovo_pms.py b/pms_integration/plugins/bnovo_pms.py index dc5fc251..e624b32a 100644 --- a/pms_integration/plugins/bnovo_pms.py +++ b/pms_integration/plugins/bnovo_pms.py @@ -4,20 +4,51 @@ from datetime import datetime, timedelta from asgiref.sync import sync_to_async from .base_plugin import BasePMSPlugin from pms_integration.models import PMSConfiguration +from hotels.models import Reservation, Hotel from touchh.utils.log import CustomLogger -from pms_integration.models import PMSConfiguration -from hotels.models import Hotel, Reservation +import logging -logger = CustomLogger(name="BnovoPMS Plugin", log_level="DEBUG").get_logger() +import logging + +# Настройка логирования +logging.basicConfig( + level=logging.WARNING, # Установите уровень логов для всех обработчиков + format='%(asctime)s [%(levelname)s] %(message)s', + handlers=[ + logging.FileHandler("bnovo_plugin.log"), # Логи пишутся в файл + logging.StreamHandler() # Логи выводятся в консоль + ] +) + +# Создаем отдельный логгер для консоли с уровнем INFO +console_handler = logging.StreamHandler() +console_handler.setLevel(logging.WARNING) # Меняем уровень логов для консоли +console_handler.setFormatter(logging.Formatter('%(asctime)s [%(levelname)s] %(message)s')) + +# Основной логгер +logger = logging.getLogger("BnovoPMS Plugin") +logger.addHandler(console_handler) +logger.setLevel(logging.WARNING) # Основной уровень логов class BnovoPMSPlugin(BasePMSPlugin): """Плагин для работы с PMS Bnovo.""" - def __init__(self, config): - super().__init__(config) - self.api_url = config.url.rstrip("/") - self.username = config.username - self.password = config.password + def __init__(self, hotel): + super().__init__(hotel) + + if not isinstance(hotel, Hotel): + logger.error("Ожидался объект Hotel, но получен другой тип.") + raise ValueError("Для инициализации плагина требуется объект Hotel.") + + self.hotel = hotel + self.pms_config = hotel.pms # Связь отеля с PMSConfiguration + if not self.pms_config: + logger.error(f"Отель {hotel.id} не связан с конфигурацией PMS.") + raise ValueError(f"Отель {hotel.id} не связан с конфигурацией PMS.") + + self.api_url = self.pms_config.url.rstrip("/") + self.username = self.pms_config.username + self.password = self.pms_config.password self.token = None if not self.api_url: @@ -35,27 +66,26 @@ class BnovoPMSPlugin(BasePMSPlugin): "timezone": "UTC" } - async def _get_stored_token(self, hotel_id): - """Получение токена из базы данных для конкретного отеля.""" + async def _get_stored_token(self): + """Получение токена из конфигурации PMS отеля.""" try: - logger.debug(f"Попытка получения токена из базы данных для отеля: {hotel_id}.") - config = await sync_to_async(PMSConfiguration.objects.get)(plugin_name="bnovo", hotel_id=hotel_id) - logger.debug(f"Токен из базы данных: {config.token}") - return config.token - except PMSConfiguration.DoesNotExist: - logger.warning(f"Токен отсутствует в базе данных для отеля: {hotel_id}.") + logger.debug(f"Попытка получения токена для отеля {self.hotel.id}.") + token = self.pms_config.token + logger.debug(f"Токен из базы данных: {token}") + return token + except Exception as e: + logger.warning(f"Ошибка при получении токена для отеля {self.hotel.id}: {e}") return None - async def _save_token_to_db(self, sid, hotel_id): - """Сохраняет токен (SID) в базу данных для конкретного отеля.""" + async def _save_token_to_db(self, sid): + """Сохраняет токен (SID) в конфигурации PMS отеля.""" try: - logger.debug(f"Сохранение токена в базу данных для отеля {hotel_id}: {sid}") - await sync_to_async(PMSConfiguration.objects.update_or_create)( - plugin_name="bnovo", hotel_id=hotel_id, defaults={"token": sid} - ) + logger.debug(f"Сохранение токена для отеля {self.hotel.id}: {sid}") + self.pms_config.token = sid + await sync_to_async(self.pms_config.save)() logger.debug("Токен успешно сохранен.") except Exception as e: - logger.error(f"Ошибка сохранения токена в базу данных для отеля {hotel_id}: {e}") + logger.error(f"Ошибка сохранения токена для отеля {self.hotel.id}: {e}") def _get_auth_headers(self): """Создает заголовки авторизации.""" @@ -69,8 +99,8 @@ class BnovoPMSPlugin(BasePMSPlugin): logger.debug(f"Добавлен токен в заголовки: {self.token}") return headers - async def _fetch_session(self, hotel_id): - """Получение нового токена (SID) через запрос для конкретного отеля.""" + async def _fetch_session(self): + """Получение нового токена (SID) через запрос.""" url = f"{self.api_url}/" payload = {"username": self.username, "password": self.password} headers = self._get_auth_headers() @@ -85,7 +115,7 @@ class BnovoPMSPlugin(BasePMSPlugin): if sid: self.token = sid logger.debug(f"Получен новый SID: {sid}") - await self._save_token_to_db(sid, hotel_id) + await self._save_token_to_db(sid) else: logger.error("Не удалось извлечь SID из ответа.") raise ValueError("Не удалось извлечь SID из ответа.") @@ -93,14 +123,14 @@ class BnovoPMSPlugin(BasePMSPlugin): logger.error(f"Ошибка авторизации: {response.status_code}, {response.text}") raise ValueError(f"Ошибка авторизации: {response.status_code}, {response.text}") - async def _fetch_account_data(self, hotel_id): - """Получение данных аккаунта через эндпоинт /account/current для конкретного отеля.""" - logger.info(f"Начало получения данных аккаунта для отеля: {hotel_id}.") - self.token = await self._get_stored_token(hotel_id) + async def _fetch_account_data(self): + """Получение данных аккаунта через эндпоинт /account/current.""" + logger.info(f"Начало получения данных аккаунта для отеля {self.hotel.id}.") + self.token = await self._get_stored_token() if not self.token: logger.info("Токен отсутствует, выполняем авторизацию.") - await self._fetch_session(hotel_id) + await self._fetch_session() url = f"{self.api_url}/account/current" headers = self._get_auth_headers() @@ -114,51 +144,49 @@ class BnovoPMSPlugin(BasePMSPlugin): try: account_data = response.json() - logger.debug(f"Полученные данные аккаунта: {json.dumps(account_data, indent=2)}") + logger.debug(f"Полученные данные аккаунта:") except json.JSONDecodeError as e: logger.error(f"Ошибка декодирования JSON: {e}") raise ValueError(f"Ошибка декодирования JSON: {e}") return account_data - async def fetch_and_log_account_data(self, hotel_id): + async def _fetch_and_log_account_data(self): """Вызов метода _fetch_account_data и вывод результата в лог.""" - logger.info(f"Запуск получения и логирования данных аккаунта для отеля: {hotel_id}.") + logger.info(f"Запуск получения и логирования данных аккаунта для отеля {self.hotel.id}.") try: - account_data = await self._fetch_account_data(hotel_id) - logger.info(f"Успешно полученные данные аккаунта: {json.dumps(account_data, indent=2)}") + account_data = await self._fetch_account_data() + logger.info(f"Успешно полученные данные аккаунта:") return account_data except Exception as e: logger.error(f"Ошибка при получении данных аккаунта: {e}") raise - async def fetch_data_with_account_info(self, hotel_id): - """Получение данных аккаунта и бронирований для конкретного отеля.""" - logger.info(f"Запуск процесса получения данных аккаунта и бронирований для отеля: {hotel_id}.") + async def _fetch_data_with_account_info(self): + """Получение данных аккаунта и бронирований.""" + logger.info(f"Запуск процесса получения данных аккаунта и бронирований для отеля {self.hotel.id}.") try: - account_data = await self.fetch_and_log_account_data(hotel_id) + account_data = await self.fetch_and_log_account_data() logger.info("Данные аккаунта успешно получены, продолжение с бронированиями.") await self.__fetch_data() except Exception as e: logger.error(f"Ошибка при выполнении полной операции: {e}") - + async def _fetch_data(self): """Получение данных о бронированиях с помощью эндпоинта /dashboard.""" - # Проверяем наличие токена в базе данных - logger.info("Начало процесса получения данных.") - self.token = await self._get_stored_token() + logger.info("Начало процесса получения данных о бронированиях.") - if not self.token: - logger.info("Токен отсутствует, выполняем авторизацию.") - await self._fetch_session() + # # Вызов функции получения данных аккаунта + # try: + # account_data = await self._fetch_and_log_account_data() + # logger.info(f"Данные аккаунта успешно получены:") + # except Exception as e: + # logger.error(f"Ошибка получения данных аккаунта: {e}") + # raise - - accounts = await self._fetch_account_data() - print(f'\n------\nACCOUNTS: {accounts}\n-------\n') - url = f"{self.api_url}/dashboard" now = datetime.now() - create_from = (now - timedelta(days=90)).strftime("%d.%m.%Y") + create_from = (now - timedelta(days=1)).strftime("%d.%m.%Y") create_to = now.strftime("%d.%m.%Y") params = { @@ -171,69 +199,83 @@ class BnovoPMSPlugin(BasePMSPlugin): } headers = self._get_auth_headers() - logger.debug(f"Начальный запрос к {url} с параметрами: {json.dumps(params, indent=2)}") all_bookings = [] while True: - logger.debug(f"Выполнение запроса к {url}") - response = requests.get(url, headers=headers, params=params) - - logger.debug(f"Ответ запроса: статус {response.status_code}, тело {response.text}") - if response.status_code != 200: - logger.error(f"Ошибка при запросе: {response.status_code}, {response.text}") - raise ValueError("Ошибка запроса к /dashboard") - + logger.debug(f"Запрос к {url} с параметрами: {json.dumps(params, indent=2)}") try: + response = requests.get(url, headers=headers, params=params, allow_redirects=False) + + if response.status_code == 302: + logger.warning("Получен код 302. Перенаправление.") + redirected_url = response.headers.get("Location") + if redirected_url: + logger.debug(f"Перенаправление на {redirected_url}") + url = redirected_url + continue + else: + logger.error("Ответ с кодом 302 не содержит заголовка Location.") + raise ValueError("Перенаправление без указанного URL.") + + if response.status_code != 200: + logger.error(f"Ошибка при запросе: {response.status_code}, {response.text}") + raise ValueError("Ошибка запроса к /dashboard") + data = response.json() - logger.debug(f"Полученные данные: {json.dumps(data, indent=2)}") + bookings = data.get("bookings", []) + rooms = data.get("rooms", []) + + + logger.debug(f'bookings: {bookings}\n rooms: {rooms}') + all_bookings.extend(bookings) + + logger.info(f"Получено бронирований: {len(bookings)}. Всего: {len(all_bookings)}.") + + pages_info = data.get("pages", {}) + current_page = pages_info.get("current_page", 1) + total_pages = pages_info.get("total_pages", 1) + + logger.debug(f"Информация о страницах: текущая {current_page}, всего {total_pages}") + if current_page >= total_pages: + break + + params["page"] += 1 + except json.JSONDecodeError as e: - logger.error(f"Ошибка декодирования JSON: {e}") + logger.error(f"Ошибка декодирования JSON: {e}. Ответ: {response.text}") raise ValueError(f"Ошибка декодирования JSON: {e}") - bookings = data.get("bookings", []) - all_bookings.extend(bookings) - - logger.info(f"Получено бронирований за запрос: {len(bookings)}. Всего: {len(all_bookings)}.") - - # Проверяем наличие следующей страницы - pages_info = data.get("pages", {}) - current_page = pages_info.get("current_page", 1) - total_pages = pages_info.get("total_pages", 1) - - logger.debug(f"Информация о страницах: текущая {current_page}, всего {total_pages}") - if current_page >= total_pages: - break - - params["page"] += 1 + except Exception as e: + logger.error(f"Неизвестная ошибка при обработке запроса: {e}") + raise # Сопоставляем бронирования с существующими записями for booking in all_bookings: - booking_id = booking.get("id") - hotel_id = booking.get("hotel_id") - if not booking_id or not hotel_id: - logger.warning("У бронирования отсутствует id или hotel_id. Пропуск.") - continue + try: + booking_id = booking.get("id") + hotel_id = booking.get("hotel_id") - if hotel_id != str(self.config.hotel.external_id_pms): - logger.debug(f"Бронирование {booking_id} не относится к отелю {self.config.hotel.external_id_pms}. Пропуск.") - continue + if hotel_id != str(self.hotel.external_id_pms): + logger.debug(f"Бронирование {booking_id} не относится к отелю {self.hotel.external_id_pms}. Пропуск.") + continue - reservation, created = await sync_to_async(Reservation.objects.update_or_create)( - external_id=booking_id, - defaults={ - "hotel": self.config.hotel, - "status": booking.get("status_name"), - "create_date": booking.get("create_date"), - "arrival": booking.get("arrival"), - "departure": booking.get("departure"), - "room_type": booking.get("initial_room_type_name"), - "data": booking - } - ) + reservation, created = await sync_to_async(Reservation.objects.update_or_create)( + external_id=booking_id, + defaults = { + "hotel": self.hotel, # Объект модели Hotel + "status": booking.get("status_name"), # Статус бронирования + "room_number": booking.get("current_room"), # Номер комнаты (исправлено с create_date) + "check_in": booking.get("arrival"), # Дата заезда + "check_out": booking.get("departure"), # Дата выезда + "room_type": booking.get("initial_room_type_name") # Тип комнаты + } + ) + if created: + logger.info(f"Создана новая запись бронирования: {reservation}") + else: + logger.info(f"Обновлено существующее бронирование: {reservation}") - if created: - logger.info(f"Создана новая запись бронирования: {reservation}") - else: - logger.info(f"Обновлено существующее бронирование: {reservation}") + except Exception as e: + logger.error(f"Ошибка обработки бронирования {booking.get('id')}: {e}") logger.info(f"Все бронирования получены и обработаны. Итоговое количество: {len(all_bookings)}") return all_bookings diff --git a/pms_integration/plugins/ecvi_pms.py b/pms_integration/plugins/ecvi_pms.py index 7e7fff48..73df78df 100644 --- a/pms_integration/plugins/ecvi_pms.py +++ b/pms_integration/plugins/ecvi_pms.py @@ -12,7 +12,7 @@ class EcviPMSPlugin(BasePMSPlugin): def __init__(self, pms_config): super().__init__(pms_config) - + # Инициализация логгера self.logger = logging.getLogger(self.__class__.__name__) # Логгер с именем класса handler_console = logging.StreamHandler() # Потоковый обработчик для вывода в консоль