diff --git a/bot/apps.py b/bot/apps.py index 1cd7ff2e..961c29a5 100644 --- a/bot/apps.py +++ b/bot/apps.py @@ -4,3 +4,4 @@ from django.apps import AppConfig class BotConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'bot' + verbose_name="Бот" diff --git a/hotels/admin.py b/hotels/admin.py index 190b2d88..88e57f40 100644 --- a/hotels/admin.py +++ b/hotels/admin.py @@ -5,8 +5,6 @@ from .models import ( UserHotel, APIConfiguration, APIRequestLog, - PMSConfiguration, - PMSIntegrationLog, Reservation, Guest ) @@ -58,22 +56,6 @@ class ApiRequestLogAdmin(admin.ModelAdmin): ordering = ('-request_time',) -@admin.register(PMSConfiguration) -class PMSConfigurationAdmin(admin.ModelAdmin): - list_display = ('name', 'parser_settings', 'description', 'created_at') - search_fields = ('name', 'description') - list_filter = ('created_at',) - ordering = ('-created_at',) - - -@admin.register(PMSIntegrationLog) -class PMSIntegrationLogAdmin(admin.ModelAdmin): - list_display = ('hotel', 'checked_at', 'status', 'message') - search_fields = ('hotel__name', 'status', 'message') - list_filter = ('status', 'checked_at') - ordering = ('-checked_at',) - - @admin.register(Reservation) class ReservationAdmin(admin.ModelAdmin): list_display = ('reservation_id', 'hotel', 'room_number', 'room_type', 'check_in', 'check_out', 'status', 'price', 'discount') diff --git a/hotels/apps.py b/hotels/apps.py index d8bd62fa..3787f3e7 100644 --- a/hotels/apps.py +++ b/hotels/apps.py @@ -4,3 +4,4 @@ from django.apps import AppConfig class HotelsConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'hotels' + verbose_name="Отели" diff --git a/hotels/migrations/0021_alter_hotel_pms_remove_pmsintegrationlog_hotel_and_more.py b/hotels/migrations/0021_alter_hotel_pms_remove_pmsintegrationlog_hotel_and_more.py new file mode 100644 index 00000000..f42a574f --- /dev/null +++ b/hotels/migrations/0021_alter_hotel_pms_remove_pmsintegrationlog_hotel_and_more.py @@ -0,0 +1,30 @@ +# Generated by Django 5.1.4 on 2024-12-08 23:48 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('hotels', '0020_alter_userhotel_user'), + ('pms_integration', '0001_initial'), + ] + + operations = [ + migrations.AlterField( + 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.RemoveField( + model_name='pmsintegrationlog', + name='hotel', + ), + migrations.DeleteModel( + name='PMSConfiguration', + ), + migrations.DeleteModel( + name='PMSIntegrationLog', + ), + ] diff --git a/hotels/models.py b/hotels/models.py index 13ddf4b7..2e8e15f1 100644 --- a/hotels/models.py +++ b/hotels/models.py @@ -1,22 +1,7 @@ from django.db import models -from users.models import User from django.core.validators import MinValueValidator, MaxValueValidator -class PMSConfiguration(models.Model): - name = models.CharField(max_length=255, verbose_name="Название PMS") - parser_settings = models.JSONField(default=dict, verbose_name="Настройки разбора данных") - description = models.TextField(blank=True, null=True, verbose_name="Описание") - created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания") - - def __str__(self): - return self.name - - class Meta: - verbose_name = "PMS система" - verbose_name_plural = "PMS системы" - - class APIConfiguration(models.Model): name = models.CharField(max_length=255, verbose_name="Название API") url = models.URLField(verbose_name="URL API") @@ -44,7 +29,13 @@ class Hotel(models.Model): help_text="API, связанный с этим отелем." ) created_at = models.DateTimeField(auto_now_add=True, verbose_name="Создан") - pms = models.ForeignKey(PMSConfiguration, on_delete=models.SET_NULL, null=True, blank=True, verbose_name="PMS система") + pms = models.ForeignKey( + 'pms_integration.PMSConfiguration', # Используем строковую ссылку + on_delete=models.SET_NULL, + null=True, + blank=True, + verbose_name="PMS система" + ) def __str__(self): return self.name @@ -54,32 +45,19 @@ class Hotel(models.Model): verbose_name_plural = "Отели" -class PMSIntegrationLog(models.Model): - hotel = models.ForeignKey(Hotel, on_delete=models.CASCADE, verbose_name="Отель") - checked_at = models.DateTimeField(auto_now_add=True, verbose_name="Время проверки") - status = models.CharField(max_length=50, verbose_name="Статус", choices=[('success', 'Успех'), ('error', 'Ошибка')]) - message = models.TextField(verbose_name="Сообщение", blank=True, null=True) - - def __str__(self): - return f"{self.hotel.name} - {self.status} - {self.checked_at}" - - class Meta: - verbose_name = "Журнал интеграции PMS" - verbose_name_plural = "Журналы интеграции PMS" - indexes = [ - models.Index(fields=['hotel']), - models.Index(fields=['checked_at']), - models.Index(fields=['status']), - ] - - class UserHotel(models.Model): user = models.ForeignKey( - User, on_delete=models.CASCADE, related_name="user_hotels", verbose_name="Пользователь" + 'users.User', # Используем строковую ссылку + on_delete=models.CASCADE, + related_name="user_hotels", + verbose_name="Пользователь" ) hotel = models.ForeignKey( - Hotel, on_delete=models.CASCADE, related_name="hotel_users", verbose_name="Отель" -) + 'hotels.Hotel', + on_delete=models.CASCADE, + related_name="hotel_users", + verbose_name="Отель" + ) def __str__(self): return f"{self.user.username} - {self.hotel.name}" diff --git a/pms_config_backup.json b/pms_config_backup.json new file mode 100644 index 00000000..e69de29b diff --git a/pms_integration/admin.py b/pms_integration/admin.py index 8c38f3f3..aee974df 100644 --- a/pms_integration/admin.py +++ b/pms_integration/admin.py @@ -1,3 +1,18 @@ from django.contrib import admin - +from .models import PMSConfiguration, PMSIntegrationLog # Register your models here. + +@admin.register(PMSConfiguration) +class PMSConfigurationAdmin(admin.ModelAdmin): + list_display = ('name', 'parser_settings', 'description', 'created_at') + search_fields = ('name', 'description') + list_filter = ('created_at',) + ordering = ('-created_at',) + + +@admin.register(PMSIntegrationLog) +class PMSIntegrationLogAdmin(admin.ModelAdmin): + list_display = ('hotel', 'checked_at', 'status', 'message') + search_fields = ('hotel__name', 'status', 'message') + list_filter = ('status', 'checked_at') + ordering = ('-checked_at',) diff --git a/pms_integration/apps.py b/pms_integration/apps.py index 329cfc61..e9aa868b 100644 --- a/pms_integration/apps.py +++ b/pms_integration/apps.py @@ -2,5 +2,6 @@ from django.apps import AppConfig class PmsIntegrationConfig(AppConfig): - default_auto_field = 'django.db.models.BigAutoField' - name = 'pms_integration' + default_auto_field = "django.db.models.BigAutoField" + name = "pms_integration" + verbose_name = "Интеграция с PMS" diff --git a/pms_integration/manager.py b/pms_integration/manager.py index c527e8d0..5b1aa4df 100644 --- a/pms_integration/manager.py +++ b/pms_integration/manager.py @@ -1,53 +1,68 @@ -# pms_integration/manager.py - import importlib -from hotels.models import Hotel, PMSIntegrationLog, Reservation -from asgiref.sync import sync_to_async +from django.core.exceptions import ObjectDoesNotExist +from hotels.models import Hotel +from .models import PMSIntegrationLog +import os +from pathlib import Path +from plugins.base_plugin import BasePMSPlugin + + +class PluginLoader: + """ + Класс для автоматической загрузки плагинов PMS. + """ + PLUGIN_PATH = Path(__file__).parent / "plugins" + + @staticmethod + def load_plugins(): + plugins = {} + for file in os.listdir(PluginLoader.PLUGIN_PATH): + if file.endswith("_pms.py") and not file.startswith("__"): + module_name = f"pms_integration.plugins.{file[:-3]}" + try: + module = importlib.import_module(module_name) + # Ищем класс, наследующийся от BasePMSPlugin + for attr in dir(module): + cls = getattr(module, attr) + if isinstance(cls, type) and issubclass(cls, BasePMSPlugin) and cls is not BasePMSPlugin: + plugins[cls.__name__] = cls + except Exception as e: + print(f"Ошибка загрузки плагина {module_name}: {e}") + return plugins + + + + class PMSIntegrationManager: - """Универсальный менеджер интеграции с PMS.""" - def __init__(self, hotel_id): self.hotel_id = hotel_id self.hotel = None + self.pms_config = None self.plugin = None - async def load_hotel_data(self): - """Загружает данные отеля и инициализирует соответствующий плагин.""" - self.hotel = await sync_to_async(Hotel.objects.get)(id=self.hotel_id) - pms_system = self.hotel.pms + def load_hotel(self): + from hotels.models import Hotel # Импорт здесь, чтобы избежать кругового импорта + self.hotel = Hotel.objects.get(id=self.hotel_id) + self.pms_config = self.hotel.pms + if not self.pms_config: + raise ValueError(f"Отель {self.hotel.name} не связан с PMS системой.") - if not pms_system: - raise ValueError("PMS система не настроена для отеля.") + def load_plugin(self): + plugins = PluginLoader.load_plugins() + if self.pms_config.name not in plugins: + raise ValueError(f"Плагин для PMS {self.pms_config.name} не найден.") + self.plugin = plugins[self.pms_config.name](self.pms_config) - plugin_module = f"pms_integration.plugins.{pms_system.name.lower()}_pms" - try: - plugin_class = getattr(importlib.import_module(plugin_module), f"{pms_system.name.capitalize()}PMSPlugin") - self.plugin = plugin_class(self.hotel.api) - except (ImportError, AttributeError) as e: - raise ValueError(f"Плагин для PMS '{pms_system.name}' не найден: {e}") + def fetch_data(self): + if not self.plugin: + raise ValueError("Плагин не загружен.") + return self.plugin.fetch_data() - async def fetch_and_save_data(self): - """Получение данных от PMS и их сохранение.""" - raw_data = self.plugin.fetch_data() - parsed_data = self.plugin.parse_data(raw_data) - - # Сохраняем данные в БД - for res in parsed_data: - await sync_to_async(Reservation.objects.update_or_create)( - reservation_id=res["id"], - defaults={ - "hotel": self.hotel, - "room_number": res["room_number"], - "room_type": res["room_type"], - "check_in": res["check_in"], - "check_out": res["check_out"], - "status": res["status"], - "price": res.get("price"), - }, - ) - - # Логируем успех - await sync_to_async(PMSIntegrationLog.objects.create)( - hotel=self.hotel, status="success", message="Данные успешно обновлены." - ) + def save_log(self, status, message): + from .models import PMSIntegrationLog # Избегаем кругового импорта + PMSIntegrationLog.objects.create( + hotel=self.hotel, + status=status, + message=message, + ) \ No newline at end of file diff --git a/pms_integration/migrations/0001_initial.py b/pms_integration/migrations/0001_initial.py new file mode 100644 index 00000000..5168707a --- /dev/null +++ b/pms_integration/migrations/0001_initial.py @@ -0,0 +1,45 @@ +# Generated by Django 5.1.4 on 2024-12-08 23:48 + +import django.db.models.deletion +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + ('hotels', '0020_alter_userhotel_user'), + ] + + 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')), + ('parser_settings', models.JSONField(default=dict, verbose_name='Настройки разбора данных')), + ('description', models.TextField(blank=True, null=True, 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')], + }, + ), + ] diff --git a/pms_integration/models.py b/pms_integration/models.py index 71a83623..8204915e 100644 --- a/pms_integration/models.py +++ b/pms_integration/models.py @@ -1,3 +1,38 @@ from django.db import models -# Create your models here. + +class PMSConfiguration(models.Model): + name = models.CharField(max_length=255, verbose_name="Название PMS") + parser_settings = models.JSONField(default=dict, verbose_name="Настройки разбора данных") + description = models.TextField(blank=True, null=True, verbose_name="Описание") + created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания") + + def __str__(self): + return self.name + + class Meta: + verbose_name = "PMS система" + verbose_name_plural = "PMS системы" + + +class PMSIntegrationLog(models.Model): + hotel = models.ForeignKey( + 'hotels.Hotel', # Используем строковую ссылку + on_delete=models.CASCADE, + verbose_name="Отель" + ) + checked_at = models.DateTimeField(auto_now_add=True, verbose_name="Время проверки") + status = models.CharField(max_length=50, verbose_name="Статус", choices=[('success', 'Успех'), ('error', 'Ошибка')]) + message = models.TextField(verbose_name="Сообщение", blank=True, null=True) + + def __str__(self): + return f"{self.hotel.name} - {self.status} - {self.checked_at}" + + class Meta: + verbose_name = "Журнал интеграции PMS" + verbose_name_plural = "Журналы интеграции PMS" + indexes = [ + models.Index(fields=['hotel']), + models.Index(fields=['checked_at']), + models.Index(fields=['status']), + ] diff --git a/pms_integration/plugins/base_plugin.py b/pms_integration/plugins/base_plugin.py index dce554bd..82e521ee 100644 --- a/pms_integration/plugins/base_plugin.py +++ b/pms_integration/plugins/base_plugin.py @@ -1,27 +1,15 @@ -# pms_integration/plugins/base_plugin.py - from abc import ABC, abstractmethod + class BasePMSPlugin(ABC): - """Базовый класс для всех плагинов PMS интеграции.""" - - def __init__(self, api_config): - """ - Конструктор плагина. - :param api_config: Конфигурация API (объект модели APIConfiguration). - """ - self.api_config = api_config + def __init__(self, hotel, pms_config): + self.hotel = hotel + self.pms_config = pms_config @abstractmethod - def fetch_data(self): + def fetch_and_parse(self): """ - Метод для получения данных от PMS. - """ - pass - - @abstractmethod - def parse_data(self, raw_data): - """ - Метод для обработки сырых данных от PMS. + Этот метод должен быть реализован в каждом плагине. + Он должен получать данные из API и возвращать их в нужном формате. """ pass diff --git a/pms_integration/plugins/bnovo_pms.py b/pms_integration/plugins/bnovo_pms.py index 7b2d6dc6..333da2cf 100644 --- a/pms_integration/plugins/bnovo_pms.py +++ b/pms_integration/plugins/bnovo_pms.py @@ -1,31 +1,30 @@ -# pms_integration/plugins/example_pms.py - import requests from .base_plugin import BasePMSPlugin -class ExamplePMSPlugin(BasePMSPlugin): - """Плагин для интеграции с Example PMS.""" - def fetch_data(self): - """Получение данных от Example PMS.""" - url = self.api_config.url - headers = {"Authorization": f"Bearer {self.api_config.token}"} - response = requests.get(url, headers=headers, timeout=10) - response.raise_for_status() - return response.json() +class BnovoPMS(BasePMSPlugin): + def fetch_and_parse(self): + response = requests.get( + self.pms_config.url, + headers={"Authorization": f"Bearer {self.pms_config.token}"} + ) + if response.status_code != 200: + raise ValueError(f"Ошибка запроса к PMS Bnovo: {response.text}") - def parse_data(self, raw_data): - """Обработка данных от Example PMS.""" - reservations = raw_data.get("reservations", []) - return [ - { - "id": res["id"], - "room_number": res["room_number"], - "room_type": res["room_type"], - "check_in": res["check_in"], - "check_out": res["check_out"], - "status": res["status"], - "price": res.get("price"), + data = response.json() + parsed_data = self.parse_data(data) + return parsed_data + + def parse_data(self, data): + # Пример разбора данных на основе JSON-маски + reservations = [] + for item in data["reservations"]: + reservation = { + "id": item["id"], + "room_number": item["roomNumber"], + "check_in": item["checkIn"], + "check_out": item["checkOut"], + "status": item["status"], } - for res in reservations - ] + reservations.append(reservation) + return reservations diff --git a/pms_integration/plugins/ecvi_pms.py b/pms_integration/plugins/ecvi_pms.py index e69de29b..681179b6 100644 --- a/pms_integration/plugins/ecvi_pms.py +++ b/pms_integration/plugins/ecvi_pms.py @@ -0,0 +1,7 @@ +from .base_plugin import BasePMSPlugin + + +class EcviPMS(BasePMSPlugin): + def fetch_and_parse(self): + # Реализация логики для ECVI + pass diff --git a/reports/Golden Hills 4*_report.pdf b/reports/Golden Hills 4*_report.pdf index 62d124c2..1103f0aa 100644 Binary files a/reports/Golden Hills 4*_report.pdf and b/reports/Golden Hills 4*_report.pdf differ