/', self.sync_hotel_data),
+ ]
+ return custom_urls + urls
+
+ def sync_hotel_data(self, request, hotel_id):
+ try:
+ hotel = Hotel.objects.get(id=hotel_id)
+ client = APIClient(hotel.pms)
+ client.run(hotel)
+ self.message_user(request, f"Данные отеля {hotel.name} успешно синхронизированы.")
+ except Exception as e:
+ self.message_user(request, f"Ошибка: {str(e)}", level="error")
+ return redirect("..")
@admin.register(UserHotel)
class UserHotelAdmin(admin.ModelAdmin):
list_display = ('user', 'hotel')
search_fields = ('user__username', 'hotel__name')
- list_filter = ('hotel',)
- ordering = ('-hotel',)
+ # list_filter = ('hotel',)
+ # ordering = ('-hotel',)
@admin.register(APIConfiguration)
diff --git a/hotels/migrations/0001_initial.py b/hotels/migrations/0001_initial.py
deleted file mode 100644
index 3df5b4c0..00000000
--- a/hotels/migrations/0001_initial.py
+++ /dev/null
@@ -1,33 +0,0 @@
-# Generated by Django 5.1.4 on 2024-12-05 23:39
-
-import django.db.models.deletion
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- initial = True
-
- dependencies = [
- ]
-
- operations = [
- 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='Hotel Name')),
- ('pms_type', models.CharField(choices=[('bnovo', 'Bnovo'), ('travelline', 'Travel Line')], max_length=50, verbose_name='PMS Type')),
- ('api_key', models.CharField(blank=True, max_length=255, null=True, verbose_name='API Key')),
- ('public_key', models.CharField(blank=True, max_length=255, null=True, verbose_name='Public Key')),
- ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
- ],
- ),
- migrations.CreateModel(
- name='UserHotel',
- fields=[
- ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
- ('hotel', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='hotels.hotel', verbose_name='Hotel')),
- ],
- ),
- ]
diff --git a/hotels/migrations/0002_initial.py b/hotels/migrations/0002_initial.py
deleted file mode 100644
index bd71123e..00000000
--- a/hotels/migrations/0002_initial.py
+++ /dev/null
@@ -1,22 +0,0 @@
-# Generated by Django 5.1.4 on 2024-12-05 23:39
-
-import django.db.models.deletion
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- initial = True
-
- dependencies = [
- ('hotels', '0001_initial'),
- ('users', '0001_initial'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='userhotel',
- name='user',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.user', verbose_name='User'),
- ),
- ]
diff --git a/hotels/migrations/0003_alter_hotel_options_alter_userhotel_options_and_more.py b/hotels/migrations/0003_alter_hotel_options_alter_userhotel_options_and_more.py
deleted file mode 100644
index f989f535..00000000
--- a/hotels/migrations/0003_alter_hotel_options_alter_userhotel_options_and_more.py
+++ /dev/null
@@ -1,58 +0,0 @@
-# Generated by Django 5.1.4 on 2024-12-06 04:26
-
-import django.db.models.deletion
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('hotels', '0002_initial'),
- ('users', '0004_alter_user_options_alter_userconfirmation_options_and_more'),
- ]
-
- operations = [
- migrations.AlterModelOptions(
- name='hotel',
- options={'verbose_name': 'Отель', 'verbose_name_plural': 'Отели'},
- ),
- migrations.AlterModelOptions(
- name='userhotel',
- options={'verbose_name': 'Пользователь отеля', 'verbose_name_plural': 'Пользователи отелей'},
- ),
- migrations.AlterField(
- model_name='hotel',
- name='api_key',
- field=models.CharField(blank=True, max_length=255, null=True, verbose_name='API ключ'),
- ),
- migrations.AlterField(
- model_name='hotel',
- name='created_at',
- field=models.DateTimeField(auto_now_add=True, verbose_name='Создан'),
- ),
- migrations.AlterField(
- model_name='hotel',
- name='name',
- field=models.CharField(max_length=255, verbose_name='Название отеля'),
- ),
- migrations.AlterField(
- model_name='hotel',
- name='pms_type',
- field=models.CharField(choices=[('bnovo', 'Bnovo'), ('travelline', 'Travel Line')], max_length=50, verbose_name='PMS система'),
- ),
- migrations.AlterField(
- model_name='hotel',
- name='public_key',
- field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Публичный ключ'),
- ),
- migrations.AlterField(
- model_name='userhotel',
- name='hotel',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='hotels.hotel', verbose_name='Отель'),
- ),
- migrations.AlterField(
- model_name='userhotel',
- name='user',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.user', verbose_name='Пользователь'),
- ),
- ]
diff --git a/hotels/migrations/0004_datalog.py b/hotels/migrations/0004_datalog.py
deleted file mode 100644
index 9cc06059..00000000
--- a/hotels/migrations/0004_datalog.py
+++ /dev/null
@@ -1,26 +0,0 @@
-# Generated by Django 5.1.4 on 2024-12-06 13:41
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('hotels', '0003_alter_hotel_options_alter_userhotel_options_and_more'),
- ]
-
- operations = [
- migrations.CreateModel(
- name='DataLog',
- 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='Дата создания')),
- ('data', models.JSONField(verbose_name='Данные')),
- ],
- options={
- 'verbose_name': 'Лог данных',
- 'verbose_name_plural': 'Логи данных',
- },
- ),
- ]
diff --git a/hotels/migrations/0005_apiconfiguration_apirequestlog_delete_datalog.py b/hotels/migrations/0005_apiconfiguration_apirequestlog_delete_datalog.py
deleted file mode 100644
index 9c670d56..00000000
--- a/hotels/migrations/0005_apiconfiguration_apirequestlog_delete_datalog.py
+++ /dev/null
@@ -1,47 +0,0 @@
-# Generated by Django 5.1.4 on 2024-12-06 13:51
-
-import django.db.models.deletion
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('hotels', '0004_datalog'),
- ]
-
- 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='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(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.DeleteModel(
- name='DataLog',
- ),
- ]
diff --git a/hotels/migrations/0006_pmsconfiguration_remove_hotel_api_key_and_more.py b/hotels/migrations/0006_pmsconfiguration_remove_hotel_api_key_and_more.py
deleted file mode 100644
index c21e8176..00000000
--- a/hotels/migrations/0006_pmsconfiguration_remove_hotel_api_key_and_more.py
+++ /dev/null
@@ -1,45 +0,0 @@
-# Generated by Django 5.1.4 on 2024-12-06 14:09
-
-import django.db.models.deletion
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('hotels', '0005_apiconfiguration_apirequestlog_delete_datalog'),
- ]
-
- 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.RemoveField(
- model_name='hotel',
- name='api_key',
- ),
- migrations.RemoveField(
- model_name='hotel',
- name='pms_type',
- ),
- migrations.RemoveField(
- model_name='hotel',
- name='public_key',
- ),
- migrations.AddField(
- model_name='hotel',
- name='api',
- field=models.OneToOneField(blank=True, help_text='API, связанный с этим отелем.', null=True, on_delete=django.db.models.deletion.SET_NULL, to='hotels.apiconfiguration', verbose_name='API'),
- ),
- ]
diff --git a/hotels/migrations/0007_pmsintegrationlog.py b/hotels/migrations/0007_pmsintegrationlog.py
deleted file mode 100644
index fe50d71e..00000000
--- a/hotels/migrations/0007_pmsintegrationlog.py
+++ /dev/null
@@ -1,28 +0,0 @@
-# Generated by Django 5.1.4 on 2024-12-06 23:02
-
-import django.db.models.deletion
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('hotels', '0006_pmsconfiguration_remove_hotel_api_key_and_more'),
- ]
-
- operations = [
- 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',
- },
- ),
- ]
diff --git a/hotels/migrations/0008_hotel_pms.py b/hotels/migrations/0008_hotel_pms.py
deleted file mode 100644
index cad2ce81..00000000
--- a/hotels/migrations/0008_hotel_pms.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Generated by Django 5.1.4 on 2024-12-06 23:38
-
-import django.db.models.deletion
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('hotels', '0007_pmsintegrationlog'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='hotel',
- name='pms',
- field=models.OneToOneField(blank=True, help_text='PMS система? используемая в заведении', null=True, on_delete=django.db.models.deletion.SET_NULL, to='hotels.pmsconfiguration', verbose_name='PMS система'),
- ),
- ]
diff --git a/hotels/migrations/0009_alter_hotel_pms.py b/hotels/migrations/0009_alter_hotel_pms.py
deleted file mode 100644
index 9e98065b..00000000
--- a/hotels/migrations/0009_alter_hotel_pms.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Generated by Django 5.1.4 on 2024-12-06 23:57
-
-import django.db.models.deletion
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('hotels', '0008_hotel_pms'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='hotel',
- name='pms',
- field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='hotels.pmsconfiguration', verbose_name='PMS система'),
- ),
- ]
diff --git a/hotels/migrations/0010_apirequestlog_hotels_apir_api_id_686bb0_idx_and_more.py b/hotels/migrations/0010_apirequestlog_hotels_apir_api_id_686bb0_idx_and_more.py
deleted file mode 100644
index 38bb7e0a..00000000
--- a/hotels/migrations/0010_apirequestlog_hotels_apir_api_id_686bb0_idx_and_more.py
+++ /dev/null
@@ -1,33 +0,0 @@
-# Generated by Django 5.1.4 on 2024-12-07 00:00
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('hotels', '0009_alter_hotel_pms'),
- ]
-
- operations = [
- 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'),
- ),
- migrations.AddIndex(
- model_name='pmsintegrationlog',
- index=models.Index(fields=['hotel'], name='hotels_pmsi_hotel_i_718b32_idx'),
- ),
- migrations.AddIndex(
- model_name='pmsintegrationlog',
- index=models.Index(fields=['checked_at'], name='hotels_pmsi_checked_e7768d_idx'),
- ),
- migrations.AddIndex(
- model_name='pmsintegrationlog',
- index=models.Index(fields=['status'], name='hotels_pmsi_status_dbf1d8_idx'),
- ),
- ]
diff --git a/hotels/migrations/0011_reservation_guest.py b/hotels/migrations/0011_reservation_guest.py
deleted file mode 100644
index 5fa662bf..00000000
--- a/hotels/migrations/0011_reservation_guest.py
+++ /dev/null
@@ -1,48 +0,0 @@
-# Generated by Django 5.1.4 on 2024-12-07 00:35
-
-import django.db.models.deletion
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('hotels', '0010_apirequestlog_hotels_apir_api_id_686bb0_idx_and_more'),
- ]
-
- operations = [
- 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='Скидка')),
- ('hotel', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='hotels.hotel', verbose_name='Отель')),
- ],
- 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')),
- ('reservation', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='guests', to='hotels.reservation', verbose_name='Бронирование')),
- ],
- options={
- 'verbose_name': 'Гость',
- 'verbose_name_plural': 'Гости',
- },
- ),
- ]
diff --git a/hotels/migrations/0012_userhotel_role_alter_userhotel_hotel_and_more.py b/hotels/migrations/0012_userhotel_role_alter_userhotel_hotel_and_more.py
deleted file mode 100644
index 29d97fac..00000000
--- a/hotels/migrations/0012_userhotel_role_alter_userhotel_hotel_and_more.py
+++ /dev/null
@@ -1,30 +0,0 @@
-# Generated by Django 5.1.4 on 2024-12-07 06:28
-
-import django.db.models.deletion
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('hotels', '0011_reservation_guest'),
- ('users', '0005_notificationsettings'),
- ]
-
- operations = [
- migrations.AddField(
- model_name='userhotel',
- name='role',
- field=models.CharField(choices=[('admin', 'Admin'), ('manager', 'Manager')], default='manager', max_length=50, verbose_name='Роль'),
- ),
- migrations.AlterField(
- model_name='userhotel',
- name='hotel',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='hotel_users', to='hotels.hotel', verbose_name='Отель'),
- ),
- migrations.AlterField(
- model_name='userhotel',
- name='user',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_hotels', to='users.user', verbose_name='Пользователь'),
- ),
- ]
diff --git a/hotels/migrations/0013_remove_userhotel_role_alter_userhotel_user.py b/hotels/migrations/0013_remove_userhotel_role_alter_userhotel_user.py
deleted file mode 100644
index 7655c612..00000000
--- a/hotels/migrations/0013_remove_userhotel_role_alter_userhotel_user.py
+++ /dev/null
@@ -1,24 +0,0 @@
-# Generated by Django 5.1.4 on 2024-12-07 07:04
-
-import django.db.models.deletion
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('hotels', '0012_userhotel_role_alter_userhotel_hotel_and_more'),
- ('users', '0005_notificationsettings'),
- ]
-
- operations = [
- migrations.RemoveField(
- model_name='userhotel',
- name='role',
- ),
- migrations.AlterField(
- model_name='userhotel',
- name='user',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.user', verbose_name='Пользователь'),
- ),
- ]
diff --git a/hotels/migrations/0014_alter_userhotel_user.py b/hotels/migrations/0014_alter_userhotel_user.py
deleted file mode 100644
index e7396f30..00000000
--- a/hotels/migrations/0014_alter_userhotel_user.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# Generated by Django 5.1.4 on 2024-12-07 08:24
-
-import django.db.models.deletion
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('hotels', '0013_remove_userhotel_role_alter_userhotel_user'),
- ('users', '0005_notificationsettings'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='userhotel',
- name='user',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_hotels', to='users.user', verbose_name='Пользователь'),
- ),
- ]
diff --git a/hotels/migrations/0015_alter_userhotel_user.py b/hotels/migrations/0015_alter_userhotel_user.py
deleted file mode 100644
index 3c6a4a5b..00000000
--- a/hotels/migrations/0015_alter_userhotel_user.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# Generated by Django 5.1.4 on 2024-12-07 08:29
-
-import django.db.models.deletion
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('hotels', '0014_alter_userhotel_user'),
- ('users', '0005_notificationsettings'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='userhotel',
- name='user',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='userhotels', to='users.user', verbose_name='Пользователь'),
- ),
- ]
diff --git a/hotels/migrations/0016_alter_userhotel_user.py b/hotels/migrations/0016_alter_userhotel_user.py
deleted file mode 100644
index e14c8459..00000000
--- a/hotels/migrations/0016_alter_userhotel_user.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# Generated by Django 5.1.4 on 2024-12-07 08:30
-
-import django.db.models.deletion
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('hotels', '0015_alter_userhotel_user'),
- ('users', '0005_notificationsettings'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='userhotel',
- name='user',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_hotels', to='users.user', verbose_name='Пользователь'),
- ),
- ]
diff --git a/hotels/migrations/0017_alter_userhotel_hotel_alter_userhotel_user.py b/hotels/migrations/0017_alter_userhotel_hotel_alter_userhotel_user.py
deleted file mode 100644
index b420cb7a..00000000
--- a/hotels/migrations/0017_alter_userhotel_hotel_alter_userhotel_user.py
+++ /dev/null
@@ -1,25 +0,0 @@
-# Generated by Django 5.1.4 on 2024-12-07 08:31
-
-import django.db.models.deletion
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('hotels', '0016_alter_userhotel_user'),
- ('users', '0005_notificationsettings'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='userhotel',
- name='hotel',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='hotelusers', to='hotels.hotel', verbose_name='Отель'),
- ),
- migrations.AlterField(
- model_name='userhotel',
- name='user',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='userhotel', to='users.user', verbose_name='Пользователь'),
- ),
- ]
diff --git a/hotels/migrations/0018_alter_userhotel_hotel_alter_userhotel_user.py b/hotels/migrations/0018_alter_userhotel_hotel_alter_userhotel_user.py
deleted file mode 100644
index 57d4e89c..00000000
--- a/hotels/migrations/0018_alter_userhotel_hotel_alter_userhotel_user.py
+++ /dev/null
@@ -1,25 +0,0 @@
-# Generated by Django 5.1.4 on 2024-12-07 08:36
-
-import django.db.models.deletion
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('hotels', '0017_alter_userhotel_hotel_alter_userhotel_user'),
- ('users', '0005_notificationsettings'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='userhotel',
- name='hotel',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='hotel_users', to='hotels.hotel', verbose_name='Отель'),
- ),
- migrations.AlterField(
- model_name='userhotel',
- name='user',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_hotel', to='users.user', verbose_name='Пользователь'),
- ),
- ]
diff --git a/hotels/migrations/0019_alter_apirequestlog_response_status.py b/hotels/migrations/0019_alter_apirequestlog_response_status.py
deleted file mode 100644
index cdb4cd10..00000000
--- a/hotels/migrations/0019_alter_apirequestlog_response_status.py
+++ /dev/null
@@ -1,19 +0,0 @@
-# Generated by Django 5.1.4 on 2024-12-07 11:11
-
-import django.core.validators
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('hotels', '0018_alter_userhotel_hotel_alter_userhotel_user'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='apirequestlog',
- name='response_status',
- field=models.IntegerField(validators=[django.core.validators.MinValueValidator(100), django.core.validators.MaxValueValidator(599)], verbose_name='HTTP статус ответа'),
- ),
- ]
diff --git a/hotels/migrations/0020_alter_userhotel_user.py b/hotels/migrations/0020_alter_userhotel_user.py
deleted file mode 100644
index b2a682df..00000000
--- a/hotels/migrations/0020_alter_userhotel_user.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# Generated by Django 5.1.4 on 2024-12-07 14:09
-
-import django.db.models.deletion
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('hotels', '0019_alter_apirequestlog_response_status'),
- ('users', '0005_notificationsettings'),
- ]
-
- operations = [
- migrations.AlterField(
- model_name='userhotel',
- name='user',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='user_hotels', to='users.user', 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
deleted file mode 100644
index f42a574f..00000000
--- a/hotels/migrations/0021_alter_hotel_pms_remove_pmsintegrationlog_hotel_and_more.py
+++ /dev/null
@@ -1,30 +0,0 @@
-# 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/migrations/__init__.py b/hotels/migrations/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/hotels/models.py b/hotels/models.py
index 2e8e15f1..3a9af67a 100644
--- a/hotels/models.py
+++ b/hotels/models.py
@@ -30,7 +30,7 @@ class Hotel(models.Model):
)
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Создан")
pms = models.ForeignKey(
- 'pms_integration.PMSConfiguration', # Используем строковую ссылку
+ 'pms_integration.PMSConfiguration',
on_delete=models.SET_NULL,
null=True,
blank=True,
diff --git a/pms_integration/admin.py b/pms_integration/admin.py
index aee974df..ccbcf45a 100644
--- a/pms_integration/admin.py
+++ b/pms_integration/admin.py
@@ -1,14 +1,59 @@
from django.contrib import admin
-from .models import PMSConfiguration, PMSIntegrationLog
# Register your models here.
+from .manager import PluginLoader
+from django.http import HttpResponseRedirect
+from django.urls import path
+from django.utils.html import format_html
+from django.shortcuts import render
+from django import forms
+from pms_integration.models import PMSConfiguration, PMSIntegrationLog
+
+class PMSConfigurationForm(forms.ModelForm):
+ class Meta:
+ model = PMSConfiguration
+ fields = "__all__"
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ plugins = PluginLoader.load_plugins()
+ self.fields['plugin_name'].choices = [(plugin, plugin) for plugin in plugins.keys()]
@admin.register(PMSConfiguration)
class PMSConfigurationAdmin(admin.ModelAdmin):
- list_display = ('name', 'parser_settings', 'description', 'created_at')
+ form = PMSConfigurationForm
+ list_display = ('name', 'plugin_name', 'created_at', 'check_plugins_button')
search_fields = ('name', 'description')
list_filter = ('created_at',)
ordering = ('-created_at',)
+ def get_urls(self):
+ """Добавляем URL для проверки плагинов."""
+ urls = super().get_urls()
+ custom_urls = [
+ path("check-plugins/", self.check_plugins, name="check-plugins"),
+ ]
+ return custom_urls + urls
+
+ def check_plugins(self, request):
+ """Проверка и отображение плагинов."""
+ plugins = PluginLoader.load_plugins()
+ plugin_details = [
+ {"name": plugin_name, "doc": plugins[plugin_name].__doc__ or "Нет документации"}
+ for plugin_name in plugins
+ ]
+ context = {
+ "title": "Проверка плагинов",
+ "plugin_details": plugin_details,
+ }
+ return render(request, "admin/check_plugins.html", context)
+
+ def check_plugins_button(self, obj):
+ """Добавляем кнопку для проверки плагинов."""
+ return format_html(
+ 'Проверить плагины',
+ "/admin/pms_integration/pmsconfiguration/check-plugins/",
+ )
+ check_plugins_button.short_description = "Проверить плагины"
@admin.register(PMSIntegrationLog)
class PMSIntegrationLogAdmin(admin.ModelAdmin):
diff --git a/pms_integration/api_client.py b/pms_integration/api_client.py
new file mode 100644
index 00000000..96f63ab2
--- /dev/null
+++ b/pms_integration/api_client.py
@@ -0,0 +1,92 @@
+import requests
+from datetime import datetime
+
+class APIClient:
+ """
+ Универсальный клиент для работы с API PMS.
+ """
+ def __init__(self, base_url, access_token=None, username=None, password=None):
+ """
+ Инициализация API клиента.
+
+ :param base_url: Базовый URL API
+ :param access_token: Токен доступа (если требуется)
+ :param username: Имя пользователя (если используется basic auth)
+ :param password: Пароль (если используется basic auth)
+ """
+ self.base_url = base_url
+ self.access_token = access_token
+ self.auth = (username, password) if username and password else None
+
+ def _build_headers(self):
+ """
+ Создает заголовки для запроса.
+ :return: Словарь заголовков.
+ """
+ headers = {'Content-Type': 'application/json'}
+ if self.access_token:
+ headers['Authorization'] = f'Bearer {self.access_token}'
+ return headers
+
+ def _make_request(self, method, endpoint, params=None, data=None):
+ """
+ Выполняет запрос к API.
+
+ :param method: HTTP метод ('GET', 'POST', и т.д.)
+ :param endpoint: Конечная точка API.
+ :param params: Параметры строки запроса (для GET).
+ :param data: Данные для тела запроса (для POST).
+ :return: Ответ API в формате JSON.
+ """
+ url = f"{self.base_url}{endpoint}"
+ headers = self._build_headers()
+ try:
+ response = requests.request(
+ method=method,
+ url=url,
+ headers=headers,
+ params=params,
+ json=data,
+ auth=self.auth
+ )
+ response.raise_for_status() # Генерирует исключение для HTTP ошибок
+ return response.json()
+ except requests.exceptions.RequestException as e:
+ print(f"Ошибка при запросе {method} {url}: {e}")
+ return None
+
+ def get(self, endpoint, params=None):
+ """
+ Выполняет GET запрос.
+ :param endpoint: Конечная точка API.
+ :param params: Параметры строки запроса.
+ :return: Ответ API в формате JSON.
+ """
+ return self._make_request('GET', endpoint, params=params)
+
+ def post(self, endpoint, data=None):
+ """
+ Выполняет POST запрос.
+ :param endpoint: Конечная точка API.
+ :param data: Данные для тела запроса.
+ :return: Ответ API в формате JSON.
+ """
+ return self._make_request('POST', endpoint, data=data)
+
+ def fetch_reservations(self, endpoint, from_date, to_date, pagination_start=0, pagination_count=50):
+ """
+ Получение данных о бронированиях.
+
+ :param endpoint: Конечная точка API для бронирований.
+ :param from_date: Дата начала в формате 'YYYY-MM-DDTHH:MM:SS'.
+ :param to_date: Дата окончания в формате 'YYYY-MM-DDTHH:MM:SS'.
+ :param pagination_start: Начальная точка пагинации.
+ :param pagination_count: Количество записей для выборки.
+ :return: Ответ API в формате JSON.
+ """
+ data = {
+ "from": from_date,
+ "until": to_date,
+ "pagination": {"from": pagination_start, "count": pagination_count}
+ }
+ return self.post(endpoint, data=data)
diff --git a/pms_integration/forms.py b/pms_integration/forms.py
new file mode 100644
index 00000000..6d052da7
--- /dev/null
+++ b/pms_integration/forms.py
@@ -0,0 +1,13 @@
+from django import forms
+from .models import PMSConfiguration
+from .manager import PluginLoader
+
+class PMSConfigurationForm(forms.ModelForm):
+ class Meta:
+ model = PMSConfiguration
+ fields = "__all__"
+
+ def __init__(self, *args, **kwargs):
+ super().__init__(*args, **kwargs)
+ plugins = PluginLoader.load_plugins()
+ self.fields['plugin_name'].choices = [(plugin, plugin) for plugin in plugins.keys()]
\ No newline at end of file
diff --git a/pms_integration/manager.py b/pms_integration/manager.py
index 5b1aa4df..c37813b3 100644
--- a/pms_integration/manager.py
+++ b/pms_integration/manager.py
@@ -1,18 +1,14 @@
import importlib
-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
-
+from django.conf import settings
+from .plugins.base_plugin import BasePMSPlugin
+from asgiref.sync import sync_to_async
class PluginLoader:
- """
- Класс для автоматической загрузки плагинов PMS.
- """
PLUGIN_PATH = Path(__file__).parent / "plugins"
-
+ print("Путь к папке плагинов:", PLUGIN_PATH.resolve())
+ print("Содержимое папки:", list(PLUGIN_PATH.iterdir()))
@staticmethod
def load_plugins():
plugins = {}
@@ -21,19 +17,17 @@ class PluginLoader:
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
+ print(f"Загружен плагин: {cls.__name__}")
except Exception as e:
- print(f"Ошибка загрузки плагина {module_name}: {e}")
+ print(f"Ошибка при загрузке модуля {module_name}: {e}")
+ print(f"Итоговый список плагинов: {list(plugins.keys())}")
return plugins
-
-
-
class PMSIntegrationManager:
def __init__(self, hotel_id):
self.hotel_id = hotel_id
@@ -41,28 +35,40 @@ class PMSIntegrationManager:
self.pms_config = None
self.plugin = None
- def load_hotel(self):
- from hotels.models import Hotel # Импорт здесь, чтобы избежать кругового импорта
- self.hotel = Hotel.objects.get(id=self.hotel_id)
+ async def load_hotel(self):
+ """
+ Загружает данные отеля и PMS конфигурацию.
+ """
+ from hotels.models import Hotel
+ self.hotel = await sync_to_async(Hotel.objects.select_related("pms").get)(id=self.hotel_id)
self.pms_config = self.hotel.pms
if not self.pms_config:
- raise ValueError(f"Отель {self.hotel.name} не связан с PMS системой.")
+ raise ValueError(f"Отель {self.hotel.name} не имеет связанной PMS конфигурации.")
def load_plugin(self):
+ """
+ Загружает плагин для PMS на основе конфигурации.
+ """
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)
+ if self.pms_config.plugin_name not in plugins:
+ raise ValueError(f"Плагин для PMS {self.pms_config.plugin_name} не найден.")
+ self.plugin = plugins[self.pms_config.plugin_name](self.pms_config)
def fetch_data(self):
+ """
+ Получает данные из PMS с использованием загруженного плагина.
+ """
if not self.plugin:
- raise ValueError("Плагин не загружен.")
+ self.load_plugin()
return self.plugin.fetch_data()
- def save_log(self, status, message):
- from .models import PMSIntegrationLog # Избегаем кругового импорта
- PMSIntegrationLog.objects.create(
+ async def save_log(self, status, message):
+ """
+ Сохраняет запись в лог интеграции.
+ """
+ from .models import PMSIntegrationLog
+ await sync_to_async(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
index 5168707a..8229ed7b 100644
--- a/pms_integration/migrations/0001_initial.py
+++ b/pms_integration/migrations/0001_initial.py
@@ -1,4 +1,4 @@
-# Generated by Django 5.1.4 on 2024-12-08 23:48
+# Generated by Django 5.1.4 on 2024-12-09 04:50
import django.db.models.deletion
from django.db import migrations, models
@@ -9,7 +9,7 @@ class Migration(migrations.Migration):
initial = True
dependencies = [
- ('hotels', '0020_alter_userhotel_user'),
+ ('hotels', '__first__'),
]
operations = [
@@ -18,8 +18,11 @@ class Migration(migrations.Migration):
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='Описание')),
+ ('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={
diff --git a/pms_integration/models.py b/pms_integration/models.py
index 8204915e..5083d409 100644
--- a/pms_integration/models.py
+++ b/pms_integration/models.py
@@ -1,11 +1,18 @@
from django.db import models
+from django.db import models
+from django.contrib.auth.models import AbstractUser
+
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="Дата создания")
+ url = models.URLField(verbose_name="URL API")
+ token = models.CharField(max_length=255, blank=True, null=True, verbose_name="Токен")
+ username = models.CharField(max_length=255, blank=True, null=True, verbose_name="Логин")
+ password = models.CharField(max_length=255, blank=True, null=True, verbose_name="Пароль")
+ plugin_name = models.CharField(max_length=255, verbose_name="Название плагина")
+ created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания") # Добавлено поле
+
def __str__(self):
return self.name
@@ -15,6 +22,7 @@ class PMSConfiguration(models.Model):
verbose_name_plural = "PMS системы"
+
class PMSIntegrationLog(models.Model):
hotel = models.ForeignKey(
'hotels.Hotel', # Используем строковую ссылку
diff --git a/pms_integration/plugins/base_plugin.py b/pms_integration/plugins/base_plugin.py
index 82e521ee..716a1764 100644
--- a/pms_integration/plugins/base_plugin.py
+++ b/pms_integration/plugins/base_plugin.py
@@ -1,15 +1,62 @@
from abc import ABC, abstractmethod
-
class BasePMSPlugin(ABC):
- def __init__(self, hotel, pms_config):
- self.hotel = hotel
+ """
+ Базовый класс для всех PMS плагинов.
+ Плагин должен уметь:
+ - Возвращать данные fetch_data()
+ - Предоставлять дефолтные parser_settings
+ - Проходить базовую валидацию (validate_plugin)
+ """
+
+ def __init__(self, pms_config):
+ """
+ pms_config: объект PMSConfiguration
+ """
self.pms_config = pms_config
+
@abstractmethod
- def fetch_and_parse(self):
+ def _fetch_data(self):
"""
- Этот метод должен быть реализован в каждом плагине.
- Он должен получать данные из API и возвращать их в нужном формате.
+ Абстрактный метод для получения данных.
"""
pass
+
+ def fetch_data(self):
+ """
+ Обертка для выполнения _fetch_data с возможной дополнительной обработкой.
+ """
+ return self._fetch_data()
+
+ @abstractmethod
+ def get_default_parser_settings(self):
+ """
+ Возвращает словарь/JSON с дефолтными настройками разбора.
+ Например:
+ {
+ "fields_mapping": {
+ "reservation_id": "id",
+ "check_in": "from",
+ "check_out": "until"
+ },
+ "conditions": {
+ "checkInStatus": "Заселен"
+ }
+ }
+ """
+ print("get_default_parser_settings. pms_config:", self.pms_config)
+ return {}
+
+ def validate_plugin(self):
+ """
+ Проверка на соответствие требованиям.
+ Можно проверить наличие методов или полей.
+ """
+ # Например, проверить наличие fetch_data и get_default_parser_settings
+ required_methods = ["fetch_data", "get_default_parser_settings"]
+ for m in required_methods:
+ if not hasattr(self, m):
+ raise ValueError(f"Плагин {type(self).__name__} не реализует метод {m}.")
+ # Можно добавить дополнительные проверки
+ return True
diff --git a/pms_integration/plugins/bnovo_pms.py b/pms_integration/plugins/bnovo_pms.py
index 333da2cf..89374bee 100644
--- a/pms_integration/plugins/bnovo_pms.py
+++ b/pms_integration/plugins/bnovo_pms.py
@@ -3,11 +3,51 @@ from .base_plugin import BasePMSPlugin
class BnovoPMS(BasePMSPlugin):
+ """
+ Плагин для интеграции с Bnovo.
+ """
+ json_schema = {
+ "type": "object",
+ "properties": {
+ "id": {"type": "integer"},
+ "number": {"type": "integer"},
+ "roomTypeName": {"type": "string"},
+ "checkInStatus": {"type": "string"},
+ "guests": {"type": "array"},
+ },
+ "required": ["id", "number", "roomTypeName", "checkInStatus", "guests"]
+ }
+ def get_default_parser_settings(self):
+ """
+ Возвращает настройки парсера по умолчанию.
+ """
+ return {
+ "field_mapping": {
+ "room_name": "roomNumber",
+ "check_in": "from",
+ "check_out": "until",
+ },
+ "date_format": "%Y-%m-%dT%H:%M:%S"
+ }
+ def fetch_data(self):
+ response = requests.get(self.pms_config.url, headers={"Authorization": f"Bearer {self.pms_config.token}"})
+ response.raise_for_status()
+ data = response.json()
+
+ # Проверка структуры
+ expected_fields = self.pms_config.parser_settings.get("fields_mapping", {})
+ for field in expected_fields.values():
+ if field not in data[0]: # Проверяем первую запись
+ raise ValueError(f"Поле {field} отсутствует в ответе API.")
+
+ return data
+
def fetch_and_parse(self):
response = requests.get(
self.pms_config.url,
headers={"Authorization": f"Bearer {self.pms_config.token}"}
)
+ self.validate_response(response) # Проверка соответствия структуры
if response.status_code != 200:
raise ValueError(f"Ошибка запроса к PMS Bnovo: {response.text}")
@@ -28,3 +68,4 @@ class BnovoPMS(BasePMSPlugin):
}
reservations.append(reservation)
return reservations
+
diff --git a/pms_integration/plugins/ecvi_pms.py b/pms_integration/plugins/ecvi_pms.py
index 681179b6..4caec542 100644
--- a/pms_integration/plugins/ecvi_pms.py
+++ b/pms_integration/plugins/ecvi_pms.py
@@ -2,6 +2,38 @@ from .base_plugin import BasePMSPlugin
class EcviPMS(BasePMSPlugin):
- def fetch_and_parse(self):
- # Реализация логики для ECVI
- pass
+ """
+ Плагин для PMS Shelter.
+ """
+ def fetch_data(self):
+ print("Fetching data from Ecvi PMS...")
+ # Реализация метода получения данных из PMS Shelter
+ response = requests.get(self.pms_config.url, headers={"Authorization": f"Bearer {self.pms_config.token}"})
+ print("Response status:", response.status_code)
+ response.raise_for_status()
+ data = response.json()
+ print("Number of rooms:", len(data))
+ return data
+ json_schema = {
+ "type": "object",
+ "properties": {
+ "id": {"type": "integer"},
+ "number": {"type": "integer"},
+ "roomTypeName": {"type": "string"},
+ "checkInStatus": {"type": "string"},
+ "guests": {"type": "array"},
+ },
+ "required": ["id", "number", "roomTypeName", "checkInStatus", "guests"]
+ }
+ def get_default_parser_settings(self):
+ """
+ Возвращает настройки парсера по умолчанию.
+ """
+ return {
+ "field_mapping": {
+ "room_name": "roomNumber",
+ "check_in": "from",
+ "check_out": "until",
+ },
+ "date_format": "%Y-%m-%dT%H:%M:%S"
+ }
\ No newline at end of file
diff --git a/pms_integration/plugins/shelter_pms.py b/pms_integration/plugins/shelter_pms.py
index 728de7ac..22aad21a 100644
--- a/pms_integration/plugins/shelter_pms.py
+++ b/pms_integration/plugins/shelter_pms.py
@@ -1,31 +1,100 @@
-# pms_integration/plugins/shelter_pms.py
-
import requests
+import json
+from datetime import datetime, timedelta
+from asgiref.sync import sync_to_async
from .base_plugin import BasePMSPlugin
+from hotels.models import Reservation
+from hotels.models import Hotel
-class ShelterPMSPlugin(BasePMSPlugin):
- """Плагин для интеграции с Shelter PMS."""
- def fetch_data(self):
- """Получение данных от Shelter 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 Shelter(BasePMSPlugin):
+ def __init__(self, config):
+ super().__init__(config)
+ self.token = config.token
- def parse_data(self, raw_data):
- """Обработка данных от Shelter 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"),
+ def get_default_parser_settings(self):
+ """
+ Возвращает настройки по умолчанию для обработки данных.
+ """
+ return {
+ "date_format": "%Y-%m-%dT%H:%M:%S",
+ "timezone": "UTC"
+ }
+
+ def _fetch_data(self):
+ """
+ Выполняет запрос к API PMS для получения данных.
+ """
+ url = 'https://pms.frontdesk24.ru/sheltercloudapi/Reservations/ByFilter'
+ headers = {
+ 'accept': 'text/plain',
+ 'Authorization': f'Bearer {self.token}',
+ 'Content-Type': 'application/json',
+ }
+
+ from_index = 0
+ count_per_request = 50
+ total_count = None
+ all_items = []
+ now = datetime.now()
+ start_date = (now - timedelta(days=60)).strftime('%Y-%m-%dT%H:%M:%SZ')
+ end_date = (now + timedelta(days=60)).strftime('%Y-%m-%dT%H:%M:%SZ')
+
+ while total_count is None or from_index < total_count:
+ data = {
+ "from": start_date,
+ "until": end_date,
+ "pagination": {
+ "from": from_index,
+ "count": count_per_request
+ }
}
- for res in reservations
- ]
+
+ response = requests.post(url, headers=headers, data=json.dumps(data))
+ if response.status_code == 200:
+ response_data = response.json()
+ items = response_data.get("items", [])
+ all_items.extend(items)
+
+ if total_count is None:
+ total_count = response_data.get("count", 0)
+
+ from_index += len(items)
+ else:
+ raise ValueError(f'Shelter API Error: {response.status_code}')
+
+ return all_items
+
+ async def _save_to_db(self, data, hotel_id):
+ """
+ Сохраняет данные о бронированиях в таблицу Reservation.
+ :param data: Список данных о бронированиях.
+ :param hotel_id: ID отеля, к которому относятся бронирования.
+ """
+ hotel = await sync_to_async(Hotel.objects.get)(id=hotel_id)
+ for item in data:
+ print(f"Данные для сохранения: {item}")
+
+ try:
+ reservation, created = await sync_to_async(Reservation.objects.update_or_create)(
+ reservation_id=item["id"],
+ hotel=hotel,
+ defaults={
+ "room_number": item.get("roomNumber", ""), # Номер комнаты
+ "room_type": item.get("roomTypeName", ""), # Тип комнаты
+ "check_in": datetime.strptime(item["from"], '%Y-%m-%dT%H:%M:%S'), # Дата заезда
+ "check_out": datetime.strptime(item["until"], '%Y-%m-%dT%H:%M:%S'), # Дата выезда
+ "status": item.get("checkInStatus", ""), # Статус бронирования
+ "price": item.get("reservationPrice", 0), # Цена
+ "discount": item.get("discount", 0), # Скидка
+ }
+ )
+ if created:
+ print(f"Создана запись: {reservation}")
+ else:
+ print(f"Обновлена запись: {reservation}")
+ except Exception as e:
+ print(f"Ошибка при сохранении бронирования ID {item['id']}: {e}")
+
+
+
\ No newline at end of file
diff --git a/pms_integration/templates/admin/check_plugins.html b/pms_integration/templates/admin/check_plugins.html
new file mode 100644
index 00000000..6d757f8c
--- /dev/null
+++ b/pms_integration/templates/admin/check_plugins.html
@@ -0,0 +1,16 @@
+{% extends "admin/base_site.html" %}
+
+{% block content %}
+Проверка доступных плагинов
+Ниже перечислены плагины, доступные в системе:
+
+ {% for plugin_name in plugins.keys %}
+ -
+ {{ plugin_name }}: {{ plugins[plugin_name].__doc__ }}
+
+ {% endfor %}
+
+
+ Вернуться в админку
+
+{% endblock %}
diff --git a/reports/Golden Hills 3_report.pdf b/reports/Golden Hills 3_report.pdf
new file mode 100644
index 00000000..7d188cc7
Binary files /dev/null and b/reports/Golden Hills 3_report.pdf differ
diff --git a/reports/Golden Hills 4*_report.pdf b/reports/Golden Hills 4*_report.pdf
index 1103f0aa..476d268f 100644
Binary files a/reports/Golden Hills 4*_report.pdf and b/reports/Golden Hills 4*_report.pdf differ
diff --git a/requirements.txt b/requirements.txt
index 6a14681d..037e7ec7 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -31,3 +31,4 @@ sqlparse==0.5.2
tzdata==2024.2
tzlocal==5.2
urllib3==2.2.3
+jsonschema
\ No newline at end of file
diff --git a/touchh/settings.py b/touchh/settings.py
index 75518734..bb44e1ff 100644
--- a/touchh/settings.py
+++ b/touchh/settings.py
@@ -41,9 +41,9 @@ INSTALLED_APPS = [
'django.contrib.messages',
'django.contrib.staticfiles',
'bot',
+ 'pms_integration',
'hotels',
'users',
- 'pms_integration'
]
MIDDLEWARE = [
@@ -112,6 +112,19 @@ AUTH_PASSWORD_VALIDATORS = [
},
]
+LOGGING = {
+ 'version': 1,
+ 'disable_existing_loggers': False,
+ 'handlers': {
+ 'console': {
+ 'class': 'logging.StreamHandler',
+ },
+ },
+ 'root': {
+ 'handlers': ['console'],
+ 'level': 'WARNING',
+ },
+}
# Internationalization
# https://docs.djangoproject.com/en/5.1/topics/i18n/
@@ -146,6 +159,7 @@ JAZZMIN_SETTINGS = {
"welcome_sign": "Welcome to Hotel Management System",
"show_sidebar": True,
"navigation_expanded": True,
+ "hide_models": ["users", "guests"],
"site_logo": None, # Путь к логотипу, например "static/images/logo.png"
"site_logo_classes": "img-circle", # Классы CSS для логотипа
"site_icon": None, # Иконка сайта (favicon), например "static/images/favicon.ico"
diff --git a/users/migrations/0001_initial.py b/users/migrations/0001_initial.py
index 67cccc82..9157ca0e 100644
--- a/users/migrations/0001_initial.py
+++ b/users/migrations/0001_initial.py
@@ -1,4 +1,4 @@
-# Generated by Django 5.1.4 on 2024-12-05 23:39
+# Generated by Django 5.1.4 on 2024-12-09 04:50
import django.contrib.auth.models
import django.contrib.auth.validators
@@ -17,6 +17,49 @@ class Migration(migrations.Migration):
]
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=[
@@ -31,29 +74,47 @@ class Migration(migrations.Migration):
('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='Telegram ID')),
- ('chat_id', models.BigIntegerField(blank=True, null=True, unique=True, verbose_name='Chat ID')),
- ('role', models.CharField(choices=[('admin', 'Administrator'), ('hotel_user', 'Hotel User')], default='hotel_user', max_length=20, verbose_name='Role')),
- ('confirmed', models.BooleanField(default=False, verbose_name='Confirmed')),
+ ('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': 'user',
- 'verbose_name_plural': 'users',
- 'abstract': False,
+ '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='Confirmation Code')),
- ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created At')),
- ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.user', verbose_name='User')),
+ ('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': 'Подтверждения пользователей',
+ },
),
]
diff --git a/users/migrations/0002_localuseractivitylog_useractivitylog.py b/users/migrations/0002_localuseractivitylog_useractivitylog.py
deleted file mode 100644
index 6cf3698b..00000000
--- a/users/migrations/0002_localuseractivitylog_useractivitylog.py
+++ /dev/null
@@ -1,56 +0,0 @@
-# Generated by Django 5.1.4 on 2024-12-06 03:59
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('users', '0001_initial'),
- ]
-
- 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)),
- ('user_id', models.BigIntegerField()),
- ('ip', models.CharField(blank=True, max_length=100, null=True)),
- ('created', models.DateTimeField(auto_now_add=True)),
- ('timestamp', models.IntegerField()),
- ('date_time', models.DateTimeField()),
- ('referred', models.CharField(blank=True, max_length=255, null=True)),
- ('agent', models.CharField(blank=True, max_length=255, null=True)),
- ('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': 'User Activity Log',
- 'verbose_name_plural': 'User Activity Logs',
- 'db_table': 'user_activity_log',
- },
- ),
- ]
diff --git a/users/migrations/0003_alter_useractivitylog_options_alter_user_confirmed_and_more.py b/users/migrations/0003_alter_useractivitylog_options_alter_user_confirmed_and_more.py
deleted file mode 100644
index 15c26425..00000000
--- a/users/migrations/0003_alter_useractivitylog_options_alter_user_confirmed_and_more.py
+++ /dev/null
@@ -1,44 +0,0 @@
-# Generated by Django 5.1.4 on 2024-12-06 04:14
-
-import django.db.models.deletion
-import uuid
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('users', '0002_localuseractivitylog_useractivitylog'),
- ]
-
- operations = [
- migrations.AlterModelOptions(
- name='useractivitylog',
- options={'verbose_name': 'Журнал активности', 'verbose_name_plural': 'Журналы активности'},
- ),
- migrations.AlterField(
- model_name='user',
- name='confirmed',
- field=models.BooleanField(default=False, verbose_name='Подтвержден'),
- ),
- migrations.AlterField(
- model_name='user',
- name='role',
- field=models.CharField(choices=[('admin', 'Администратор системы'), ('hotel_user', 'Сотрудник отеля')], default='hotel_user', max_length=20, verbose_name='Роль'),
- ),
- migrations.AlterField(
- model_name='userconfirmation',
- name='confirmation_code',
- field=models.UUIDField(default=uuid.uuid4, verbose_name='Код подтверждения'),
- ),
- migrations.AlterField(
- model_name='userconfirmation',
- name='created_at',
- field=models.DateTimeField(auto_now_add=True, verbose_name='Создан: '),
- ),
- migrations.AlterField(
- model_name='userconfirmation',
- name='user',
- field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='users.user', verbose_name='Пользователь'),
- ),
- ]
diff --git a/users/migrations/0004_alter_user_options_alter_userconfirmation_options_and_more.py b/users/migrations/0004_alter_user_options_alter_userconfirmation_options_and_more.py
deleted file mode 100644
index a890fc39..00000000
--- a/users/migrations/0004_alter_user_options_alter_userconfirmation_options_and_more.py
+++ /dev/null
@@ -1,66 +0,0 @@
-# Generated by Django 5.1.4 on 2024-12-06 04:26
-
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('users', '0003_alter_useractivitylog_options_alter_user_confirmed_and_more'),
- ]
-
- operations = [
- migrations.AlterModelOptions(
- name='user',
- options={'verbose_name': 'Пользователь', 'verbose_name_plural': 'Пользователи'},
- ),
- migrations.AlterModelOptions(
- name='userconfirmation',
- options={'verbose_name': 'Подтверждение пользователя', 'verbose_name_plural': 'Подтверждения пользователей'},
- ),
- migrations.AlterField(
- model_name='user',
- name='chat_id',
- field=models.BigIntegerField(blank=True, null=True, unique=True, verbose_name='ID чата в телеграм'),
- ),
- migrations.AlterField(
- model_name='user',
- name='telegram_id',
- field=models.BigIntegerField(blank=True, null=True, unique=True, verbose_name='ID Телеграм'),
- ),
- migrations.AlterField(
- model_name='useractivitylog',
- name='agent',
- field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Браузер'),
- ),
- migrations.AlterField(
- model_name='useractivitylog',
- name='created',
- field=models.DateTimeField(auto_now_add=True, verbose_name='Создан'),
- ),
- migrations.AlterField(
- model_name='useractivitylog',
- name='date_time',
- field=models.DateTimeField(verbose_name='Дата'),
- ),
- migrations.AlterField(
- model_name='useractivitylog',
- name='id',
- field=models.BigAutoField(primary_key=True, serialize=False, verbose_name='ID'),
- ),
- migrations.AlterField(
- model_name='useractivitylog',
- name='ip',
- field=models.CharField(blank=True, max_length=100, null=True, verbose_name='IP адрес'),
- ),
- migrations.AlterField(
- model_name='useractivitylog',
- name='timestamp',
- field=models.IntegerField(verbose_name='Время'),
- ),
- migrations.AlterField(
- model_name='useractivitylog',
- name='user_id',
- field=models.BigIntegerField(verbose_name='ID пользователя'),
- ),
- ]
diff --git a/users/migrations/0005_notificationsettings.py b/users/migrations/0005_notificationsettings.py
deleted file mode 100644
index 2d2bc730..00000000
--- a/users/migrations/0005_notificationsettings.py
+++ /dev/null
@@ -1,29 +0,0 @@
-# Generated by Django 5.1.4 on 2024-12-06 12:01
-
-import django.db.models.deletion
-from django.db import migrations, models
-
-
-class Migration(migrations.Migration):
-
- dependencies = [
- ('users', '0004_alter_user_options_alter_userconfirmation_options_and_more'),
- ]
-
- operations = [
- 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': 'Способы оповещений',
- },
- ),
- ]