Compare commits
27 Commits
2c1157b116
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| eb662f7fe6 | |||
| 2ad382f01a | |||
| 67e487c378 | |||
| 0cc3e469cb | |||
| d771df32d5 | |||
| 51569fb5c6 | |||
| 6721ae3e8c | |||
| 24f1a40561 | |||
|
|
d8bb7493e3 | ||
|
|
aca071f450 | ||
|
|
6b98cda299 | ||
| d826232dca | |||
| 05509f79fb | |||
| 157f47d86d | |||
| e5e7a7f054 | |||
| d59ebaf005 | |||
| 31cf30e344 | |||
| 7350989113 | |||
| 5e3ed91b3a | |||
| 56175078d6 | |||
| 0e45074ea5 | |||
| bc865303c5 | |||
| 5c58a46e18 | |||
| 3dd5f2238e | |||
| 61fffd3be4 | |||
| c535a51953 | |||
| 5f434d8248 |
6
.docker/admin/.dockerignore
Normal file
6
.docker/admin/.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
||||
.venv
|
||||
.venv/
|
||||
.log
|
||||
__pycache__
|
||||
.history
|
||||
.vscode
|
||||
30
.docker/admin/Dockerfile
Normal file
30
.docker/admin/Dockerfile
Normal 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
14
.docker/admin/entrypoint.sh
Executable 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 "$@"
|
||||
44
.docker/admin/requirements.txt
Normal file
44
.docker/admin/requirements.txt
Normal 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
|
||||
6
.docker/bot/.dockerignore
Normal file
6
.docker/bot/.dockerignore
Normal file
@@ -0,0 +1,6 @@
|
||||
.venv
|
||||
.venv/
|
||||
.log
|
||||
__pycache__
|
||||
.history
|
||||
.vscode
|
||||
12
.docker/bot/Dockerfile
Normal file
12
.docker/bot/Dockerfile
Normal 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"]
|
||||
44
.docker/bot/requirements.txt
Normal file
44
.docker/bot/requirements.txt
Normal 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
|
||||
14
.docker/scheduler/Dockerfile
Normal file
14
.docker/scheduler/Dockerfile
Normal 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"]
|
||||
6
.docker/scheduler/dockerignore
Normal file
6
.docker/scheduler/dockerignore
Normal file
@@ -0,0 +1,6 @@
|
||||
.venv
|
||||
.venv/
|
||||
.log
|
||||
__pycache__
|
||||
.history
|
||||
.vscode
|
||||
7
.docker/scheduler/entrypoint.sh
Executable file
7
.docker/scheduler/entrypoint.sh
Executable file
@@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Выполняем миграции
|
||||
python manage.py migrate --no-input
|
||||
|
||||
# Запускаем приложение
|
||||
exec "$@"
|
||||
44
.docker/scheduler/requirements.txt
Normal file
44
.docker/scheduler/requirements.txt
Normal 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
|
||||
19
.drone.yml
19
.drone.yml
@@ -12,20 +12,11 @@ steps:
|
||||
- git reset --hard $DRONE_COMMIT
|
||||
|
||||
# Шаг 2: Обновление и запуск с помощью update.sh
|
||||
- name: deploy_app
|
||||
image: docker:24
|
||||
environment:
|
||||
MYSQL_PASSWORD: touchh
|
||||
volumes:
|
||||
- name: docker_sock
|
||||
path: /var/run/docker.sock
|
||||
commands:
|
||||
- apk add --no-cache bash
|
||||
- chmod +x ./bin/update
|
||||
- docker-compose up -d
|
||||
- until docker inspect -f '{{.State.Running}}' src-web-1 | grep true; do echo "Waiting for container to be running..."; sleep 5; done
|
||||
- git branch --set-upstream-to=origin/PMSManager_refactor PMSManager_refactor || true
|
||||
- ./bin/update
|
||||
- name: docker-build
|
||||
image: plugins/docker
|
||||
settings:
|
||||
repo: trevor198507/touchh-py
|
||||
dry_run: true
|
||||
|
||||
# Шаг 3: Миграция базы данных
|
||||
- name: run_migrations
|
||||
|
||||
@@ -151,6 +151,7 @@ class UserActivityLogAdmin(admin.ModelAdmin):
|
||||
get_hotel_name.short_description = "Отель"
|
||||
get_room_number.short_description = "Комната"
|
||||
|
||||
|
||||
# from .views import import_selected_hotels
|
||||
# # Регистрируем admin класс для ImportedHotel
|
||||
# @admin.register(ImportedHotel)
|
||||
@@ -247,4 +248,3 @@ class RoomDiscrepancyAdmin(admin.ModelAdmin):
|
||||
class Meta:
|
||||
model = RoomDiscrepancy
|
||||
fields = ['hotel', 'room_number', 'booking_id','created_at', 'check_in_date_expected','check_in_date_actual','discrepancy_type']
|
||||
|
||||
@@ -1,27 +1,149 @@
|
||||
# import json
|
||||
# from datetime import timedelta
|
||||
# from django.utils import timezone
|
||||
# from django.db.models import Q
|
||||
# from hotels.models import Reservation, Hotel
|
||||
# from .models import UserActivityLog, RoomDiscrepancy
|
||||
# from touchh.utils.log import CustomLogger
|
||||
|
||||
# # Настройка логирования
|
||||
# logger = CustomLogger(__name__).get_logger()
|
||||
|
||||
# class ReservationChecker:
|
||||
# """
|
||||
# Класс для проверки несоответствий между бронированиями и логами заселения.
|
||||
# """
|
||||
|
||||
# def __init__(self):
|
||||
# self.checkin_diff_hours = 3
|
||||
|
||||
# def log_info(self, message):
|
||||
# logger.info(message)
|
||||
|
||||
# def log_warning(self, message):
|
||||
# logger.warning(message)
|
||||
|
||||
# def log_error(self, message):
|
||||
# logger.error(message)
|
||||
|
||||
# def run_check(self):
|
||||
# """Запуск проверки фродовых событий."""
|
||||
# self.log_info("Запуск проверки фродовых данных.")
|
||||
# try:
|
||||
# check_in_diff = timedelta(hours=self.checkin_diff_hours)
|
||||
|
||||
# # Кэшируем отели в словарь для быстрого доступа
|
||||
# hotels_map = {hotel.hotel_id: hotel for hotel in Hotel.objects.all()}
|
||||
|
||||
# # Загружаем бронирования и активности пользователей
|
||||
# user_logs = UserActivityLog.objects.filter(fraud_checked=False)
|
||||
# reservations = Reservation.objects.filter(fraud_checked=False).select_related('hotel')
|
||||
|
||||
# # Преобразуем бронирования в словарь для быстрого поиска
|
||||
# reservations_map = {
|
||||
# (res.hotel.hotel_id, res.room_number): res for res in reservations
|
||||
# }
|
||||
|
||||
# violations = []
|
||||
# missing_reservations = set(reservations) # Сет для поиска пропавших бронирований
|
||||
|
||||
# for user_log in user_logs:
|
||||
# try:
|
||||
# params = json.loads(user_log.url_parameters.replace("'", '"')) if user_log.url_parameters else {}
|
||||
# hotel_id = params.get('utm_content')
|
||||
# room = params.get('utm_term')
|
||||
|
||||
# if not hotel_id or not room:
|
||||
# continue # Пропускаем записи без нужных параметров
|
||||
|
||||
# key = (hotel_id, room)
|
||||
# reserv = reservations_map.get(key)
|
||||
|
||||
# discrepancy_type = None
|
||||
|
||||
# if reserv:
|
||||
# if reserv in missing_reservations:
|
||||
# missing_reservations.remove(reserv)
|
||||
|
||||
# if user_log.date_time < reserv.check_in:
|
||||
# discrepancy_type = 'early'
|
||||
# elif user_log.date_time > reserv.check_in + check_in_diff:
|
||||
# discrepancy_type = 'late'
|
||||
# else:
|
||||
# discrepancy_type = 'no_booking'
|
||||
|
||||
# if discrepancy_type:
|
||||
# violations.append(RoomDiscrepancy(
|
||||
# hotel=hotels_map.get(hotel_id),
|
||||
# room_number=room,
|
||||
# discrepancy_type=discrepancy_type,
|
||||
# booking_id=reserv.reservation_id if reserv else None,
|
||||
# check_in_date_expected=reserv.check_in if reserv else None,
|
||||
# check_in_date_actual=user_log.date_time,
|
||||
# ))
|
||||
|
||||
# user_log.fraud_checked = True # Отмечаем логи как проверенные
|
||||
|
||||
# except json.JSONDecodeError:
|
||||
# self.log_error(f"Ошибка декодирования JSON в URL-параметрах: {user_log.url_parameters}")
|
||||
# except Exception as e:
|
||||
# self.log_error(f"Ошибка при обработке логов: {e}")
|
||||
|
||||
# # Добавляем пропущенные бронирования
|
||||
# for miss_reserv in missing_reservations:
|
||||
# violations.append(RoomDiscrepancy(
|
||||
# hotel=miss_reserv.hotel,
|
||||
# room_number=miss_reserv.room_number,
|
||||
# discrepancy_type='missed',
|
||||
# booking_id=miss_reserv.reservation_id,
|
||||
# check_in_date_expected=miss_reserv.check_in,
|
||||
# ))
|
||||
|
||||
# # Массово сохраняем нарушения
|
||||
# if violations:
|
||||
# RoomDiscrepancy.objects.bulk_create(violations)
|
||||
# self.log_info(f"Записано {len(violations)} новых несоответствий.")
|
||||
|
||||
# # Обновляем флаги fraud_checked
|
||||
# UserActivityLog.objects.filter(id__in=[log.id for log in user_logs]).update(fraud_checked=True)
|
||||
# Reservation.objects.filter(id__in=[res.id for res in reservations]).update(fraud_checked=True)
|
||||
|
||||
# except Exception as e:
|
||||
# self.log_error(f"Ошибка при выполнении проверки: {e}")
|
||||
|
||||
# self.log_info("Проверка завершена.")
|
||||
|
||||
# # Функция для запуска из планировщика
|
||||
# def run_reservation_check():
|
||||
# """Запуск проверки через планировщик."""
|
||||
# logger.info("Планировщик вызывает run_reservation_check.")
|
||||
# try:
|
||||
# checker = ReservationChecker()
|
||||
# checker.run_check()
|
||||
# except Exception as e:
|
||||
# logger.error(f"Ошибка при запуске проверки: {e}")
|
||||
# logger.info("run_reservation_check завершена.")
|
||||
|
||||
|
||||
|
||||
import json
|
||||
from datetime import timedelta
|
||||
from urllib.parse import parse_qs
|
||||
from django.utils import timezone
|
||||
from django.db.models import Q
|
||||
from hotels.models import Reservation, Hotel
|
||||
from .models import UserActivityLog, ViolationLog, RoomDiscrepancy
|
||||
from .models import UserActivityLog, RoomDiscrepancy
|
||||
from touchh.utils.log import CustomLogger
|
||||
|
||||
# Настройка логирования
|
||||
logger = CustomLogger(__name__).get_logger()
|
||||
|
||||
|
||||
class ReservationChecker:
|
||||
"""
|
||||
Класс для проверки несоответствий между бронированиями и логами заселения.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
"""
|
||||
Инициализация времени проверки и списка нарушений.
|
||||
"""
|
||||
self.start_time = timezone.now() - timedelta(days=30)
|
||||
self.end_time = timezone.now()
|
||||
self.violations = []
|
||||
self.checkin_diff_hours = 3
|
||||
self.checkin_diff_hours = 3 # Разрешенное отклонение от времени заселения
|
||||
|
||||
def log_info(self, message):
|
||||
logger.info(message)
|
||||
@@ -32,112 +154,106 @@ class ReservationChecker:
|
||||
def log_error(self, message):
|
||||
logger.error(message)
|
||||
|
||||
def fetch_user_logs(self):
|
||||
try:
|
||||
self.log_info("Начинается извлечение логов активности пользователей.")
|
||||
user_logs = UserActivityLog.objects.filter(created__range=(self.start_time, self.end_time))
|
||||
self.log_info(f"Найдено {user_logs.count()} логов активности для анализа.")
|
||||
return user_logs
|
||||
except Exception as e:
|
||||
self.log_error(f"Ошибка при извлечении логов активности: {e}")
|
||||
return UserActivityLog.objects.none()
|
||||
|
||||
def fetch_reservations(self):
|
||||
try:
|
||||
self.log_info("Начинается извлечение бронирований.")
|
||||
reservations = Reservation.objects.filter(
|
||||
Q(check_in__lte=self.end_time) & Q(check_out__gte=self.start_time)
|
||||
)
|
||||
self.log_info(f"Найдено {reservations.count()} бронирований для анализа.")
|
||||
return reservations
|
||||
except Exception as e:
|
||||
self.log_error(f"Ошибка при извлечении бронирований: {e}")
|
||||
return Reservation.objects.none()
|
||||
|
||||
def find_violations(self):
|
||||
self.log_info("Начинается анализ несоответствий.")
|
||||
user_logs = self.fetch_user_logs()
|
||||
reservations = self.fetch_reservations()
|
||||
|
||||
log_lookup = {}
|
||||
for log in user_logs:
|
||||
params = parse_qs(log.url_parameters or "")
|
||||
hotel_id = params.get("utm_content", [None])[0]
|
||||
room_number = params.get("utm_term", [None])[0]
|
||||
if hotel_id and room_number:
|
||||
key = (hotel_id, room_number)
|
||||
log_lookup.setdefault(key, []).append(log)
|
||||
|
||||
for reservation in reservations:
|
||||
key = (reservation.hotel.hotel_id, reservation.room_number)
|
||||
logs = log_lookup.get(key, [])
|
||||
|
||||
if reservation.status == "заселен" and not logs:
|
||||
self.record_violation(
|
||||
hotel=reservation.hotel,
|
||||
room_number=reservation.room_number,
|
||||
violation_type="no_qr_scan",
|
||||
details=f"Бронирование для номера {reservation.room_number} в отеле '{reservation.hotel.name}' "
|
||||
f"не имеет записи сканирования QR."
|
||||
)
|
||||
|
||||
for log in logs:
|
||||
if log.created < reservation.check_in:
|
||||
self.record_violation(
|
||||
hotel=reservation.hotel,
|
||||
room_number=reservation.room_number,
|
||||
violation_type="early_check_in",
|
||||
details=f"Раннее заселение: сканирование QR {log.created} раньше времени заезда "
|
||||
f"{reservation.check_in} для номера {reservation.room_number}."
|
||||
)
|
||||
|
||||
for (hotel_id, room_number), logs in log_lookup.items():
|
||||
matching_reservations = reservations.filter(
|
||||
hotel__hotel_id=hotel_id,
|
||||
room_number=room_number
|
||||
)
|
||||
if not matching_reservations.exists():
|
||||
for log in logs:
|
||||
self.record_violation(
|
||||
hotel=Hotel.objects.filter(hotel_id=hotel_id).first(),
|
||||
room_number=room_number,
|
||||
violation_type="no_reservation",
|
||||
details=f"Сканирование QR {log.created} для номера {room_number} в отеле с ID '{hotel_id}' "
|
||||
f"не соответствует ни одному бронированию."
|
||||
)
|
||||
|
||||
def record_violation(self, hotel, room_number, violation_type, details):
|
||||
if hotel:
|
||||
self.violations.append(ViolationLog(
|
||||
hotel=hotel,
|
||||
room_number=room_number,
|
||||
violation_type=violation_type,
|
||||
violation_details=details
|
||||
))
|
||||
self.log_warning(f"Зафиксировано нарушение: {details}")
|
||||
|
||||
def save_violations(self):
|
||||
if self.violations:
|
||||
ViolationLog.objects.bulk_create(self.violations)
|
||||
self.log_info(f"Создано {len(self.violations)} записей в ViolationLog.")
|
||||
else:
|
||||
self.log_info("Нарушений не обнаружено.")
|
||||
|
||||
def run_check(self):
|
||||
self.log_info(f"Запуск проверки с {self.start_time} по {self.end_time}.")
|
||||
"""Запуск проверки фродовых событий."""
|
||||
self.log_info("🔍 Запуск проверки фродовых данных.")
|
||||
try:
|
||||
self.find_violations()
|
||||
self.save_violations()
|
||||
check_in_diff = timedelta(hours=self.checkin_diff_hours)
|
||||
|
||||
# Кэшируем отели в словарь для быстрого доступа
|
||||
hotels_map = {hotel.hotel_id: hotel for hotel in Hotel.objects.all()}
|
||||
|
||||
# Загружаем бронирования и активности пользователей
|
||||
user_logs = UserActivityLog.objects.filter(fraud_checked=False)
|
||||
reservations = Reservation.objects.filter(fraud_checked=False).select_related('hotel')
|
||||
|
||||
# Преобразуем бронирования в словарь для быстрого поиска
|
||||
reservations_map = {
|
||||
(res.hotel.hotel_id, res.room_number): res for res in reservations
|
||||
}
|
||||
|
||||
violations = []
|
||||
checked_reservations = set() # Сет для бронирований, которые были проверены
|
||||
|
||||
self.log_info(f"✅ Загружено {len(user_logs)} логов активности и {len(reservations)} бронирований.")
|
||||
|
||||
for user_log in user_logs:
|
||||
try:
|
||||
params = json.loads(user_log.url_parameters.replace("'", '"')) if user_log.url_parameters else {}
|
||||
hotel_id = params.get('utm_content')
|
||||
room = params.get('utm_term')
|
||||
|
||||
if not hotel_id or not room:
|
||||
self.log_warning(f"🚫 Пропущен лог без hotel_id или room_number: {user_log.url_parameters}")
|
||||
continue # Пропускаем записи без нужных параметров
|
||||
|
||||
key = (hotel_id, room)
|
||||
reserv = reservations_map.get(key)
|
||||
|
||||
discrepancy_type = "match" # По умолчанию считаем, что всё соответствует
|
||||
|
||||
if reserv:
|
||||
checked_reservations.add(reserv)
|
||||
|
||||
if user_log.date_time < reserv.check_in:
|
||||
discrepancy_type = 'early'
|
||||
self.log_warning(f"⚠️ Обнаружено раннее заселение: {user_log.date_time} < {reserv.check_in}")
|
||||
elif user_log.date_time > reserv.check_in + check_in_diff:
|
||||
discrepancy_type = 'late'
|
||||
self.log_warning(f"⚠️ Обнаружено позднее заселение: {user_log.date_time} > {reserv.check_in + check_in_diff}")
|
||||
else:
|
||||
discrepancy_type = 'no_booking'
|
||||
self.log_warning(f"🚨 Заселение без бронирования: {user_log.date_time} (Отель {hotel_id}, Комната {room})")
|
||||
|
||||
violations.append(RoomDiscrepancy(
|
||||
hotel=hotels_map.get(hotel_id),
|
||||
room_number=room,
|
||||
discrepancy_type=discrepancy_type,
|
||||
booking_id=reserv.reservation_id if reserv else None,
|
||||
check_in_date_expected=reserv.check_in if reserv else None,
|
||||
check_in_date_actual=user_log.date_time,
|
||||
))
|
||||
|
||||
user_log.fraud_checked = True # Отмечаем логи как проверенные
|
||||
|
||||
except json.JSONDecodeError:
|
||||
self.log_error(f"❌ Ошибка декодирования JSON в URL-параметрах: {user_log.url_parameters}")
|
||||
except Exception as e:
|
||||
self.log_error(f"❌ Ошибка при обработке логов: {e}")
|
||||
|
||||
# Добавляем пропущенные бронирования (неявки)
|
||||
for reserv in reservations:
|
||||
if reserv not in checked_reservations:
|
||||
violations.append(RoomDiscrepancy(
|
||||
hotel=reserv.hotel,
|
||||
room_number=reserv.room_number,
|
||||
discrepancy_type='missed',
|
||||
booking_id=reserv.reservation_id,
|
||||
check_in_date_expected=reserv.check_in,
|
||||
))
|
||||
self.log_warning(f"⚠️ Обнаружена неявка (missed) | Отель: {reserv.hotel.hotel_id}, Номер: {reserv.room_number}, Ожидаемая дата заезда: {reserv.check_in}")
|
||||
|
||||
# Массово сохраняем все записи, включая корректные совпадения
|
||||
if violations:
|
||||
RoomDiscrepancy.objects.bulk_create(violations)
|
||||
self.log_info(f"✅ Записано {len(violations)} новых записей в RoomDiscrepancy.")
|
||||
|
||||
# Обновляем флаги fraud_checked
|
||||
UserActivityLog.objects.filter(id__in=[log.id for log in user_logs]).update(fraud_checked=True)
|
||||
Reservation.objects.filter(id__in=[res.id for res in reservations]).update(fraud_checked=True)
|
||||
|
||||
except Exception as e:
|
||||
self.log_error(f"Ошибка при выполнении проверки: {e}")
|
||||
self.log_info("Проверка завершена.")
|
||||
self.log_error(f"❌ Ошибка при выполнении проверки: {e}")
|
||||
|
||||
self.log_info("✅ Проверка фродовых данных завершена.")
|
||||
|
||||
# Функция для запуска из планировщика
|
||||
def run_reservation_check():
|
||||
logger.info("Планировщик вызывает run_reservation_check.")
|
||||
"""Запуск проверки через планировщик."""
|
||||
logger.info("📅 Планировщик вызывает run_reservation_check.")
|
||||
try:
|
||||
checker = ReservationChecker()
|
||||
checker.run_check()
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при запуске проверки: {e}")
|
||||
logger.info("run_reservation_check завершена.")
|
||||
logger.error(f"❌ Ошибка при запуске проверки: {e}")
|
||||
logger.info("✅ run_reservation_check завершена.")
|
||||
|
||||
@@ -314,7 +314,7 @@ def scheduled_sync():
|
||||
except Exception as e:
|
||||
logger.error(f"Error syncing connection {db_settings}: {e}")
|
||||
|
||||
with ThreadPoolExecutor(max_workers=5) as executor:
|
||||
with ThreadPoolExecutor(max_workers=10) as executor:
|
||||
futures = [executor.submit(sync_task, db_settings) for db_settings in active_db_settings]
|
||||
for future in futures:
|
||||
try:
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.1.4 on 2025-02-01 09:37
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('antifroud', '0005_roomdiscrepancy_fraud_checked_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='roomdiscrepancy',
|
||||
name='fraud_checked',
|
||||
field=models.BooleanField(db_index=True, default=False, verbose_name='Проверено на несоответствия'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='useractivitylog',
|
||||
name='fraud_checked',
|
||||
field=models.BooleanField(db_index=True, default=False, verbose_name='Проверено на несоответствия'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,17 @@
|
||||
# Generated by Django 5.1.4 on 2025-02-01 09:48
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('antifroud', '0006_alter_roomdiscrepancy_fraud_checked_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='roomdiscrepancy',
|
||||
name='fraud_checked',
|
||||
),
|
||||
]
|
||||
@@ -31,7 +31,7 @@ class UserActivityLog(models.Model):
|
||||
honeypot = models.BooleanField(verbose_name="Метка honeypot", blank=True, null=True)
|
||||
reply = models.BooleanField(verbose_name="Ответ пользователя", blank=True, null=True)
|
||||
page_url = models.URLField(blank=True, null=True, verbose_name="URL страницы")
|
||||
fraud_checked = models.BooleanField(default=False, verbose_name="Проверено на несоответствия")
|
||||
fraud_checked = models.BooleanField(default=False, verbose_name="Проверено на несоответствия", db_index=True)
|
||||
|
||||
@property
|
||||
def formatted_timestamp(self):
|
||||
@@ -79,10 +79,10 @@ class UserActivityLog(models.Model):
|
||||
except AddressNotFoundError:
|
||||
return "IP-адрес не найден в базе"
|
||||
except FileNotFoundError:
|
||||
logger.error(f"Файл базы данных GeoIP не найден по пути: {db_path}")
|
||||
# logger.error(f"Файл базы данных GeoIP не найден по пути: {db_path}")
|
||||
return "Файл базы данных GeoIP не найден"
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при определении местоположения: {e}")
|
||||
# logger.error(f"Ошибка при определении местоположения: {e}")
|
||||
return "Местоположение недоступно"
|
||||
class ExternalDBSettings(models.Model):
|
||||
name = models.CharField(max_length=255, unique=True, help_text="Имя подключения для идентификации.")
|
||||
@@ -117,7 +117,6 @@ class RoomDiscrepancy(models.Model):
|
||||
verbose_name="Тип несоответствия"
|
||||
)
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания")
|
||||
fraud_checked = models.BooleanField(default=False, verbose_name="Проверено на несоответствия")
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.hotel.name} - Room {self.room_number}: {self.discrepancy_type}"
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
|
||||
from decouple import config
|
||||
from django.conf import settings
|
||||
from django.apps import apps
|
||||
|
||||
def load_database_settings(databases):
|
||||
@@ -8,6 +10,18 @@ def load_database_settings(databases):
|
||||
"""
|
||||
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:
|
||||
local_db_settings = LocalDatabase.objects.filter(is_active=True)
|
||||
for db in local_db_settings:
|
||||
|
||||
File diff suppressed because one or more lines are too long
9
app_settings/signals.py
Normal file
9
app_settings/signals.py
Normal 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
11
app_settings/urls.py
Normal 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
13373
bnovo_page_1.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -150,7 +150,7 @@ async def check_pms(update, context):
|
||||
# Проверка наличия fetch_data и вызов плагина
|
||||
if hasattr(pms_manager.plugin, 'fetch_data') and callable(pms_manager.plugin.fetch_data):
|
||||
report = await pms_manager.plugin.fetch_data()
|
||||
logger.debug(f"Отчет типа: {type(report)}")
|
||||
logger.debug(f"Отчет типа: {type(report)}: {report}")
|
||||
else:
|
||||
logger.error("Плагин не поддерживает fetch_data.")
|
||||
await query.edit_message_text("Подходящий способ интеграции с PMS не найден.")
|
||||
|
||||
73
docker-compose.yml
Normal file
73
docker-compose.yml
Normal 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:
|
||||
@@ -85,14 +85,14 @@ class HotelAdmin(admin.ModelAdmin):
|
||||
class UserHotelAdmin(admin.ModelAdmin):
|
||||
list_display = ('user', 'hotel')
|
||||
search_fields = ('user__username', 'hotel__name')
|
||||
# list_filter = ('hotel',)
|
||||
# ordering = ('-hotel',)
|
||||
list_filter = ('hotel',)
|
||||
ordering = ('-hotel',)
|
||||
|
||||
@admin.register(Reservation)
|
||||
class ReservationAdmin(admin.ModelAdmin):
|
||||
list_display = ('reservation_id', 'hotel', 'room_number', 'room_type', 'check_in', 'check_out', 'status', 'price', 'discount')
|
||||
search_fields = ('hotel', 'room_number', 'room_type', 'check_in', 'check_out', 'status', 'price', 'discount')
|
||||
list_filter = ('hotel', 'room_number', 'room_type', 'check_in', 'check_out', 'status', 'price', 'discount')
|
||||
list_display = ('reservation_id', 'hotel', 'room_number', 'room_type', 'check_in', 'check_out', 'status', 'fraud_checked')
|
||||
search_fields = ('hotel', 'room_number', 'room_type', 'check_in', 'check_out', 'status')
|
||||
list_filter = ('hotel', 'room_number', 'room_type', 'check_in', 'check_out', 'status')
|
||||
ordering = ('-check_in',)
|
||||
|
||||
@admin.register(Room)
|
||||
|
||||
18
hotels/migrations/0004_reservation_fraud_checked.py
Normal file
18
hotels/migrations/0004_reservation_fraud_checked.py
Normal file
@@ -0,0 +1,18 @@
|
||||
# Generated by Django 5.1.4 on 2025-02-01 09:48
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('hotels', '0003_rename_external_id_hotel_external_id_pms_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='reservation',
|
||||
name='fraud_checked',
|
||||
field=models.BooleanField(db_index=True, default=False, verbose_name='Проверено на несоответствия'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 5.1.4 on 2025-02-02 00:58
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('hotels', '0004_reservation_fraud_checked'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='reservation',
|
||||
name='check_in',
|
||||
field=models.DateTimeField(blank=True, null=True, verbose_name='Дата заезда'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='reservation',
|
||||
name='check_out',
|
||||
field=models.DateTimeField(blank=True, null=True, verbose_name='Дата выезда'),
|
||||
),
|
||||
]
|
||||
@@ -111,11 +111,12 @@ class Reservation(models.Model):
|
||||
reservation_id = models.BigIntegerField(unique=True, verbose_name="ID бронирования")
|
||||
room_number = models.CharField(max_length=255, null=True, blank=True, verbose_name="Номер комнаты")
|
||||
room_type = models.CharField(max_length=255, verbose_name="Тип комнаты")
|
||||
check_in = models.DateTimeField(verbose_name="Дата заезда")
|
||||
check_out = models.DateTimeField(verbose_name="Дата выезда")
|
||||
check_in = models.DateTimeField(verbose_name="Дата заезда", null=True, blank=True)
|
||||
check_out = models.DateTimeField(verbose_name="Дата выезда", null=True, blank=True)
|
||||
status = models.CharField(max_length=50, verbose_name="Статус")
|
||||
price = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name="Цена")
|
||||
discount = models.DecimalField(max_digits=10, decimal_places=2, null=True, blank=True, verbose_name="Скидка")
|
||||
fraud_checked = models.BooleanField(default=False, verbose_name="Проверено на несоответствия", db_index=True)
|
||||
|
||||
def clean(self):
|
||||
if self.check_out and self.check_in and self.check_out <= self.check_in:
|
||||
|
||||
@@ -18,7 +18,6 @@ class PluginLoader:
|
||||
print("Загрузка плагинов:")
|
||||
for file in os.listdir(PluginLoader.PLUGIN_PATH):
|
||||
if file.endswith("_pms.py") and not file.startswith("__"):
|
||||
# print(f" Plugin {file}")
|
||||
module_name = f"pms_integration.plugins.{file[:-3]}"
|
||||
try:
|
||||
module = importlib.import_module(module_name)
|
||||
@@ -26,8 +25,8 @@ class PluginLoader:
|
||||
cls = getattr(module, attr)
|
||||
if isinstance(cls, type) and issubclass(cls, BasePMSPlugin) and cls is not BasePMSPlugin:
|
||||
plugin_name = file[:-7] # Убираем `_pms` из имени файла
|
||||
# print(f" Загружен плагин {plugin_name}: {cls.__name__}")
|
||||
plugins[plugin_name] = cls
|
||||
print(f" Загружен плагин {plugin_name}: {cls.__name__}")
|
||||
except Exception as e:
|
||||
print(f" Ошибка загрузки плагина {module_name}: {e}")
|
||||
return plugins
|
||||
@@ -40,6 +39,7 @@ class PMSIntegrationManager:
|
||||
"""
|
||||
self.hotel = hotel
|
||||
self.plugin = None
|
||||
self.plugins = PluginLoader.load_plugins()
|
||||
|
||||
def load_hotel(self):
|
||||
"""
|
||||
@@ -52,6 +52,16 @@ class PMSIntegrationManager:
|
||||
"""
|
||||
Загружает плагин, соответствующий 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() # Приводим название плагина к нижнему регистру
|
||||
|
||||
# Формируем имя модуля и класса плагина
|
||||
@@ -72,6 +82,7 @@ class PMSIntegrationManager:
|
||||
|
||||
except ImportError as e:
|
||||
raise ValueError(f"Ошибка загрузки плагина для PMS {pms_name}: {e}")
|
||||
>>>>>>> PMSManager_refactor
|
||||
def fetch_data(self):
|
||||
"""
|
||||
Получает данные из PMS с использованием загруженного плагина.
|
||||
|
||||
@@ -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 json
|
||||
from datetime import datetime, timedelta
|
||||
@@ -5,14 +415,11 @@ 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, # Установите уровень логов для всех обработчиков
|
||||
level=logging.WARNING,
|
||||
format='%(asctime)s [%(levelname)s] %(message)s',
|
||||
handlers=[
|
||||
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.addHandler(console_handler)
|
||||
logger.setLevel(logging.WARNING) # Основной уровень логов
|
||||
logger.setLevel(logging.INFO)
|
||||
|
||||
|
||||
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.pms_config = hotel.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",
|
||||
@@ -67,28 +454,25 @@ class BnovoPMSPlugin(BasePMSPlugin):
|
||||
}
|
||||
|
||||
async def _get_stored_token(self):
|
||||
"""Получение токена из конфигурации PMS отеля."""
|
||||
"""
|
||||
Получает токен из конфигурации PMS отеля.
|
||||
"""
|
||||
try:
|
||||
logger.debug(f"Попытка получения токена для отеля {self.hotel.id}.")
|
||||
token = self.pms_config.token
|
||||
logger.debug(f"Токен из базы данных: {token}")
|
||||
if not token:
|
||||
logger.info("Токен отсутствует в конфигурации.")
|
||||
else:
|
||||
logger.debug(f"Токен найден: {token}")
|
||||
return token
|
||||
except Exception as e:
|
||||
logger.warning(f"Ошибка при получении токена для отеля {self.hotel.id}: {e}")
|
||||
logger.error(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):
|
||||
"""Создает заголовки авторизации."""
|
||||
"""
|
||||
Создает заголовки авторизации для запросов к API.
|
||||
"""
|
||||
logger.debug("Создание заголовков авторизации.")
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
@@ -97,81 +481,54 @@ class BnovoPMSPlugin(BasePMSPlugin):
|
||||
if self.token:
|
||||
headers["Cookie"] = f"SID={self.token}"
|
||||
logger.debug(f"Добавлен токен в заголовки: {self.token}")
|
||||
else:
|
||||
logger.warning("Токен отсутствует, запрос может быть неавторизованным.")
|
||||
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()
|
||||
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}")
|
||||
|
||||
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")
|
||||
|
||||
async def _save_token_to_db(self, sid):
|
||||
"""
|
||||
Сохраняет токен (SID) в конфигурации PMS отеля.
|
||||
"""
|
||||
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
|
||||
logger.debug(f"Сохранение токена для отеля {self.hotel.id}: {sid}")
|
||||
self.pms_config.token = sid
|
||||
await sync_to_async(self.pms_config.save)()
|
||||
logger.info(f"Токен {sid} успешно сохранен в базу данных.")
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка при получении данных аккаунта: {e}")
|
||||
logger.error(f"Ошибка сохранения токена для отеля {self.hotel.id}: {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_session(self):
|
||||
"""Получение токена (SID) через авторизацию."""
|
||||
url = f"{self.api_url}/"
|
||||
payload = {"username": self.username, "password": self.password}
|
||||
headers = {"Content-Type": "application/json"}
|
||||
await self._save_token_to_db(self.token)
|
||||
|
||||
<<<<<<< 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):
|
||||
"""Получение данных о бронированиях с помощью эндпоинта /dashboard."""
|
||||
logger.info("Начало процесса получения данных о бронированиях.")
|
||||
@@ -183,10 +540,19 @@ class BnovoPMSPlugin(BasePMSPlugin):
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка получения данных аккаунта: {e}")
|
||||
raise
|
||||
>>>>>>> PMSManager_refactor
|
||||
|
||||
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_from = (now - timedelta(days=7)).strftime("%d.%m.%Y") # Получаем данные за последнюю неделю
|
||||
create_to = now.strftime("%d.%m.%Y")
|
||||
|
||||
params = {
|
||||
@@ -197,30 +563,25 @@ class BnovoPMSPlugin(BasePMSPlugin):
|
||||
"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:
|
||||
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. Перенаправление.")
|
||||
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.")
|
||||
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("Ошибка запроса к /dashboard")
|
||||
raise ValueError(f"Ошибка при получении данных: {response.text}")
|
||||
|
||||
data = response.json()
|
||||
logger.debug(f"Полученный ответ API: {json.dumps(data, indent=2, ensure_ascii=False)}")
|
||||
bookings = data.get("bookings", [])
|
||||
rooms = data.get("rooms", [])
|
||||
|
||||
@@ -228,54 +589,88 @@ class BnovoPMSPlugin(BasePMSPlugin):
|
||||
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 Exception as e:
|
||||
logger.error(f"Ошибка при обработке данных: {e}")
|
||||
raise
|
||||
|
||||
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
|
||||
async def _process_and_save_bookings(self, bookings):
|
||||
"""
|
||||
Обрабатывает и сохраняет бронирования в базу.
|
||||
"""
|
||||
logger.info("Начало обработки данных о бронированиях для сохранения в базу.")
|
||||
processed_items = 0
|
||||
errors = []
|
||||
|
||||
# Сопоставляем бронирования с существующими записями
|
||||
for booking in all_bookings:
|
||||
for record in bookings:
|
||||
try:
|
||||
booking_id = booking.get("id")
|
||||
hotel_id = booking.get("hotel_id")
|
||||
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 hotel_id != str(self.hotel.external_id_pms):
|
||||
logger.debug(f"Бронирование {booking_id} не относится к отелю {self.hotel.external_id_pms}. Пропуск.")
|
||||
# Проверка обязательных данных
|
||||
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, # Объект модели 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") # Тип комнаты
|
||||
}
|
||||
defaults={
|
||||
"hotel": self.hotel,
|
||||
"status": status,
|
||||
"room_number": room_number,
|
||||
"check_in": arrival,
|
||||
"check_out": departure,
|
||||
},
|
||||
)
|
||||
|
||||
if created:
|
||||
logger.info(f"Создана новая запись бронирования: {reservation}")
|
||||
print(reservation)
|
||||
else:
|
||||
logger.info(f"Обновлено существующее бронирование: {reservation}")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Ошибка обработки бронирования {booking.get('id')}: {e}")
|
||||
processed_items += 1
|
||||
|
||||
logger.info(f"Все бронирования получены и обработаны. Итоговое количество: {len(all_bookings)}")
|
||||
return all_bookings
|
||||
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_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
|
||||
|
||||
@@ -1,4 +1,144 @@
|
||||
# # ecvi_pms.py
|
||||
# import logging
|
||||
# import requests
|
||||
# import json
|
||||
# import os
|
||||
# from datetime import datetime, timedelta
|
||||
# from asgiref.sync import sync_to_async
|
||||
# from hotels.models import Hotel, Reservation
|
||||
# from .base_plugin import BasePMSPlugin
|
||||
|
||||
# class EcviPMSPlugin(BasePMSPlugin):
|
||||
# """
|
||||
# Плагин для интеграции с PMS Ecvi.
|
||||
# """
|
||||
|
||||
# def __init__(self, hotel):
|
||||
# super().__init__(hotel.pms)
|
||||
# self.hotel = hotel
|
||||
|
||||
# if not self.hotel.pms:
|
||||
# raise ValueError(f"Отель {self.hotel.name} не имеет связанной PMS конфигурации.")
|
||||
|
||||
# self.api_url = self.hotel.pms.url.rstrip("/")
|
||||
# self.token = self.hotel.pms.token
|
||||
# self.username = self.hotel.pms.username
|
||||
# self.password = self.hotel.pms.password
|
||||
|
||||
# self.logger = logging.getLogger(self.__class__.__name__)
|
||||
# handler_console = logging.StreamHandler()
|
||||
# handler_file = logging.FileHandler('var/log/ecvi_pms_plugin.log')
|
||||
# formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
|
||||
# handler_console.setFormatter(formatter)
|
||||
# handler_file.setFormatter(formatter)
|
||||
# self.logger.addHandler(handler_console)
|
||||
# self.logger.addHandler(handler_file)
|
||||
# self.logger.setLevel(logging.WARNING)
|
||||
|
||||
# def get_default_parser_settings(self):
|
||||
# return {
|
||||
# "field_mapping": {
|
||||
# "check_in": "checkin",
|
||||
# "check_out": "checkout",
|
||||
# "room_number": "room_name",
|
||||
# "room_type_name": "room_type",
|
||||
# "status": "occupancy",
|
||||
# },
|
||||
# "date_format": "%Y-%m-%d %H:%M:%S"
|
||||
# }
|
||||
|
||||
# async def _fetch_data(self):
|
||||
# headers = {"Content-Type": "application/json"}
|
||||
# data = {"token": self.token}
|
||||
|
||||
# try:
|
||||
# response = await sync_to_async(requests.post)(
|
||||
# self.api_url, headers=headers, json=data, auth=(self.username, self.password)
|
||||
# )
|
||||
# response.raise_for_status()
|
||||
# response_data = response.json()
|
||||
# self.logger.debug(f"Полученные данные с API: {response_data}")
|
||||
|
||||
# # Группировка данных по номеру комнаты
|
||||
# structured_data = {}
|
||||
# for item in response_data:
|
||||
# room_number = item.get("room_name", "unknown")
|
||||
# if room_number not in structured_data:
|
||||
# structured_data[room_number] = []
|
||||
# structured_data[room_number].append(item)
|
||||
|
||||
# # Сохранение данных во временный JSON-файл
|
||||
# temp_dir = os.path.join("temp", "ecvi")
|
||||
# os.makedirs(temp_dir, exist_ok=True)
|
||||
# temp_file = os.path.join(temp_dir, f"ecvi_data_{datetime.now().strftime('%Y%m%d%H%M%S')}.json")
|
||||
# with open(temp_file, 'w') as file:
|
||||
# json.dump(structured_data, file, indent=4, ensure_ascii=False)
|
||||
|
||||
# return await self._process_data(response_data)
|
||||
# except requests.exceptions.RequestException as e:
|
||||
# self.logger.error(f"Ошибка API: {e}")
|
||||
# return {
|
||||
# "processed_intervals": 0,
|
||||
# "processed_items": 0,
|
||||
# "errors": [str(e)]
|
||||
# }
|
||||
|
||||
# async def _process_data(self, data):
|
||||
# processed_items = 0
|
||||
# errors = []
|
||||
# date_formats = ["%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"]
|
||||
|
||||
# for item in data:
|
||||
# try:
|
||||
# checkin = item['checkin']
|
||||
# checkout = item['checkout']
|
||||
# if checkin in [None, "0000-00-00 00:00:00"] or checkout in [None, "0000-00-00 00:00:00"]:
|
||||
# continue
|
||||
# checkin = self._parse_date(checkin, date_formats)
|
||||
# checkout = self._parse_date(checkout, date_formats)
|
||||
|
||||
# reservation, created = await sync_to_async(Reservation.objects.update_or_create)(
|
||||
# reservation_id=item['task_id'],
|
||||
# defaults={
|
||||
# 'room_number': item['room_name'],
|
||||
# 'room_type': item['room_type'],
|
||||
# 'check_in': checkin,
|
||||
# 'check_out': checkout,
|
||||
# 'status': item['occupancy'],
|
||||
# 'hotel': self.hotel,
|
||||
# }
|
||||
# )
|
||||
# processed_items += 1
|
||||
# except Exception as e:
|
||||
# self.logger.error(f"Ошибка обработки записи: {e}")
|
||||
# errors.append(str(e))
|
||||
|
||||
# return {
|
||||
# "processed_intervals": 1,
|
||||
# "processed_items": processed_items,
|
||||
# "errors": errors
|
||||
# }
|
||||
|
||||
# @staticmethod
|
||||
# def _parse_date(date_str, formats):
|
||||
# for fmt in formats:
|
||||
# try:
|
||||
# return datetime.strptime(date_str, fmt)
|
||||
# except ValueError:
|
||||
# continue
|
||||
# raise ValueError(f"Дата '{date_str}' не соответствует ожидаемым форматам: {formats}")
|
||||
|
||||
# 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.debug(f"Плагин {self.__class__.__name__} прошел валидацию.")
|
||||
# return True
|
||||
|
||||
import logging
|
||||
import os
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
import requests
|
||||
from asgiref.sync import sync_to_async
|
||||
@@ -11,20 +151,17 @@ class EcviPMSPlugin(BasePMSPlugin):
|
||||
"""
|
||||
|
||||
def __init__(self, hotel):
|
||||
super().__init__(hotel.pms) # Передаем PMS-конфигурацию в базовый класс
|
||||
self.hotel = hotel # Сохраняем объект отеля
|
||||
super().__init__(hotel.pms)
|
||||
self.hotel = hotel
|
||||
|
||||
# Проверка PMS-конфигурации
|
||||
if not self.hotel.pms:
|
||||
raise ValueError(f"Отель {self.hotel.name} не имеет связанной PMS конфигурации.")
|
||||
|
||||
# Инициализация параметров API
|
||||
self.api_url = self.hotel.pms.url
|
||||
self.token = self.hotel.pms.token
|
||||
self.username = self.hotel.pms.username
|
||||
self.password = self.hotel.pms.password
|
||||
|
||||
# Настройка логгера
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
handler_console = logging.StreamHandler()
|
||||
handler_file = logging.FileHandler('var/log/ecvi_pms_plugin.log')
|
||||
@@ -35,6 +172,10 @@ class EcviPMSPlugin(BasePMSPlugin):
|
||||
self.logger.addHandler(handler_file)
|
||||
self.logger.setLevel(logging.WARNING)
|
||||
|
||||
# Директория для сохранения JSON-файлов
|
||||
self.data_dir = "var/data/ecvi"
|
||||
os.makedirs(self.data_dir, exist_ok=True)
|
||||
|
||||
def get_default_parser_settings(self):
|
||||
"""
|
||||
Возвращает настройки парсера по умолчанию.
|
||||
@@ -47,7 +188,7 @@ class EcviPMSPlugin(BasePMSPlugin):
|
||||
"room_type_name": "room_type",
|
||||
"status": "occupancy",
|
||||
},
|
||||
"date_format": "%Y-%m-%d %H:%M:%S" # Формат изменен на соответствующий данным
|
||||
"date_format": "%Y-%m-%d %H:%M:%S"
|
||||
}
|
||||
|
||||
async def _fetch_data(self):
|
||||
@@ -58,14 +199,23 @@ class EcviPMSPlugin(BasePMSPlugin):
|
||||
data = {"token": self.token}
|
||||
|
||||
try:
|
||||
# Запрос данных из PMS API
|
||||
response = await sync_to_async(requests.post)(
|
||||
self.api_url, headers=headers, json=data, auth=(self.username, self.password)
|
||||
)
|
||||
response.raise_for_status()
|
||||
response_data = response.json()
|
||||
self.logger.debug(f"Полученные данные с API: {response_data}")
|
||||
|
||||
# Сохраняем весь ответ API в файл для анализа
|
||||
file_name = f"ecvi_data_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
||||
file_path = os.path.join(self.data_dir, file_name)
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
json.dump(response_data, f, ensure_ascii=False, indent=4)
|
||||
|
||||
self.logger.info(f"API-ответ сохранен в файл: {file_path}")
|
||||
|
||||
return await self._process_data(response_data)
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
self.logger.error(f"Ошибка API: {e}")
|
||||
return {
|
||||
@@ -80,22 +230,62 @@ class EcviPMSPlugin(BasePMSPlugin):
|
||||
"""
|
||||
processed_items = 0
|
||||
errors = []
|
||||
date_formats = ["%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"]
|
||||
unix_epoch = datetime(1970, 1, 1, 0, 0, 0)
|
||||
|
||||
date_formats = ["%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"] # Поддержка нескольких форматов даты
|
||||
|
||||
valid_reservations = []
|
||||
print(data)
|
||||
for item in data:
|
||||
try:
|
||||
# Парсинг даты с поддержкой нескольких форматов
|
||||
checkin = self._parse_date(item['checkin'], date_formats)
|
||||
checkout = self._parse_date(item['checkout'], date_formats)
|
||||
checkin = item.get('checkin')
|
||||
checkout = item.get('checkout')
|
||||
|
||||
# Фильтруем записи с некорректными датами
|
||||
if checkin in [None, "0000-00-00 00:00:00", "1970-01-01 00:00:00", 0] or \
|
||||
checkout in [None, "0000-00-00 00:00:00", "1970-01-01 00:00:00", 0]:
|
||||
self.logger.warning(f"Игнорируется запись с некорректной датой: {item}")
|
||||
continue
|
||||
|
||||
checkin = self._parse_date(checkin, date_formats)
|
||||
checkout = self._parse_date(checkout, date_formats)
|
||||
|
||||
# Проверяем на Unix epoch
|
||||
if checkin == unix_epoch or checkout == unix_epoch:
|
||||
self.logger.warning(f"Игнорируется запись с Unix epoch датой: {item}")
|
||||
continue
|
||||
|
||||
# Проверяем timestamp
|
||||
if checkin.timestamp() == 0 or checkout.timestamp() == 0:
|
||||
self.logger.warning(f"Игнорируется запись с timestamp = 0: {item}")
|
||||
continue
|
||||
|
||||
valid_reservations.append(item)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка обработки записи: {e}")
|
||||
errors.append(str(e))
|
||||
|
||||
# Логируем количество отфильтрованных записей
|
||||
self.logger.info(f"Обработано бронирований: {len(valid_reservations)}")
|
||||
|
||||
# Сохранение валидных бронирований в JSON для проверки
|
||||
valid_file_name = f"valid_reservations_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json"
|
||||
valid_file_path = os.path.join(self.data_dir, valid_file_name)
|
||||
with open(valid_file_path, "w", encoding="utf-8") as f:
|
||||
json.dump(valid_reservations, f, ensure_ascii=False, indent=4)
|
||||
|
||||
self.logger.info(f"Валидные бронирования сохранены в файл: {valid_file_path}")
|
||||
|
||||
# Сохранение данных в БД
|
||||
for item in valid_reservations:
|
||||
try:
|
||||
reservation, created = await sync_to_async(Reservation.objects.update_or_create)(
|
||||
reservation_id=item['task_id'],
|
||||
defaults={
|
||||
'room_number': item['room_name'],
|
||||
'room_type': item['room_type'],
|
||||
'check_in': checkin,
|
||||
'check_out': checkout,
|
||||
'check_in': self._parse_date(item['checkin'], date_formats),
|
||||
'check_out': self._parse_date(item['checkout'], date_formats),
|
||||
'status': item['occupancy'],
|
||||
'hotel': self.hotel,
|
||||
}
|
||||
@@ -109,7 +299,7 @@ class EcviPMSPlugin(BasePMSPlugin):
|
||||
processed_items += 1
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка обработки записи: {e}")
|
||||
self.logger.error(f"Ошибка сохранения бронирования: {e}")
|
||||
errors.append(str(e))
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,313 +1,111 @@
|
||||
import logging
|
||||
import requests
|
||||
import hashlib
|
||||
import json
|
||||
from .base_plugin import BasePMSPlugin
|
||||
import os
|
||||
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
|
||||
from .base_plugin import BasePMSPlugin
|
||||
|
||||
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")
|
||||
class RealtyCalendarPMSPlugin(BasePMSPlugin):
|
||||
def __init__(self, hotel):
|
||||
super().__init__(hotel.pms)
|
||||
self.hotel = hotel
|
||||
|
||||
if not self.hotel.pms:
|
||||
raise ValueError(f"Отель {self.hotel.name} не имеет связанной PMS конфигурации.")
|
||||
|
||||
self.api_url = self.hotel.pms.url.rstrip("/")
|
||||
self.public_key = self.hotel.pms.public_key
|
||||
self.private_key = self.hotel.pms.private_key
|
||||
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
handler_console = logging.StreamHandler()
|
||||
handler_file = logging.FileHandler('var/log/realtycalendar_pms_plugin.log')
|
||||
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
|
||||
handler_console.setFormatter(formatter)
|
||||
handler_file.setFormatter(formatter)
|
||||
self.logger.addHandler(handler_console)
|
||||
self.logger.addHandler(handler_file)
|
||||
self.logger.setLevel(logging.WARNING)
|
||||
|
||||
def get_default_parser_settings(self):
|
||||
"""
|
||||
Возвращает настройки по умолчанию для обработки данных.
|
||||
"""
|
||||
return {
|
||||
"date_format": "%Y-%m-%dT%H:%M:%S",
|
||||
"timezone": "UTC"
|
||||
"field_mapping": {
|
||||
"check_in": "begin_date",
|
||||
"check_out": "end_date",
|
||||
"room_number": "apartment_id",
|
||||
"room_type_name": "notes",
|
||||
"status": "status",
|
||||
},
|
||||
"date_format": "%Y-%m-%d %H:%M:%S"
|
||||
}
|
||||
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_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):
|
||||
# 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)
|
||||
|
||||
# try:
|
||||
# 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", [])
|
||||
# # self.logger.debug(f"Полученные данные: {bookings}")
|
||||
|
||||
# if not isinstance(bookings, list):
|
||||
# self.logger.error(f"Ожидался список, но получен: {type(bookings)}")
|
||||
# raise ValueError("Некорректный формат данных для bookings")
|
||||
# except json.JSONDecodeError as e:
|
||||
# self.logger.error(f"Ошибка декодирования JSON: {e}")
|
||||
# raise ValueError("Ответ не является корректным JSON.")
|
||||
# except Exception as e:
|
||||
# self.logger.error(f"Ошибка обработки ответа API: {e}")
|
||||
# raise
|
||||
|
||||
# except Exception as e:
|
||||
# self.logger.error(f"Ошибка запроса к API RealtyCalendar: {e}")
|
||||
# raise
|
||||
|
||||
# try:
|
||||
# hotel = await sync_to_async(Hotel.objects.get)(pms=self.pms_config)
|
||||
# hotel_tz = hotel.timezone
|
||||
# self.logger.debug(f"Настройки отеля: {hotel.name}, Timezone: {hotel_tz}")
|
||||
|
||||
# hotel_settings = await sync_to_async(GlobalHotelSettings.objects.first)()
|
||||
# check_in_time = hotel_settings.check_in_time.strftime("%H:%M:%S") if hotel_settings else "14:00:00"
|
||||
# check_out_time = hotel_settings.check_out_time.strftime("%H:%M:%S") if hotel_settings else "12:00:00"
|
||||
# except Exception as e:
|
||||
# self.logger.error(f"Ошибка получения настроек отеля: {e}")
|
||||
# check_in_time, check_out_time = "14:00:00", "12:00:00"
|
||||
|
||||
# filtered_data = []
|
||||
# for item in bookings:
|
||||
# try:
|
||||
# if not isinstance(item, dict):
|
||||
# self.logger.error(f"Некорректный формат элемента: {item}")
|
||||
# continue
|
||||
|
||||
# reservation_id = item.get('id')
|
||||
# if not reservation_id:
|
||||
# self.logger.error(f"ID резервации отсутствует: {item}")
|
||||
# continue
|
||||
|
||||
# begin_date = item.get('begin_date')
|
||||
# end_date = item.get('end_date')
|
||||
# if not begin_date or not end_date:
|
||||
# self.logger.error(f"Отсутствуют даты в записи: {item}")
|
||||
# continue
|
||||
|
||||
# checkin = timezone.make_aware(
|
||||
# datetime.strptime(f"{begin_date} {check_in_time}", "%Y-%m-%d %H:%M:%S")
|
||||
# )
|
||||
# checkout = timezone.make_aware(
|
||||
# datetime.strptime(f"{end_date} {check_out_time}", "%Y-%m-%d %H:%M:%S")
|
||||
# )
|
||||
|
||||
# filtered_data.append({
|
||||
# 'reservation_id': reservation_id,
|
||||
# 'checkin': checkin,
|
||||
# 'checkout': checkout,
|
||||
# 'room_number': item.get('apartment_id'),
|
||||
# 'room_type': item.get('notes', 'Описание отсутствует'),
|
||||
# 'status': item.get('status')
|
||||
# })
|
||||
# except Exception as e:
|
||||
# self.logger.error(f"Ошибка обработки элемента: {e}")
|
||||
|
||||
# # self.logger.debug(f"Отфильтрованные данные: {filtered_data}")
|
||||
# await self._save_to_db(filtered_data)
|
||||
|
||||
async def _fetch_data(self):
|
||||
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",
|
||||
}
|
||||
|
||||
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"),
|
||||
"sign": self._generate_sign()
|
||||
}
|
||||
data["sign"] = self._generate_sign(data)
|
||||
|
||||
try:
|
||||
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}")
|
||||
return {
|
||||
"processed_intervals": 0,
|
||||
"processed_items": 0,
|
||||
"errors": [f"Ошибка API RealtyCalendar: {response.status_code}"]
|
||||
}
|
||||
|
||||
response = await sync_to_async(requests.post)(
|
||||
f"{self.api_url}/api/v1/bookings/{self.public_key}/",
|
||||
headers=headers, json=data
|
||||
)
|
||||
response.raise_for_status()
|
||||
response_data = response.json()
|
||||
bookings = response_data.get("bookings", [])
|
||||
return await self._process_data(response_data)
|
||||
except requests.exceptions.RequestException as e:
|
||||
self.logger.error(f"Ошибка API: {e}")
|
||||
return {"processed_intervals": 0, "processed_items": 0, "errors": [str(e)]}
|
||||
|
||||
if not isinstance(bookings, list):
|
||||
self.logger.error(f"Ожидался список, но получен: {type(bookings)}")
|
||||
return {
|
||||
"processed_intervals": 0,
|
||||
"processed_items": 0,
|
||||
"errors": ["Некорректный формат данных для bookings"]
|
||||
}
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
self.logger.error(f"Ошибка декодирования JSON: {e}")
|
||||
return {
|
||||
"processed_intervals": 0,
|
||||
"processed_items": 0,
|
||||
"errors": ["Ответ не является корректным JSON."]
|
||||
}
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка запроса к API RealtyCalendar: {e}")
|
||||
return {
|
||||
"processed_intervals": 0,
|
||||
"processed_items": 0,
|
||||
"errors": [str(e)]
|
||||
}
|
||||
|
||||
# Получение настроек отеля
|
||||
try:
|
||||
hotel = await sync_to_async(Hotel.objects.get)(pms=self.pms_config)
|
||||
hotel_tz = hotel.timezone
|
||||
self.logger.debug(f"Настройки отеля: {hotel.name}, Timezone: {hotel_tz}")
|
||||
|
||||
hotel_settings = await sync_to_async(GlobalHotelSettings.objects.first)()
|
||||
check_in_time = hotel_settings.check_in_time.strftime("%H:%M:%S") if hotel_settings else "14:00:00"
|
||||
check_out_time = hotel_settings.check_out_time.strftime("%H:%M:%S") if hotel_settings else "12:00:00"
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка получения настроек отеля: {e}")
|
||||
check_in_time, check_out_time = "14:00:00", "12:00:00"
|
||||
|
||||
# Обработка записей
|
||||
async def _process_data(self, data):
|
||||
processed_items = 0
|
||||
errors = []
|
||||
filtered_data = []
|
||||
date_formats = ["%Y-%m-%d %H:%M:%S", "%Y-%m-%dT%H:%M:%S"]
|
||||
|
||||
for item in bookings:
|
||||
for item in data.get("bookings", []):
|
||||
try:
|
||||
if not isinstance(item, dict):
|
||||
raise ValueError(f"Некорректный формат элемента: {item}")
|
||||
checkin = self._parse_date(item['begin_date'], date_formats)
|
||||
checkout = self._parse_date(item['end_date'], date_formats)
|
||||
|
||||
reservation_id = item.get('id')
|
||||
if not reservation_id:
|
||||
raise ValueError(f"ID резервации отсутствует: {item}")
|
||||
|
||||
begin_date = item.get('begin_date')
|
||||
end_date = item.get('end_date')
|
||||
if not begin_date or not end_date:
|
||||
raise ValueError(f"Отсутствуют даты в записи: {item}")
|
||||
|
||||
checkin = timezone.make_aware(
|
||||
datetime.strptime(f"{begin_date} {check_in_time}", "%Y-%m-%d %H:%M:%S")
|
||||
reservation, created = await sync_to_async(Reservation.objects.update_or_create)(
|
||||
reservation_id=item['id'],
|
||||
defaults={
|
||||
'room_number': item['apartment_id'],
|
||||
'room_type': item.get('notes', 'Описание отсутствует'),
|
||||
'check_in': checkin,
|
||||
'check_out': checkout,
|
||||
'status': item['status'],
|
||||
'hotel': self.hotel,
|
||||
}
|
||||
)
|
||||
checkout = timezone.make_aware(
|
||||
datetime.strptime(f"{end_date} {check_out_time}", "%Y-%m-%d %H:%M:%S")
|
||||
)
|
||||
|
||||
filtered_data.append({
|
||||
'reservation_id': reservation_id,
|
||||
'checkin': checkin,
|
||||
'checkout': checkout,
|
||||
'room_number': item.get('apartment_id'),
|
||||
'room_type': item.get('notes', 'Описание отсутствует'),
|
||||
'status': item.get('status')
|
||||
})
|
||||
processed_items += 1
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка обработки элемента: {e}")
|
||||
self.logger.error(f"Ошибка обработки записи: {e}")
|
||||
errors.append(str(e))
|
||||
|
||||
# Сохранение в БД
|
||||
try:
|
||||
await self._save_to_db(filtered_data)
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка сохранения данных в БД: {e}")
|
||||
errors.append(f"Ошибка сохранения данных в БД: {str(e)}")
|
||||
return {"processed_intervals": 1, "processed_items": processed_items, "errors": errors}
|
||||
|
||||
# Формирование отчета
|
||||
report = {
|
||||
"processed_intervals": 1, # Пример значения
|
||||
"processed_items": processed_items,
|
||||
"errors": errors
|
||||
}
|
||||
self.logger.debug(f"Сформированный отчет: {report}")
|
||||
return report
|
||||
def _generate_sign(self):
|
||||
return hashlib.md5((self.public_key + self.private_key).encode("utf-8")).hexdigest()
|
||||
|
||||
|
||||
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):
|
||||
@staticmethod
|
||||
def _parse_date(date_str, formats):
|
||||
for fmt in formats:
|
||||
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}")
|
||||
return datetime.strptime(date_str, fmt)
|
||||
except ValueError:
|
||||
continue
|
||||
raise ValueError(f"Дата '{date_str}' не соответствует ожидаемым форматам: {formats}")
|
||||
|
||||
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__} прошел валидацию.")
|
||||
for method in required_methods:
|
||||
if not hasattr(self, method):
|
||||
raise ValueError(f"Плагин {type(self).__name__} не реализует метод {method}.")
|
||||
self.logger.debug(f"Плагин {self.__class__.__name__} успешно прошел валидацию.")
|
||||
return True
|
||||
|
||||
@@ -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 requests
|
||||
import json
|
||||
@@ -6,6 +133,25 @@ from datetime import datetime, timedelta
|
||||
from asgiref.sync import sync_to_async
|
||||
from hotels.models import Hotel, Reservation
|
||||
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
|
||||
class ShelterPMSPlugin(BasePMSPlugin):
|
||||
"""
|
||||
@@ -34,24 +180,65 @@ class ShelterPMSPlugin(BasePMSPlugin):
|
||||
self.logger.addHandler(handler_console)
|
||||
self.logger.addHandler(handler_file)
|
||||
self.logger.setLevel(logging.WARNING)
|
||||
>>>>>>> PMSManager_refactor
|
||||
|
||||
def get_default_parser_settings(self):
|
||||
"""
|
||||
Возвращает настройки парсера по умолчанию.
|
||||
Возвращает настройки по умолчанию для обработки данных.
|
||||
"""
|
||||
return {
|
||||
"field_mapping": {
|
||||
<<<<<<< HEAD
|
||||
"check_in": "check_in",
|
||||
"check_out": "check_out",
|
||||
"room_number": "room_number",
|
||||
"status": "status",
|
||||
=======
|
||||
"check_in": "from",
|
||||
"check_out": "until",
|
||||
"room_number": "roomNumber",
|
||||
"room_type_name": "roomTypeName",
|
||||
"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):
|
||||
"""
|
||||
<<<<<<< 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 и сохраняет их в базу.
|
||||
"""
|
||||
now = datetime.now()
|
||||
@@ -180,58 +367,69 @@ class ShelterPMSPlugin(BasePMSPlugin):
|
||||
raise ValueError(f"Плагин {type(self).__name__} не реализует метод {method}.")
|
||||
self.logger.debug(f"Плагин {self.__class__.__name__} прошел валидацию.")
|
||||
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:
|
||||
# Проверяем, что item — это словарь
|
||||
if not isinstance(item, dict):
|
||||
self.logger.error(f"Ожидался словарь, но получен: {type(item)}. Данные: {item}")
|
||||
return
|
||||
bookings = await self._fetch_data()
|
||||
report = await self._process_and_save_bookings(bookings)
|
||||
self.logger.info(f"Данные успешно обработаны. Отчет: {report}")
|
||||
return report
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка загрузки и обработки данных: {e}")
|
||||
raise
|
||||
|
||||
# Получаем отель по настройкам PMS
|
||||
hotel = await sync_to_async(Hotel.objects.get)(pms=self.pms_config)
|
||||
self.logger.debug(f"Отель найден: {hotel.name}")
|
||||
async def _process_and_save_bookings(self, bookings):
|
||||
"""
|
||||
Обрабатывает и сохраняет бронирования в базу данных.
|
||||
"""
|
||||
self.logger.info("Начало обработки данных о бронированиях для сохранения в базу.")
|
||||
processed_items = 0
|
||||
errors = []
|
||||
|
||||
# Сохраняем данные бронирования
|
||||
room_number = item.get("room_number")
|
||||
check_in = item.get("checkin")
|
||||
check_out = item.get("checkout")
|
||||
room_type = item.get("room_type")
|
||||
for record in bookings:
|
||||
try:
|
||||
# Пример обработки данных бронирования
|
||||
booking_id = record.get("id")
|
||||
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}")
|
||||
|
||||
# Проверяем, существует ли запись с таким номером и датой заезда
|
||||
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,
|
||||
# Сохраняем или обновляем запись в базе данных
|
||||
reservation, created = await sync_to_async(Reservation.objects.update_or_create)(
|
||||
external_id=booking_id,
|
||||
defaults={
|
||||
"hotel": self.pms_config.hotel,
|
||||
"room_number": room_number,
|
||||
"check_in": check_in,
|
||||
"check_out": check_out,
|
||||
"hotel": hotel,
|
||||
"room_type": room_type,
|
||||
"status": status,
|
||||
},
|
||||
)
|
||||
self.logger.debug(f"Обновлена существующая резервация.")
|
||||
else:
|
||||
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:
|
||||
self.logger.error(f"Ошибка сохранения данных: {e}")
|
||||
processed_items += 1
|
||||
|
||||
except Exception as 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
65
req1.txt
Normal 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
|
||||
0
scheduler/management/commands/__init__.py
Normal file
0
scheduler/management/commands/__init__.py
Normal file
27
scheduler/management/commands/run_scheduler.py
Normal file
27
scheduler/management/commands/run_scheduler.py
Normal 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
@@ -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
95
scheduler/task_loader.py
Normal 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
|
||||
@@ -1,6 +0,0 @@
|
||||
def test_function():
|
||||
"""тестовая функция для проверки планировщика
|
||||
|
||||
"""
|
||||
print("Hello, World!")
|
||||
return "Hello, World!"
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
def test_function():
|
||||
"""тестовая функция для проверки планировщика
|
||||
|
||||
"""
|
||||
print("Hello, World!")
|
||||
return "Hello, World!"
|
||||
|
||||
14
scheduler/urls.py
Normal file
14
scheduler/urls.py
Normal 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"),
|
||||
]
|
||||
18
staticfiles/admin/custom.css
Normal file
18
staticfiles/admin/custom.css
Normal file
@@ -0,0 +1,18 @@
|
||||
.ml-4 {
|
||||
margin-left: 1rem !important;
|
||||
}
|
||||
|
||||
.ml-6 {
|
||||
margin-left: 1.5rem !important;
|
||||
}
|
||||
|
||||
.nav-sidebar ul.nav-treeview .nav-link {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
.nav-sidebar .nav-link {
|
||||
display: flex;
|
||||
}
|
||||
.nav-sidebar .nav-link .nav-icon {
|
||||
margin-top: .2rem;
|
||||
margin-right: .3rem;
|
||||
}
|
||||
43
staticfiles/admin/js/collapse.js
Normal file
43
staticfiles/admin/js/collapse.js
Normal 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);
|
||||
});
|
||||
});
|
||||
}
|
||||
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
18
tmp/tests.py
Normal file
18
tmp/tests.py
Normal file
@@ -0,0 +1,18 @@
|
||||
import os
|
||||
import django
|
||||
import unittest
|
||||
from antifroud.check_fraud import run_reservation_check
|
||||
|
||||
# Установка переменной окружения для Django settings
|
||||
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "your_project.settings")
|
||||
django.setup()
|
||||
|
||||
class FraudCheckTests(unittest.TestCase):
|
||||
def test_run_reservation_check(self):
|
||||
"""Тест проверки бронирования на мошенничество"""
|
||||
result = run_reservation_check()
|
||||
self.assertIsNotNone(result) # Проверяем, что результат не None
|
||||
self.assertIsInstance(result, dict) # Проверяем, что возвращается словарь
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user