import requests import json 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 import logging import logging # Настройка логирования logging.basicConfig( level=logging.WARNING, # Установите уровень логов для всех обработчиков format='%(asctime)s [%(levelname)s] %(message)s', handlers=[ logging.FileHandler("var/log/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, 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: logger.error("Не указан URL для работы плагина.") raise ValueError("Не указан URL для работы плагина.") if not self.username or not self.password: logger.error("Не указаны логин или пароль для авторизации.") raise ValueError("Не указаны логин или пароль для авторизации.") def get_default_parser_settings(self): """Возвращает настройки по умолчанию для обработки данных.""" logger.debug("Получение настроек парсера по умолчанию.") return { "date_format": "%Y-%m-%dT%H:%M:%S", "timezone": "UTC" } async def _get_stored_token(self): """Получение токена из конфигурации PMS отеля.""" try: 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): """Сохраняет токен (SID) в конфигурации PMS отеля.""" try: 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"Ошибка сохранения токена для отеля {self.hotel.id}: {e}") def _get_auth_headers(self): """Создает заголовки авторизации.""" logger.debug("Создание заголовков авторизации.") headers = { "Content-Type": "application/json", "Accept": "application/json", } if self.token: headers["Cookie"] = f"SID={self.token}" logger.debug(f"Добавлен токен в заголовки: {self.token}") return headers async def _fetch_session(self): """Получение нового токена (SID) через запрос.""" url = f"{self.api_url}/" payload = {"username": self.username, "password": self.password} headers = self._get_auth_headers() logger.debug(f"Авторизация по адресу: {url} с данными: {json.dumps(payload)}") response = requests.post(url, json=payload, headers=headers, allow_redirects=False) logger.debug(f"Ответ авторизации: статус {response.status_code}, заголовки {response.headers}") if response.status_code == 302: cookies = response.cookies.get_dict() sid = cookies.get("SID") if sid: self.token = sid logger.debug(f"Получен новый SID: {sid}") await self._save_token_to_db(sid) else: logger.error("Не удалось извлечь SID из ответа.") raise ValueError("Не удалось извлечь SID из ответа.") else: logger.error(f"Ошибка авторизации: {response.status_code}, {response.text}") raise ValueError(f"Ошибка авторизации: {response.status_code}, {response.text}") 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() url = f"{self.api_url}/account/current" headers = self._get_auth_headers() logger.debug(f"Выполнение запроса к {url}") response = requests.get(url, headers=headers) if response.status_code != 200: logger.error(f"Ошибка при запросе данных аккаунта: {response.status_code}, {response.text}") raise ValueError("Ошибка запроса к /account/current") try: account_data = response.json() 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): """Вызов метода _fetch_account_data и вывод результата в лог.""" logger.info(f"Запуск получения и логирования данных аккаунта для отеля {self.hotel.id}.") try: 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): """Получение данных аккаунта и бронирований.""" logger.info(f"Запуск процесса получения данных аккаунта и бронирований для отеля {self.hotel.id}.") try: 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("Начало процесса получения данных о бронированиях.") # # Вызов функции получения данных аккаунта # try: # account_data = await self._fetch_and_log_account_data() # logger.info(f"Данные аккаунта успешно получены:") # except Exception as e: # logger.error(f"Ошибка получения данных аккаунта: {e}") # raise url = f"{self.api_url}/dashboard" now = datetime.now() create_from = (now - timedelta(days=1)).strftime("%d.%m.%Y") create_to = now.strftime("%d.%m.%Y") params = { "create_from": create_from, "create_to": create_to, "advanced_search": 2, "c": 100, "page": 1, "order_by": "create_date.asc", } headers = self._get_auth_headers() all_bookings = [] while True: 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() 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}. Ответ: {response.text}") raise ValueError(f"Ошибка декодирования JSON: {e}") except Exception as e: logger.error(f"Неизвестная ошибка при обработке запроса: {e}") raise # Сопоставляем бронирования с существующими записями for booking in all_bookings: try: booking_id = booking.get("id") hotel_id = booking.get("hotel_id") 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.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}") except Exception as e: logger.error(f"Ошибка обработки бронирования {booking.get('id')}: {e}") logger.info(f"Все бронирования получены и обработаны. Итоговое количество: {len(all_bookings)}") return all_bookings