ssMerge branch 'pms_plugins'
This commit is contained in:
@@ -112,7 +112,7 @@ class ExternalDBSettingsAdmin(admin.ModelAdmin):
|
||||
@admin.register(UserActivityLog)
|
||||
class UserActivityLogAdmin(admin.ModelAdmin):
|
||||
list_display = ("id", 'get_location',"formatted_timestamp", "date_time", "page_id", "url_parameters", "page_url" ,"created", "page_title", "type", "hits")
|
||||
search_fields = ("page_title", "url_parameters")
|
||||
search_fields = ("page_title", "url_parameters", "page_title")
|
||||
list_filter = ("page_title", "created")
|
||||
readonly_fields = ("created", "timestamp")
|
||||
|
||||
@@ -151,69 +151,83 @@ 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)
|
||||
class ImportedHotelAdmin(admin.ModelAdmin):
|
||||
change_list_template = "antifroud/admin/import_hotels.html"
|
||||
list_display = ("external_id", "display_name", "name", "created", "updated", "imported")
|
||||
search_fields = ("name", "display_name", "external_id")
|
||||
list_filter = ("name", "display_name", "external_id")
|
||||
actions = ['mark_as_imported', 'delete_selected_hotels_action']
|
||||
# from .views import import_selected_hotels
|
||||
# # Регистрируем admin класс для ImportedHotel
|
||||
# @admin.register(ImportedHotel)
|
||||
# class ImportedHotelAdmin(admin.ModelAdmin):
|
||||
# change_list_template = "antifroud/admin/import_hotels.html"
|
||||
# list_display = ("external_id", "display_name", "name", "created", "updated", "imported")
|
||||
# search_fields = ("name", "display_name", "external_id")
|
||||
# list_filter = ("name", "display_name", "external_id")
|
||||
# actions = ['mark_as_imported', 'delete_selected_hotels_action']
|
||||
|
||||
def get_urls(self):
|
||||
# Получаем стандартные URL-адреса и добавляем наши
|
||||
urls = super().get_urls()
|
||||
custom_urls = [
|
||||
path('import_selected_hotels/', import_selected_hotels, name='antifroud_importedhotels_import_selected_hotels'),
|
||||
path('delete_selected_hotels/', self.delete_selected_hotels, name='delete_selected_hotels'),
|
||||
path('delete_hotel/<int:hotel_id>/', self.delete_hotel, name='delete_hotel'), # Изменили на URL параметр
|
||||
]
|
||||
return custom_urls + urls
|
||||
# def get_urls(self):
|
||||
# # Получаем стандартные URL-адреса и добавляем наши
|
||||
# urls = super().get_urls()
|
||||
# custom_urls = [
|
||||
# path('import_selected_hotels/', import_selected_hotels, name='antifroud_importedhotels_import_selected_hotels'),
|
||||
# path('delete_selected_hotels/', self.delete_selected_hotels, name='delete_selected_hotels'),
|
||||
# path('delete_hotel/<int:hotel_id>/', self.delete_hotel, name='delete_hotel'), # Изменили на URL параметр
|
||||
# ]
|
||||
# return custom_urls + urls
|
||||
|
||||
@transaction.atomic
|
||||
def delete_selected_hotels(self, request):
|
||||
if request.method == 'POST':
|
||||
selected = request.POST.get('selected', '')
|
||||
if selected:
|
||||
external_ids = selected.split(',')
|
||||
deleted_count, _ = ImportedHotel.objects.filter(external_id__in=external_ids).delete()
|
||||
messages.success(request, f"Удалено отелей: {deleted_count}")
|
||||
else:
|
||||
messages.warning(request, "Не выбрано ни одного отеля для удаления.")
|
||||
return redirect('admin:antifroud_importedhotel_changelist')
|
||||
# @transaction.atomic
|
||||
# def delete_selected_hotels(self, request):
|
||||
# if request.method == 'POST':
|
||||
# selected = request.POST.get('selected', '')
|
||||
# if selected:
|
||||
# external_ids = selected.split(',')
|
||||
# deleted_count, _ = ImportedHotel.objects.filter(external_id__in=external_ids).delete()
|
||||
# messages.success(request, f"Удалено отелей: {deleted_count}")
|
||||
# else:
|
||||
# messages.warning(request, "Не выбрано ни одного отеля для удаления.")
|
||||
# return redirect('admin:antifroud_importedhotel_changelist')
|
||||
|
||||
def delete_selected_hotels(self, request, queryset):
|
||||
deleted_count, _ = queryset.delete()
|
||||
self.message_user(request, f'{deleted_count} отелей было удалено.')
|
||||
delete_selected_hotels.short_description = "Удалить выбранные отели"
|
||||
# def delete_selected_hotels(self, request, queryset):
|
||||
# deleted_count, _ = queryset.delete()
|
||||
# self.message_user(request, f'{deleted_count} отелей было удалено.')
|
||||
# delete_selected_hotels.short_description = "Удалить выбранные отели"
|
||||
|
||||
def mark_as_imported(self, request, queryset):
|
||||
updated = queryset.update(imported=True)
|
||||
self.message_user(request, f"Отмечено как импортированное: {updated}", messages.SUCCESS)
|
||||
mark_as_imported.short_description = "Отметить выбранные как импортированные"
|
||||
# def mark_as_imported(self, request, queryset):
|
||||
# updated = queryset.update(imported=True)
|
||||
# self.message_user(request, f"Отмечено как импортированное: {updated}", messages.SUCCESS)
|
||||
# mark_as_imported.short_description = "Отметить выбранные как импортированные"
|
||||
|
||||
# Метод для удаления одного отеля
|
||||
@transaction.atomic
|
||||
def delete_hotel(self, request, hotel_id):
|
||||
imported_hotel = get_object_or_404(ImportedHotel, id=hotel_id)
|
||||
imported_hotel.delete()
|
||||
messages.success(request, f"Отель {imported_hotel.name} успешно удалён.")
|
||||
return redirect('admin:antifroud_importedhotel_changelist')
|
||||
# # Метод для удаления одного отеля
|
||||
# @transaction.atomic
|
||||
# def delete_hotel(self, request, hotel_id):
|
||||
# imported_hotel = get_object_or_404(ImportedHotel, id=hotel_id)
|
||||
# imported_hotel.delete()
|
||||
# messages.success(request, f"Отель {imported_hotel.name} успешно удалён.")
|
||||
# return redirect('admin:antifroud_importedhotel_changelist')
|
||||
|
||||
|
||||
@admin.register(SyncLog)
|
||||
class SyncLogAdmin(admin.ModelAdmin):
|
||||
change_list_template = "antifroud/admin/sync_log.html"
|
||||
list_display =['id', 'hotel', 'created', 'recieved_records', 'processed_records']
|
||||
search_fields = ['id', 'hotel', 'created', 'recieved_records', 'processed_records']
|
||||
list_filter = ['id', 'hotel', 'created', 'recieved_records', 'processed_records']
|
||||
change_list_template = "antifroud/admin/sync_log.html" # Путь к вашему кастомному шаблону
|
||||
list_display = ['id', 'hotel', 'created', 'recieved_records', 'processed_records']
|
||||
search_fields = ['id', 'hotel__name', 'recieved_records', 'processed_records']
|
||||
list_filter = ['hotel', 'created']
|
||||
|
||||
class Meta:
|
||||
model = SyncLog
|
||||
fields = ['hotel', 'recieved_records', 'processed_records']
|
||||
|
||||
def changelist_view(self, request, extra_context=None):
|
||||
"""
|
||||
Добавляет фильтрацию по отелям в шаблон.
|
||||
"""
|
||||
extra_context = extra_context or {}
|
||||
|
||||
# Получаем выбранный фильтр отеля из GET-параметров
|
||||
hotel_id = request.GET.get('hotel')
|
||||
hotels = Hotel.objects.all()
|
||||
sync_logs = SyncLog.objects.all()
|
||||
|
||||
if hotel_id:
|
||||
sync_logs = sync_logs.filter(hotel_id=hotel_id)
|
||||
|
||||
extra_context['sync_logs'] = sync_logs
|
||||
extra_context['hotels'] = hotels
|
||||
extra_context['selected_hotel'] = hotel_id # Чтобы отобразить выбранный отель
|
||||
|
||||
return super().changelist_view(request, extra_context=extra_context)
|
||||
@admin.register(ViolationLog)
|
||||
class ViolationLogAdmin(admin.ModelAdmin):
|
||||
list_display = ['id', 'hotel', 'room_number' , 'hits', 'created_at', 'violation_type', 'violation_details', 'detected_at']
|
||||
@@ -222,4 +236,15 @@ class ViolationLogAdmin(admin.ModelAdmin):
|
||||
|
||||
class Meta:
|
||||
model = ViolationLog
|
||||
fields = ['hotel', 'room_number', 'created_at', 'violation_type', 'violation_details', 'detected_at']
|
||||
fields = ['hotel', 'room_number', 'created_at', 'violation_type', 'violation_details', 'detected_at']
|
||||
|
||||
@admin.register(RoomDiscrepancy)
|
||||
class RoomDiscrepancyAdmin(admin.ModelAdmin):
|
||||
list_display = ['hotel', 'room_number', 'booking_id','created_at', 'check_in_date_expected','check_in_date_actual','discrepancy_type']
|
||||
search_fields = ['hotel', 'room_number', 'booking_id','created_at', 'check_in_date_expected','check_in_date_actual','discrepancy_type']
|
||||
list_filter = ['hotel', 'room_number', 'booking_id','created_at', 'check_in_date_expected','check_in_date_actual','discrepancy_type']
|
||||
|
||||
class Meta:
|
||||
model = RoomDiscrepancy
|
||||
fields = ['hotel', 'room_number', 'booking_id','created_at', 'check_in_date_expected','check_in_date_actual','discrepancy_type']
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
import logging
|
||||
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
|
||||
|
||||
from touchh.utils.log import CustomLogger
|
||||
# Настройка логирования
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
logger = logging.getLogger(__name__)
|
||||
logger = CustomLogger(__name__).get_logger()
|
||||
|
||||
|
||||
class ReservationChecker:
|
||||
"""
|
||||
|
||||
@@ -202,63 +202,56 @@ class DataSyncManager:
|
||||
self.logger.error(f"Error fetching data: {e}")
|
||||
return []
|
||||
def update_sync_log(self, hotel, recieved_records, processed_records):
|
||||
"""
|
||||
Обновляет или создает запись в таблице SyncLog.
|
||||
"""
|
||||
try:
|
||||
log, created = SyncLog.objects.get_or_create(hotel=hotel)
|
||||
log, created = SyncLog.objects.update_or_create(
|
||||
hotel=hotel,
|
||||
defaults={
|
||||
"recieved_records": recieved_records,
|
||||
"processed_records": processed_records,
|
||||
"created": timezone.now(), # Убедитесь, что дата обновляется
|
||||
}
|
||||
)
|
||||
if created:
|
||||
log.recieved_records = recieved_records
|
||||
log.processed_records = processed_records
|
||||
self.logger.info(f"Sync log created for hotel '{hotel.name}'.")
|
||||
else:
|
||||
log.recieved_records += recieved_records
|
||||
log.processed_records += processed_records
|
||||
log.save()
|
||||
self.logger.info(f"Sync log updated for hotel '{hotel.name}'.")
|
||||
self.logger.info(f"Sync log updated for hotel '{hotel.name}'.")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error updating sync log for hotel '{hotel.name}': {e}")
|
||||
|
||||
self.logger.info(f"Attempting to update sync log for hotel: {hotel.name}")
|
||||
self.update_sync_log(hotel, recieved_records, processed_records)
|
||||
|
||||
def process_and_save_data(self, rows):
|
||||
"""
|
||||
Обрабатывает и сохраняет данные из внешнего источника.
|
||||
|
||||
:param rows: Список строк данных, полученных из базы данных.
|
||||
"""
|
||||
seen_entries = set()
|
||||
hotel_processed_counts = {} # Словарь для подсчёта записей по каждому отелю
|
||||
|
||||
for row in rows:
|
||||
# Получение и декодирование URL-параметров
|
||||
url_parameters = row.get("url_parameters")
|
||||
if not url_parameters:
|
||||
self.logger.warning(f"Skipping record with missing URL parameters: {row}")
|
||||
continue
|
||||
|
||||
parsed_params = self.data_processor.parse_url_parameters(url_parameters)
|
||||
hotel_id = parsed_params.get("utm_content") # Извлекаем hotel_id из параметров
|
||||
room_number = parsed_params.get("utm_term") # Извлекаем room_number из параметров
|
||||
|
||||
if not hotel_id or not room_number:
|
||||
self.logger.warning(f"Skipping record with missing data: hotel_id={hotel_id}, room_number={room_number}")
|
||||
continue
|
||||
|
||||
# Проверка на дубликаты
|
||||
if (hotel_id, room_number) in seen_entries:
|
||||
self.logger.warning(f"Duplicate record skipped: hotel_id={hotel_id}, room_number={room_number}")
|
||||
continue
|
||||
|
||||
seen_entries.add((hotel_id, room_number))
|
||||
|
||||
try:
|
||||
# Получение или создание отеля
|
||||
url_parameters = row.get("url_parameters")
|
||||
if not url_parameters:
|
||||
self.logger.warning(f"Skipping record with missing URL parameters: {row}")
|
||||
continue
|
||||
|
||||
parsed_params = self.data_processor.parse_url_parameters(url_parameters)
|
||||
hotel_id = parsed_params.get("utm_content")
|
||||
room_number = parsed_params.get("utm_term")
|
||||
|
||||
if not hotel_id or not room_number:
|
||||
self.logger.warning(f"Skipping record with missing data: hotel_id={hotel_id}, room_number={room_number}")
|
||||
continue
|
||||
|
||||
hotel = self.hotel_manager.get_or_create_hotel(hotel_id, row.get("page_title"))
|
||||
if not hotel:
|
||||
self.logger.warning(f"Skipping record: Failed to create or retrieve hotel with ID {hotel_id}")
|
||||
continue
|
||||
|
||||
# Получение или создание комнаты
|
||||
room = self.hotel_manager.get_or_create_room(hotel, room_number)
|
||||
if not room:
|
||||
self.logger.warning(f"Skipping record: Failed to create or retrieve room {room_number} in hotel {hotel.name}")
|
||||
continue
|
||||
|
||||
# Создание или обновление записи активности пользователя
|
||||
UserActivityLog.objects.update_or_create(
|
||||
external_id=row.get("id"),
|
||||
defaults={
|
||||
@@ -270,14 +263,23 @@ class DataSyncManager:
|
||||
"url_parameters": parsed_params,
|
||||
"page_title": self.data_processor.decode_html_entities(row.get("page_title")) or "Untitled",
|
||||
"page_url": row.get("page_url") or "",
|
||||
"page_id": row.get("page_id") or 0,
|
||||
"hits": row.get("hits") or 0,
|
||||
}
|
||||
)
|
||||
self.logger.info(f"Record ID {row.get('id')} processed successfully.")
|
||||
|
||||
if hotel.id not in hotel_processed_counts:
|
||||
hotel_processed_counts[hotel.id] = {"recieved_records": 0, "processed_records": 0}
|
||||
hotel_processed_counts[hotel.id]["processed_records"] += 1
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error(f"Error processing record ID {row.get('id')}: {e}")
|
||||
|
||||
self.logger.info(f"Data processing completed. Processed {len(seen_entries)} unique records.")
|
||||
for hotel_id, counts in hotel_processed_counts.items():
|
||||
hotel = Hotel.objects.get(id=hotel_id)
|
||||
self.update_sync_log(hotel, recieved_records=len(rows), processed_records=counts["processed_records"])
|
||||
|
||||
|
||||
|
||||
def sync(self):
|
||||
@@ -292,7 +294,8 @@ class DataSyncManager:
|
||||
|
||||
|
||||
def scheduled_sync():
|
||||
logger = CustomLogger(name="DatabaseSyncScheduler", log_level="ERROR").get_logger()
|
||||
import os
|
||||
logger = CustomLogger(name="DatabaseSyncScheduler", log_level=os.getenv("SCHEDULED_SYNC_LOG_LEVEL", default="ERROR")).get_logger()
|
||||
logger.info("Starting scheduled sync.")
|
||||
|
||||
active_db_settings = ExternalDBSettings.objects.filter(is_active=True)
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-12 12:28
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('hotels', '0004_alter_reservation_room_number'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ExternalDBSettings',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(help_text='Имя подключения для идентификации.', max_length=255, unique=True)),
|
||||
('host', models.CharField(help_text='Адрес сервера базы данных.', max_length=255)),
|
||||
('port', models.PositiveIntegerField(default=3306, help_text='Порт сервера базы данных.')),
|
||||
('database', models.CharField(help_text='Имя базы данных.', max_length=255)),
|
||||
('user', models.CharField(help_text='Имя пользователя базы данных.', max_length=255)),
|
||||
('password', models.CharField(help_text='Пароль для подключения.', max_length=255)),
|
||||
('table_name', models.CharField(blank=True, help_text='Имя таблицы для загрузки данных.', max_length=255, null=True)),
|
||||
('selected_fields', models.TextField(blank=True, help_text='Список полей для загрузки (через запятую).', null=True)),
|
||||
('is_active', models.BooleanField(default=True, help_text='Флаг активности подключения.')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True)),
|
||||
('updated_at', models.DateTimeField(auto_now=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Настройки подключения к БД',
|
||||
'verbose_name_plural': 'Настройки подключений к БД',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserActivityLog',
|
||||
fields=[
|
||||
('id', models.BigIntegerField(primary_key=True, serialize=False)),
|
||||
('user_id', models.BigIntegerField(verbose_name='ID пользователя')),
|
||||
('ip', models.GenericIPAddressField(verbose_name='IP-адрес')),
|
||||
('created', models.DateTimeField(verbose_name='Дата создания')),
|
||||
('timestamp', models.BigIntegerField(verbose_name='Метка времени')),
|
||||
('date_time', models.DateTimeField(verbose_name='Дата и время')),
|
||||
('referred', models.TextField(blank=True, null=True, verbose_name='Реферальная ссылка')),
|
||||
('agent', models.TextField(verbose_name='Агент пользователя')),
|
||||
('platform', models.CharField(blank=True, max_length=255, null=True, verbose_name='Платформа')),
|
||||
('version', models.CharField(blank=True, max_length=255, null=True, verbose_name='Версия')),
|
||||
('model', models.CharField(blank=True, max_length=255, null=True, verbose_name='Модель устройства')),
|
||||
('device', models.CharField(blank=True, max_length=255, null=True, verbose_name='Тип устройства')),
|
||||
('UAString', models.TextField(verbose_name='User-Agent строка')),
|
||||
('location', models.CharField(blank=True, max_length=255, null=True, verbose_name='Местоположение')),
|
||||
('page_id', models.BigIntegerField(blank=True, null=True, verbose_name='ID страницы')),
|
||||
('url_parameters', models.TextField(blank=True, null=True, verbose_name='Параметры URL')),
|
||||
('page_title', models.TextField(blank=True, null=True, verbose_name='Заголовок страницы')),
|
||||
('type', models.CharField(max_length=50, verbose_name='Тип')),
|
||||
('last_counter', models.IntegerField(verbose_name='Последний счетчик')),
|
||||
('hits', models.IntegerField(verbose_name='Количество обращений')),
|
||||
('honeypot', models.BooleanField(verbose_name='Метка honeypot')),
|
||||
('reply', models.BooleanField(verbose_name='Ответ пользователя')),
|
||||
('page_url', models.URLField(blank=True, null=True, verbose_name='URL страницы')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Регистрация посетителей',
|
||||
'verbose_name_plural': 'Регистрации посетителей',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='RoomDiscrepancy',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('room_number', models.CharField(max_length=50, verbose_name='Номер комнаты')),
|
||||
('booking_id', models.CharField(max_length=255, verbose_name='ID бронирования')),
|
||||
('check_in_date_expected', models.DateField(verbose_name='Ожидаемая дата заселения')),
|
||||
('check_in_date_actual', models.DateField(verbose_name='Фактическая дата заселения')),
|
||||
('discrepancy_type', models.CharField(choices=[('early', 'Раннее заселение'), ('late', 'Позднее заселение'), ('missed', 'Неявка')], max_length=50, verbose_name='Тип несоответствия')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
|
||||
('hotel', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='hotels.hotel', verbose_name='Отель')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Несовпадение в заселении',
|
||||
'verbose_name_plural': 'Несовпадения в заселении',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,42 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-12 13:07
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('antifroud', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='externaldbsettings',
|
||||
name='database',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='externaldbsettings',
|
||||
name='host',
|
||||
field=models.CharField(default='', help_text='Адрес сервера базы данных.', max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='externaldbsettings',
|
||||
name='is_active',
|
||||
field=models.BooleanField(default=False, help_text='Флаг активности подключения.'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='externaldbsettings',
|
||||
name='name',
|
||||
field=models.CharField(default='Новая настройка', help_text='Имя подключения для идентификации.', max_length=255, unique=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='externaldbsettings',
|
||||
name='password',
|
||||
field=models.CharField(default='', help_text='Пароль для подключения.', max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='externaldbsettings',
|
||||
name='user',
|
||||
field=models.CharField(default='', help_text='Имя пользователя базы данных.', max_length=255),
|
||||
),
|
||||
]
|
||||
@@ -1,43 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-12 13:10
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('antifroud', '0002_remove_externaldbsettings_database_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='externaldbsettings',
|
||||
name='database',
|
||||
field=models.CharField(default='', help_text='Имя базы данных.', max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='externaldbsettings',
|
||||
name='host',
|
||||
field=models.CharField(help_text='Адрес сервера базы данных.', max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='externaldbsettings',
|
||||
name='is_active',
|
||||
field=models.BooleanField(default=True, help_text='Флаг активности подключения.'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='externaldbsettings',
|
||||
name='name',
|
||||
field=models.CharField(help_text='Имя подключения для идентификации.', max_length=255, unique=True),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='externaldbsettings',
|
||||
name='password',
|
||||
field=models.CharField(help_text='Пароль для подключения.', max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='externaldbsettings',
|
||||
name='user',
|
||||
field=models.CharField(help_text='Имя пользователя базы данных.', max_length=255),
|
||||
),
|
||||
]
|
||||
@@ -1,27 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-12 14:42
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('antifroud', '0003_externaldbsettings_database_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='externaldbsettings',
|
||||
options={'verbose_name': 'Настройка подключения к БД', 'verbose_name_plural': 'Настройки подключений к БД'},
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='externaldbsettings',
|
||||
name='database',
|
||||
field=models.CharField(default='u1510415_wp832', help_text='Имя базы данных.', max_length=255),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='externaldbsettings',
|
||||
name='table_name',
|
||||
field=models.CharField(blank=True, default='wpts_user_activity_log', help_text='Имя таблицы для загрузки данных.', max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,24 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-12 23:57
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('antifroud', '0004_alter_externaldbsettings_options_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ImportedHotel',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('external_id', models.CharField(max_length=255, unique=True, verbose_name='Внешний ID отеля')),
|
||||
('name', models.CharField(max_length=255, verbose_name='Имя отеля')),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
|
||||
('updated', models.DateTimeField(auto_now=True, verbose_name='Дата обновления')),
|
||||
('imported', models.BooleanField(default=False, verbose_name='Импортирован в основную базу')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -1,17 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-12 23:58
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('antifroud', '0005_importedhotel'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelOptions(
|
||||
name='importedhotel',
|
||||
options={'verbose_name': 'Импортированный отель', 'verbose_name_plural': 'Импортированные отели'},
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-13 00:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('antifroud', '0006_alter_importedhotel_options'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='useractivitylog',
|
||||
name='external_id',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-13 00:09
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('antifroud', '0007_useractivitylog_external_id'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='useractivitylog',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-13 00:32
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('antifroud', '0008_alter_useractivitylog_id'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='importedhotel',
|
||||
name='display_name',
|
||||
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Отображаемое имя'),
|
||||
),
|
||||
]
|
||||
@@ -1,30 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-14 04:29
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('antifroud', '0009_importedhotel_display_name'),
|
||||
('hotels', '0010_alter_hotel_timezone'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='SyncLog',
|
||||
fields=[
|
||||
('id', models.BigIntegerField(primary_key=True, serialize=False, unique=True, verbose_name='ID')),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
|
||||
('recieved_records', models.IntegerField(verbose_name='Полученные записи')),
|
||||
('processed_records', models.IntegerField(verbose_name='Обработанные записи')),
|
||||
('hotel', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='hotels.hotel', verbose_name='Отель')),
|
||||
('reservation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='hotels.reservation', verbose_name='Бронирование')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Журнал синхронизации',
|
||||
'verbose_name_plural': 'Журналы синхронизации',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-14 06:13
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('antifroud', '0010_synclog'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='importedhotel',
|
||||
name='external_id',
|
||||
field=models.CharField(max_length=255, verbose_name='Внешний ID отеля'),
|
||||
),
|
||||
]
|
||||
@@ -1,31 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-16 23:37
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('antifroud', '0011_alter_importedhotel_external_id'),
|
||||
('hotels', '0010_alter_hotel_timezone'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ViolationLog',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('room_number', models.CharField(blank=True, max_length=50, null=True, verbose_name='Номер комнаты')),
|
||||
('violation_type', models.CharField(choices=[('missed', 'Неявка'), ('early', 'Раннее заселение'), ('late', 'Позднее заселение')], max_length=50, verbose_name='Тип нарушения')),
|
||||
('violation_details', models.TextField(blank=True, null=True, verbose_name='Детали нарушения')),
|
||||
('detected_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата обнаружения')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
|
||||
('hotel', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='hotels.hotel', verbose_name='Отель')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Журнал нарушений',
|
||||
'verbose_name_plural': 'Журналы нарушений',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-17 03:23
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('antifroud', '0012_violationlog'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='useractivitylog',
|
||||
name='timestamp',
|
||||
field=models.BigIntegerField(blank=True, null=True, verbose_name='Метка времени'),
|
||||
),
|
||||
]
|
||||
@@ -1,68 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-17 03:26
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('antifroud', '0013_alter_useractivitylog_timestamp'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='useractivitylog',
|
||||
name='UAString',
|
||||
field=models.TextField(blank=True, null=True, verbose_name='User-Agent строка'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='useractivitylog',
|
||||
name='agent',
|
||||
field=models.TextField(blank=True, null=True, verbose_name='Агент пользователя'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='useractivitylog',
|
||||
name='created',
|
||||
field=models.DateTimeField(blank=True, null=True, verbose_name='Дата создания'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='useractivitylog',
|
||||
name='date_time',
|
||||
field=models.DateTimeField(blank=True, null=True, verbose_name='Дата и время'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='useractivitylog',
|
||||
name='hits',
|
||||
field=models.IntegerField(blank=True, null=True, verbose_name='Количество обращений'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='useractivitylog',
|
||||
name='honeypot',
|
||||
field=models.BooleanField(blank=True, null=True, verbose_name='Метка honeypot'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='useractivitylog',
|
||||
name='ip',
|
||||
field=models.GenericIPAddressField(blank=True, null=True, verbose_name='IP-адрес'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='useractivitylog',
|
||||
name='last_counter',
|
||||
field=models.IntegerField(blank=True, null=True, verbose_name='Последний счетчик'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='useractivitylog',
|
||||
name='reply',
|
||||
field=models.BooleanField(blank=True, null=True, verbose_name='Ответ пользователя'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='useractivitylog',
|
||||
name='type',
|
||||
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='Тип'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='useractivitylog',
|
||||
name='user_id',
|
||||
field=models.BigIntegerField(blank=True, null=True, verbose_name='ID пользователя'),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-17 04:49
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('antifroud', '0014_alter_useractivitylog_uastring_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='useractivitylog',
|
||||
name='page_id',
|
||||
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='ID страницы'),
|
||||
),
|
||||
]
|
||||
@@ -1,39 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-17 05:27
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('antifroud', '0015_alter_useractivitylog_page_id'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='useractivitylog',
|
||||
name='created',
|
||||
field=models.DateTimeField(blank=True, db_index=True, null=True, verbose_name='Дата создания'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='useractivitylog',
|
||||
name='external_id',
|
||||
field=models.CharField(db_index=True, default=1, max_length=255, unique=True, verbose_name='Внешний ID'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='useractivitylog',
|
||||
name='ip',
|
||||
field=models.GenericIPAddressField(blank=True, db_index=True, null=True, verbose_name='IP-адрес'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='useractivitylog',
|
||||
name='page_id',
|
||||
field=models.BigIntegerField(blank=True, db_index=True, null=True, verbose_name='ID страницы'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='useractivitylog',
|
||||
name='user_id',
|
||||
field=models.BigIntegerField(blank=True, db_index=True, null=True, verbose_name='ID пользователя'),
|
||||
),
|
||||
]
|
||||
@@ -1,19 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-18 01:13
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('antifroud', '0016_alter_useractivitylog_created_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='violationlog',
|
||||
name='hits',
|
||||
field=models.IntegerField(default=1, verbose_name='Срабатывания'),
|
||||
preserve_default=False,
|
||||
),
|
||||
]
|
||||
@@ -1,44 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-18 04:46
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('antifroud', '0017_violationlog_hits'),
|
||||
('hotels', '0014_alter_room_number'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='synclog',
|
||||
name='reservation',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='synclog',
|
||||
name='created',
|
||||
field=models.DateTimeField(auto_now=True, verbose_name='Дата обновления'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='synclog',
|
||||
name='hotel',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='hotels.hotel', unique=True, verbose_name='Отель'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='synclog',
|
||||
name='id',
|
||||
field=models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='synclog',
|
||||
name='processed_records',
|
||||
field=models.IntegerField(default=0, verbose_name='Обработанные записи'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='synclog',
|
||||
name='recieved_records',
|
||||
field=models.IntegerField(default=0, verbose_name='Полученные записи'),
|
||||
),
|
||||
]
|
||||
@@ -1,20 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-18 04:55
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('antifroud', '0018_remove_synclog_reservation_alter_synclog_created_and_more'),
|
||||
('hotels', '0014_alter_room_number'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='synclog',
|
||||
name='hotel',
|
||||
field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='hotels.hotel', verbose_name='Отель'),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-18 10:45
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('antifroud', '0019_alter_synclog_hotel'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='useractivitylog',
|
||||
name='hits',
|
||||
field=models.IntegerField(blank=True, default='0', null=True, verbose_name='Количество обращений'),
|
||||
),
|
||||
]
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
<style>
|
||||
#table-data-preview {
|
||||
max-height: 300px; /* Ограничиваем высоту предпросмотра */
|
||||
max-height: 500px; /* Ограничиваем высоту предпросмотра */
|
||||
overflow-y: auto; /* Прокрутка по вертикали */
|
||||
overflow-x: auto; /* Прокрутка по горизонтали */
|
||||
}
|
||||
@@ -36,27 +36,27 @@
|
||||
<form id="connection-form" method="post">
|
||||
{% csrf_token %}
|
||||
<div class="form-group mb-3">
|
||||
<label for="db-name">Name</label>
|
||||
<label for="db-name">Имя подключения</label>
|
||||
<input id="db-name" class="form-control" type="text" name="name" value="{{ original.name }}" required />
|
||||
</div>
|
||||
<div class="form-group mb-3">
|
||||
<label for="db-host">DB Host</label>
|
||||
<label for="db-host">Сервер БД</label>
|
||||
<input id="db-host" class="form-control" type="text" name="host" value="{{ original.host }}" required />
|
||||
</div>
|
||||
<div class="form-group mb-3">
|
||||
<label for="db-port">DB Port</label>
|
||||
<label for="db-port">Порт БД</label>
|
||||
<input id="db-port" class="form-control" type="number" name="port" value="{{ original.port }}" required />
|
||||
</div>
|
||||
<div class="form-group mb-3">
|
||||
<label for="db-user">User</label>
|
||||
<label for="db-user">Пользователь</label>
|
||||
<input id="db-user" class="form-control" type="text" name="user" value="{{ original.user }}" required />
|
||||
</div>
|
||||
<div class="form-group mb-3">
|
||||
<label for="db-password">Password</label>
|
||||
<label for="db-password">Пароль</label>
|
||||
<input id="db-password" class="form-control" type="password" name="password" value="{{ original.password }}" />
|
||||
</div>
|
||||
<div class="form-group mb-3">
|
||||
<label for="db-database">Database</label>
|
||||
<label for="db-database">Имя Базы данных</label>
|
||||
<input id="db-database" class="form-control" type="text" name="database" value="{{ original.database }}" required />
|
||||
</div>
|
||||
<div class="form-group mb-3">
|
||||
@@ -78,10 +78,11 @@
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group mb-3">
|
||||
<label for="is-active">Активное подключение</label>
|
||||
<input id="is-active" class="form-check-input" type="checkbox" name="is_active" {% if original.is_active %}checked{% endif %} />
|
||||
</div>
|
||||
<div class="form-group mb-3" style="text-center">
|
||||
|
||||
<input class="form-check-input" id="is-active" class="form-check-input" type="checkbox" name="is_active" {% if original.is_active %}checked{% endif %}>
|
||||
<label class="form-check-label" for="inlineCheckbox2"><b>Активное подключение</b></label>
|
||||
|
||||
<div class="form-group text-center">
|
||||
<button class="btn btn-success" type="submit">Сохранить</button>
|
||||
<button class="btn btn-secondary" type="button" id="close-button">Закрыть</button>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
{% extends "admin/change_list.html" %}
|
||||
{% block content %}
|
||||
<div class="container-fluid">
|
||||
|
||||
<div class="row mt-4">
|
||||
<div class="col">
|
||||
<div class="card shadow-sm mb-2 db-graph">
|
||||
@@ -9,53 +8,22 @@
|
||||
<h6 class="text-white m-0 font-md">Журнал синхронизации</h6>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form method="post" action="{% url 'antifroud:sync_log_create' %}">
|
||||
{% csrf_token %}
|
||||
<div class="form-row">
|
||||
<div class="col-md-9 col-xl-9">
|
||||
<div class="box-bg">
|
||||
<div class="form-row">
|
||||
<div class="col-md-2 col-xl-2 align-self-center font-md text-dark-blue">
|
||||
<label class="col-form-label p-0" for="hotel-id"><strong>Отель:</strong></label>
|
||||
</div>
|
||||
<div class="col-md-4 col-xl-3">
|
||||
<div class="form-group mb-0">
|
||||
<select class="custom-select custom-select-sm font-sm" name="hotel" id="hotel-id">
|
||||
<option value="">--Выберите Отель --</option>
|
||||
{% for hotel in hotels %}
|
||||
<option value="{{ hotel.id }}">{{ hotel.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-3 col-xl-3">
|
||||
<div class="box-bg">
|
||||
<div class="text-dark form-row">
|
||||
<div class="col-xl-5 offset-xl-0 align-self-center">
|
||||
<h6 class="mb-0 font-sm">Полученные записи:</h6>
|
||||
</div>
|
||||
<div class="col-xl-7 offset-xl-0 text-right align-self-center">
|
||||
<div class="form-group mb-1">
|
||||
<input class="form-control form-control-sm form-control font-sm" type="number" name="received_records" required />
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xl-5 offset-xl-0 align-self-center">
|
||||
<h6 class="mb-0 font-sm">Обработанные записи:</h6>
|
||||
</div>
|
||||
<div class="col-xl-7 offset-xl-0 text-right align-self-center">
|
||||
<div class="form-group mb-1">
|
||||
<input class="form-control form-control-sm form-control font-sm" type="number" name="processed_records" required />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Форма фильтрации по отелям -->
|
||||
<form method="get" class="form-inline mb-3">
|
||||
<label for="hotel-filter" class="mr-2">Фильтр по отелям:</label>
|
||||
<select name="hotel" id="hotel-filter" class="form-control mr-2">
|
||||
<option value="">-- Все отели --</option>
|
||||
{% for hotel in hotels %}
|
||||
<option value="{{ hotel.id }}" {% if hotel.id|stringformat:"s" == selected_hotel %}selected{% endif %}>
|
||||
{{ hotel.name }}
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
<button type="submit" class="btn btn-primary">Применить</button>
|
||||
</form>
|
||||
|
||||
|
||||
|
||||
<!-- Список существующих журналов синхронизации -->
|
||||
<div class="table-responsive tbl-wfx mt-1 kot-table">
|
||||
<table class="table table-sm">
|
||||
@@ -63,10 +31,8 @@
|
||||
<tr class="text-dark-blue">
|
||||
<th>#</th>
|
||||
<th>Отель</th>
|
||||
<th>ID бронирования</th>
|
||||
<th> Дата синхронизации</th>
|
||||
<th>Обработанные записи</th>
|
||||
<th>Полученные записи</th>
|
||||
<th>Создан</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -74,14 +40,12 @@
|
||||
<tr>
|
||||
<td>{{ log.id }}</td>
|
||||
<td>{{ log.hotel.name }}</td>
|
||||
<td>{{ log.reservation_id }}</td>
|
||||
<td>{{ log.processed_records }}</td>
|
||||
<td>{{ log.recieved_records }}</td>
|
||||
<td>{{ log.created }}</td>
|
||||
<td>{{ log.processed_records }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="6" class="text-center">Нет журналов.</td>
|
||||
<td colspan="5" class="text-center">Нет записей.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
@@ -93,4 +57,4 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
{% endblock %}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# settings.py
|
||||
|
||||
<<<<<<< HEAD
|
||||
<<<<<<< HEAD
|
||||
from .models import LocalDatabase
|
||||
from decouple import config
|
||||
@@ -11,11 +11,17 @@ def load_database_settings():
|
||||
=======
|
||||
from decouple import config
|
||||
from django.conf import settings
|
||||
=======
|
||||
>>>>>>> pms_plugins
|
||||
from django.apps import apps
|
||||
|
||||
def load_database_settings():
|
||||
# Загружаем настройки из базы данных
|
||||
def load_database_settings(databases):
|
||||
"""
|
||||
Загружает дополнительные базы данных из таблицы LocalDatabase и добавляет их в конфигурацию.
|
||||
:param databases: Существующий словарь DATABASES
|
||||
"""
|
||||
LocalDatabase = apps.get_model('app_settings', 'LocalDatabase')
|
||||
<<<<<<< HEAD
|
||||
>>>>>>> antifraud
|
||||
local_db_settings = LocalDatabase.objects.all()
|
||||
|
||||
@@ -29,32 +35,20 @@ def load_database_settings():
|
||||
'HOST': db.host,
|
||||
'PORT': db.port,
|
||||
}
|
||||
=======
|
||||
>>>>>>> pms_plugins
|
||||
|
||||
# Вызов этой функции при старте проекта, например, в файле wsgi.py
|
||||
load_database_settings()
|
||||
|
||||
# Чтение локальных баз данных
|
||||
local_databases = LocalDatabase.objects.filter(is_active=True)
|
||||
|
||||
# Основная база данных
|
||||
DATABASES = {
|
||||
'default': {
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
'NAME': config('DB_NAME'),
|
||||
'USER': config('DB_USER'),
|
||||
'PASSWORD': config('DB_PASSWORD'),
|
||||
'HOST': config('DB_HOST'),
|
||||
'PORT': config('DB_PORT'),
|
||||
},
|
||||
}
|
||||
|
||||
# Добавление локальных баз данных
|
||||
for db in local_databases:
|
||||
DATABASES[db.name] = {
|
||||
'ENGINE': 'django.db.backends.postgresql',
|
||||
'NAME': db.name,
|
||||
'USER': db.user,
|
||||
'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:
|
||||
databases[db.name] = {
|
||||
'ENGINE': db.engine, # Можно хранить тип движка в базе
|
||||
'NAME': db.database,
|
||||
'USER': db.user,
|
||||
'PASSWORD': db.password,
|
||||
'HOST': db.host,
|
||||
'PORT': db.port,
|
||||
'ATOMIC_REQUESTS': True, # Убедитесь, что добавляете ATOMIC_REQUESTS
|
||||
}
|
||||
except Exception as e:
|
||||
print(f"Ошибка загрузки локальных баз данных: {e}")
|
||||
|
||||
@@ -1,16 +1,79 @@
|
||||
# import os
|
||||
# import django
|
||||
# import asyncio
|
||||
# from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
# from django.core.management.base import BaseCommand
|
||||
# from telegram.ext import Application
|
||||
# from bot.utils.bot_setup import setup_bot
|
||||
# from scheduler.tasks import load_tasks_to_scheduler
|
||||
# from app_settings.models import TelegramSettings
|
||||
# from touchh.utils.log import CustomLogger
|
||||
|
||||
# class Command(BaseCommand):
|
||||
# help = "Запуск Telegram бота и планировщика"
|
||||
|
||||
# def handle(self, *args, **options):
|
||||
# # Установка Django окружения
|
||||
# os.environ.setdefault("DJANGO_SETTINGS_MODULE", "touchh.settings")
|
||||
# django.setup()
|
||||
|
||||
# # Создаем новый цикл событий
|
||||
# loop = asyncio.new_event_loop()
|
||||
# asyncio.set_event_loop(loop)
|
||||
|
||||
# # Настройка планировщика
|
||||
# scheduler = AsyncIOScheduler(event_loop=loop)
|
||||
# scheduler.start()
|
||||
|
||||
# # Загрузка задач в планировщик
|
||||
# try:
|
||||
# load_tasks_to_scheduler(scheduler)
|
||||
# except Exception as e:
|
||||
# self.stderr.write(f"Ошибка при загрузке задач в планировщик: {e}")
|
||||
# return
|
||||
|
||||
# # Настройка Telegram бота
|
||||
# # bot_token = os.getenv("TELEGRAM_BOT_TOKEN")
|
||||
# bot_token = TelegramSettings.objects.first().bot_token
|
||||
|
||||
# if not bot_token:
|
||||
# raise ValueError("Токен бота не найден в переменных окружения.")
|
||||
# application = Application.builder().token(bot_token).build()
|
||||
# setup_bot(application)
|
||||
# # Основная асинхронная функция
|
||||
# async def main():
|
||||
# await application.initialize()
|
||||
# await application.start()
|
||||
# await application.updater.start_polling()
|
||||
# self.stdout.write(self.style.SUCCESS("Telegram бот и планировщик успешно запущены."))
|
||||
# try:
|
||||
# while True:
|
||||
# await asyncio.sleep(3600)
|
||||
# except asyncio.CancelledError:
|
||||
# await application.stop()
|
||||
# scheduler.shutdown()
|
||||
|
||||
# # Запуск асинхронной программы
|
||||
# try:
|
||||
# loop.run_until_complete(main())
|
||||
# except KeyboardInterrupt:
|
||||
# self.stdout.write(self.style.ERROR("Завершение работы Telegram бота и планировщика"))
|
||||
# finally:
|
||||
# loop.close()
|
||||
|
||||
|
||||
|
||||
import os
|
||||
import django
|
||||
import asyncio
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from django.core.management.base import BaseCommand
|
||||
from telegram.ext import Application
|
||||
from bot.utils.bot_setup import setup_bot
|
||||
from scheduler.tasks import load_tasks_to_scheduler
|
||||
from app_settings.models import TelegramSettings
|
||||
from touchh.utils.log import CustomLogger
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Запуск Telegram бота и планировщика"
|
||||
help = "Запуск Telegram бота"
|
||||
|
||||
def handle(self, *args, **options):
|
||||
# Установка Django окружения
|
||||
@@ -21,42 +84,31 @@ class Command(BaseCommand):
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
# Настройка планировщика
|
||||
scheduler = AsyncIOScheduler(event_loop=loop)
|
||||
scheduler.start()
|
||||
|
||||
# Загрузка задач в планировщик
|
||||
try:
|
||||
load_tasks_to_scheduler(scheduler)
|
||||
except Exception as e:
|
||||
self.stderr.write(f"Ошибка при загрузке задач в планировщик: {e}")
|
||||
return
|
||||
|
||||
# Настройка Telegram бота
|
||||
# bot_token = os.getenv("TELEGRAM_BOT_TOKEN")
|
||||
bot_token = TelegramSettings.objects.first().bot_token
|
||||
|
||||
|
||||
if not bot_token:
|
||||
raise ValueError("Токен бота не найден в переменных окружения.")
|
||||
|
||||
application = Application.builder().token(bot_token).build()
|
||||
setup_bot(application)
|
||||
|
||||
# Основная асинхронная функция
|
||||
async def main():
|
||||
await application.initialize()
|
||||
await application.start()
|
||||
await application.updater.start_polling()
|
||||
self.stdout.write(self.style.SUCCESS("Telegram бот и планировщик успешно запущены."))
|
||||
self.stdout.write(self.style.SUCCESS("Telegram бот успешно запущен."))
|
||||
try:
|
||||
while True:
|
||||
await asyncio.sleep(3600)
|
||||
except asyncio.CancelledError:
|
||||
await application.stop()
|
||||
scheduler.shutdown()
|
||||
|
||||
# Запуск асинхронной программы
|
||||
try:
|
||||
loop.run_until_complete(main())
|
||||
except KeyboardInterrupt:
|
||||
self.stdout.write(self.style.ERROR("Завершение работы Telegram бота и планировщика"))
|
||||
self.stdout.write(self.style.ERROR("Завершение работы Telegram бота"))
|
||||
finally:
|
||||
loop.close()
|
||||
|
||||
@@ -4,6 +4,9 @@ from hotels.models import Hotel, UserHotel
|
||||
from users.models import User
|
||||
from pms_integration.manager import PMSIntegrationManager
|
||||
from bot.utils.froud_check import detect_fraud
|
||||
from touchh.utils.log import CustomLogger
|
||||
|
||||
logger = CustomLogger(name="BOT-hotels Manager", log_level="DEBUG").get_logger()
|
||||
async def manage_hotels(update: Update, context):
|
||||
"""Отображение списка отелей, связанных с пользователем."""
|
||||
query = update.callback_query
|
||||
@@ -81,7 +84,8 @@ async def check_pms(update, context):
|
||||
try:
|
||||
# Получение ID отеля из callback_data
|
||||
hotel_id = query.data.split("_")[2]
|
||||
|
||||
logger.debug(f"Hotel ID: {hotel_id}")
|
||||
logger.debug(f"Hotel ID type : {type(hotel_id)}")
|
||||
# Получение конфигурации отеля и PMS
|
||||
hotel = await sync_to_async(Hotel.objects.select_related('pms').get)(id=hotel_id)
|
||||
pms_config = hotel.pms
|
||||
@@ -99,6 +103,7 @@ async def check_pms(update, context):
|
||||
if hasattr(pms_manager.plugin, 'fetch_data') and callable(pms_manager.plugin.fetch_data):
|
||||
# Плагин поддерживает метод fetch_data
|
||||
report = await pms_manager.plugin._fetch_data()
|
||||
|
||||
else:
|
||||
await query.edit_message_text("Подходящий способ интеграции с PMS не найден.")
|
||||
return
|
||||
@@ -110,6 +115,7 @@ async def check_pms(update, context):
|
||||
f"Обработано записей: {report['processed_items']}\n"
|
||||
f"Ошибки: {len(report['errors'])}"
|
||||
)
|
||||
logger.info(f'Result_Message: {result_message}\n Result_meaage_type: {type(result_message)}')
|
||||
if report["errors"]:
|
||||
result_message += "\n\nСписок ошибок:\n" + "\n".join(report["errors"])
|
||||
|
||||
|
||||
@@ -11,8 +11,13 @@ from datetime import datetime
|
||||
from django.utils.timezone import make_aware, is_aware, is_naive
|
||||
import os
|
||||
import traceback
|
||||
import logging
|
||||
from touchh.utils.log import CustomLogger
|
||||
|
||||
|
||||
|
||||
logger = CustomLogger(__name__).get_logger()
|
||||
|
||||
async def statistics(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""Вывод списка отелей для статистики."""
|
||||
query = update.callback_query
|
||||
@@ -33,7 +38,7 @@ async def statistics(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
|
||||
# Формируем кнопки для выбора отеля
|
||||
keyboard = [
|
||||
[InlineKeyboardButton(hotel.hotel.name, callback_data=f"stats_hotel_{hotel.hotel.id}")]
|
||||
[InlineKeyboardButton(f'🏨 {hotel.hotel.name}', callback_data=f"stats_hotel_{hotel.hotel.id}")]
|
||||
for hotel in user_hotels
|
||||
]
|
||||
keyboard.append([InlineKeyboardButton("🏠 Главная", callback_data="main_menu")])
|
||||
@@ -61,27 +66,31 @@ async def stats_select_period(update: Update, context: ContextTypes.DEFAULT_TYPE
|
||||
|
||||
|
||||
def ensure_datetime(value):
|
||||
# print(f"statistics.py [DEBUG] ensure_datetime: Received value: {value} ({type(value)})")
|
||||
"""
|
||||
Ensure that the given value is a timezone-aware datetime object.
|
||||
|
||||
If the given value is a string, it is assumed to be in the format
|
||||
'%Y-%m-%d %H:%M:%S'. If the given value is a naive datetime object,
|
||||
it is converted to a timezone-aware datetime object using
|
||||
django.utils.timezone.make_aware.
|
||||
|
||||
:param value: The value to be converted
|
||||
:type value: str or datetime
|
||||
:return: A timezone-aware datetime object
|
||||
:rtype: datetime
|
||||
Преобразует значение в timezone-aware datetime объект, если это возможно.
|
||||
|
||||
:param value: Значение для преобразования
|
||||
:type value: str, datetime или другое
|
||||
:return: timezone-aware datetime
|
||||
"""
|
||||
if isinstance(value, str):
|
||||
value = datetime.strptime(value, '%Y-%m-%d %H:%M:%S')
|
||||
if isinstance(value, datetime) and is_naive(value):
|
||||
value = make_aware(value)
|
||||
# print(f"statistics.py [DEBUG] ensure_datetime: Returning value: {value} ({type(value)})")
|
||||
return value
|
||||
|
||||
if isinstance(value, datetime):
|
||||
# Если это объект datetime, проверяем, наивен ли он
|
||||
return make_aware(value) if is_naive(value) else value
|
||||
elif isinstance(value, str):
|
||||
# Если это строка, пытаемся преобразовать в datetime
|
||||
try:
|
||||
return make_aware(datetime.strptime(value, '%Y-%m-%d %H:%M:%S'))
|
||||
except ValueError:
|
||||
# Если формат не соответствует, пробуем более общую обработку
|
||||
try:
|
||||
return make_aware(datetime.fromisoformat(value))
|
||||
except ValueError:
|
||||
# Если не получилось распознать формат
|
||||
logging.warning(f"Невозможно преобразовать строку в datetime: {value}")
|
||||
else:
|
||||
# Если тип неизвестен, просто возвращаем None
|
||||
logging.warning(f"Получено значение неизвестного типа для преобразования в datetime: {value}")
|
||||
return None
|
||||
|
||||
async def generate_statistics(update: Update, context: ContextTypes.DEFAULT_TYPE):
|
||||
"""Генерация и отправка статистики."""
|
||||
@@ -90,77 +99,45 @@ async def generate_statistics(update: Update, context: ContextTypes.DEFAULT_TYPE
|
||||
|
||||
try:
|
||||
hotel_id = context.user_data.get("selected_hotel")
|
||||
|
||||
if not hotel_id:
|
||||
raise ValueError(f"ID отеля не найден в user_data: {context.user_data}")
|
||||
|
||||
period = query.data.split("_")[2]
|
||||
now = ensure_datetime(datetime.utcnow())
|
||||
|
||||
|
||||
now = datetime.utcnow().replace(tzinfo=timezone.utc)
|
||||
|
||||
print(type(now))
|
||||
|
||||
print(type(period))
|
||||
|
||||
# Получаем диапазон дат
|
||||
start_date, end_date = get_period_dates(period, now)
|
||||
|
||||
|
||||
try:
|
||||
# Получаем бронирования
|
||||
reservations = await sync_to_async(list)(
|
||||
Reservation.objects.filter(
|
||||
hotel_id=hotel_id,
|
||||
check_in__gte=start_date,
|
||||
check_in__lte=end_date
|
||||
).select_related('hotel')
|
||||
)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"statistics.py Ошибка при выборке бронирований: {e}") from e
|
||||
reservations = await sync_to_async(list)(
|
||||
Reservation.objects.filter(
|
||||
hotel_id=hotel_id,
|
||||
check_in__gte=start_date,
|
||||
check_in__lte=end_date
|
||||
).select_related('hotel')
|
||||
)
|
||||
|
||||
if not reservations:
|
||||
await query.edit_message_text("statistics.py Нет данных для статистики за выбранный период.")
|
||||
await query.edit_message_text("Нет данных для статистики за выбранный период.")
|
||||
return
|
||||
|
||||
try:
|
||||
# Получаем данные об отеле
|
||||
hotel = await sync_to_async(Hotel.objects.get)(id=hotel_id)
|
||||
|
||||
except Hotel.DoesNotExist:
|
||||
raise RuntimeError(f"statistics.py Отель с ID {hotel_id} не найден")
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"statistics.py Ошибка при выборке отеля: {e}") from e
|
||||
hotel = await sync_to_async(Hotel.objects.get)(id=hotel_id)
|
||||
|
||||
try:
|
||||
# Генерация отчета
|
||||
file_path = await generate_pdf_report(hotel.name, reservations, start_date, end_date)
|
||||
file_path = await generate_pdf_report(hotel.name, reservations, start_date, end_date)
|
||||
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"statistics.py [ERROR] Ошибка при генерации PDF-отчета: {e}") from e
|
||||
with open(file_path, "rb") as file:
|
||||
await query.message.reply_document(document=file, filename=f"{hotel.name}_report.pdf")
|
||||
|
||||
try:
|
||||
# Отправка файла через Telegram
|
||||
with open(file_path, "rb") as file:
|
||||
await query.message.reply_document(document=file, filename=f"{hotel.name}_report.pdf")
|
||||
|
||||
except Exception as e:
|
||||
raise RuntimeError(f"Ошибка при отправке PDF-файла: {e}") from e
|
||||
|
||||
# Удаляем временный файл
|
||||
if os.path.exists(file_path):
|
||||
os.remove(file_path)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
# Логируем стек вызовов для детального анализа
|
||||
error_trace = traceback.format_exc()
|
||||
|
||||
logging.error(f"Ошибка в generate_statistics: {str(e)}", exc_info=True)
|
||||
logging.error(f'start_date_type: {type(start_date)}, \n end_date_type: {type(end_date)}\n')
|
||||
await query.edit_message_text(f"Произошла ошибка: {str(e)}")
|
||||
|
||||
|
||||
def get_period_dates(period, now):
|
||||
now = ensure_datetime(now)
|
||||
if period == "day":
|
||||
start_date = (now - timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
end_date = now.replace(hour=23, minute=59, second=59, microsecond=999999)
|
||||
@@ -178,6 +155,7 @@ def get_period_dates(period, now):
|
||||
end_date = now.replace(hour=23, minute=59, second=59, microsecond=999999)
|
||||
return start_date, end_date
|
||||
|
||||
|
||||
async def stats_back(update: Update, context):
|
||||
"""Возврат к выбору отеля."""
|
||||
query = update.callback_query
|
||||
@@ -203,3 +181,6 @@ async def stats_back(update: Update, context):
|
||||
keyboard.append([InlineKeyboardButton("🏠 Главная", callback_data="main_menu")])
|
||||
reply_markup = InlineKeyboardMarkup(keyboard)
|
||||
await query.edit_message_text("Выберите отель:", reply_markup=reply_markup)
|
||||
|
||||
|
||||
|
||||
|
||||
53
bot/utils/date_utils.py
Normal file
53
bot/utils/date_utils.py
Normal file
@@ -0,0 +1,53 @@
|
||||
# bot/utils/date_utils.py
|
||||
from datetime import datetime, timedelta
|
||||
import pytz
|
||||
|
||||
def ensure_datetime(value):
|
||||
"""
|
||||
Приводит значение к объекту datetime с учетом временной зоны.
|
||||
|
||||
:param value: Значение даты (строка или datetime).
|
||||
:return: Объект datetime.
|
||||
:raises: TypeError, если передан некорректный тип.
|
||||
"""
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
# Если строка соответствует формату ISO 8601
|
||||
return datetime.fromisoformat(value)
|
||||
except ValueError:
|
||||
raise ValueError(f"Некорректный формат даты: {value}")
|
||||
elif isinstance(value, datetime):
|
||||
return value
|
||||
else:
|
||||
raise TypeError(f"Ожидался тип str или datetime, получено: {type(value)}")
|
||||
|
||||
def get_period_dates(period, now=None):
|
||||
"""
|
||||
Возвращает диапазон дат (start_date, end_date) для заданного периода.
|
||||
|
||||
:param period: Период (строка: 'today', 'yesterday', 'last_week', 'last_month').
|
||||
:param now: Текущая дата/время (опционально).
|
||||
:return: Кортеж (start_date, end_date).
|
||||
:raises: ValueError, если период не поддерживается.
|
||||
"""
|
||||
if now is None:
|
||||
now = datetime.now(pytz.UTC)
|
||||
|
||||
if period == "today":
|
||||
start_date = now.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
end_date = now.replace(hour=23, minute=59, second=59, microsecond=999999)
|
||||
elif period == "yesterday":
|
||||
start_date = (now - timedelta(days=1)).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
end_date = (now - timedelta(days=1)).replace(hour=23, minute=59, second=59, microsecond=999999)
|
||||
elif period == "last_week":
|
||||
start_date = (now - timedelta(days=now.weekday() + 7)).replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
end_date = (start_date + timedelta(days=6)).replace(hour=23, minute=59, second=59, microsecond=999999)
|
||||
elif period == "last_month":
|
||||
first_day_of_current_month = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||
last_day_of_previous_month = first_day_of_current_month - timedelta(days=1)
|
||||
start_date = last_day_of_previous_month.replace(day=1)
|
||||
end_date = last_day_of_previous_month.replace(hour=23, minute=59, second=59, microsecond=999999)
|
||||
else:
|
||||
raise ValueError(f"Неподдерживаемый период: {period}")
|
||||
|
||||
return start_date, end_date
|
||||
@@ -17,22 +17,49 @@ os.makedirs(REPORTS_DIR, exist_ok=True)
|
||||
|
||||
# Асинхронная функция для извлечения данных о бронировании
|
||||
def ensure_datetime(value):
|
||||
"""Преобразует строку или naive datetime в timezone-aware datetime."""
|
||||
"""
|
||||
Преобразует строку или naive datetime в timezone-aware datetime.
|
||||
Если значение не удается преобразовать, возвращается None.
|
||||
"""
|
||||
if isinstance(value, datetime):
|
||||
return make_aware(value) if is_naive(value) else value
|
||||
if isinstance(value, str):
|
||||
value = datetime.strptime(value, '%Y-%m-%d %H:%M:%S')
|
||||
if isinstance(value, datetime) and is_naive(value):
|
||||
value = make_aware(value)
|
||||
return value
|
||||
try:
|
||||
return make_aware(datetime.strptime(value, '%Y-%m-%d %H:%M:%S'))
|
||||
except ValueError:
|
||||
print(f"[WARNING] Невозможно преобразовать строку в datetime: {value}")
|
||||
return None
|
||||
|
||||
# @sync_to_async
|
||||
# def get_reservation_data(res):
|
||||
# print(f"[DEBUG] Processing reservation {res.id}")
|
||||
|
||||
# # Убедитесь, что даты являются timezone-aware
|
||||
# check_in = ensure_datetime(res.check_in)
|
||||
# check_out = ensure_datetime(res.check_out)
|
||||
|
||||
# result = {
|
||||
# "hotel_name": res.hotel.name,
|
||||
# "pms": getattr(res.hotel, 'pms', 'N/A'),
|
||||
# "reservation_id": res.reservation_id,
|
||||
# "room_number": res.room_number if res.room_number else "Не указан",
|
||||
# "room_type": res.room_type,
|
||||
# "check_in": check_in,
|
||||
# "check_out": check_out,
|
||||
# "status": res.status,
|
||||
# }
|
||||
# # print(f"[DEBUG] Reservation data: {result}")
|
||||
# return result
|
||||
|
||||
@sync_to_async
|
||||
def get_reservation_data(res):
|
||||
print(f"[DEBUG] Processing reservation {res.id}")
|
||||
|
||||
# Убедитесь, что даты являются timezone-aware
|
||||
check_in = ensure_datetime(res.check_in)
|
||||
check_out = ensure_datetime(res.check_out)
|
||||
|
||||
result = {
|
||||
|
||||
if not check_in or not check_out:
|
||||
raise ValueError(f"Некорректные даты бронирования: check_in={res.check_in}, check_out={res.check_out}")
|
||||
|
||||
return {
|
||||
"hotel_name": res.hotel.name,
|
||||
"pms": getattr(res.hotel, 'pms', 'N/A'),
|
||||
"reservation_id": res.reservation_id,
|
||||
@@ -42,8 +69,6 @@ def get_reservation_data(res):
|
||||
"check_out": check_out,
|
||||
"status": res.status,
|
||||
}
|
||||
# print(f"[DEBUG] Reservation data: {result}")
|
||||
return result
|
||||
|
||||
|
||||
|
||||
@@ -61,27 +86,21 @@ class CustomPDF(FPDF):
|
||||
self.end_date = end_date
|
||||
|
||||
def header(self):
|
||||
"""Добавление заголовка и заголовков таблицы на каждой странице."""
|
||||
# Заголовок отчёта
|
||||
if self.page == 1: # Заголовок отчёта только на первой странице
|
||||
if self.page == 1:
|
||||
self.set_font("DejaVuSans-Bold", size=14)
|
||||
self.cell(0, 10, f"Отчет о бронированиях отеля {self.hotel_name}", ln=1, align="C")
|
||||
self.ln(5)
|
||||
|
||||
self.set_font("DejaVuSans", size=10)
|
||||
self.cell(0, 10, f"за период {self.start_date.strftime('%Y-%m-%d %H:%M:%S')} - {self.end_date.strftime('%Y-%m-%d %H:%M:%S')}", ln=1, align="C")
|
||||
self.cell(
|
||||
0,
|
||||
10,
|
||||
f"за период {self.start_date.strftime('%Y-%m-%d %H:%M:%S')} - {self.end_date.strftime('%Y-%m-%d %H:%M:%S')}",
|
||||
ln=1,
|
||||
align="C"
|
||||
)
|
||||
self.ln(10)
|
||||
|
||||
# Заголовки таблицы
|
||||
self.set_font("DejaVuSans-Bold", size=8)
|
||||
headers = ["Отель", "№ бронирования", "№ комнаты", "Тип комнаты", "Заезд", "Выезд", "Статус"]
|
||||
col_widths = [30, 30, 30, 60, 35, 35, 30]
|
||||
row_height = 10
|
||||
|
||||
for col_width, header in zip(col_widths, headers):
|
||||
self.cell(col_width, row_height, header, border=1, align="C")
|
||||
self.ln()
|
||||
|
||||
def footer(self):
|
||||
"""Добавление колонтитула внизу страницы."""
|
||||
self.set_y(-15)
|
||||
@@ -97,13 +116,68 @@ class CustomPDF(FPDF):
|
||||
text = text[:-1]
|
||||
return text + "..." if len(text) > 3 else text
|
||||
|
||||
# async def generate_pdf_report(hotel_name, reservations, start_date, end_date):
|
||||
|
||||
# # Преобразование дат в timezone-aware datetime
|
||||
# start_date = ensure_datetime(start_date)
|
||||
# end_date = ensure_datetime(end_date)
|
||||
|
||||
# if not start_date or not end_date:
|
||||
# raise ValueError(f"Некорректные периоды: start_date={start_date}, end_date={end_date}")
|
||||
|
||||
# # Создание экземпляра PDF с передачей параметров
|
||||
# pdf = CustomPDF(hotel_name=hotel_name, start_date=start_date, end_date=end_date, orientation="L", unit="mm", format="A4")
|
||||
# pdf.alias_nb_pages()
|
||||
# pdf.add_page() # Заголовок отчёта и таблица будут добавлены через методы header и footer
|
||||
|
||||
# # Таблица
|
||||
# pdf.set_font("DejaVuSans", size=8)
|
||||
# col_widths = [30, 30, 30, 60, 35, 35, 30]
|
||||
# row_height = 10
|
||||
|
||||
# for res in reservations:
|
||||
# try:
|
||||
# res_data = await get_reservation_data(res)
|
||||
|
||||
# row_data = [
|
||||
# res_data["hotel_name"],
|
||||
# str(res_data["reservation_id"]),
|
||||
# res_data["room_number"],
|
||||
# res_data["room_type"],
|
||||
# res_data["check_in"].strftime('%Y-%m-%d %H:%M:%S'),
|
||||
# res_data["check_out"].strftime('%Y-%m-%d %H:%M:%S'),
|
||||
# res_data["status"],
|
||||
# ]
|
||||
|
||||
# for col_width, data in zip(col_widths, row_data):
|
||||
# pdf.cell(col_width, row_height, data, border=1, align="C")
|
||||
# pdf.ln()
|
||||
# except Exception as e:
|
||||
# print(f"pdf_report.py [ERROR] Error processing reservation {res.id}: {e}")
|
||||
|
||||
# # Сохранение PDF
|
||||
# hotel_name_safe = hotel_name.replace(" ", "_").replace("/", "_")
|
||||
# start_date_str = start_date.strftime('%Y-%m-%d')
|
||||
# end_date_str = end_date.strftime('%Y-%m-%d')
|
||||
|
||||
# pdf_output_path = os.path.join(REPORTS_DIR, f"{hotel_name_safe}_report_{start_date_str}-{end_date_str}.pdf")
|
||||
# pdf.output(pdf_output_path)
|
||||
|
||||
# if not os.path.exists(pdf_output_path):
|
||||
# raise RuntimeError(f"PDF file was not created at: {pdf_output_path}")
|
||||
|
||||
# return pdf_output_path
|
||||
|
||||
|
||||
async def generate_pdf_report(hotel_name, reservations, start_date, end_date):
|
||||
# Создание экземпляра PDF с передачей параметров
|
||||
# Преобразование дат
|
||||
start_date = ensure_datetime(start_date)
|
||||
end_date = ensure_datetime(end_date)
|
||||
|
||||
pdf = CustomPDF(hotel_name=hotel_name, start_date=start_date, end_date=end_date, orientation="L", unit="mm", format="A4")
|
||||
pdf.alias_nb_pages()
|
||||
pdf.add_page() # Заголовок отчёта и таблица будут добавлены через методы header и footer
|
||||
pdf.add_page()
|
||||
|
||||
# Таблица
|
||||
pdf.set_font("DejaVuSans", size=8)
|
||||
col_widths = [30, 30, 30, 60, 35, 35, 30]
|
||||
row_height = 10
|
||||
@@ -111,29 +185,36 @@ async def generate_pdf_report(hotel_name, reservations, start_date, end_date):
|
||||
for res in reservations:
|
||||
try:
|
||||
res_data = await get_reservation_data(res)
|
||||
|
||||
# Отладочный вывод
|
||||
print(f"[DEBUG] Reservation Data: {res_data}")
|
||||
print(f"[DEBUG] check_in type: {type(res_data['check_in'])}, value: {res_data['check_in']}")
|
||||
print(f"[DEBUG] check_out type: {type(res_data['check_out'])}, value: {res_data['check_out']}")
|
||||
|
||||
# Проверка и корректировка данных
|
||||
res_data["check_in"] = ensure_datetime(res_data["check_in"])
|
||||
res_data["check_out"] = ensure_datetime(res_data["check_out"])
|
||||
|
||||
row_data = [
|
||||
res_data["hotel_name"],
|
||||
str(res_data["hotel_name"]),
|
||||
str(res_data["reservation_id"]),
|
||||
res_data["room_number"],
|
||||
res_data["room_type"],
|
||||
str(res_data["room_number"]),
|
||||
str(res_data["room_type"]),
|
||||
res_data["check_in"].strftime('%Y-%m-%d %H:%M:%S'),
|
||||
res_data["check_out"].strftime('%Y-%m-%d %H:%M:%S'),
|
||||
res_data["status"],
|
||||
str(res_data["status"]),
|
||||
]
|
||||
|
||||
|
||||
for col_width, data in zip(col_widths, row_data):
|
||||
pdf.cell(col_width, row_height, data, border=1, align="C")
|
||||
pdf.ln()
|
||||
except Exception as e:
|
||||
print(f"pdf_report.py [ERROR] Error processing reservation {res.id}: {e}")
|
||||
print(f"[ERROR] Error processing reservation {res.id}: {e}")
|
||||
|
||||
print("[DEBUG] PDF metadata:", vars(pdf))
|
||||
|
||||
# Сохранение PDF
|
||||
hotel_name_safe = hotel_name.replace(" ", "_").replace("/", "_")
|
||||
start_date_str = start_date.strftime('%Y-%m-%d')
|
||||
end_date_str = end_date.strftime('%Y-%m-%d')
|
||||
|
||||
pdf_output_path = os.path.join(REPORTS_DIR, f"{hotel_name_safe}_report_{start_date_str}-{end_date_str}.pdf")
|
||||
pdf_output_path = os.path.join(REPORTS_DIR, f"{hotel_name.replace(' ', '_')}_report_{start_date.strftime('%Y-%m-%d')}-{end_date.strftime('%Y-%m-%d')}.pdf")
|
||||
pdf.output(pdf_output_path)
|
||||
|
||||
if not os.path.exists(pdf_output_path):
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
from bot.operations.notifications import schedule_notifications
|
||||
|
||||
|
||||
def setup_scheduler():
|
||||
"""Настройка планировщика уведомлений."""
|
||||
print("Настройка планировщика...")
|
||||
scheduler = AsyncIOScheduler()
|
||||
scheduler.add_job(schedule_notifications, "cron", minute="*")
|
||||
return scheduler
|
||||
@@ -1,116 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-09 09:30
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='APIConfiguration',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255, verbose_name='Название API')),
|
||||
('url', models.URLField(verbose_name='URL API')),
|
||||
('token', models.CharField(blank=True, max_length=255, null=True, verbose_name='Токен')),
|
||||
('username', models.CharField(blank=True, max_length=255, null=True, verbose_name='Логин')),
|
||||
('password', models.CharField(blank=True, max_length=255, null=True, verbose_name='Пароль')),
|
||||
('last_updated', models.DateTimeField(auto_now=True, verbose_name='Дата последнего обновления')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Конфигурация API',
|
||||
'verbose_name_plural': 'Конфигурации API',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='FraudLog',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('reservation_id', models.BigIntegerField(unique=True, verbose_name='ID бронирования')),
|
||||
('guest_name', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('check_in_date', models.DateField()),
|
||||
('detected_at', models.DateTimeField(auto_now_add=True)),
|
||||
('message', models.TextField()),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Журнал мошенничества',
|
||||
'verbose_name_plural': 'Журналы мошенничества',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Guest',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255, verbose_name='Имя гостя')),
|
||||
('birthdate', models.DateField(blank=True, null=True, verbose_name='Дата рождения')),
|
||||
('phone', models.CharField(blank=True, max_length=50, null=True, verbose_name='Телефон')),
|
||||
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='Email')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Гость',
|
||||
'verbose_name_plural': 'Гости',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Reservation',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('reservation_id', models.BigIntegerField(unique=True, verbose_name='ID бронирования')),
|
||||
('room_number', models.CharField(max_length=50, verbose_name='Номер комнаты')),
|
||||
('room_type', models.CharField(max_length=255, verbose_name='Тип комнаты')),
|
||||
('check_in', models.DateTimeField(verbose_name='Дата заезда')),
|
||||
('check_out', models.DateTimeField(verbose_name='Дата выезда')),
|
||||
('status', models.CharField(max_length=50, verbose_name='Статус')),
|
||||
('price', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='Цена')),
|
||||
('discount', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='Скидка')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Бронирование',
|
||||
'verbose_name_plural': 'Бронирования',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserHotel',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Пользователь отеля',
|
||||
'verbose_name_plural': 'Пользователи отелей',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='APIRequestLog',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('request_time', models.DateTimeField(auto_now_add=True, verbose_name='Время запроса')),
|
||||
('response_status', models.IntegerField(validators=[django.core.validators.MinValueValidator(100), django.core.validators.MaxValueValidator(599)], verbose_name='HTTP статус ответа')),
|
||||
('response_data', models.JSONField(blank=True, null=True, verbose_name='Данные ответа')),
|
||||
('api', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='hotels.apiconfiguration', verbose_name='API')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Журнал запросов API',
|
||||
'verbose_name_plural': 'Журналы запросов API',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Hotel',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255, verbose_name='Название отеля')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Создан')),
|
||||
('api', models.OneToOneField(blank=True, help_text='API, связанный с этим отелем.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='hotels.apiconfiguration', verbose_name='API')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Отель',
|
||||
'verbose_name_plural': 'Отели',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,42 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-09 09:30
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('hotels', '0001_initial'),
|
||||
('pms_integration', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='hotel',
|
||||
name='pms',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='pms_integration.pmsconfiguration', verbose_name='PMS система'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='fraudlog',
|
||||
name='hotel',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='frauds', to='hotels.hotel'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='reservation',
|
||||
name='hotel',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='hotels.hotel', verbose_name='Отель'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='guest',
|
||||
name='reservation',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='guests', to='hotels.reservation', verbose_name='Бронирование'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='userhotel',
|
||||
name='hotel',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='hotel_users', to='hotels.hotel', verbose_name='Отель'),
|
||||
),
|
||||
]
|
||||
@@ -1,30 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-09 09:30
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('hotels', '0002_initial'),
|
||||
('users', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='userhotel',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_hotels', to='users.user', verbose_name='Пользователь'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='apirequestlog',
|
||||
index=models.Index(fields=['api'], name='hotels_apir_api_id_686bb0_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='apirequestlog',
|
||||
index=models.Index(fields=['request_time'], name='hotels_apir_request_f65147_idx'),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-11 10:52
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('hotels', '0003_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='reservation',
|
||||
name='room_number',
|
||||
field=models.CharField(blank=True, max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,29 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-13 01:01
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('hotels', '0004_alter_reservation_room_number'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='hotel',
|
||||
name='import_status',
|
||||
field=models.CharField(choices=[('not_started', 'Не начат'), ('in_progress', 'В процессе'), ('completed', 'Завершен')], default='not_started', max_length=50, verbose_name='Статус импорта'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='hotel',
|
||||
name='imported_at',
|
||||
field=models.DateTimeField(blank=True, null=True, verbose_name='Дата импорта'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='hotel',
|
||||
name='imported_from',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='imported_hotels', to='hotels.hotel', verbose_name='Импортированный отель'),
|
||||
),
|
||||
]
|
||||
@@ -1,33 +0,0 @@
|
||||
# hotels/migrations/0006_remove_hotel_import_status_remove_hotel_imported_at_and_more.py
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
def remove_unused_fields(apps, schema_editor):
|
||||
Hotel = apps.get_model('hotels', 'Hotel')
|
||||
Hotel._meta.get_field('import_status').remote_field.model._meta.db_table
|
||||
Hotel._meta.get_field('imported_at').remote_field.model._meta.db_table
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('hotels', '0005_hotel_import_status_hotel_imported_at_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='hotel',
|
||||
name='import_status',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='hotel',
|
||||
name='imported_at',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='hotel',
|
||||
name='imported_from',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='hotel',
|
||||
name='api',
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-14 02:43
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('hotels', '0006_remove_hotel_api_remove_hotel_import_status_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='hotel',
|
||||
name='hotel_id',
|
||||
field=models.CharField(blank=True, max_length=255, null=True, unique=True, verbose_name='ID отеля'),
|
||||
),
|
||||
]
|
||||
@@ -1,38 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-14 02:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('hotels', '0007_hotel_hotel_id'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='hotel',
|
||||
name='address',
|
||||
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Адрес'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='hotel',
|
||||
name='city',
|
||||
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Город'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='hotel',
|
||||
name='email',
|
||||
field=models.EmailField(blank=True, max_length=254, null=True, verbose_name='Email'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='hotel',
|
||||
name='phone',
|
||||
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='Телефон'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='hotel',
|
||||
name='timezone',
|
||||
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Часовой пояс'),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-14 02:55
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('hotels', '0008_hotel_address_hotel_city_hotel_email_hotel_phone_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='hotel',
|
||||
name='description',
|
||||
field=models.TextField(blank=True, null=True, verbose_name='Описание'),
|
||||
),
|
||||
]
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-17 11:21
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('hotels', '0011_room_alter_fraudlog_check_in_date_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='room',
|
||||
name='number',
|
||||
field=models.CharField(max_length=50, unique=True, verbose_name='Номер комнаты'),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-17 11:21
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('hotels', '0012_alter_room_number'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='room',
|
||||
name='number',
|
||||
field=models.CharField(max_length=50, verbose_name='Номер комнаты'),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-17 11:24
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('hotels', '0013_alter_room_number'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='room',
|
||||
name='number',
|
||||
field=models.CharField(max_length=50, unique=True, verbose_name='Номер комнаты'),
|
||||
),
|
||||
]
|
||||
@@ -18,7 +18,7 @@ class PluginLoader:
|
||||
print("Загрузка плагинов:")
|
||||
for file in os.listdir(PluginLoader.PLUGIN_PATH):
|
||||
if file.endswith("_pms.py") and not file.startswith("__"):
|
||||
print(f" Plugin {file}")
|
||||
# print(f" Plugin {file}")
|
||||
module_name = f"pms_integration.plugins.{file[:-3]}"
|
||||
try:
|
||||
module = importlib.import_module(module_name)
|
||||
@@ -26,7 +26,7 @@ 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__}")
|
||||
# print(f" Загружен плагин {plugin_name}: {cls.__name__}")
|
||||
plugins[plugin_name] = cls
|
||||
except Exception as e:
|
||||
print(f" Ошибка загрузки плагина {module_name}: {e}")
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-09 09:30
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('hotels', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='PMSConfiguration',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=255, verbose_name='Название PMS')),
|
||||
('url', models.URLField(verbose_name='URL API')),
|
||||
('token', models.CharField(blank=True, max_length=255, null=True, verbose_name='Токен')),
|
||||
('username', models.CharField(blank=True, max_length=255, null=True, verbose_name='Логин')),
|
||||
('password', models.CharField(blank=True, max_length=255, null=True, verbose_name='Пароль')),
|
||||
('plugin_name', models.CharField(max_length=255, verbose_name='Название плагина')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'PMS система',
|
||||
'verbose_name_plural': 'PMS системы',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PMSIntegrationLog',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('checked_at', models.DateTimeField(auto_now_add=True, verbose_name='Время проверки')),
|
||||
('status', models.CharField(choices=[('success', 'Успех'), ('error', 'Ошибка')], max_length=50, verbose_name='Статус')),
|
||||
('message', models.TextField(blank=True, null=True, verbose_name='Сообщение')),
|
||||
('hotel', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='hotels.hotel', verbose_name='Отель')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Журнал интеграции PMS',
|
||||
'verbose_name_plural': 'Журналы интеграции PMS',
|
||||
'indexes': [models.Index(fields=['hotel'], name='pms_integra_hotel_i_ade4da_idx'), models.Index(fields=['checked_at'], name='pms_integra_checked_938acc_idx'), models.Index(fields=['status'], name='pms_integra_status_358b64_idx')],
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-10 02:52
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pms_integration', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='pmsconfiguration',
|
||||
name='plugin_name',
|
||||
field=models.CharField(blank=True, choices=[], max_length=255, null=True),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-10 02:58
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pms_integration', '0002_alter_pmsconfiguration_plugin_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='pmsconfiguration',
|
||||
name='plugin_name',
|
||||
field=models.CharField(blank=True, choices=[], max_length=255, null=True, verbose_name='Плагин'),
|
||||
),
|
||||
]
|
||||
@@ -1,18 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-10 03:00
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pms_integration', '0003_alter_pmsconfiguration_plugin_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='pmsconfiguration',
|
||||
name='plugin_name',
|
||||
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Плагин'),
|
||||
),
|
||||
]
|
||||
@@ -1,23 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-10 03:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('pms_integration', '0004_alter_pmsconfiguration_plugin_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='pmsconfiguration',
|
||||
name='private_key',
|
||||
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Приватный ключ'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='pmsconfiguration',
|
||||
name='public_key',
|
||||
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Публичный ключ'),
|
||||
),
|
||||
]
|
||||
@@ -74,7 +74,7 @@ class EcviPMSPlugin(BasePMSPlugin):
|
||||
except requests.exceptions.RequestException as e:
|
||||
self.logger.error(f"Ошибка запроса: {e}")
|
||||
return []
|
||||
self.logger.debug(f"\n\n\n\n\ndata: {data}\n\n\n\n\n")
|
||||
|
||||
# Фильтрация данных
|
||||
filtered_data = [
|
||||
{
|
||||
@@ -87,7 +87,6 @@ class EcviPMSPlugin(BasePMSPlugin):
|
||||
} for item in data if isinstance(item, dict) and item.get('occupancy') in ['проживание', 'под выезд', 'под заезд']
|
||||
]
|
||||
|
||||
|
||||
self.logger.debug(f"filtered_data: {filtered_data}")
|
||||
|
||||
# Сохранение данных в базу данных
|
||||
@@ -108,7 +107,6 @@ class EcviPMSPlugin(BasePMSPlugin):
|
||||
|
||||
# Проверяем, существует ли уже резервация с таким внешним ID
|
||||
reservation_id = item.get('reservation_id')
|
||||
self.logger.debug(f"-----\n\n\nITEM : {item}\n\n\n---")
|
||||
if not reservation_id:
|
||||
self.logger.error("Ошибка: 'reservation_id' отсутствует в данных.")
|
||||
return
|
||||
@@ -123,6 +121,7 @@ class EcviPMSPlugin(BasePMSPlugin):
|
||||
await sync_to_async(Reservation.objects.update_or_create)(
|
||||
reservation_id=reservation_id,
|
||||
defaults={
|
||||
''
|
||||
'room_number': item.get('room_number'),
|
||||
'room_type': item.get('room_type'),
|
||||
'check_in': item.get('checkin'),
|
||||
|
||||
@@ -1,139 +1,20 @@
|
||||
# import requests
|
||||
# import hashlib
|
||||
# import json
|
||||
# from .base_plugin import BasePMSPlugin
|
||||
# from datetime import datetime, timedelta
|
||||
# from asgiref.sync import sync_to_async
|
||||
|
||||
|
||||
# class RealtyCalendarPlugin(BasePMSPlugin):
|
||||
# """Плагин для импорта данных из системы RealtyCalendar
|
||||
# """
|
||||
# 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("/") # Убираем лишний `/` в конце URL
|
||||
|
||||
# if not self.public_key or not self.private_key:
|
||||
# raise ValueError("Публичный или приватный ключ отсутствует для RealtyCalendar")
|
||||
# def get_default_parser_settings(self):
|
||||
# """
|
||||
# Возвращает настройки по умолчанию для обработки данных.
|
||||
# """
|
||||
# return {
|
||||
# "date_format": "%Y-%m-%dT%H:%M:%S",
|
||||
# "timezone": "UTC"
|
||||
# }
|
||||
# def _get_sorted_keys(self, obj):
|
||||
# """
|
||||
# Возвращает отсортированный по имени список ключей.
|
||||
# """
|
||||
# return sorted(list(obj.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)
|
||||
# return string + self.private_key
|
||||
|
||||
# def _generate_md5(self, string):
|
||||
# """
|
||||
# Генерирует MD5-хеш от строки.
|
||||
# """
|
||||
# return hashlib.md5(string.encode("utf-8")).hexdigest()
|
||||
|
||||
# def _generate_sign(self, data):
|
||||
# """
|
||||
# Генерирует подпись для данных запроса.
|
||||
# """
|
||||
# data_string = self._generate_data_string(data)
|
||||
# return self._generate_md5(data_string)
|
||||
|
||||
# def fetch_data(self):
|
||||
# """
|
||||
# Выполняет запрос к API RealtyCalendar для получения данных о бронированиях.
|
||||
# """
|
||||
# base_url = f"https://realtycalendar.ru/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)
|
||||
|
||||
# # Отправляем запрос
|
||||
# print(f"URL запроса: {base_url}")
|
||||
# print(f"Заголовки: {headers}")
|
||||
# print(f"Данные запроса: {data}")
|
||||
|
||||
# response = requests.post(url=base_url, headers=headers, json=data)
|
||||
|
||||
# # Логируем результат
|
||||
# print(f"Статус ответа: {response.status_code}")
|
||||
# print(f"Ответ: {response.text}")
|
||||
|
||||
# # Проверяем успешность запроса
|
||||
# if response.status_code == 200:
|
||||
# return response.json().get("bookings", [])
|
||||
# else:
|
||||
# raise ValueError(f"Ошибка API RealtyCalendar: {response.status_code}, {response.text}")
|
||||
|
||||
# async def _save_to_db(self, data, hotel_id):
|
||||
# """
|
||||
# Сохраняет данные о бронированиях в базу данных.
|
||||
# """
|
||||
# from hotels.models import Reservation, Hotel
|
||||
|
||||
# hotel = await sync_to_async(Hotel.objects.get)(id=hotel_id)
|
||||
# for item in data:
|
||||
# try:
|
||||
# reservation, created = await sync_to_async(Reservation.objects.update_or_create)(
|
||||
# reservation_id=item["id"],
|
||||
# hotel=hotel,
|
||||
# defaults={
|
||||
# "room_number": item.get("apartment_id", ""), # ID квартиры
|
||||
# "check_in": datetime.strptime(item["begin_date"], "%Y-%m-%d"), # Дата заезда
|
||||
# "check_out": datetime.strptime(item["end_date"], "%Y-%m-%d"), # Дата выезда
|
||||
# "status": item.get("status", ""), # Статус бронирования
|
||||
# "price": item.get("amount", 0), # Сумма оплаты
|
||||
# "client_name": item["client"].get("fio", ""), # Имя клиента
|
||||
# "client_email": item["client"].get("email", ""), # Email клиента
|
||||
# "client_phone": item["client"].get("phone", ""), # Телефон клиента
|
||||
# }
|
||||
# )
|
||||
# print(f"{'Создана' if created else 'Обновлена'} запись: {reservation}")
|
||||
# except Exception as e:
|
||||
# print(f"Ошибка при сохранении бронирования ID {item['id']}: {e}")
|
||||
|
||||
|
||||
import requests
|
||||
import hashlib
|
||||
import json
|
||||
from .base_plugin import BasePMSPlugin
|
||||
from datetime import datetime, timedelta
|
||||
from asgiref.sync import sync_to_async
|
||||
|
||||
|
||||
from touchh.utils.log import CustomLogger
|
||||
from hotels.models import Hotel, Reservation
|
||||
from app_settings.models import GlobalHotelSettings
|
||||
from django.utils import timezone
|
||||
class RealtyCalendarPlugin(BasePMSPlugin):
|
||||
"""Плагин для импорта данных из системы RealtyCalendar
|
||||
"""
|
||||
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")
|
||||
|
||||
@@ -150,8 +31,8 @@ class RealtyCalendarPlugin(BasePMSPlugin):
|
||||
"""
|
||||
Возвращает отсортированный по имени список ключей.
|
||||
"""
|
||||
sorted_keys = sorted(list(obj.keys()))
|
||||
print(f"[DEBUG] Отсортированные ключи: {sorted_keys}")
|
||||
sorted_keys = sorted(obj.keys())
|
||||
self.logger.debug(f"Отсортированные ключи: {sorted_keys}")
|
||||
return sorted_keys
|
||||
|
||||
def _generate_data_string(self, obj):
|
||||
@@ -160,7 +41,7 @@ class RealtyCalendarPlugin(BasePMSPlugin):
|
||||
"""
|
||||
sorted_keys = self._get_sorted_keys(obj)
|
||||
string = "".join(f"{key}={obj[key]}" for key in sorted_keys)
|
||||
print(f"[DEBUG] Сформированная строка данных: {string}")
|
||||
self.logger.debug(f"Сформированная строка данных: {string}")
|
||||
return string + self.private_key
|
||||
|
||||
def _generate_md5(self, string):
|
||||
@@ -168,7 +49,7 @@ class RealtyCalendarPlugin(BasePMSPlugin):
|
||||
Генерирует MD5-хеш от строки.
|
||||
"""
|
||||
md5_hash = hashlib.md5(string.encode("utf-8")).hexdigest()
|
||||
print(f"[DEBUG] Сформированный MD5-хеш: {md5_hash}")
|
||||
self.logger.debug(f"Сформированный MD5-хеш: {md5_hash}")
|
||||
return md5_hash
|
||||
|
||||
def _generate_sign(self, data):
|
||||
@@ -176,79 +57,133 @@ class RealtyCalendarPlugin(BasePMSPlugin):
|
||||
Генерирует подпись для данных запроса.
|
||||
"""
|
||||
data_string = self._generate_data_string(data)
|
||||
print(f"[DEBUG] Строка для подписи: {data_string}")
|
||||
self.logger.debug(f"Строка для подписи: {data_string}")
|
||||
sign = self._generate_md5(data_string)
|
||||
print(f"[DEBUG] Подпись: {sign}")
|
||||
self.logger.debug(f"Подпись: {sign}")
|
||||
return sign
|
||||
|
||||
def _fetch_data(self):
|
||||
async def _fetch_data(self):
|
||||
"""
|
||||
Выполняет запрос к API RealtyCalendar для получения данных о бронированиях.
|
||||
"""
|
||||
base_url = f"https://realtycalendar.ru/api/v1/bookings/{self.public_key}/"
|
||||
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"),
|
||||
}
|
||||
|
||||
print(f"[DEBUG] Даты выборки: {data}")
|
||||
|
||||
# Генерация подписи
|
||||
data["sign"] = self._generate_sign(data)
|
||||
|
||||
# Отправляем запрос
|
||||
print(f"[DEBUG] URL запроса: {base_url}")
|
||||
print(f"[DEBUG] Заголовки: {headers}")
|
||||
print(f"[DEBUG] Данные запроса: {data}")
|
||||
|
||||
response = requests.post(url=base_url, headers=headers, json=data)
|
||||
self.logger.debug(f"Статус ответа: {response.status_code}")
|
||||
|
||||
# Логируем результат
|
||||
print(f"[DEBUG] Статус ответа: {response.status_code}")
|
||||
print(f"[DEBUG] Ответ: {response.text}")
|
||||
if response.status_code != 200:
|
||||
self.logger.error(f"Ошибка API: {response.status_code}, {response.text}")
|
||||
raise ValueError(f"Ошибка API RealtyCalendar: {response.status_code}")
|
||||
|
||||
# Проверяем успешность запроса
|
||||
if response.status_code == 200:
|
||||
bookings = response.json().get("bookings", [])
|
||||
print(f"[DEBUG] Полученные данные бронирований: {bookings}")
|
||||
return bookings
|
||||
else:
|
||||
raise ValueError(f"Ошибка API RealtyCalendar: {response.status_code}, {response.text}")
|
||||
try:
|
||||
response_data = response.json()
|
||||
bookings = response_data.get("bookings", [])
|
||||
if not isinstance(bookings, list):
|
||||
raise ValueError(f"Ожидался список, но получен {type(bookings)}")
|
||||
except Exception as e:
|
||||
self.logger.error(f"Ошибка обработки ответа API: {e}")
|
||||
raise
|
||||
|
||||
# Получаем глобальные настройки отеля
|
||||
hotel = await sync_to_async(Hotel.objects.get)(pms=self.pms_config)
|
||||
hotel_tz = hotel.timezone
|
||||
try:
|
||||
hotel_settings = await sync_to_async(GlobalHotelSettings.objects.first)()
|
||||
check_in_time = hotel_settings.check_in_time.strftime("%H:%M:%S")
|
||||
check_out_time = hotel_settings.check_out_time.strftime("%H:%M:%S")
|
||||
except AttributeError:
|
||||
# Используем значения по умолчанию, если настроек нет
|
||||
check_in_time = "14:00:00"
|
||||
check_out_time = "12:00:00"
|
||||
|
||||
async def _save_to_db(self, data, hotel_id):
|
||||
filtered_data = [
|
||||
{
|
||||
'reservation_id': item.get('id'),
|
||||
'checkin': timezone.make_aware(
|
||||
datetime.strptime(
|
||||
f"{item.get('begin_date')} {check_in_time}",
|
||||
"%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
),
|
||||
'checkout': timezone.make_aware(
|
||||
datetime.strptime(
|
||||
f"{item.get('end_date')} {check_out_time}",
|
||||
"%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
),
|
||||
'room_number': item.get('apartment_id'),
|
||||
'room_type': item.get('notes', 'Описание отсутствует'),
|
||||
'status': item.get('status')
|
||||
}
|
||||
for item in bookings
|
||||
if isinstance(item, dict) and item.get("status") in ["booked", "request"]
|
||||
]
|
||||
|
||||
await self._save_to_db(filtered_data)
|
||||
|
||||
async def _save_to_db(self, data):
|
||||
"""
|
||||
Сохраняет данные о бронированиях в базу данных.
|
||||
Сохраняет данные в БД (например, информацию о номере).
|
||||
"""
|
||||
from hotels.models import Reservation, Hotel
|
||||
if not isinstance(data, list):
|
||||
self.logger.error(f"Ожидался список записей, но получен {type(data).__name__}")
|
||||
return
|
||||
|
||||
hotel = await sync_to_async(Hotel.objects.get)(id=hotel_id)
|
||||
print(f"[DEBUG] Загружен отель: {hotel.name}")
|
||||
|
||||
for item in data:
|
||||
print(f"[DEBUG] Обработка бронирования: {item}")
|
||||
for index, item in enumerate(data, start=1):
|
||||
try:
|
||||
reservation, created = await sync_to_async(Reservation.objects.update_or_create)(
|
||||
reservation_id=item["id"],
|
||||
hotel=hotel,
|
||||
defaults={
|
||||
"room_number": item.get("apartment_id", ""), # ID квартиры
|
||||
"check_in": datetime.strptime(item["begin_date"], "%Y-%m-%d"), # Дата заезда
|
||||
"check_out": datetime.strptime(item["end_date"], "%Y-%m-%d"), # Дата выезда
|
||||
"status": item.get("status", ""), # Статус бронирования
|
||||
"price": item.get("amount", 0), # Сумма оплаты
|
||||
"client_name": item["client"].get("fio", ""), # Имя клиента
|
||||
"client_email": item["client"].get("email", ""), # Email клиента
|
||||
"client_phone": item["client"].get("phone", ""), # Телефон клиента
|
||||
}
|
||||
)
|
||||
print(f"[DEBUG] {'Создана' if created else 'Обновлена'} запись: {reservation}")
|
||||
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:
|
||||
print(f"[DEBUG] Ошибка при сохранении бронирования ID {item['id']}: {e}")
|
||||
self.logger.error(f"Ошибка при обработке записи {index}: {e}")
|
||||
|
||||
def validate_plugin(self):
|
||||
"""
|
||||
Проверка на соответствие требованиям.
|
||||
Можно проверить наличие методов или полей.
|
||||
"""
|
||||
# Проверяем наличие обязательных методов
|
||||
required_methods = ["fetch_data", "get_default_parser_settings", "_fetch_data"]
|
||||
for m in required_methods:
|
||||
if not hasattr(self, m):
|
||||
raise ValueError(f"Плагин {type(self).__name__} не реализует метод {m}.")
|
||||
self.logger.debug(f"Плагин {self.__class__.__name__} прошел валидацию.")
|
||||
return True
|
||||
|
||||
116
pms_integration/plugins/trevelline_pms.py
Normal file
116
pms_integration/plugins/trevelline_pms.py
Normal file
@@ -0,0 +1,116 @@
|
||||
import logging
|
||||
import requests
|
||||
from datetime import datetime
|
||||
from .base_plugin import BasePMSPlugin
|
||||
|
||||
class TravelLinePMSPlugin(BasePMSPlugin):
|
||||
"""
|
||||
Плагин для интеграции с PMS TravelLine.
|
||||
"""
|
||||
BASE_URL = "https://partner.tlintegration.com/api/webpms/v1"
|
||||
|
||||
def __init__(self, pms_config):
|
||||
"""
|
||||
Инициализация плагина с конфигурацией PMS.
|
||||
|
||||
:param pms_config: Конфигурация PMS (объект PMSConfiguration).
|
||||
"""
|
||||
super().__init__(pms_config)
|
||||
self.api_key = pms_config.api_key
|
||||
self.logger = logging.getLogger(self.__class__.__name__)
|
||||
|
||||
def _get_headers(self):
|
||||
"""
|
||||
Возвращает заголовки для запросов.
|
||||
"""
|
||||
return {
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
|
||||
def _fetch_data(self):
|
||||
"""
|
||||
Получение данных из API TravelLine (поиск бронирований).
|
||||
|
||||
:return: Список номеров бронирований.
|
||||
"""
|
||||
url = f"{self.BASE_URL}/bookings"
|
||||
params = {
|
||||
"roomId": self.pms_config.room_id,
|
||||
"modifiedFrom": self.pms_config.modified_from,
|
||||
"modifiedTo": self.pms_config.modified_to,
|
||||
"state": self.pms_config.state,
|
||||
"affectsPeriodFrom": self.pms_config.affects_period_from,
|
||||
"affectsPeriodTo": self.pms_config.affects_period_to,
|
||||
}
|
||||
params = {k: v for k, v in params.items() if v is not None}
|
||||
|
||||
try:
|
||||
response = requests.get(url, headers=self._get_headers(), params=params)
|
||||
response.raise_for_status()
|
||||
self.logger.info("Данные успешно получены из API TravelLine.")
|
||||
return response.json().get("bookingNumbers", [])
|
||||
except requests.RequestException as e:
|
||||
self.logger.error(f"Ошибка при запросе к API TravelLine: {e}")
|
||||
return []
|
||||
|
||||
def fetch_data(self):
|
||||
"""
|
||||
Обертка для получения данных из API TravelLine с дополнительной обработкой.
|
||||
|
||||
:return: Список номеров бронирований.
|
||||
"""
|
||||
return self._fetch_data()
|
||||
|
||||
def get_default_parser_settings(self):
|
||||
"""
|
||||
Возвращает настройки парсера по умолчанию.
|
||||
"""
|
||||
return {
|
||||
"field_mapping": {
|
||||
"reservation_id": "bookingNumber",
|
||||
"check_in": "actualCheckInDateTime",
|
||||
"check_out": "actualCheckOutDateTime",
|
||||
"room_number": "roomId",
|
||||
"status": "state",
|
||||
},
|
||||
"date_format": "%Y-%m-%dT%H:%M"
|
||||
}
|
||||
|
||||
def process_data(self, booking_number, room_stay_id, action, actual_date_time):
|
||||
"""
|
||||
Обработка данных для заселения или выселения проживания.
|
||||
|
||||
:param booking_number: Номер бронирования.
|
||||
:param room_stay_id: Идентификатор проживания.
|
||||
:param action: Действие ("check-in" или "check-out").
|
||||
:param actual_date_time: Фактические дата и время.
|
||||
:return: Ответ API.
|
||||
"""
|
||||
if action not in ["check-in", "check-out"]:
|
||||
raise ValueError("Invalid action. Must be 'check-in' or 'check-out'.")
|
||||
|
||||
url = f"{self.BASE_URL}/bookings/{booking_number}/room-stays/{room_stay_id}/{action}"
|
||||
payload = {
|
||||
f"actual{action.capitalize()}DateTime": actual_date_time
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(url, headers=self._get_headers(), json=payload)
|
||||
response.raise_for_status()
|
||||
self.logger.info(f"Успешно выполнено действие '{action}' для бронирования {booking_number}.")
|
||||
return response.json()
|
||||
except requests.RequestException as e:
|
||||
self.logger.error(f"Ошибка при выполнении действия '{action}': {e}")
|
||||
return {}
|
||||
|
||||
def validate_plugin(self):
|
||||
"""
|
||||
Проверка плагина на соответствие требованиям.
|
||||
"""
|
||||
required_methods = ["fetch_data", "get_default_parser_settings", "_fetch_data"]
|
||||
for method in required_methods:
|
||||
if not hasattr(self, method):
|
||||
raise ValueError(f"Плагин {type(self).__name__} не реализует метод {method}.")
|
||||
self.logger.info(f"Плагин {self.__class__.__name__} успешно прошел валидацию.")
|
||||
return True
|
||||
@@ -1,16 +1,14 @@
|
||||
from django.contrib import admin
|
||||
from django import forms
|
||||
from django.utils.functional import cached_property
|
||||
from .models import ScheduledTask
|
||||
from django.templatetags.static import static
|
||||
from scheduler.utils import get_project_functions
|
||||
|
||||
class CustomAdmin(admin.ModelAdmin):
|
||||
class Media:
|
||||
css = {"all": (static("scheduler/admin.css"),)}
|
||||
js = (static("scheduler/admin.js"),)
|
||||
|
||||
class ScheduledTaskForm(forms.ModelForm):
|
||||
"""
|
||||
Форма для модели ScheduledTask с кастомным полем для выбора дней недели.
|
||||
"""
|
||||
DAYS_OF_WEEK_CHOICES = [
|
||||
(0, "Воскресенье"),
|
||||
(1, "Понедельник"),
|
||||
@@ -25,7 +23,7 @@ class ScheduledTaskForm(forms.ModelForm):
|
||||
choices=DAYS_OF_WEEK_CHOICES,
|
||||
widget=forms.CheckboxSelectMultiple,
|
||||
label="Дни недели",
|
||||
required=False, # Опционально
|
||||
required=False,
|
||||
)
|
||||
|
||||
class Meta:
|
||||
@@ -36,24 +34,32 @@ class ScheduledTaskForm(forms.ModelForm):
|
||||
"minutes",
|
||||
"hours",
|
||||
"months",
|
||||
"weekdays", # Используем только поле с галочками
|
||||
"weekdays",
|
||||
"active",
|
||||
]
|
||||
|
||||
def clean_weekdays(self):
|
||||
"""
|
||||
Преобразуем список выбранных дней в строку для хранения в базе.
|
||||
Преобразует список выбранных дней в строку для сохранения в базе.
|
||||
"""
|
||||
weekdays = self.cleaned_data.get("weekdays", [])
|
||||
return ",".join(map(str, weekdays))
|
||||
|
||||
|
||||
|
||||
@admin.register(ScheduledTask)
|
||||
class ScheduledTaskAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Кастомный класс для управления ScheduledTask в админке.
|
||||
"""
|
||||
form = ScheduledTaskForm
|
||||
list_display = ("task_name", "function_path", "minutes", "hours", "months", "weekdays", "active", "formatted_last_run")
|
||||
list_filter = ("active",)
|
||||
search_fields = ("task_name", "function_path")
|
||||
|
||||
def formatted_last_run(self, obj):
|
||||
"""
|
||||
Отформатированный вывод времени последнего запуска задачи.
|
||||
"""
|
||||
return obj.last_run.strftime("%Y-%m-%d %H:%M:%S") if obj.last_run else "Никогда"
|
||||
formatted_last_run.short_description = "Последний запуск"
|
||||
|
||||
formatted_last_run.short_description = "Последний запуск"
|
||||
|
||||
@@ -1,7 +1,15 @@
|
||||
from django.apps import AppConfig
|
||||
from apscheduler.schedulers.asyncio import AsyncIOScheduler
|
||||
|
||||
scheduler_instance = AsyncIOScheduler()
|
||||
|
||||
class SchedulerConfig(AppConfig):
|
||||
default_auto_field = 'django.db.models.BigAutoField'
|
||||
name = 'scheduler'
|
||||
verbose_name="Планировщик заданий"
|
||||
verbose_name = 'Планировщик задач'
|
||||
def ready(self):
|
||||
"""
|
||||
Метод ready вызывается при старте приложения.
|
||||
Здесь не нужно запускать scheduler_instance.start(), чтобы избежать ошибок.
|
||||
"""
|
||||
pass
|
||||
|
||||
39
scheduler/management/commands/start_scheduler.py
Normal file
39
scheduler/management/commands/start_scheduler.py
Normal file
@@ -0,0 +1,39 @@
|
||||
import asyncio
|
||||
from django.core.management.base import BaseCommand
|
||||
from scheduler.apps import scheduler_instance
|
||||
from scheduler.tasks import load_tasks_to_scheduler
|
||||
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Запуск планировщика задач"
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
"""
|
||||
Создаёт новый event loop, запускает планировщик и загружает задачи.
|
||||
"""
|
||||
try:
|
||||
print("Проверка состояния перед запуском:")
|
||||
print(f"Scheduler instance: {scheduler_instance}")
|
||||
|
||||
# Создаём новый event loop
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
# Устанавливаем event loop в планировщик
|
||||
scheduler_instance.configure(event_loop=loop)
|
||||
|
||||
# Запускаем планировщик
|
||||
scheduler_instance.start()
|
||||
|
||||
# Загружаем задачи
|
||||
load_tasks_to_scheduler(scheduler_instance)
|
||||
|
||||
self.stdout.write(self.style.SUCCESS("Планировщик успешно запущен."))
|
||||
|
||||
# Удерживаем цикл событий
|
||||
loop.run_forever()
|
||||
except KeyboardInterrupt:
|
||||
self.stdout.write(self.style.WARNING("Остановка планировщика."))
|
||||
finally:
|
||||
# Завершаем работу планировщика
|
||||
scheduler_instance.shutdown(wait=False)
|
||||
@@ -1,30 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-10 08:38
|
||||
|
||||
import scheduler.models
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ScheduledTask',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('task_name', models.CharField(max_length=255, verbose_name='Название задачи')),
|
||||
('function_path', models.CharField(choices=scheduler.models.get_available_functions, max_length=500, verbose_name='Путь к функции (модуль.функция)')),
|
||||
('minutes', models.CharField(default='*', max_length=255, verbose_name='Минуты')),
|
||||
('hours', models.CharField(default='*', max_length=255, verbose_name='Часы')),
|
||||
('days', models.CharField(default='*', max_length=255, verbose_name='Дни')),
|
||||
('months', models.CharField(default='*', max_length=255, verbose_name='Месяцы')),
|
||||
('weekdays', models.JSONField(default=list, verbose_name='Дни недели')),
|
||||
('active', models.BooleanField(default=True, verbose_name='Активно')),
|
||||
('last_run', models.DateTimeField(blank=True, null=True, verbose_name='Последний запуск')),
|
||||
],
|
||||
),
|
||||
]
|
||||
17
scheduler/reload_tasks.py
Normal file
17
scheduler/reload_tasks.py
Normal file
@@ -0,0 +1,17 @@
|
||||
from django.core.management.base import BaseCommand
|
||||
from scheduler.tasks import load_tasks_to_scheduler
|
||||
from scheduler.apps import scheduler_instance
|
||||
|
||||
class Command(BaseCommand):
|
||||
help = "Перезагрузка задач в планировщике"
|
||||
|
||||
def handle(self, *args, **kwargs):
|
||||
try:
|
||||
# Удаляем все существующие задачи
|
||||
scheduler_instance.remove_all_jobs()
|
||||
|
||||
# Загружаем задачи заново
|
||||
load_tasks_to_scheduler(scheduler_instance)
|
||||
self.stdout.write(self.style.SUCCESS("Задачи успешно перезагружены."))
|
||||
except Exception as e:
|
||||
self.stdout.write(self.style.ERROR(f"Ошибка перезагрузки задач: {e}"))
|
||||
@@ -5,7 +5,6 @@ from scheduler.models import ScheduledTask
|
||||
import importlib
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
|
||||
|
||||
def format_weekdays(weekdays):
|
||||
"""Преобразует список дней недели в строку."""
|
||||
if isinstance(weekdays, list):
|
||||
@@ -43,7 +42,10 @@ def setup_scheduler():
|
||||
print("Планировщик запущен.")
|
||||
return scheduler
|
||||
|
||||
def load_tasks_to_scheduler(scheduler: BaseScheduler):
|
||||
def load_tasks_to_scheduler(scheduler):
|
||||
"""
|
||||
Загружает активные задачи в планировщик.
|
||||
"""
|
||||
tasks = ScheduledTask.objects.filter(active=True)
|
||||
for task in tasks:
|
||||
try:
|
||||
|
||||
@@ -16,6 +16,9 @@ import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
import os
|
||||
from pprint import pprint
|
||||
from app_settings.app_settings import load_database_settings
|
||||
|
||||
|
||||
# Build paths inside the project like this: BASE_DIR / 'subdir'.
|
||||
@@ -31,10 +34,10 @@ SECRET_KEY = 'django-insecure-l_8uu8#p*^zf)9zry80)6u+!+2g1a4tg!wx7@^!uw(+^axyh&h
|
||||
# SECURITY WARNING: don't run with debug turned on in production!
|
||||
DEBUG = True
|
||||
|
||||
ALLOWED_HOSTS = ['0.0.0.0', '192.168.219.140', '127.0.0.1', '192.168.219.114', 'a66a-182-226-158-253.ngrok-free.app', '*.ngrok-free.app']
|
||||
ALLOWED_HOSTS = ['0.0.0.0', '192.168.219.140', '127.0.0.1', 'localhost', '192.168.219.114', '8f6e-182-226-158-253.ngrok-free.app', '*.ngrok-free.app']
|
||||
|
||||
CSRF_TRUSTED_ORIGINS = [
|
||||
'http://a66a-182-226-158-253.ngrok-free.app',
|
||||
'https://8f6e-182-226-158-253.ngrok-free.app',
|
||||
'https://*.ngrok-free.app', # Это подойдет для любых URL, связанных с ngrok
|
||||
]
|
||||
|
||||
@@ -103,9 +106,11 @@ DATABASES = {
|
||||
'PASSWORD': os.getenv('DB_PASSWORD'), # Пароль пользователя
|
||||
'HOST': os.getenv('DB_HOST', default='0.0.0.0'), # Хост (по умолчанию localhost)
|
||||
'PORT': os.getenv('DB_PORT', default=3308), # Порт (по умолчанию 3306)
|
||||
'ATOMIC_REQUESTS': True,
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
# load_database_settings(DATABASES)
|
||||
|
||||
# Password validation
|
||||
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
|
||||
@@ -197,20 +202,54 @@ JAZZMIN_SETTINGS = {
|
||||
"welcome_sign": "Welcome to Touchh Admin", # Приветствие на странице входа
|
||||
"copyright": "Touchh", # Кастомный текст в футере
|
||||
"icons": {
|
||||
"auth": "fas fa-users-cog",
|
||||
"users": "fas fa-user-circle",
|
||||
# Приложения
|
||||
"hotels": "fas fa-hotel",
|
||||
"hotels.hotel": "fas fa-hotel",
|
||||
"hotels.room":"fas fa-bed",
|
||||
"hotels.reservation":"fas fa-calendar-week",
|
||||
"hotels.userhotel":"fas fa-user-shield",
|
||||
"app_settings": "fas fa-wrench",
|
||||
"scheduler": "fas fa-clock",
|
||||
"pms_integration": "fas fa-sync",
|
||||
"antifroud": "fas fa-shield-alt",
|
||||
"pms_integration": "fas fa-sync",
|
||||
"antifraud": "fas fa-shield-alt",
|
||||
"app_settings": "fas fa-wrench",
|
||||
|
||||
# Модели
|
||||
"hotels.reservation": "fas fa-calendar-check", # Бронирования
|
||||
"hotels.userhotel": "fas fa-user-shield", # Пользователи отеля
|
||||
"app_settings.telegramsettings": "fab fa-telegram", # Настройки Telegram
|
||||
"app_settings.emailsettings": "fas fa-envelope", # Настройки Email
|
||||
"app_settings.localdatabase": "fas fa-database", # Локальная база данных
|
||||
"app_settings.globalhotelsettings": "fas fa-tools", # Глобальные настройки отеля
|
||||
"app_settings.globalsystemsettings": "fas fa-cogs", # Глобальные системные настройки
|
||||
"antifraud.externaldbsettings": "fas fa-server", # Подключение к внешним базам данных
|
||||
"antifraud.roomdiscrepancy": "fas fa-exclamation-circle", # Несоответствия в комнатах
|
||||
"scheduler.scheduledtask": "fas fa-tasks", # Запланированные задачи
|
||||
"antifraud.violationlog": "fas fa-ban", # Журнал нарушений
|
||||
"hotels.importedhotel": "fas fa-download", # Импортированные отели
|
||||
"hotels.synclog": "fas fa-sync-alt", # Журнал синхронизации
|
||||
|
||||
"auth": "fas fa-users-cog",
|
||||
"users": "fas fa-user-circle",
|
||||
"hotels": "fas fa-hotel",
|
||||
"hotels.hotel": "fas fa-hotel",
|
||||
"hotels.room":"fas fa-bed",
|
||||
"hotels.reservation":"fas fa-calendar-week",
|
||||
"hotels.userhotel":"fas fa-user-shield",
|
||||
"app_settings": "fas fa-wrench",
|
||||
"app_settings.telegramsettings":"fab fa-telegram",
|
||||
"app_settings.emailsettings":"fab fa-at",
|
||||
"app_settings.localdatabase":"fas fa-database",
|
||||
"app_settings.globalhotelsettings":"fas fa-hammer",
|
||||
"app_settings.globalsystemsettings":"fas fa-cogs",
|
||||
"scheduler": "fas fa-clock",
|
||||
"pms_integration": "fas fa-sync",
|
||||
"antifroud": "fas fa-shield-alt",
|
||||
"antifroud.externaldbsettings": "fas fa-database",
|
||||
"antifroud.roomdiscrepancy": "fas fa-user-secret",
|
||||
"antifroud.violationlog": "fas fa-ban",
|
||||
"antifroud.importedhotel": "fas fa-download",
|
||||
"antifroud.synclog": "fas fa-sync-alt",
|
||||
"antifroud.useractivitylog": "fas fa-qrcode",
|
||||
},
|
||||
|
||||
|
||||
|
||||
},
|
||||
"theme": "sandstone",
|
||||
"dark_mode_theme": "darkly",
|
||||
"footer": {
|
||||
@@ -223,10 +262,10 @@ JAZZMIN_SETTINGS = {
|
||||
],
|
||||
|
||||
|
||||
"show_ui_builder": True,
|
||||
"show_ui_builder_breadcrumbs": True,
|
||||
"show_ui_builder_tabs": True,
|
||||
"show_ui_builder_tabs_breadcrumbs": True,
|
||||
"show_ui_builder": False,
|
||||
"show_ui_builder_breadcrumbs": False,
|
||||
"show_ui_builder_tabs": False,
|
||||
"show_ui_builder_tabs_breadcrumbs": False,
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ from antifroud import views
|
||||
app_name = 'touchh'
|
||||
|
||||
urlpatterns = [
|
||||
path('admin/', admin.site.urls),
|
||||
|
||||
path('', admin.site.urls),
|
||||
path('health/', include('health_check.urls')),
|
||||
path('antifroud/', include('antifroud.urls')),
|
||||
|
||||
|
||||
@@ -21,7 +21,10 @@ class CustomLogger:
|
||||
|
||||
# Уровень логирования по умолчанию из .env
|
||||
default_level = os.getenv("LOG_LEVEL", "INFO").upper()
|
||||
self.log_level = getattr(logging, log_level.upper(), getattr(logging, default_level, logging.INFO))
|
||||
if log_level:
|
||||
self.log_level = getattr(logging, log_level.upper(), getattr(logging, default_level, logging.INFO))
|
||||
else:
|
||||
self.log_level = getattr(logging, default_level, logging.INFO)
|
||||
|
||||
self.setup_logger()
|
||||
|
||||
|
||||
@@ -1,120 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-09 09:30
|
||||
|
||||
import django.contrib.auth.models
|
||||
import django.contrib.auth.validators
|
||||
import django.db.models.deletion
|
||||
import django.utils.timezone
|
||||
import uuid
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('auth', '0012_alter_user_first_name_max_length'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='LocalUserActivityLog',
|
||||
fields=[
|
||||
('id', models.AutoField(primary_key=True, serialize=False)),
|
||||
('user_id', models.IntegerField()),
|
||||
('activity_type', models.CharField(max_length=255)),
|
||||
('timestamp', models.DateTimeField()),
|
||||
('additional_data', models.JSONField(blank=True, null=True)),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserActivityLog',
|
||||
fields=[
|
||||
('id', models.BigAutoField(primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('user_id', models.BigIntegerField(verbose_name='ID пользователя')),
|
||||
('ip', models.CharField(blank=True, max_length=100, null=True, verbose_name='IP адрес')),
|
||||
('created', models.DateTimeField(auto_now_add=True, verbose_name='Создан')),
|
||||
('timestamp', models.IntegerField(verbose_name='Время')),
|
||||
('date_time', models.DateTimeField(verbose_name='Дата')),
|
||||
('referred', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('agent', models.CharField(blank=True, max_length=255, null=True, verbose_name='Браузер')),
|
||||
('platform', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('version', models.CharField(blank=True, max_length=50, null=True)),
|
||||
('model', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('device', models.CharField(blank=True, max_length=50, null=True)),
|
||||
('UAString', models.TextField(blank=True, null=True)),
|
||||
('location', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('page_id', models.BigIntegerField(blank=True, null=True)),
|
||||
('url_parameters', models.TextField(blank=True, null=True)),
|
||||
('page_title', models.CharField(blank=True, max_length=255, null=True)),
|
||||
('type', models.CharField(blank=True, max_length=50, null=True)),
|
||||
('last_counter', models.IntegerField(blank=True, null=True)),
|
||||
('hits', models.IntegerField(blank=True, null=True)),
|
||||
('honeypot', models.BooleanField(blank=True, null=True)),
|
||||
('reply', models.BooleanField(blank=True, null=True)),
|
||||
('page_url', models.CharField(blank=True, max_length=255, null=True)),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Журнал активности',
|
||||
'verbose_name_plural': 'Журналы активности',
|
||||
'db_table': 'user_activity_log',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='User',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
||||
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
||||
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
||||
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
||||
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
|
||||
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
||||
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
||||
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
||||
('telegram_id', models.BigIntegerField(blank=True, null=True, unique=True, verbose_name='ID Телеграм')),
|
||||
('chat_id', models.BigIntegerField(blank=True, null=True, unique=True, verbose_name='ID чата в телеграм')),
|
||||
('role', models.CharField(choices=[('admin', 'Администратор системы'), ('hotel_user', 'Сотрудник отеля')], default='hotel_user', max_length=20, verbose_name='Роль')),
|
||||
('confirmed', models.BooleanField(default=False, verbose_name='Подтвержден')),
|
||||
('groups', models.ManyToManyField(blank=True, related_name='custom_user_set', to='auth.group')),
|
||||
('user_permissions', models.ManyToManyField(blank=True, related_name='custom_user_set', to='auth.permission')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Пользователь',
|
||||
'verbose_name_plural': 'Пользователи',
|
||||
},
|
||||
managers=[
|
||||
('objects', django.contrib.auth.models.UserManager()),
|
||||
],
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='NotificationSettings',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('telegram_enabled', models.BooleanField(default=True, verbose_name='Уведомления в Telegram')),
|
||||
('email_enabled', models.BooleanField(default=False, verbose_name='Уведомления по Email')),
|
||||
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='Email для уведомлений')),
|
||||
('notification_time', models.TimeField(default='09:00', verbose_name='Время отправки уведомлений')),
|
||||
('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to='users.user', verbose_name='Пользователь')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Способ оповещения',
|
||||
'verbose_name_plural': 'Способы оповещений',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='UserConfirmation',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('confirmation_code', models.UUIDField(default=uuid.uuid4, verbose_name='Код подтверждения')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Создан: ')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.user', verbose_name='Пользователь')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Подтверждение пользователя',
|
||||
'verbose_name_plural': 'Подтверждения пользователей',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -1,22 +0,0 @@
|
||||
# Generated by Django 5.1.4 on 2024-12-13 00:15
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('users', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel(
|
||||
name='LocalUserActivityLog',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='UserActivityLog',
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='UserConfirmation',
|
||||
),
|
||||
]
|
||||
Reference in New Issue
Block a user