13 Commits

Author SHA1 Message Date
eb662f7fe6 Merge branch 'PMSManager_refactor'
Some checks reported errors
continuous-integration/drone Build was killed
2025-07-19 19:15:03 +09:00
d59ebaf005 migration merger issues fix 2024-12-28 17:47:38 +09:00
31cf30e344 sMerge branch 'zorn-dev' 2024-12-28 13:50:12 +09:00
7350989113 mergse 2024-12-28 09:44:58 +09:00
5e3ed91b3a main Merge branch 'PMSManager_refactor' 2024-12-27 14:48:40 +09:00
56175078d6 sMerge branch 'PMSManager_refactor' 2024-12-27 10:01:58 +09:00
0e45074ea5 sadasd 2024-12-25 13:12:26 +09:00
bc865303c5 sssMerge branch 'pms_plugins' 2024-12-25 12:02:04 +09:00
5c58a46e18 ssMerge branch 'pms_plugins' 2024-12-24 21:36:24 +09:00
3dd5f2238e merge antifraud 2024-12-23 10:27:44 +09:00
61fffd3be4 sMerge branch 'master' of git.smartsoltech.kr:trevor/touchh_bot 2024-12-21 21:56:40 +09:00
c535a51953 merge 2024-12-21 21:56:15 +09:00
5f434d8248 Merge pull request 'antifraud' (#4) from antifraud into master
Reviewed-on: trevor/touchh_bot#4
2024-12-18 11:45:35 +00:00
43 changed files with 15075 additions and 198 deletions

View File

@@ -0,0 +1,6 @@
.venv
.venv/
.log
__pycache__
.history
.vscode

30
.docker/admin/Dockerfile Normal file
View File

@@ -0,0 +1,30 @@
FROM python:3.9-alpine
WORKDIR /app
# Устанавливаем временную директорию
ENV TMPDIR=/tmp/tempdir
RUN mkdir -p $TMPDIR && chmod 1777 $TMPDIR
# Устанавливаем системные зависимости для Alpine
RUN apk add --no-cache \
gcc \
musl-dev \
mariadb-dev \
netcat-openbsd \
net-tools \
iputils
# Копируем только requirements.txt для кэширования зависимостей
COPY .docker/admin/requirements.txt /app/requirements.txt
# Устанавливаем Python-зависимости
RUN pip install --upgrade pip && pip install --no-cache-dir -r /app/requirements.txt
# Копируем весь проект
COPY . /app
RUN chmod +x .docker/admin/entrypoint.sh
ENTRYPOINT [".docker/admin/entrypoint.sh"]
CMD ["python", "manage.py", "runserver", "0.0.0.0:8000"]

14
.docker/admin/entrypoint.sh Executable file
View File

@@ -0,0 +1,14 @@
#!/bin/sh
# Ожидание доступности базы данных
until nc -z -v -w30 $DB_HOST $DB_PORT; do
echo "Ожидание базы данных..."
sleep 1
done
# Выполняем миграции
python manage.py makemigrations --no-input
python manage.py migrate --no-input
# Запускаем приложение
exec "$@"

View File

@@ -0,0 +1,44 @@
ace_tools
aiohappyeyeballs
aiohttp
aiosignal
APScheduler
Django
django-environ
django_extensions
django-filter
django-health-check
django-jazzmin
django-jet
et_xmlfile
fonttools
fpdf2
geoip2
git-filter-repo
httpcore
httpx
jsonschema
jsonschema-specifications
maxminddb
multidict
PyMySQL
numpy
openpyxl
pandas
pathspec
pillow
propcache
psycopg
PyMySQL
python-dateutil
python-decouple
python-dotenv
python-telegram-bot
PyYAML
requests
sqlparse
ua-parser
ua-parser-builtins
user-agents
yarl
cryptography

View File

@@ -0,0 +1,6 @@
.venv
.venv/
.log
__pycache__
.history
.vscode

12
.docker/bot/Dockerfile Normal file
View File

@@ -0,0 +1,12 @@
FROM python:3.10-slim
WORKDIR /app
# Копируем весь проект в контейнер
COPY ../../ /app
# Устанавливаем зависимости только для bot
RUN pip install --upgrade pip && pip install --no-cache-dir -r .docker/bot/requirements.txt
# Команда запуска для бота
CMD ["python", "manage.py" ,"run_bot.py"]

View File

@@ -0,0 +1,44 @@
ace_tools
aiohappyeyeballs
aiohttp
aiosignal
APScheduler
Django
django-environ
django_extensions
django-filter
django-health-check
django-jazzmin
django-jet
et_xmlfile
fonttools
fpdf2
geoip2
git-filter-repo
httpcore
httpx
jsonschema
jsonschema-specifications
maxminddb
multidict
PyMySQL
numpy
openpyxl
pandas
pathspec
pillow
propcache
psycopg
PyMySQL
python-dateutil
python-decouple
python-dotenv
python-telegram-bot
PyYAML
requests
sqlparse
ua-parser
ua-parser-builtins
user-agents
yarl
cryptography

View File

@@ -0,0 +1,14 @@
FROM python:3.10-slim
WORKDIR /app
# Копируем весь проект в контейнер
COPY ../../ /app
RUN chmod +x .docker/scheduler/entrypoint.sh
ENTRYPOINT [".docker/scheduler/entrypoint.sh"]
# Устанавливаем зависимости только для scheduler
RUN pip install --upgrade pip && pip install --no-cache-dir -r .docker/scheduler/requirements.txt
# Команда запуска для планировщика
CMD ["python", "manage.py", "run_scheduler"]

View File

@@ -0,0 +1,6 @@
.venv
.venv/
.log
__pycache__
.history
.vscode

View File

@@ -0,0 +1,7 @@
#!/bin/sh
# Выполняем миграции
python manage.py migrate --no-input
# Запускаем приложение
exec "$@"

View File

@@ -0,0 +1,44 @@
ace_tools
aiohappyeyeballs
aiohttp
aiosignal
APScheduler
Django
django-environ
django-extensions
django-filter
django-health-check
django-jazzmin
django-jet
et_xmlfile
fonttools
fpdf2
geoip2
git-filter-repo
httpcore
httpx
jsonschema
jsonschema-specifications
maxminddb
multidict
PyMySQL
numpy
openpyxl
pandas
pathspec
pillow
propcache
psycopg
PyMySQL
python-dateutil
python-decouple
python-dotenv
python-telegram-bot
PyYAML
requests
sqlparse
ua-parser
ua-parser-builtins
user-agents
yarl
cryptography

View File

@@ -1,4 +1,6 @@
from decouple import config
from django.conf import settings
from django.apps import apps from django.apps import apps
def load_database_settings(databases): def load_database_settings(databases):
@@ -8,6 +10,18 @@ def load_database_settings(databases):
""" """
LocalDatabase = apps.get_model('app_settings', 'LocalDatabase') LocalDatabase = apps.get_model('app_settings', 'LocalDatabase')
local_db_settings = LocalDatabase.objects.all()
for db in local_db_settings:
# Пример добавления дополнительной базы данных
settings.DATABASES[db.name] = {
'ENGINE': 'django.db.backends.mysql',
'NAME': db.db_name,
'USER': db.username,
'PASSWORD': db.password,
'HOST': db.host,
'PORT': db.port,
}
try: try:
local_db_settings = LocalDatabase.objects.filter(is_active=True) local_db_settings = LocalDatabase.objects.filter(is_active=True)
for db in local_db_settings: for db in local_db_settings:

File diff suppressed because one or more lines are too long

9
app_settings/signals.py Normal file
View File

@@ -0,0 +1,9 @@
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import GlobalSystemSettings
@receiver(post_save, sender=GlobalSystemSettings)
def update_system_settings(sender, instance, **kwargs):
# Безопасное использование сигнала
if instance:
print(f"Настройки системы обновлены: {instance.system_name}")

11
app_settings/urls.py Normal file
View File

@@ -0,0 +1,11 @@
from django.urls import path
from django.http import HttpResponse
app_name = 'settings'
def placeholder_view(request):
return HttpResponse("Placeholder for settings app.")
urlpatterns = [
path('', placeholder_view, name='settings_placeholder'),
]

13373
bnovo_page_1.json Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -150,7 +150,7 @@ async def check_pms(update, context):
# Проверка наличия fetch_data и вызов плагина # Проверка наличия fetch_data и вызов плагина
if hasattr(pms_manager.plugin, 'fetch_data') and callable(pms_manager.plugin.fetch_data): if hasattr(pms_manager.plugin, 'fetch_data') and callable(pms_manager.plugin.fetch_data):
report = await pms_manager.plugin.fetch_data() report = await pms_manager.plugin.fetch_data()
logger.debug(f"Отчет типа: {type(report)}") logger.debug(f"Отчет типа: {type(report)}: {report}")
else: else:
logger.error("Плагин не поддерживает fetch_data.") logger.error("Плагин не поддерживает fetch_data.")
await query.edit_message_text("Подходящий способ интеграции с PMS не найден.") await query.edit_message_text("Подходящий способ интеграции с PMS не найден.")

73
docker-compose.yml Normal file
View File

@@ -0,0 +1,73 @@
version: '3.9'
services:
mysql:
image: mysql:8.0
container_name: mysql
environment:
MYSQL_ROOT_PASSWORD: ${DB_PASSWORD}
MYSQL_DATABASE: ${DB_NAME}
MYSQL_USER: ${DB_USER}
MYSQL_PASSWORD: ${DB_PASSWORD}
TMPDIR: /var/tmp
ports:
- "${DB_PORT}:3306"
volumes:
- mysql_data:/var/lib/mysql
- /var/tmp:/var/tmp
django-admin:
build:
context: .
dockerfile: .docker/admin/Dockerfile
container_name: django-admin
restart: on-failure
volumes:
- .:/app
env_file:
- .env
environment:
- DJANGO_SETTINGS_MODULE=touchh.settings
- DATABASE_URL=mysql://${DB_USER}:${DB_PASSWORD}@mysql:3306/${DB_NAME}
- LOG_LEVEL=${LOG_LEVEL}
depends_on:
- mysql
ports:
- "8000:8000"
command: python manage.py runserver 0.0.0.0:8000
bot:
build:
context: .
dockerfile: .docker/bot/Dockerfile
container_name: bot
restart: on-failure
volumes:
- .:/app
environment:
- TELEGRAM_BOT_TOKEN=${TELEGRAM_BOT_TOKEN}
- DJANGO_SETTINGS_MODULE=project.settings
- DATABASE_URL=mysql://${DB_USER}:${DB_PASSWORD}@mysql:3306/${DB_NAME}
- LOG_LEVEL=${LOG_LEVEL}
depends_on:
- mysql
scheduler:
build:
context: .
dockerfile: .docker/scheduler/Dockerfile
container_name: scheduler
restart: on-failure
volumes:
- .:/app
env_file:
- .env
environment:
- DJANGO_SETTINGS_MODULE=project.settings
- DATABASE_URL=mysql://${DB_USER}:${DB_PASSWORD}@mysql:3306/${DB_NAME}
- SCHEDULED_SYNC_LOG_LEVEL=${SCHEDULED_SYNC_LOG_LEVEL}
depends_on:
- mysql
volumes:
mysql_data:

View File

@@ -18,7 +18,6 @@ class PluginLoader:
print("Загрузка плагинов:") print("Загрузка плагинов:")
for file in os.listdir(PluginLoader.PLUGIN_PATH): for file in os.listdir(PluginLoader.PLUGIN_PATH):
if file.endswith("_pms.py") and not file.startswith("__"): if file.endswith("_pms.py") and not file.startswith("__"):
# print(f" Plugin {file}")
module_name = f"pms_integration.plugins.{file[:-3]}" module_name = f"pms_integration.plugins.{file[:-3]}"
try: try:
module = importlib.import_module(module_name) module = importlib.import_module(module_name)
@@ -26,8 +25,8 @@ class PluginLoader:
cls = getattr(module, attr) cls = getattr(module, attr)
if isinstance(cls, type) and issubclass(cls, BasePMSPlugin) and cls is not BasePMSPlugin: if isinstance(cls, type) and issubclass(cls, BasePMSPlugin) and cls is not BasePMSPlugin:
plugin_name = file[:-7] # Убираем `_pms` из имени файла plugin_name = file[:-7] # Убираем `_pms` из имени файла
# print(f" Загружен плагин {plugin_name}: {cls.__name__}")
plugins[plugin_name] = cls plugins[plugin_name] = cls
print(f" Загружен плагин {plugin_name}: {cls.__name__}")
except Exception as e: except Exception as e:
print(f" Ошибка загрузки плагина {module_name}: {e}") print(f" Ошибка загрузки плагина {module_name}: {e}")
return plugins return plugins
@@ -40,6 +39,7 @@ class PMSIntegrationManager:
""" """
self.hotel = hotel self.hotel = hotel
self.plugin = None self.plugin = None
self.plugins = PluginLoader.load_plugins()
def load_hotel(self): def load_hotel(self):
""" """
@@ -52,6 +52,16 @@ class PMSIntegrationManager:
""" """
Загружает плагин, соответствующий PMS конфигурации отеля. Загружает плагин, соответствующий PMS конфигурации отеля.
""" """
<<<<<<< HEAD
pms_name = self.hotel.pms.plugin_name.lower()
if pms_name in self.plugins:
plugin_class = self.plugins[pms_name]
self.plugin = plugin_class(self.hotel)
print(f"Плагин {pms_name} успешно загружен.")
else:
raise ValueError(f"Неизвестный PMS: {pms_name}")
=======
pms_name = self.hotel.pms.plugin_name.lower() # Приводим название плагина к нижнему регистру pms_name = self.hotel.pms.plugin_name.lower() # Приводим название плагина к нижнему регистру
# Формируем имя модуля и класса плагина # Формируем имя модуля и класса плагина
@@ -72,6 +82,7 @@ class PMSIntegrationManager:
except ImportError as e: except ImportError as e:
raise ValueError(f"Ошибка загрузки плагина для PMS {pms_name}: {e}") raise ValueError(f"Ошибка загрузки плагина для PMS {pms_name}: {e}")
>>>>>>> PMSManager_refactor
def fetch_data(self): def fetch_data(self):
""" """
Получает данные из PMS с использованием загруженного плагина. Получает данные из PMS с использованием загруженного плагина.

View File

@@ -1,3 +1,413 @@
# 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("bnovo_plugin.log"), # Логи пишутся в файл
# logging.StreamHandler() # Логи выводятся в консоль
# ]
# )
# # Создаем отдельный логгер для консоли с уровнем INFO
# console_handler = logging.StreamHandler()
# console_handler.setLevel(logging.INFO) # Меняем уровень логов для консоли
# 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}")
# async def _ensure_token(self):
# """
# Убеждается, что токен (SID) доступен. Если его нет, выполняет авторизацию.
# """
# logger.debug(f"Проверка токена для отеля {self.hotel.id}.")
# if not self.token:
# self.token = await self._get_stored_token()
# if not self.token:
# logger.info("Токен отсутствует, выполняем авторизацию.")
# await self._fetch_session()
# else:
# logger.debug(f"Используется сохраненный токен: {self.token}")
# 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_paginated_data(self):
# """
# Получает все данные с API, обрабатывая страницы с пагинацией.
# """
# logger.info("Начало получения данных с пагинацией.")
# await self._ensure_token()
# url = f"{self.api_url}/dashboard"
# headers = self._get_auth_headers()
# 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",
# }
# all_bookings = []
# try:
# while True:
# logger.debug(f"Запрос к {url} с параметрами: {json.dumps(params, indent=2)}")
# response = requests.get(url, headers=headers, params=params, allow_redirects=False)
# if response.status_code == 302:
# logger.warning("Получен код 302. Перенаправление.")
# await self._fetch_session()
# headers = self._get_auth_headers()
# response = requests.get(url, headers=headers, params=params)
# if response.status_code != 200:
# logger.error(f"Ошибка при запросе: {response.status_code}, {response.text}")
# raise ValueError("Ошибка при получении данных.")
# data = response.json()
# bookings = data.get("bookings", [])
# all_bookings.extend(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
# logger.info(f"Всего бронирований: {len(all_bookings)}")
# return all_bookings
# async def _save_hotel_data(self, hotel_data):
# """
# Сохраняет данные об отеле в базу.
# """
# try:
# hotel_id = hotel_data.get("id")
# if not hotel_id:
# logger.warning("Данные об отеле не содержат идентификатор.")
# return
# await sync_to_async(Hotel.objects.update_or_create)(
# external_id=hotel_id,
# defaults={
# "name": hotel_data.get("name"),
# "address": hotel_data.get("address"),
# "city": hotel_data.get("city"),
# "timezone": hotel_data.get("timezone"),
# "rating": hotel_data.get("rating"),
# "phone": hotel_data.get("phone"),
# "email": hotel_data.get("email"),
# "country": hotel_data.get("country"),
# "booking_url": hotel_data.get("booking_url"),
# "tripadvisor_url": hotel_data.get("tripadvisor_url"),
# },
# )
# logger.info(f"Информация об отеле {hotel_id} успешно обновлена.")
# except Exception as e:
# logger.error(f"Ошибка при сохранении данных об отеле: {e}")
# async def _fetch_data(self):
# """
# Получает данные о бронированиях с API и возвращает их.
# """
# logger.info("Начало процесса получения данных о бронированиях.")
# try:
# await self._ensure_token() # Проверка токена
# url = f"{self.api_url}/dashboard"
# headers = self._get_auth_headers()
# 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",
# }
# all_data = []
# while True:
# logger.debug(f"Запрос к {url} с параметрами: {json.dumps(params, indent=2)}")
# response = requests.get(url, headers=headers, params=params, allow_redirects=False)
# if response.status_code == 302:
# logger.warning("Получен код 302. Перенаправление.")
# await self._fetch_session() # Обновляем токен
# headers = self._get_auth_headers()
# continue
# if response.status_code != 200:
# logger.error(f"Ошибка при запросе: {response.status_code}, {response.text}")
# raise ValueError(f"Ошибка при получении данных: {response.text}")
# try:
# data = response.json()
# except json.JSONDecodeError as e:
# logger.error(f"Ошибка декодирования JSON: {e}. Ответ: {response.text}")
# raise ValueError(f"Ошибка декодирования JSON: {e}")
# bookings = data.get("bookings", [])
# logger.debug(f"Получено бронирований: {len(bookings)}")
# all_data.extend(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
# if not all_data:
# logger.error("Полученные данные пусты или отсутствуют бронирования.")
# raise ValueError("API вернуло пустые данные.")
# logger.info(f"Всего бронирований: {len(all_data)}")
# return all_data
# except Exception as e:
# logger.error(f"Ошибка при получении данных: {e}")
# raise
# async def _process_and_save_data(self, data):
# """
# Обрабатывает и сохраняет данные о бронированиях в базу.
# """
# logger.info("Начало обработки данных о бронированиях для сохранения в базу.")
# processed_items = 0
# errors = []
# for record in data:
# try:
# booking_id = record.get("id")
# room_number = record.get("current_room")
# arrival = record.get("arrival")
# departure = record.get("departure")
# status = record.get("status_name")
# # Проверка обязательных данных
# if not (booking_id and room_number and arrival and departure and status):
# logger.warning(f"Пропуск записи из-за отсутствия обязательных данных: {record}")
# continue
# # Сохраняем или обновляем запись в базе данных
# reservation, created = await sync_to_async(Reservation.objects.update_or_create)(
# external_id=booking_id,
# defaults={
# "hotel": self.hotel,
# "status": status,
# "room_number": room_number,
# "check_in": arrival,
# "check_out": departure,
# },
# )
# if created:
# logger.info(f"Создана новая запись бронирования: {reservation}")
# else:
# logger.info(f"Обновлено существующее бронирование: {reservation}")
# processed_items += 1
# except Exception as e:
# logger.error(f"Ошибка обработки бронирования {record.get('id')}: {e}")
# errors.append(str(e))
# logger.info(f"Обработано бронирований: {processed_items}, ошибок: {len(errors)}")
# return {"processed_items": processed_items, "errors": errors}
# async def fetch_and_process_data(self):
# """
# Загружает данные с API и сохраняет их в базу.
# """
# logger.info("Начало процесса загрузки и обработки данных.")
# try:
# data = await self._fetch_paginated_data()
# report = await self._process_and_save_data(data)
# logger.info(f"Загрузка и обработка завершены. Отчет: {report}")
# return report
# except Exception as e:
# logger.error(f"Ошибка в процессе загрузки и обработки данных: {e}")
# raise
import requests import requests
import json import json
from datetime import datetime, timedelta from datetime import datetime, timedelta
@@ -5,14 +415,11 @@ from asgiref.sync import sync_to_async
from .base_plugin import BasePMSPlugin from .base_plugin import BasePMSPlugin
from pms_integration.models import PMSConfiguration from pms_integration.models import PMSConfiguration
from hotels.models import Reservation, Hotel from hotels.models import Reservation, Hotel
from touchh.utils.log import CustomLogger
import logging
import logging import logging
# Настройка логирования # Настройка логирования
logging.basicConfig( logging.basicConfig(
level=logging.WARNING, # Установите уровень логов для всех обработчиков level=logging.WARNING,
format='%(asctime)s [%(levelname)s] %(message)s', format='%(asctime)s [%(levelname)s] %(message)s',
handlers=[ handlers=[
logging.FileHandler("var/log/bnovo_plugin.log"), # Логи пишутся в файл logging.FileHandler("var/log/bnovo_plugin.log"), # Логи пишутся в файл
@@ -20,46 +427,26 @@ logging.basicConfig(
] ]
) )
# Создаем отдельный логгер для консоли с уровнем 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 = logging.getLogger("BnovoPMS Plugin")
logger.addHandler(console_handler) logger.setLevel(logging.INFO)
logger.setLevel(logging.WARNING) # Основной уровень логов
class BnovoPMSPlugin(BasePMSPlugin): class BnovoPMSPlugin(BasePMSPlugin):
"""Плагин для работы с PMS Bnovo.""" """Плагин для работы с PMS Bnovo."""
def __init__(self, hotel): def __init__(self, hotel):
super().__init__(hotel) super().__init__(hotel)
if not isinstance(hotel, Hotel):
logger.error("Ожидался объект Hotel, но получен другой тип.")
raise ValueError("Для инициализации плагина требуется объект Hotel.")
self.hotel = hotel self.hotel = hotel
self.pms_config = hotel.pms # Связь отеля с PMSConfiguration self.pms_config = hotel.pms
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.api_url = self.pms_config.url.rstrip("/")
self.username = self.pms_config.username self.username = self.pms_config.username
self.password = self.pms_config.password self.password = self.pms_config.password
self.token = None 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): def get_default_parser_settings(self):
"""Возвращает настройки по умолчанию для обработки данных.""" """
Возвращает настройки по умолчанию для обработки данных.
"""
logger.debug("Получение настроек парсера по умолчанию.") logger.debug("Получение настроек парсера по умолчанию.")
return { return {
"date_format": "%Y-%m-%dT%H:%M:%S", "date_format": "%Y-%m-%dT%H:%M:%S",
@@ -67,28 +454,25 @@ class BnovoPMSPlugin(BasePMSPlugin):
} }
async def _get_stored_token(self): async def _get_stored_token(self):
"""Получение токена из конфигурации PMS отеля.""" """
Получает токен из конфигурации PMS отеля.
"""
try: try:
logger.debug(f"Попытка получения токена для отеля {self.hotel.id}.") logger.debug(f"Попытка получения токена для отеля {self.hotel.id}.")
token = self.pms_config.token token = self.pms_config.token
logger.debug(f"Токен из базы данных: {token}") if not token:
logger.info("Токен отсутствует в конфигурации.")
else:
logger.debug(f"Токен найден: {token}")
return token return token
except Exception as e: except Exception as e:
logger.warning(f"Ошибка при получении токена для отеля {self.hotel.id}: {e}") logger.error(f"Ошибка при получении токена для отеля {self.hotel.id}: {e}")
return None 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): def _get_auth_headers(self):
"""Создает заголовки авторизации.""" """
Создает заголовки авторизации для запросов к API.
"""
logger.debug("Создание заголовков авторизации.") logger.debug("Создание заголовков авторизации.")
headers = { headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -97,81 +481,54 @@ class BnovoPMSPlugin(BasePMSPlugin):
if self.token: if self.token:
headers["Cookie"] = f"SID={self.token}" headers["Cookie"] = f"SID={self.token}"
logger.debug(f"Добавлен токен в заголовки: {self.token}") logger.debug(f"Добавлен токен в заголовки: {self.token}")
else:
logger.warning("Токен отсутствует, запрос может быть неавторизованным.")
return headers 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)}") async def _ensure_token(self):
response = requests.post(url, json=payload, headers=headers, allow_redirects=False) """
Убеждается, что токен (SID) доступен. Если его нет, выполняет авторизацию.
logger.debug(f"Ответ авторизации: статус {response.status_code}, заголовки {response.headers}") """
if response.status_code == 302: logger.debug(f"Проверка токена для отеля {self.hotel.id}.")
cookies = response.cookies.get_dict() if not self.token:
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() self.token = await self._get_stored_token()
if not self.token: if not self.token:
logger.info("Токен отсутствует, выполняем авторизацию.") logger.info("Токен отсутствует, выполняем авторизацию.")
await self._fetch_session() await self._fetch_session()
else:
logger.debug(f"Используется сохраненный токен: {self.token}")
url = f"{self.api_url}/account/current" async def _save_token_to_db(self, sid):
headers = self._get_auth_headers() """
Сохраняет токен (SID) в конфигурации PMS отеля.
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: try:
account_data = response.json() logger.debug(f"Сохранение токена для отеля {self.hotel.id}: {sid}")
logger.debug(f"Полученные данные аккаунта:") self.pms_config.token = sid
except json.JSONDecodeError as e: await sync_to_async(self.pms_config.save)()
logger.error(f"Ошибка декодирования JSON: {e}") logger.info(f"Токен {sid} успешно сохранен в базу данных.")
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: except Exception as e:
logger.error(f"Ошибка при получении данных аккаунта: {e}") logger.error(f"Ошибка сохранения токена для отеля {self.hotel.id}: {e}")
raise raise
async def _fetch_data_with_account_info(self): async def _fetch_session(self):
"""Получение данных аккаунта и бронирований.""" """Получение токена (SID) через авторизацию."""
logger.info(f"Запуск процесса получения данных аккаунта и бронирований для отеля {self.hotel.id}.") url = f"{self.api_url}/"
try: payload = {"username": self.username, "password": self.password}
account_data = await self.fetch_and_log_account_data() headers = {"Content-Type": "application/json"}
logger.info("Данные аккаунта успешно получены, продолжение с бронированиями.") await self._save_token_to_db(self.token)
await self.__fetch_data()
except Exception as e:
logger.error(f"Ошибка при выполнении полной операции: {e}")
<<<<<<< HEAD
response = requests.post(url, json=payload, headers=headers, allow_redirects=False)
if response.status_code == 302:
self.token = response.cookies.get("SID")
await self._save_token_to_db(self.token)
else:
logger.error(f"Ошибка авторизации: {response.status_code}, {response.text}")
raise ValueError("Ошибка авторизации")
=======
async def _fetch_data(self): async def _fetch_data(self):
"""Получение данных о бронированиях с помощью эндпоинта /dashboard.""" """Получение данных о бронированиях с помощью эндпоинта /dashboard."""
logger.info("Начало процесса получения данных о бронированиях.") logger.info("Начало процесса получения данных о бронированиях.")
@@ -183,10 +540,19 @@ class BnovoPMSPlugin(BasePMSPlugin):
except Exception as e: except Exception as e:
logger.error(f"Ошибка получения данных аккаунта: {e}") logger.error(f"Ошибка получения данных аккаунта: {e}")
raise raise
>>>>>>> PMSManager_refactor
async def _fetch_paginated_data(self):
"""
Получает все данные с API, обрабатывая страницы с пагинацией.
"""
logger.info("Начало получения данных с пагинацией.")
await self._ensure_token()
url = f"{self.api_url}/dashboard" url = f"{self.api_url}/dashboard"
headers = self._get_auth_headers()
now = datetime.now() now = datetime.now()
create_from = (now - timedelta(days=1)).strftime("%d.%m.%Y") create_from = (now - timedelta(days=7)).strftime("%d.%m.%Y") # Получаем данные за последнюю неделю
create_to = now.strftime("%d.%m.%Y") create_to = now.strftime("%d.%m.%Y")
params = { params = {
@@ -197,30 +563,25 @@ class BnovoPMSPlugin(BasePMSPlugin):
"page": 1, "page": 1,
"order_by": "create_date.asc", "order_by": "create_date.asc",
} }
headers = self._get_auth_headers()
all_bookings = [] all_bookings = []
try:
while True: while True:
logger.debug(f"Запрос к {url} с параметрами: {json.dumps(params, indent=2)}") logger.debug(f"Запрос к {url} с параметрами: {json.dumps(params, indent=2)}")
try:
response = requests.get(url, headers=headers, params=params, allow_redirects=False) response = requests.get(url, headers=headers, params=params, allow_redirects=False)
if response.status_code == 302: if response.status_code == 302:
logger.warning("Получен код 302. Перенаправление.") logger.warning("Получен код 302. Перенаправление.")
redirected_url = response.headers.get("Location") await self._fetch_session()
if redirected_url: headers = self._get_auth_headers()
logger.debug(f"Перенаправление на {redirected_url}")
url = redirected_url
continue continue
else:
logger.error("Ответ с кодом 302 не содержит заголовка Location.")
raise ValueError("Перенаправление без указанного URL.")
if response.status_code != 200: if response.status_code != 200:
logger.error(f"Ошибка при запросе: {response.status_code}, {response.text}") logger.error(f"Ошибка при запросе: {response.status_code}, {response.text}")
raise ValueError("Ошибка запроса к /dashboard") raise ValueError(f"Ошибка при получении данных: {response.text}")
data = response.json() data = response.json()
logger.debug(f"Полученный ответ API: {json.dumps(data, indent=2, ensure_ascii=False)}")
bookings = data.get("bookings", []) bookings = data.get("bookings", [])
rooms = data.get("rooms", []) rooms = data.get("rooms", [])
@@ -228,54 +589,88 @@ class BnovoPMSPlugin(BasePMSPlugin):
logger.debug(f'bookings: {bookings}\n rooms: {rooms}') logger.debug(f'bookings: {bookings}\n rooms: {rooms}')
all_bookings.extend(bookings) all_bookings.extend(bookings)
logger.info(f"Получено бронирований: {len(bookings)}. Всего: {len(all_bookings)}.")
pages_info = data.get("pages", {}) pages_info = data.get("pages", {})
current_page = pages_info.get("current_page", 1) current_page = pages_info.get("current_page", 1)
total_pages = pages_info.get("total_pages", 1) total_pages = pages_info.get("total_pages", 1)
logger.debug(f"Информация о страницах: текущая {current_page}, всего {total_pages}")
if current_page >= total_pages: if current_page >= total_pages:
break break
params["page"] += 1 params["page"] += 1
except json.JSONDecodeError as e:
logger.error(f"Ошибка декодирования JSON: {e}. Ответ: {response.text}")
raise ValueError(f"Ошибка декодирования JSON: {e}")
except Exception as e: except Exception as e:
logger.error(f"Неизвестная ошибка при обработке запроса: {e}") logger.error(f"Ошибка при обработке данных: {e}")
raise 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): async def _process_and_save_bookings(self, bookings):
logger.debug(f"Бронирование {booking_id} не относится к отелю {self.hotel.external_id_pms}. Пропуск.") """
Обрабатывает и сохраняет бронирования в базу.
"""
logger.info("Начало обработки данных о бронированиях для сохранения в базу.")
processed_items = 0
errors = []
for record in bookings:
try:
booking_id = record.get("id")
room_number = record.get("current_room")
arrival = record.get("arrival")
departure = record.get("departure")
status = record.get("status_name")
# Проверка обязательных данных
if not (booking_id and room_number and arrival and departure and status):
logger.warning(f"Пропуск записи из-за отсутствия обязательных данных: {record}")
continue continue
# Сохраняем или обновляем запись в базе данных
reservation, created = await sync_to_async(Reservation.objects.update_or_create)( reservation, created = await sync_to_async(Reservation.objects.update_or_create)(
external_id=booking_id, external_id=booking_id,
defaults = { defaults={
"hotel": self.hotel, # Объект модели Hotel "hotel": self.hotel,
"status": booking.get("status_name"), # Статус бронирования "status": status,
"room_number": booking.get("current_room"), # Номер комнаты (исправлено с create_date) "room_number": room_number,
"check_in": booking.get("arrival"), # Дата заезда "check_in": arrival,
"check_out": booking.get("departure"), # Дата выезда "check_out": departure,
"room_type": booking.get("initial_room_type_name") # Тип комнаты },
}
) )
if created: if created:
logger.info(f"Создана новая запись бронирования: {reservation}") logger.info(f"Создана новая запись бронирования: {reservation}")
print(reservation)
else: else:
logger.info(f"Обновлено существующее бронирование: {reservation}") logger.info(f"Обновлено существующее бронирование: {reservation}")
except Exception as e: processed_items += 1
logger.error(f"Ошибка обработки бронирования {booking.get('id')}: {e}")
logger.info(f"Все бронирования получены и обработаны. Итоговое количество: {len(all_bookings)}") except Exception as e:
return all_bookings logger.error(f"Ошибка обработки бронирования {record.get('id')}: {e}")
errors.append(str(e))
logger.info(f"Обработано бронирований: {processed_items}, ошибок: {len(errors)}")
return {"processed_items": processed_items, "errors": errors}
async def _fetch_data(self):
"""
Получает данные о бронированиях с API и возвращает их.
"""
logger.info("Начало процесса получения данных о бронированиях.")
try:
bookings = await self._fetch_paginated_data() # Получаем данные с пагинацией
return bookings
except Exception as e:
logger.error(f"Ошибка в процессе получения данных: {e}")
raise ValueError("Ошибка при получении данных о бронированиях")
async def fetch_and_process_data(self):
"""Получение данных с API, обработка и сохранение в базу."""
logger.info("Начало загрузки данных с API")
try:
bookings = await self._fetch_paginated_data()
report = await self._process_and_save_bookings(bookings)
logger.info(f"Данные успешно обработаны. Отчет: {report}")
return report
except Exception as e:
logger.error(f"Ошибка загрузки и обработки данных: {e}")
raise

View File

@@ -1,3 +1,130 @@
# import logging
# import requests
# from datetime import datetime, timedelta
# from asgiref.sync import sync_to_async
# from pms_integration.models import PMSConfiguration
# from hotels.models import Hotel, Reservation
# from .base_plugin import BasePMSPlugin
# class ShelterPMSPlugin(BasePMSPlugin):
# """
# Плагин для интеграции с PMS Shelter.
# """
# def __init__(self, pms_config):
# super().__init__(pms_config)
# # Настройка логирования
# self.logger = logging.getLogger(self.__class__.__name__)
# handler = logging.StreamHandler()
# formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
# handler.setFormatter(formatter)
# self.logger.addHandler(handler)
# self.logger.setLevel(logging.DEBUG)
# # Инициализация параметров API
# self.api_url = pms_config.url
# self.token = pms_config.token
# self.username = pms_config.username
# self.password = pms_config.password
# def get_default_parser_settings(self):
# """
# Возвращает настройки парсера по умолчанию.
# """
# return {
# "field_mapping": {
# "check_in": "checkin",
# "check_out": "checkout",
# "room_number": "room_number",
# "room_type": "room_type",
# "status": "status",
# },
# "date_format": "%Y-%m-%dT%H:%M:%S"
# }
# async def _fetch_data(self):
# """
# Получает данные из Shelter PMS API и сохраняет их в базу данных.
# """
# now = datetime.now()
# start_date = (now - timedelta(days=1)).strftime('%Y-%m-%d')
# end_date = now.strftime('%Y-%m-%d')
# headers = {
# "Authorization": f"Bearer {self.token}",
# "Content-Type": "application/json",
# }
# params = {
# "start_date": start_date,
# "end_date": end_date,
# }
# try:
# response = await sync_to_async(requests.get)(f"{self.api_url}/reservations", headers=headers, params=params)
# response.raise_for_status()
# data = response.json()
# self.logger.debug(f"Получены данные с API: {data}")
# except requests.exceptions.RequestException as e:
# self.logger.error(f"Ошибка запроса к API Shelter: {e}")
# return []
# # Обработка и сохранение данных
# processed_data = []
# for item in data.get("reservations", []):
# processed_item = {
# "room_number": item.get("room_number"),
# "check_in": datetime.strptime(item.get("check_in"), '%Y-%m-%dT%H:%M:%S'),
# "check_out": datetime.strptime(item.get("check_out"), '%Y-%m-%dT%H:%M:%S'),
# "status": item.get("status"),
# "room_type": item.get("room_type"),
# }
# processed_data.append(processed_item)
# await self._save_to_db(processed_item)
# self.logger.debug("Все данные успешно сохранены в базу данных.")
# return processed_data
# async def _save_to_db(self, item):
# """
# Сохраняет данные в базу данных.
# """
# try:
# hotel = await sync_to_async(Hotel.objects.get)(pms=self.pms_config)
# reservation, created = await sync_to_async(Reservation.objects.update_or_create)(
# room_number=item["room_number"],
# check_in=item["check_in"],
# defaults={
# "check_out": item["check_out"],
# "status": item["status"],
# "hotel": hotel,
# "room_type": item["room_type"],
# },
# )
# if created:
# self.logger.debug(f"Создана новая запись бронирования: {reservation}")
# else:
# self.logger.debug(f"Обновлено существующее бронирование: {reservation}")
# except Exception as e:
# self.logger.error(f"Ошибка при сохранении данных в БД: {e}")
# async def fetch_and_process_data(self):
# """
# Загружает данные с API Shelter и сохраняет их в базу данных.
# """
# self.logger.info("Начало процесса загрузки данных из Shelter PMS.")
# try:
# data = await self._fetch_data()
# self.logger.info(f"Загрузка и обработка данных завершены. Обработано записей: {len(data)}")
# return data
# except Exception as e:
# self.logger.error(f"Ошибка в процессе загрузки данных: {e}")
# raise
import logging import logging
import requests import requests
import json import json
@@ -6,6 +133,25 @@ from datetime import datetime, timedelta
from asgiref.sync import sync_to_async from asgiref.sync import sync_to_async
from hotels.models import Hotel, Reservation from hotels.models import Hotel, Reservation
from .base_plugin import BasePMSPlugin from .base_plugin import BasePMSPlugin
<<<<<<< HEAD
class ShelterPMSPlugin(BasePMSPlugin):
"""
Плагин для интеграции с PMS Shelter (интерфейс для получения данных об отеле).
"""
def __init__(self, pms_config):
super().__init__(pms_config)
self.logger = logging.getLogger(self.__class__.__name__)
handler = logging.StreamHandler()
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
self.logger.addHandler(handler)
self.logger.setLevel(logging.DEBUG)
self.api_url = "https://pms.frontdesk24.ru/sheltercloudapi/Reservations/"
self.access_token = "679CA9C5-9847-4151-883E-5F61181AA37E"
=======
from touchh.utils.log import CustomLogger from touchh.utils.log import CustomLogger
class ShelterPMSPlugin(BasePMSPlugin): class ShelterPMSPlugin(BasePMSPlugin):
""" """
@@ -34,24 +180,65 @@ class ShelterPMSPlugin(BasePMSPlugin):
self.logger.addHandler(handler_console) self.logger.addHandler(handler_console)
self.logger.addHandler(handler_file) self.logger.addHandler(handler_file)
self.logger.setLevel(logging.WARNING) self.logger.setLevel(logging.WARNING)
>>>>>>> PMSManager_refactor
def get_default_parser_settings(self): def get_default_parser_settings(self):
""" """
Возвращает настройки парсера по умолчанию. Возвращает настройки по умолчанию для обработки данных.
""" """
return { return {
"field_mapping": { "field_mapping": {
<<<<<<< HEAD
"check_in": "check_in",
"check_out": "check_out",
"room_number": "room_number",
"status": "status",
=======
"check_in": "from", "check_in": "from",
"check_out": "until", "check_out": "until",
"room_number": "roomNumber", "room_number": "roomNumber",
"room_type_name": "roomTypeName", "room_type_name": "roomTypeName",
"status": "checkInStatus", "status": "checkInStatus",
>>>>>>> PMSManager_refactor
}, },
"date_format": "%Y-%m-%dT%H:%M:%S" "date_format": "%Y-%m-%dT%H:%M:%S",
} }
async def _fetch_data(self): async def _fetch_data(self):
""" """
<<<<<<< HEAD
Получает данные бронирований с API PMS Shelter.
"""
try:
# Формируем параметры запроса
now = datetime.now()
create_from = (now - timedelta(days=7)).strftime("%Y-%m-%d")
create_to = now.strftime("%Y-%m-%d")
data = {
"from": create_from,
"until": create_to,
"pagination": {
"from": 0,
"count": 100
}
}
headers = {
"Content-Type": "application/json",
"Accept": "text/plain",
"Authorization": f"Bearer {self.access_token}"
}
# Логирование запроса
self.logger.debug(f"Отправка запроса к API: {self.api_url}")
self.logger.debug(f"Тело запроса: {data}")
self.logger.debug(f"Заголовки: {headers}")
# Выполняем запрос
response = requests.post(self.api_url, json=data, headers=headers)
response.raise_for_status()
=======
Получает данные из PMS API и сохраняет их в базу. Получает данные из PMS API и сохраняет их в базу.
""" """
now = datetime.now() now = datetime.now()
@@ -180,58 +367,69 @@ class ShelterPMSPlugin(BasePMSPlugin):
raise ValueError(f"Плагин {type(self).__name__} не реализует метод {method}.") raise ValueError(f"Плагин {type(self).__name__} не реализует метод {method}.")
self.logger.debug(f"Плагин {self.__class__.__name__} прошел валидацию.") self.logger.debug(f"Плагин {self.__class__.__name__} прошел валидацию.")
return True return True
>>>>>>> PMSManager_refactor
async def _save_to_db(self, item): # Обрабатываем ответ
bookings = response.json()
self.logger.info(f"Получено бронирований: {len(bookings)}")
return bookings
except requests.HTTPError as http_err:
self.logger.error(f"HTTP ошибка: {http_err}")
self.logger.error(f"Текст ответа: {response.text if 'response' in locals() else 'Нет данных'}")
raise
except Exception as e:
self.logger.error(f"Ошибка получения данных PMS Shelter: {e}")
raise
async def fetch_and_process_data(self):
""" """
Сохраняет данные в БД (например, информацию о номере). Получение данных с API, обработка и сохранение в базу.
""" """
self.logger.info("Начало загрузки данных с API Shelter")
try: try:
# Проверяем, что item — это словарь bookings = await self._fetch_data()
if not isinstance(item, dict): report = await self._process_and_save_bookings(bookings)
self.logger.error(f"Ожидался словарь, но получен: {type(item)}. Данные: {item}") self.logger.info(f"Данные успешно обработаны. Отчет: {report}")
return return report
except Exception as e:
self.logger.error(f"Ошибка загрузки и обработки данных: {e}")
raise
# Получаем отель по настройкам PMS async def _process_and_save_bookings(self, bookings):
hotel = await sync_to_async(Hotel.objects.get)(pms=self.pms_config) """
self.logger.debug(f"Отель найден: {hotel.name}") Обрабатывает и сохраняет бронирования в базу данных.
"""
self.logger.info("Начало обработки данных о бронированиях для сохранения в базу.")
processed_items = 0
errors = []
# Сохраняем данные бронирования for record in bookings:
room_number = item.get("room_number") try:
check_in = item.get("checkin") # Пример обработки данных бронирования
check_out = item.get("checkout") booking_id = record.get("id")
room_type = item.get("room_type") room_number = record.get("room_number")
check_in = record.get("check_in")
check_out = record.get("check_out")
status = record.get("status")
# Логируем полученные данные # Сохраняем или обновляем запись в базе данных
self.logger.debug(f"Полученные данные для сохранения: room_number={room_number}, check_in={check_in}, check_out={check_out}, room_type={room_type}") reservation, created = await sync_to_async(Reservation.objects.update_or_create)(
external_id=booking_id,
# Проверяем, существует ли запись с таким номером и датой заезда
existing_reservation = await sync_to_async(
Reservation.objects.filter(room_number=room_number, check_in=check_in).first
)()
if existing_reservation:
self.logger.debug(f"Резервация с таким номером и датой заезда уже существует. Обновляем...")
await sync_to_async(Reservation.objects.update_or_create)(
room_number=room_number,
check_in=check_in,
defaults={ defaults={
"hotel": self.pms_config.hotel,
"room_number": room_number,
"check_in": check_in,
"check_out": check_out, "check_out": check_out,
"hotel": hotel, "status": status,
"room_type": room_type,
}, },
) )
self.logger.debug(f"Обновлена существующая резервация.")
else: processed_items += 1
self.logger.debug(f"Резервация не найдена, создаем новую...")
reservation = await sync_to_async(Reservation.objects.create)(
room_number=room_number,
check_in=check_in,
check_out=check_out,
hotel=hotel,
room_type=room_type,
)
self.logger.debug(f"Создана новая резервация. ID: {reservation.reservation_id}")
except Exception as e: except Exception as e:
self.logger.error(f"Ошибка сохранения данных: {e}") self.logger.error(f"Ошибка обработки бронирования {record.get('id')}: {e}")
errors.append(str(e))
self.logger.info(f"Обработано бронирований: {processed_items}, ошибок: {len(errors)}")
return {"processed_items": processed_items, "errors": errors}

65
req1.txt Normal file
View File

@@ -0,0 +1,65 @@
ace_tools==0.0
aiohappyeyeballs==2.4.4
aiohttp==3.11.11
aiosignal==1.3.2
anyio==4.7.0
APScheduler==3.11.0
asgiref==3.8.1
async-timeout==5.0.1
attrs==24.3.0
certifi==2024.12.14
cffi==1.17.1
charset-normalizer==3.4.0
cryptography==44.0.0
defusedxml==0.7.1
Django==5.1.4
django-environ==0.11.2
django-extensions==3.2.3
django-filter==24.3
django-health-check==3.18.3
django-jazzmin==3.0.1
django-jet==1.0.8
et_xmlfile==2.0.0
exceptiongroup==1.2.2
fonttools==4.55.3
fpdf2==2.8.2
frozenlist==1.5.0
geoip2==4.8.1
git-filter-repo==2.47.0
h11==0.14.0
httpcore==1.0.7
httpx==0.28.1
idna==3.10
jsonschema==4.23.0
jsonschema-specifications==2024.10.1
maxminddb==2.6.2
multidict==6.1.0
numpy==2.2.0
openpyxl==3.1.5
pandas==2.2.3
pathspec==0.12.1
pillow==11.0.0
propcache==0.2.1
psycopg==3.2.3
pycparser==2.22
PyMySQL==1.1.1
python-dateutil==2.9.0.post0
python-decouple==3.8
python-dotenv==1.0.1
python-telegram-bot==21.9
pytz==2024.2
PyYAML==6.0.2
referencing==0.35.1
requests==2.32.3
rpds-py==0.22.3
six==1.17.0
sniffio==1.3.1
sqlparse==0.5.3
typing_extensions==4.12.2
tzdata==2024.2
tzlocal==5.2
ua-parser==1.0.0
ua-parser-builtins==0.18.0.post1
urllib3==2.2.3
user-agents==2.2.0
yarl==1.18.3

View File

@@ -0,0 +1,27 @@
import os
import django
import asyncio
from django.core.management.base import BaseCommand
from scheduler.tasks import setup_scheduler
class Command(BaseCommand):
help = "Запуск планировщика задач"
def handle(self, *args, **options):
# Устанавливаем Django окружение
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "touchh.settings")
django.setup()
# Основная асинхронная функция
async def start_scheduler():
scheduler = await setup_scheduler()
self.stdout.write(self.style.SUCCESS("Планировщик задач успешно запущен."))
try:
while True:
await asyncio.sleep(3600) # Бесконечный цикл для поддержания работы
except asyncio.CancelledError:
scheduler.shutdown()
# Запускаем планировщик в асинхронном режиме
asyncio.run(start_scheduler())

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,19 @@
# Generated by Django 5.1.4 on 2024-12-27 05:52
import scheduler.models
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('scheduler', '0013_alter_scheduledtask_function_path'),
]
operations = [
migrations.AlterField(
model_name='scheduledtask',
name='function_path',
field=models.CharField(choices=scheduler.models.get_available_functions, max_length=500, verbose_name='Путь к функции (модуль.функция)'),
),
]

95
scheduler/task_loader.py Normal file
View File

@@ -0,0 +1,95 @@
import os
import inspect
import importlib
import asyncio
import logging
from typing import List, Tuple
from pathspec import PathSpec
from apscheduler.schedulers.asyncio import AsyncIOScheduler
# Настройка логирования
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
def reload_tasks_periodically(scheduler: AsyncIOScheduler):
"""Перезагрузка задач из базы данных каждые 5 минут."""
async def reload():
from scheduler.tasks import load_tasks_to_scheduler
try:
await load_tasks_to_scheduler(scheduler)
logger.info("Задачи успешно перезагружены.")
except Exception as e:
logger.error(f"Ошибка перезагрузки задач: {e}")
scheduler.add_job(lambda: asyncio.run(reload()), "interval", minutes=5)
def load_gitignore_patterns(project_root: str) -> PathSpec:
"""
Загружает паттерны из файла .gitignore.
"""
gitignore_path = os.path.join(project_root, ".gitignore")
try:
if os.path.exists(gitignore_path):
with open(gitignore_path, "r", encoding="utf-8") as f:
patterns = f.readlines()
return PathSpec.from_lines("gitwildmatch", patterns)
except Exception as e:
logger.warning(f"Ошибка загрузки .gitignore: {e}")
return PathSpec.from_lines("gitwildmatch", [])
def get_project_functions() -> List[Tuple[str, str]]:
"""
Сканирует проект и возвращает список всех функций в формате (путь, имя функции),
исключая файлы и папки, указанные в .gitignore.
"""
functions = []
project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Загружаем паттерны из .gitignore
gitignore_spec = load_gitignore_patterns(project_root)
for root, dirs, files in os.walk(project_root):
# Исключаем директории, указанные в .gitignore
dirs[:] = [d for d in dirs if not gitignore_spec.match_file(os.path.relpath(os.path.join(root, d), project_root))]
for file in files:
file_path = os.path.relpath(os.path.join(root, file), project_root)
if (
file.endswith(".py") and
not file.startswith("__") and
not gitignore_spec.match_file(file_path)
):
module_path = os.path.relpath(os.path.join(root, file), project_root)
module_name = module_path.replace(os.sep, ".").replace(".py", "")
try:
spec = importlib.util.find_spec(module_name)
if spec is not None:
module = importlib.import_module(module_name)
for name, func in inspect.getmembers(module, inspect.isfunction):
functions.append((f"{module_name}.{name}", name))
except Exception as e:
logger.error(f"Ошибка при обработке модуля {module_name}: {e}")
return functions
def execute_function(function_path: str):
"""
Выполняет функцию по указанному пути.
"""
try:
module_name, func_name = function_path.rsplit(".", 1)
spec = importlib.util.find_spec(module_name)
if spec is None:
raise ImportError(f"Модуль {module_name} не найден")
module = importlib.import_module(module_name)
if not hasattr(module, func_name):
raise AttributeError(f"Функция {func_name} отсутствует в модуле {module_name}")
func = getattr(module, func_name)
logger.info(f"Выполняется функция: {function_path}")
return func()
except Exception as e:
logger.error(f"Ошибка выполнения функции {function_path}: {e}")
return None

14
scheduler/urls.py Normal file
View File

@@ -0,0 +1,14 @@
from django.urls import path
from django.http import HttpResponse
def placeholder_view(request):
"""
Заглушка для URL-адресов приложения scheduler.
"""
return HttpResponse("Это заглушка для приложения scheduler.")
app_name = "scheduler"
urlpatterns = [
path("", placeholder_view, name="scheduler_placeholder"),
]

View File

@@ -0,0 +1,43 @@
/*global gettext*/
'use strict';
{
window.addEventListener('load', function() {
// Add anchor tag for Show/Hide link
const fieldsets = document.querySelectorAll('fieldset.collapse');
for (const [i, elem] of fieldsets.entries()) {
// Don't hide if fields in this fieldset have errors
if (elem.querySelectorAll('div.errors, ul.errorlist').length === 0) {
elem.classList.add('collapsed');
const h2 = elem.querySelector('h2');
const link = document.createElement('a');
link.id = 'fieldsetcollapser' + i;
link.className = 'collapse-toggle';
link.href = '#';
link.textContent = gettext('Show');
h2.appendChild(document.createTextNode(' ('));
h2.appendChild(link);
h2.appendChild(document.createTextNode(')'));
}
}
// Add toggle to hide/show anchor tag
const toggleFunc = function(ev) {
if (ev.target.matches('.collapse-toggle')) {
ev.preventDefault();
ev.stopPropagation();
const fieldset = ev.target.closest('fieldset');
if (fieldset.classList.contains('collapsed')) {
// Show
ev.target.textContent = gettext('Hide');
fieldset.classList.remove('collapsed');
} else {
// Hide
ev.target.textContent = gettext('Show');
fieldset.classList.add('collapsed');
}
}
};
document.querySelectorAll('fieldset.module').forEach(function(el) {
el.addEventListener('click', toggleFunc);
});
});
}