From 4a16de10245e13c1f889df8ff6e1fa256580fe65 Mon Sep 17 00:00:00 2001 From: trevor Date: Wed, 25 Dec 2024 15:50:33 +0900 Subject: [PATCH] Bnovo pms --- hotels/models.py | 1 + pms_integration/models.py | 6 +- pms_integration/plugins/bnovo_pms.py | 227 +++++++++++++----- pms_integration/plugins/realtycalendar_pms.py | 191 --------------- pms_integration/plugins/trevelline_pms.py | 116 --------- 5 files changed, 171 insertions(+), 370 deletions(-) delete mode 100644 pms_integration/plugins/trevelline_pms.py diff --git a/hotels/models.py b/hotels/models.py index 63c39c76..d2d9c9d6 100644 --- a/hotels/models.py +++ b/hotels/models.py @@ -23,6 +23,7 @@ class APIConfiguration(models.Model): class Hotel(models.Model): name = models.CharField(max_length=255, verbose_name="Название отеля") hotel_id = models.CharField(max_length=255, unique=True, null=True, blank=True, verbose_name="ID отеля") + external_id_pms = models.CharField(max_length=100, unique=False, null=True, blank=True, verbose_name="Внешний PMS ID") created_at = models.DateTimeField(auto_now_add=True, verbose_name="Создан") phone = models.CharField( max_length=50, diff --git a/pms_integration/models.py b/pms_integration/models.py index 63636bb0..8f37b9db 100644 --- a/pms_integration/models.py +++ b/pms_integration/models.py @@ -14,7 +14,11 @@ 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 673664b5..dc5fc251 100644 --- a/pms_integration/plugins/bnovo_pms.py +++ b/pms_integration/plugins/bnovo_pms.py @@ -1,136 +1,239 @@ import requests import json from datetime import datetime, timedelta -from .base_plugin import BasePMSPlugin from asgiref.sync import sync_to_async +from .base_plugin import BasePMSPlugin from pms_integration.models import PMSConfiguration +from touchh.utils.log import CustomLogger +from pms_integration.models import PMSConfiguration +from hotels.models import Hotel, Reservation +logger = CustomLogger(name="BnovoPMS Plugin", log_level="DEBUG").get_logger() class BnovoPMSPlugin(BasePMSPlugin): """Плагин для работы с PMS Bnovo.""" def __init__(self, config): super().__init__(config) - self.api_url = config.url.rstrip("/") # Убираем лишний `/` в конце URL + self.api_url = config.url.rstrip("/") self.username = config.username self.password = config.password - self.token = None # SID + 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 _save_token_to_db(self, sid): - """Сохраняет токен (SID) в базу данных.""" + async def _get_stored_token(self, hotel_id): + """Получение токена из базы данных для конкретного отеля.""" 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}.") + return None + + async def _save_token_to_db(self, sid, hotel_id): + """Сохраняет токен (SID) в базу данных для конкретного отеля.""" + try: + logger.debug(f"Сохранение токена в базу данных для отеля {hotel_id}: {sid}") await sync_to_async(PMSConfiguration.objects.update_or_create)( - plugin_name="bnovo", - defaults={"token": sid} + plugin_name="bnovo", hotel_id=hotel_id, defaults={"token": sid} ) - print(f"[DEBUG] Токен сохранен в БД: {sid}") + logger.debug("Токен успешно сохранен.") except Exception as e: - print(f"[ERROR] Ошибка сохранения токена в БД: {e}") + logger.error(f"Ошибка сохранения токена в базу данных для отеля {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) через запрос.""" + async def _fetch_session(self, hotel_id): + """Получение нового токена (SID) через запрос для конкретного отеля.""" url = f"{self.api_url}/" - payload = { - "username": self.username, - "password": self.password, - } - - print(f"[DEBUG] URL авторизации: {url}") - print(f"[DEBUG] Тело запроса: {json.dumps(payload, indent=2)}") + payload = {"username": self.username, "password": self.password} headers = self._get_auth_headers() - session = requests.Session() - response = session.post(url, json=payload, headers=headers, allow_redirects=False) + logger.debug(f"Авторизация по адресу: {url} с данными: {json.dumps(payload)}") + response = requests.post(url, json=payload, headers=headers, allow_redirects=False) - print(f"[DEBUG] Статус ответа: {response.status_code}") - print(f"[DEBUG] Ответ заголовков: {response.headers}") - print(f"[DEBUG] Cookies: {session.cookies}") - - if response.status_code == 302 and "SID" in session.cookies: - sid = session.cookies.get("SID") - self.token = sid - print(f"[DEBUG] Получен SID: {sid}") - - # Правильное сохранение в БД через sync_to_async - try: - await self._save_token_to_db(sid) - print(f"[DEBUG] Токен сохранен в БД") - except Exception as e: - print(f"[ERROR] Ошибка сохранения токена в БД: {e}") + 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, hotel_id) + else: + logger.error("Не удалось извлечь SID из ответа.") + raise ValueError("Не удалось извлечь SID из ответа.") else: - raise ValueError(f"Не удалось получить SID из ответа: {response.text}") + 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) + + if not self.token: + logger.info("Токен отсутствует, выполняем авторизацию.") + await self._fetch_session(hotel_id) + + 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"Полученные данные аккаунта: {json.dumps(account_data, indent=2)}") + 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): + """Вызов метода _fetch_account_data и вывод результата в лог.""" + logger.info(f"Запуск получения и логирования данных аккаунта для отеля: {hotel_id}.") + try: + account_data = await self._fetch_account_data(hotel_id) + logger.info(f"Успешно полученные данные аккаунта: {json.dumps(account_data, indent=2)}") + 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}.") + try: + account_data = await self.fetch_and_log_account_data(hotel_id) + logger.info("Данные аккаунта успешно получены, продолжение с бронированиями.") + await self.__fetch_data() + except Exception as e: + logger.error(f"Ошибка при выполнении полной операции: {e}") + async def _fetch_data(self): - """Получает данные о бронированиях с помощью эндпоинта `/dashboard`.""" - await self._fetch_session() # Авторизуемся перед каждым запросом + """Получение данных о бронированиях с помощью эндпоинта /dashboard.""" + # Проверяем наличие токена в базе данных + logger.info("Начало процесса получения данных.") + self.token = await self._get_stored_token() + if not self.token: + logger.info("Токен отсутствует, выполняем авторизацию.") + await self._fetch_session() + + + 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") # Диапазон: последние 90 дней + create_from = (now - timedelta(days=90)).strftime("%d.%m.%Y") create_to = now.strftime("%d.%m.%Y") params = { "create_from": create_from, "create_to": create_to, - "status_ids": "1", - "advanced_search": 2, # Обязательный параметр - "c": 100, # Количество элементов на странице (максимум 100) - "page": 1, # Начальная страница - "order_by": "create_date.asc", # Сортировка по возрастанию даты создания + "advanced_search": 2, + "c": 100, + "page": 1, + "order_by": "create_date.asc", } - headers = self._get_auth_headers() - all_bookings = [] # Для сохранения всех бронирований + logger.debug(f"Начальный запрос к {url} с параметрами: {json.dumps(params, indent=2)}") + all_bookings = [] while True: - print(f"[DEBUG] Запрос к /dashboard с параметрами: {json.dumps(params, indent=2)}") - response = requests.get(f"{self.api_url}/dashboard", headers=headers, params=params) + logger.debug(f"Выполнение запроса к {url}") + response = requests.get(url, headers=headers, params=params) - print(f"[DEBUG] Статус ответа: {response.status_code}") + logger.debug(f"Ответ запроса: статус {response.status_code}, тело {response.text}") if response.status_code != 200: - raise ValueError(f"Ошибка при получении данных: {response.status_code}, {response.text}") + logger.error(f"Ошибка при запросе: {response.status_code}, {response.text}") + raise ValueError("Ошибка запроса к /dashboard") + + try: + data = response.json() + logger.debug(f"Полученные данные: {json.dumps(data, indent=2)}") + except json.JSONDecodeError as e: + logger.error(f"Ошибка декодирования JSON: {e}") + raise ValueError(f"Ошибка декодирования JSON: {e}") - data = response.json() - print(json.dumps(data, indent=2)) bookings = data.get("bookings", []) all_bookings.extend(bookings) - print(f"[DEBUG] Получено бронирований: {len(bookings)}") - print(f"[DEBUG] Всего бронирований: {len(all_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 # Все страницы загружены + break - params["page"] += 1 # Переход на следующую страницу + params["page"] += 1 - if not all_bookings: - print("[DEBUG] Нет бронирований за указанный период.") - else: - print(f"[DEBUG] Полученные бронирования: {json.dumps(all_bookings, indent=2)}") - return all_bookings \ No newline at end of file + # Сопоставляем бронирования с существующими записями + 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 + + if hotel_id != str(self.config.hotel.external_id_pms): + logger.debug(f"Бронирование {booking_id} не относится к отелю {self.config.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 + } + ) + + if created: + logger.info(f"Создана новая запись бронирования: {reservation}") + else: + logger.info(f"Обновлено существующее бронирование: {reservation}") + + logger.info(f"Все бронирования получены и обработаны. Итоговое количество: {len(all_bookings)}") + return all_bookings diff --git a/pms_integration/plugins/realtycalendar_pms.py b/pms_integration/plugins/realtycalendar_pms.py index d7955ff2..0b440aef 100644 --- a/pms_integration/plugins/realtycalendar_pms.py +++ b/pms_integration/plugins/realtycalendar_pms.py @@ -1,194 +1,3 @@ -# import requests -# import hashlib -# import json -# from .base_plugin import BasePMSPlugin -# from datetime import datetime, timedelta -# from asgiref.sync import sync_to_async -# from touchh.utils.log import CustomLogger -# from hotels.models import Hotel, Reservation -# from app_settings.models import GlobalHotelSettings -# from django.utils import timezone -# class RealtyCalendarPlugin(BasePMSPlugin): -# def __init__(self, config): -# super().__init__(config) -# self.public_key = config.public_key -# self.private_key = config.private_key -# self.api_url = config.url.rstrip("/") -# self.logger = CustomLogger(name="RealtyCalendarPlugin", log_level="DEBUG").get_logger() -# if not self.public_key or not self.private_key: -# raise ValueError("Публичный или приватный ключ отсутствует для RealtyCalendar") - -# def get_default_parser_settings(self): -# """ -# Возвращает настройки по умолчанию для обработки данных. -# """ -# return { -# "date_format": "%Y-%m-%dT%H:%M:%S", -# "timezone": "UTC" -# } - -# def _get_sorted_keys(self, obj): -# """ -# Возвращает отсортированный по имени список ключей. -# """ -# sorted_keys = sorted(obj.keys()) -# self.logger.debug(f"Отсортированные ключи: {sorted_keys}") -# return sorted_keys - -# def _generate_data_string(self, obj): -# """ -# Формирует строку параметров для подписи. -# """ -# sorted_keys = self._get_sorted_keys(obj) -# string = "".join(f"{key}={obj[key]}" for key in sorted_keys) -# self.logger.debug(f"Сформированная строка данных: {string}") -# return string + self.private_key - -# def _generate_md5(self, string): -# """ -# Генерирует MD5-хеш от строки. -# """ -# md5_hash = hashlib.md5(string.encode("utf-8")).hexdigest() -# self.logger.debug(f"Сформированный MD5-хеш: {md5_hash}") -# return md5_hash - -# def _generate_sign(self, data): -# """ -# Генерирует подпись для данных запроса. -# """ -# data_string = self._generate_data_string(data) -# self.logger.debug(f"Строка для подписи: {data_string}") -# sign = self._generate_md5(data_string) -# self.logger.debug(f"Подпись: {sign}") -# return sign - -# async def _fetch_data(self): -# """ -# Выполняет запрос к API RealtyCalendar для получения данных о бронированиях. -# """ -# self.logger.debug("Начало выполнения функции _fetch_data") -# base_url = f"{self.api_url}/api/v1/bookings/{self.public_key}/" -# headers = { -# "Accept": "application/json", -# "Content-Type": "application/json", -# } - -# now = datetime.now() -# data = { -# "begin_date": (now - timedelta(days=7)).strftime("%Y-%m-%d"), -# "end_date": now.strftime("%Y-%m-%d"), -# } -# data["sign"] = self._generate_sign(data) - -# response = requests.post(url=base_url, headers=headers, json=data) -# self.logger.debug(f"Статус ответа: {response.status_code}") - -# if response.status_code != 200: -# self.logger.error(f"Ошибка API: {response.status_code}, {response.text}") -# raise ValueError(f"Ошибка API RealtyCalendar: {response.status_code}") - -# try: -# response_data = response.json() -# bookings = response_data.get("bookings", []) -# if not isinstance(bookings, list): -# raise ValueError(f"Ожидался список, но получен {type(bookings)}") -# except Exception as e: -# self.logger.error(f"Ошибка обработки ответа API: {e}") -# raise - -# # Получаем глобальные настройки отеля -# hotel = await sync_to_async(Hotel.objects.get)(pms=self.pms_config) -# hotel_tz = hotel.timezone -# try: -# hotel_settings = await sync_to_async(GlobalHotelSettings.objects.first)() -# check_in_time = hotel_settings.check_in_time.strftime("%H:%M:%S") -# check_out_time = hotel_settings.check_out_time.strftime("%H:%M:%S") -# except AttributeError: -# # Используем значения по умолчанию, если настроек нет -# check_in_time = "14:00:00" -# check_out_time = "12:00:00" - -# filtered_data = [ -# { -# 'reservation_id': item.get('id'), -# 'checkin': timezone.make_aware( -# datetime.strptime( -# f"{item.get('begin_date')} {check_in_time}", -# "%Y-%m-%d %H:%M:%S" -# ) -# ), -# 'checkout': timezone.make_aware( -# datetime.strptime( -# f"{item.get('end_date')} {check_out_time}", -# "%Y-%m-%d %H:%M:%S" -# ) -# ), -# 'room_number': item.get('apartment_id'), -# 'room_type': item.get('notes', 'Описание отсутствует'), -# 'status': item.get('status') -# } -# for item in bookings -# if isinstance(item, dict) and item.get("status") in ["booked", "request"] -# ] - -# await self._save_to_db(filtered_data) - -# async def _save_to_db(self, data): -# """ -# Сохраняет данные в БД (например, информацию о номере). -# """ -# if not isinstance(data, list): -# self.logger.error(f"Ожидался список записей, но получен {type(data).__name__}") -# return - -# for index, item in enumerate(data, start=1): -# try: -# hotel = await sync_to_async(Hotel.objects.get)(pms=self.pms_config) -# reservation_id = item.get('reservation_id') -# if not reservation_id: -# self.logger.error(f"Пропущена запись {index}: отсутствует 'id'") -# continue - -# existing_reservation = await sync_to_async(Reservation.objects.filter)(reservation_id=reservation_id) -# existing_reservation = await sync_to_async(existing_reservation.first)() - -# defaults = { -# 'room_number': item['room_number'], -# 'room_type': item['room_type'], -# 'check_in': item['checkin'], -# 'check_out': item['checkout'], -# 'status': item['status'], -# 'hotel': hotel -# } - -# if existing_reservation: -# await sync_to_async(Reservation.objects.update_or_create)( -# reservation_id=reservation_id, defaults=defaults -# ) -# self.logger.debug(f"Резервация {reservation_id} обновлена.") -# else: -# await sync_to_async(Reservation.objects.create)( -# reservation_id=reservation_id, **defaults -# ) -# self.logger.debug(f"Создана новая резервация {reservation_id}") - -# except Exception as e: -# self.logger.error(f"Ошибка при обработке записи {index}: {e}") - -# def validate_plugin(self): -# """ -# Проверка на соответствие требованиям. -# Можно проверить наличие методов или полей. -# """ -# # Проверяем наличие обязательных методов -# required_methods = ["fetch_data", "get_default_parser_settings", "_fetch_data"] -# for m in required_methods: -# if not hasattr(self, m): -# raise ValueError(f"Плагин {type(self).__name__} не реализует метод {m}.") -# self.logger.debug(f"Плагин {self.__class__.__name__} прошел валидацию.") -# return True - - import requests import hashlib import json diff --git a/pms_integration/plugins/trevelline_pms.py b/pms_integration/plugins/trevelline_pms.py deleted file mode 100644 index 6d1b22a5..00000000 --- a/pms_integration/plugins/trevelline_pms.py +++ /dev/null @@ -1,116 +0,0 @@ -import logging -import requests -from datetime import datetime -from .base_plugin import BasePMSPlugin - -class TravelLinePMSPlugin(BasePMSPlugin): - """ - Плагин для интеграции с PMS TravelLine. - """ - BASE_URL = "https://partner.tlintegration.com/api/webpms/v1" - - def __init__(self, pms_config): - """ - Инициализация плагина с конфигурацией PMS. - - :param pms_config: Конфигурация PMS (объект PMSConfiguration). - """ - super().__init__(pms_config) - self.api_key = pms_config.api_key - self.logger = logging.getLogger(self.__class__.__name__) - - def _get_headers(self): - """ - Возвращает заголовки для запросов. - """ - return { - "Authorization": f"Bearer {self.api_key}", - "Content-Type": "application/json" - } - - def _fetch_data(self): - """ - Получение данных из API TravelLine (поиск бронирований). - - :return: Список номеров бронирований. - """ - url = f"{self.BASE_URL}/bookings" - params = { - "roomId": self.pms_config.room_id, - "modifiedFrom": self.pms_config.modified_from, - "modifiedTo": self.pms_config.modified_to, - "state": self.pms_config.state, - "affectsPeriodFrom": self.pms_config.affects_period_from, - "affectsPeriodTo": self.pms_config.affects_period_to, - } - params = {k: v for k, v in params.items() if v is not None} - - try: - response = requests.get(url, headers=self._get_headers(), params=params) - response.raise_for_status() - self.logger.info("Данные успешно получены из API TravelLine.") - return response.json().get("bookingNumbers", []) - except requests.RequestException as e: - self.logger.error(f"Ошибка при запросе к API TravelLine: {e}") - return [] - - def fetch_data(self): - """ - Обертка для получения данных из API TravelLine с дополнительной обработкой. - - :return: Список номеров бронирований. - """ - return self._fetch_data() - - def get_default_parser_settings(self): - """ - Возвращает настройки парсера по умолчанию. - """ - return { - "field_mapping": { - "reservation_id": "bookingNumber", - "check_in": "actualCheckInDateTime", - "check_out": "actualCheckOutDateTime", - "room_number": "roomId", - "status": "state", - }, - "date_format": "%Y-%m-%dT%H:%M" - } - - def process_data(self, booking_number, room_stay_id, action, actual_date_time): - """ - Обработка данных для заселения или выселения проживания. - - :param booking_number: Номер бронирования. - :param room_stay_id: Идентификатор проживания. - :param action: Действие ("check-in" или "check-out"). - :param actual_date_time: Фактические дата и время. - :return: Ответ API. - """ - if action not in ["check-in", "check-out"]: - raise ValueError("Invalid action. Must be 'check-in' or 'check-out'.") - - url = f"{self.BASE_URL}/bookings/{booking_number}/room-stays/{room_stay_id}/{action}" - payload = { - f"actual{action.capitalize()}DateTime": actual_date_time - } - - try: - response = requests.post(url, headers=self._get_headers(), json=payload) - response.raise_for_status() - self.logger.info(f"Успешно выполнено действие '{action}' для бронирования {booking_number}.") - return response.json() - except requests.RequestException as e: - self.logger.error(f"Ошибка при выполнении действия '{action}': {e}") - return {} - - def validate_plugin(self): - """ - Проверка плагина на соответствие требованиям. - """ - required_methods = ["fetch_data", "get_default_parser_settings", "_fetch_data"] - for method in required_methods: - if not hasattr(self, method): - raise ValueError(f"Плагин {type(self).__name__} не реализует метод {method}.") - self.logger.info(f"Плагин {self.__class__.__name__} успешно прошел валидацию.") - return True