From caeb3f80bdd9a3418d2fe838b70a6dbaff515fe8 Mon Sep 17 00:00:00 2001 From: Andrey Smakotin Date: Wed, 14 Jan 2026 16:30:28 +0300 Subject: [PATCH] =?UTF-8?q?refactor(db):=20=D0=BA=D0=BE=D0=BD=D1=81=D0=BE?= =?UTF-8?q?=D0=BB=D0=B8=D0=B4=D0=B0=D1=86=D0=B8=D1=8F=20=D0=BC=D0=B8=D0=B3?= =?UTF-8?q?=D1=80=D0=B0=D1=86=D0=B8=D0=B9=20=D0=B8=20=D1=80=D0=B5=D1=84?= =?UTF-8?q?=D0=B0=D0=BA=D1=82=D0=BE=D1=80=D0=B8=D0=BD=D0=B3=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=B4=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Объединены изменения из промежуточных миграций в начальные миграции для упрощения истории базы данных. Удалены миграции: accounts/0002, discounts/0002, orders/0003-0004, products/0002-0005, user_roles/0002, system_settings/0001-0002, integrations/0001-0002. Добавлена автоматическая creation пользователя при установке пароля. Обновлен UI страницы установки пароля с кастомным стилем. Добавлен conditional rendering для кнопки синхронизации Recommerce. Исправлены редиректы с 'index' на '/' в accounts views. Добавлена проверка request.tenant в navbar и authenticate метод в auth backend. --- myproject/accounts/migrations/0001_initial.py | 17 +-- ...2_remove_customuser_first_name_and_more.py | 54 ------- .../accounts/password_setup_confirm.html | 135 ++++++++++++++---- myproject/accounts/views.py | 30 ++-- .../customers/migrations/0001_initial.py | 2 +- .../customers/migrations/0002_initial.py | 2 +- .../discounts/migrations/0001_initial.py | 7 +- .../migrations/0002_add_combine_mode.py | 18 --- ..._integration_models.py => 0001_initial.py} | 22 ++- .../0002_integrationcategorymapping.py | 33 ----- .../inventory/migrations/0001_initial.py | 2 +- .../inventory/migrations/0002_initial.py | 2 +- myproject/orders/migrations/0001_initial.py | 2 +- myproject/orders/migrations/0002_initial.py | 2 +- ...count_order_applied_promo_code_and_more.py | 40 ------ .../0004_remove_old_discount_fields.py | 33 ----- .../platform_admin/migrations/0001_initial.py | 2 +- myproject/products/migrations/0001_initial.py | 35 +++-- ...onfigurableproduct_archived_by_and_more.py | 68 --------- .../migrations/0003_add_marketing_flags.py | 58 -------- ...urableproduct_primary_category_and_more.py | 29 ---- ...e_primary_category_to_external_category.py | 72 ---------- .../templates/products/products_list.html | 17 +++ myproject/products/views/product_views.py | 19 ++- .../migrations/0001_initial.py | 30 ---- .../0002_delete_integrationconfig.py | 16 --- myproject/templates/navbar.html | 18 ++- myproject/tenants/migrations/0001_initial.py | 2 +- myproject/user_roles/auth_backend.py | 7 + .../user_roles/migrations/0001_initial.py | 4 +- .../migrations/0002_alter_role_code.py | 18 --- 31 files changed, 238 insertions(+), 558 deletions(-) delete mode 100644 myproject/accounts/migrations/0002_remove_customuser_first_name_and_more.py delete mode 100644 myproject/discounts/migrations/0002_add_combine_mode.py rename myproject/integrations/migrations/{0001_add_integration_models.py => 0001_initial.py} (73%) delete mode 100644 myproject/integrations/migrations/0002_integrationcategorymapping.py delete mode 100644 myproject/orders/migrations/0003_order_applied_discount_order_applied_promo_code_and_more.py delete mode 100644 myproject/orders/migrations/0004_remove_old_discount_fields.py delete mode 100644 myproject/products/migrations/0002_alter_configurableproduct_archived_by_and_more.py delete mode 100644 myproject/products/migrations/0003_add_marketing_flags.py delete mode 100644 myproject/products/migrations/0004_configurableproduct_primary_category_and_more.py delete mode 100644 myproject/products/migrations/0005_rename_primary_category_to_external_category.py delete mode 100644 myproject/system_settings/migrations/0001_initial.py delete mode 100644 myproject/system_settings/migrations/0002_delete_integrationconfig.py delete mode 100644 myproject/user_roles/migrations/0002_alter_role_code.py diff --git a/myproject/accounts/migrations/0001_initial.py b/myproject/accounts/migrations/0001_initial.py index 279399f..367dc8d 100644 --- a/myproject/accounts/migrations/0001_initial.py +++ b/myproject/accounts/migrations/0001_initial.py @@ -1,6 +1,5 @@ -# Generated by Django 5.0.10 on 2026-01-08 15:58 +# Generated by Django 5.0.10 on 2026-01-14 07:04 -import django.contrib.auth.validators import django.utils.timezone import uuid from django.db import migrations, models @@ -11,7 +10,6 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('auth', '0012_alter_user_first_name_max_length'), ] operations = [ @@ -21,21 +19,16 @@ class Migration(migrations.Migration): ('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')), - ('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')), ('email', models.EmailField(max_length=254, unique=True)), ('name', models.CharField(max_length=100)), + ('is_active', models.BooleanField(default=True)), + ('is_staff', models.BooleanField(default=False)), + ('is_superuser', models.BooleanField(default=False)), + ('date_joined', models.DateTimeField(default=django.utils.timezone.now)), ('is_email_confirmed', models.BooleanField(default=False)), ('email_confirmation_token', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)), ('email_confirmed_at', models.DateTimeField(blank=True, null=True)), ('password_reset_token', models.UUIDField(blank=True, editable=False, null=True, unique=True)), - ('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to.', related_name='custom_user_set', to='auth.group', verbose_name='groups')), - ('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='custom_user_set', to='auth.permission', verbose_name='user permissions')), ], options={ 'verbose_name': 'Пользователь магазина', diff --git a/myproject/accounts/migrations/0002_remove_customuser_first_name_and_more.py b/myproject/accounts/migrations/0002_remove_customuser_first_name_and_more.py deleted file mode 100644 index 0a07b89..0000000 --- a/myproject/accounts/migrations/0002_remove_customuser_first_name_and_more.py +++ /dev/null @@ -1,54 +0,0 @@ -# Generated by Django 5.0.10 on 2026-01-09 21:04 - -import django.utils.timezone -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('accounts', '0001_initial'), - ] - - operations = [ - migrations.RemoveField( - model_name='customuser', - name='first_name', - ), - migrations.RemoveField( - model_name='customuser', - name='groups', - ), - migrations.RemoveField( - model_name='customuser', - name='last_name', - ), - migrations.RemoveField( - model_name='customuser', - name='user_permissions', - ), - migrations.RemoveField( - model_name='customuser', - name='username', - ), - migrations.AlterField( - model_name='customuser', - name='date_joined', - field=models.DateTimeField(default=django.utils.timezone.now), - ), - migrations.AlterField( - model_name='customuser', - name='is_active', - field=models.BooleanField(default=True), - ), - migrations.AlterField( - model_name='customuser', - name='is_staff', - field=models.BooleanField(default=False), - ), - migrations.AlterField( - model_name='customuser', - name='is_superuser', - field=models.BooleanField(default=False), - ), - ] diff --git a/myproject/accounts/templates/accounts/password_setup_confirm.html b/myproject/accounts/templates/accounts/password_setup_confirm.html index a7e4af2..8919d6e 100644 --- a/myproject/accounts/templates/accounts/password_setup_confirm.html +++ b/myproject/accounts/templates/accounts/password_setup_confirm.html @@ -1,35 +1,108 @@ -{% extends 'base.html' %} + + + + + + Установка пароля + + + +
+

Установка пароля

+

для {{ tenant.name }}

-{% block title %}Установка пароля{% endblock %} + {% if messages %} +
+ {% for message in messages %} +
{{ message }}
+ {% endfor %} + {% endif %} -{% block content %} -
-
-

Установка пароля

- - -
- Добро пожаловать! Ваш магазин {{ tenant.name }} активирован. -
Установите пароль для входа в систему. -
- -
-
-
- {% csrf_token %} - {% include 'accounts/password_input.html' with field_name='password1' field_label='Пароль' required=True %} - {% include 'accounts/password_input.html' with field_name='password2' field_label='Подтверждение пароля' required=True %} - -
- - -
- - После установки пароля вы автоматически войдете в свой магазин. - -
+
+ {% csrf_token %} +
+ +
-
+
+ + +
+ +
-
-{% endblock %} + + diff --git a/myproject/accounts/views.py b/myproject/accounts/views.py index 6873c5f..2a949e2 100644 --- a/myproject/accounts/views.py +++ b/myproject/accounts/views.py @@ -61,7 +61,7 @@ def login_view(request): def logout_view(request): logout(request) - return redirect('index') + return redirect('/') @login_required @@ -142,7 +142,7 @@ def password_reset_confirm(request, token): user = CustomUser.objects.get(password_reset_token=token) except CustomUser.DoesNotExist: messages.error(request, 'Ссылка для восстановления пароля недействительна.') - return redirect('index') + return redirect('/') if request.method == 'POST': password1 = request.POST.get('password1') @@ -178,7 +178,7 @@ def password_setup_confirm(request, token): ) except TenantRegistration.DoesNotExist: messages.error(request, 'Ссылка для настройки пароля недействительна.') - return redirect('index') + return redirect('/') # Проверить истечение токена (7 дней) if registration.password_setup_token_created_at: @@ -188,24 +188,30 @@ def password_setup_confirm(request, token): request, 'Ссылка для настройки пароля истекла. Пожалуйста, свяжитесь с поддержкой.' ) - return redirect('index') + return redirect('/') # Получить тенант и пользователя-владельца from django.db import connection tenant = registration.tenant if not tenant: messages.error(request, 'Тенант не найден.') - return redirect('index') + return redirect('/') # Переключиться на схему тенанта чтобы найти владельца connection.set_tenant(tenant) - User = get_user_model() - try: - owner = User.objects.get(email=registration.owner_email) - except User.DoesNotExist: - connection.set_schema_to_public() - messages.error(request, 'Пользователь не найден.') - return redirect('index') + from accounts.models import CustomUser + + # Создаём пользователя если он не существует (для случаев когда активация прошла без создания пользователя) + owner, created = CustomUser.objects.get_or_create( + email=registration.owner_email, + defaults={ + 'name': registration.owner_name, + 'is_active': False, + } + ) + if created: + owner.is_email_confirmed = True + owner.save() # Обработать POST - установить пароль if request.method == 'POST': diff --git a/myproject/customers/migrations/0001_initial.py b/myproject/customers/migrations/0001_initial.py index 020d30a..c5f5d19 100644 --- a/myproject/customers/migrations/0001_initial.py +++ b/myproject/customers/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.0.10 on 2026-01-08 15:58 +# Generated by Django 5.0.10 on 2026-01-14 07:04 import django.db.models.deletion import phonenumber_field.modelfields diff --git a/myproject/customers/migrations/0002_initial.py b/myproject/customers/migrations/0002_initial.py index b6cc60d..bfe63e3 100644 --- a/myproject/customers/migrations/0002_initial.py +++ b/myproject/customers/migrations/0002_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.0.10 on 2026-01-08 15:58 +# Generated by Django 5.0.10 on 2026-01-14 07:04 import django.db.models.deletion from django.db import migrations, models diff --git a/myproject/discounts/migrations/0001_initial.py b/myproject/discounts/migrations/0001_initial.py index e48157c..a4b5177 100644 --- a/myproject/discounts/migrations/0001_initial.py +++ b/myproject/discounts/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.0.10 on 2026-01-10 21:07 +# Generated by Django 5.0.10 on 2026-01-14 07:04 import django.db.models.deletion from django.db import migrations, models @@ -9,10 +9,10 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('accounts', '0002_remove_customuser_first_name_and_more'), + ('accounts', '0001_initial'), ('customers', '0002_initial'), ('orders', '0002_initial'), - ('products', '0002_alter_configurableproduct_archived_by_and_more'), + ('products', '0001_initial'), ] operations = [ @@ -34,6 +34,7 @@ class Migration(migrations.Migration): ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')), ('min_order_amount', models.DecimalField(blank=True, decimal_places=2, help_text='Скидка применяется только если сумма заказа >= этого значения', max_digits=10, null=True, verbose_name='Мин. сумма заказа')), ('is_auto', models.BooleanField(default=False, help_text='Применяется автоматически при выполнении условий', verbose_name='Автоматическая')), + ('combine_mode', models.CharField(choices=[('stack', 'Складывать (суммировать)'), ('max_only', 'Только максимум'), ('exclusive', 'Исключающая (отменяет остальные)')], default='max_only', help_text='stack = суммировать с другими, max_only = применить максимальную, exclusive = отменить остальные', max_length=20, verbose_name='Режим объединения')), ('categories', models.ManyToManyField(blank=True, related_name='discounts', to='products.productcategory', verbose_name='Категории')), ('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_discounts', to='accounts.customuser', verbose_name='Создал')), ('excluded_products', models.ManyToManyField(blank=True, related_name='excluded_from_discounts', to='products.product', verbose_name='Исключенные товары')), diff --git a/myproject/discounts/migrations/0002_add_combine_mode.py b/myproject/discounts/migrations/0002_add_combine_mode.py deleted file mode 100644 index 5c9e161..0000000 --- a/myproject/discounts/migrations/0002_add_combine_mode.py +++ /dev/null @@ -1,18 +0,0 @@ -# Generated by Django 5.0.10 on 2026-01-10 23:55 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('discounts', '0001_initial'), - ] - - operations = [ - migrations.AddField( - model_name='discount', - name='combine_mode', - field=models.CharField(choices=[('stack', 'Складывать (суммировать)'), ('max_only', 'Только максимум'), ('exclusive', 'Исключающая (отменяет остальные)')], default='max_only', help_text='stack = суммировать с другими, max_only = применить максимальную, exclusive = отменить остальные', max_length=20, verbose_name='Режим объединения'), - ), - ] diff --git a/myproject/integrations/migrations/0001_add_integration_models.py b/myproject/integrations/migrations/0001_initial.py similarity index 73% rename from myproject/integrations/migrations/0001_add_integration_models.py rename to myproject/integrations/migrations/0001_initial.py index f9b1932..b2fa599 100644 --- a/myproject/integrations/migrations/0001_add_integration_models.py +++ b/myproject/integrations/migrations/0001_initial.py @@ -1,5 +1,6 @@ -# Generated by Django 5.0.10 on 2026-01-11 21:19 +# Generated by Django 5.0.10 on 2026-01-14 07:04 +import django.db.models.deletion import integrations.fields from django.db import migrations, models @@ -9,6 +10,7 @@ class Migration(migrations.Migration): initial = True dependencies = [ + ('products', '0001_initial'), ] operations = [ @@ -54,4 +56,22 @@ class Migration(migrations.Migration): 'verbose_name_plural': 'WooCommerce', }, ), + migrations.CreateModel( + name='IntegrationCategoryMapping', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('integration_type', models.CharField(choices=[('recommerce', 'Recommerce'), ('woocommerce', 'WooCommerce')], db_index=True, max_length=20, verbose_name='Интеграция')), + ('external_category_sku', models.CharField(help_text='SKU или ID категории на внешней площадке', max_length=100, verbose_name='Артикул категории во внешней системе')), + ('external_category_name', models.CharField(blank=True, help_text='Для справки, не обязательно', max_length=200, verbose_name='Название категории во внешней системе')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Создано')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Обновлено')), + ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='integration_mappings', to='products.productcategory', verbose_name='Категория')), + ], + options={ + 'verbose_name': 'Маппинг категории', + 'verbose_name_plural': 'Маппинги категорий', + 'indexes': [models.Index(fields=['integration_type', 'external_category_sku'], name='integration_integra_450473_idx')], + 'unique_together': {('category', 'integration_type')}, + }, + ), ] diff --git a/myproject/integrations/migrations/0002_integrationcategorymapping.py b/myproject/integrations/migrations/0002_integrationcategorymapping.py deleted file mode 100644 index 5893598..0000000 --- a/myproject/integrations/migrations/0002_integrationcategorymapping.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 5.0.10 on 2026-01-13 21:08 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('integrations', '0001_add_integration_models'), - ('products', '0004_configurableproduct_primary_category_and_more'), - ] - - operations = [ - migrations.CreateModel( - name='IntegrationCategoryMapping', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('integration_type', models.CharField(choices=[('recommerce', 'Recommerce'), ('woocommerce', 'WooCommerce')], db_index=True, max_length=20, verbose_name='Интеграция')), - ('external_category_sku', models.CharField(help_text='SKU или ID категории на внешней площадке', max_length=100, verbose_name='Артикул категории во внешней системе')), - ('external_category_name', models.CharField(blank=True, help_text='Для справки, не обязательно', max_length=200, verbose_name='Название категории во внешней системе')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Создано')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Обновлено')), - ('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='integration_mappings', to='products.productcategory', verbose_name='Категория')), - ], - options={ - 'verbose_name': 'Маппинг категории', - 'verbose_name_plural': 'Маппинги категорий', - 'indexes': [models.Index(fields=['integration_type', 'external_category_sku'], name='integration_integra_450473_idx')], - 'unique_together': {('category', 'integration_type')}, - }, - ), - ] diff --git a/myproject/inventory/migrations/0001_initial.py b/myproject/inventory/migrations/0001_initial.py index 81891f8..7711b90 100644 --- a/myproject/inventory/migrations/0001_initial.py +++ b/myproject/inventory/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.0.10 on 2026-01-08 15:58 +# Generated by Django 5.0.10 on 2026-01-14 07:04 import django.db.models.deletion import phonenumber_field.modelfields diff --git a/myproject/inventory/migrations/0002_initial.py b/myproject/inventory/migrations/0002_initial.py index 623af8b..7c7667d 100644 --- a/myproject/inventory/migrations/0002_initial.py +++ b/myproject/inventory/migrations/0002_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.0.10 on 2026-01-08 15:58 +# Generated by Django 5.0.10 on 2026-01-14 07:04 import django.db.models.deletion from django.db import migrations, models diff --git a/myproject/orders/migrations/0001_initial.py b/myproject/orders/migrations/0001_initial.py index ed7c1fa..343564f 100644 --- a/myproject/orders/migrations/0001_initial.py +++ b/myproject/orders/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.0.10 on 2026-01-08 15:58 +# Generated by Django 5.0.10 on 2026-01-14 07:04 import django.db.models.deletion import phonenumber_field.modelfields diff --git a/myproject/orders/migrations/0002_initial.py b/myproject/orders/migrations/0002_initial.py index c9108f4..9da36be 100644 --- a/myproject/orders/migrations/0002_initial.py +++ b/myproject/orders/migrations/0002_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.0.10 on 2026-01-08 15:58 +# Generated by Django 5.0.10 on 2026-01-14 07:04 import django.db.models.deletion from django.db import migrations, models diff --git a/myproject/orders/migrations/0003_order_applied_discount_order_applied_promo_code_and_more.py b/myproject/orders/migrations/0003_order_applied_discount_order_applied_promo_code_and_more.py deleted file mode 100644 index 3bdc4ab..0000000 --- a/myproject/orders/migrations/0003_order_applied_discount_order_applied_promo_code_and_more.py +++ /dev/null @@ -1,40 +0,0 @@ -# Generated by Django 5.0.10 on 2026-01-10 21:12 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('discounts', '0001_initial'), - ('orders', '0002_initial'), - ] - - operations = [ - migrations.AddField( - model_name='order', - name='applied_discount', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='orders', to='discounts.discount', verbose_name='Примененная скидка'), - ), - migrations.AddField( - model_name='order', - name='applied_promo_code', - field=models.CharField(blank=True, max_length=50, null=True, verbose_name='Использованный промокод'), - ), - migrations.AddField( - model_name='order', - name='discount_amount', - field=models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Сумма скидки'), - ), - migrations.AddField( - model_name='orderitem', - name='applied_discount', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='order_items', to='discounts.discount', verbose_name='Скидка на позицию'), - ), - migrations.AddField( - model_name='orderitem', - name='discount_amount', - field=models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Сумма скидки'), - ), - ] diff --git a/myproject/orders/migrations/0004_remove_old_discount_fields.py b/myproject/orders/migrations/0004_remove_old_discount_fields.py deleted file mode 100644 index b4233c3..0000000 --- a/myproject/orders/migrations/0004_remove_old_discount_fields.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 5.0.10 on 2026-01-11 10:39 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('orders', '0003_order_applied_discount_order_applied_promo_code_and_more'), - ] - - operations = [ - migrations.RemoveField( - model_name='order', - name='applied_discount', - ), - migrations.RemoveField( - model_name='order', - name='applied_promo_code', - ), - migrations.RemoveField( - model_name='order', - name='discount_amount', - ), - migrations.RemoveField( - model_name='orderitem', - name='applied_discount', - ), - migrations.RemoveField( - model_name='orderitem', - name='discount_amount', - ), - ] diff --git a/myproject/platform_admin/migrations/0001_initial.py b/myproject/platform_admin/migrations/0001_initial.py index fd3f6f4..2eee819 100644 --- a/myproject/platform_admin/migrations/0001_initial.py +++ b/myproject/platform_admin/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.0.10 on 2026-01-08 15:56 +# Generated by Django 5.0.10 on 2026-01-14 07:03 import django.utils.timezone from django.db import migrations, models diff --git a/myproject/products/migrations/0001_initial.py b/myproject/products/migrations/0001_initial.py index 68d1d4c..11b6f3b 100644 --- a/myproject/products/migrations/0001_initial.py +++ b/myproject/products/migrations/0001_initial.py @@ -1,10 +1,9 @@ -# Generated by Django 5.0.10 on 2026-01-08 15:58 +# Generated by Django 5.0.10 on 2026-01-14 07:04 import django.core.validators import django.db.models.deletion import products.models.photos from decimal import Decimal -from django.conf import settings from django.db import migrations, models @@ -13,9 +12,9 @@ class Migration(migrations.Migration): initial = True dependencies = [ + ('accounts', '0001_initial'), ('inventory', '0001_initial'), ('orders', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), ] operations = [ @@ -112,10 +111,13 @@ class Migration(migrations.Migration): ('description', models.TextField(blank=True, null=True, verbose_name='Описание')), ('short_description', models.TextField(blank=True, help_text='Используется для карточек товаров, превью и площадок', null=True, verbose_name='Краткое описание')), ('status', models.CharField(choices=[('active', 'Активный'), ('archived', 'Архивный'), ('discontinued', 'Снят')], db_index=True, default='active', max_length=20, verbose_name='Статус')), + ('is_new', models.BooleanField(default=False, help_text='Отображать как новый товар', verbose_name='Новинка')), + ('is_popular', models.BooleanField(default=False, help_text='Отображать как популярный товар', verbose_name='Популярный')), + ('is_special', models.BooleanField(default=False, help_text='Отображать как спецпредложение (акция)', verbose_name='Спецпредложение')), ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')), ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Дата обновления')), ('archived_at', models.DateTimeField(blank=True, null=True, verbose_name='Время архивирования')), - ('archived_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='archived_%(class)s_set', to=settings.AUTH_USER_MODEL, verbose_name='Архивировано пользователем')), + ('archived_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='archived_%(class)s_set', to='accounts.customuser', verbose_name='Архивировано пользователем')), ], options={ 'verbose_name': 'Вариативный товар', @@ -196,6 +198,9 @@ class Migration(migrations.Migration): ('description', models.TextField(blank=True, null=True, verbose_name='Описание')), ('short_description', models.TextField(blank=True, help_text='Используется для карточек товаров, превью и площадок', null=True, verbose_name='Краткое описание')), ('status', models.CharField(choices=[('active', 'Активный'), ('archived', 'Архивный'), ('discontinued', 'Снят')], db_index=True, default='active', max_length=20, verbose_name='Статус')), + ('is_new', models.BooleanField(default=False, help_text='Отображать как новый товар', verbose_name='Новинка')), + ('is_popular', models.BooleanField(default=False, help_text='Отображать как популярный товар', verbose_name='Популярный')), + ('is_special', models.BooleanField(default=False, help_text='Отображать как спецпредложение (акция)', verbose_name='Спецпредложение')), ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')), ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Дата обновления')), ('archived_at', models.DateTimeField(blank=True, null=True, verbose_name='Время архивирования')), @@ -206,7 +211,7 @@ class Migration(migrations.Migration): ('sale_price', models.DecimalField(blank=True, decimal_places=2, help_text='Если задана, товар продается по этой цене (дешевле основной)', max_digits=10, null=True, verbose_name='Цена со скидкой')), ('in_stock', models.BooleanField(db_index=True, default=False, help_text='Автоматически обновляется при изменении остатков на складе', verbose_name='В наличии')), ('search_keywords', models.TextField(blank=True, help_text='Автоматически генерируется из названия, артикула, описания и категории. Можно дополнить вручную.', verbose_name='Ключевые слова для поиска')), - ('archived_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='archived_%(class)s_set', to=settings.AUTH_USER_MODEL, verbose_name='Архивировано пользователем')), + ('archived_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='archived_%(class)s_set', to='accounts.customuser', verbose_name='Архивировано пользователем')), ], options={ 'verbose_name': 'Товар', @@ -290,7 +295,7 @@ class Migration(migrations.Migration): ('updated_at', models.DateTimeField(auto_now=True, null=True, verbose_name='Дата обновления')), ('is_deleted', models.BooleanField(db_index=True, default=False, verbose_name='Удалена')), ('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='Время удаления')), - ('deleted_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='deleted_categories', to=settings.AUTH_USER_MODEL, verbose_name='Удалена пользователем')), + ('deleted_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='deleted_categories', to='accounts.customuser', verbose_name='Удалена пользователем')), ('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='children', to='products.productcategory', verbose_name='Родительская категория')), ], options={ @@ -303,6 +308,16 @@ class Migration(migrations.Migration): name='categories', field=models.ManyToManyField(blank=True, related_name='products', to='products.productcategory', verbose_name='Категории'), ), + migrations.AddField( + model_name='product', + name='external_category', + field=models.ForeignKey(blank=True, help_text='Категория для интеграций с внешними площадками (Recommerce, WooCommerce и др.)', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='external_%(class)ss', to='products.productcategory', verbose_name='Внешняя категория'), + ), + migrations.AddField( + model_name='configurableproduct', + name='external_category', + field=models.ForeignKey(blank=True, help_text='Категория для интеграций с внешними площадками (Recommerce, WooCommerce и др.)', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='external_%(class)ss', to='products.productcategory', verbose_name='Внешняя категория'), + ), migrations.CreateModel( name='ProductCategoryPhoto', fields=[ @@ -341,7 +356,7 @@ class Migration(migrations.Migration): ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Создано')), ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Обновлено')), ('completed_at', models.DateTimeField(blank=True, null=True, verbose_name='Завершено')), - ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='product_import_jobs', to=settings.AUTH_USER_MODEL, verbose_name='Пользователь')), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='product_import_jobs', to='accounts.customuser', verbose_name='Пользователь')), ], options={ 'verbose_name': 'Задача импорта товаров', @@ -359,6 +374,9 @@ class Migration(migrations.Migration): ('description', models.TextField(blank=True, null=True, verbose_name='Описание')), ('short_description', models.TextField(blank=True, help_text='Используется для карточек товаров, превью и площадок', null=True, verbose_name='Краткое описание')), ('status', models.CharField(choices=[('active', 'Активный'), ('archived', 'Архивный'), ('discontinued', 'Снят')], db_index=True, default='active', max_length=20, verbose_name='Статус')), + ('is_new', models.BooleanField(default=False, help_text='Отображать как новый товар', verbose_name='Новинка')), + ('is_popular', models.BooleanField(default=False, help_text='Отображать как популярный товар', verbose_name='Популярный')), + ('is_special', models.BooleanField(default=False, help_text='Отображать как спецпредложение (акция)', verbose_name='Спецпредложение')), ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')), ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Дата обновления')), ('archived_at', models.DateTimeField(blank=True, null=True, verbose_name='Время архивирования')), @@ -368,8 +386,9 @@ class Migration(migrations.Migration): ('price_adjustment_type', models.CharField(choices=[('none', 'Без изменения'), ('increase_percent', 'Увеличить на %'), ('increase_amount', 'Увеличить на сумму'), ('decrease_percent', 'Уменьшить на %'), ('decrease_amount', 'Уменьшить на сумму')], default='none', max_length=20, verbose_name='Тип корректировки цены')), ('price_adjustment_value', models.DecimalField(decimal_places=2, default=0, help_text='Процент (%) или сумма (руб) в зависимости от типа корректировки', max_digits=10, verbose_name='Значение корректировки')), ('is_temporary', models.BooleanField(default=False, help_text='Временные комплекты не показываются в каталоге и создаются для конкретного заказа', verbose_name='Временный комплект')), - ('archived_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='archived_%(class)s_set', to=settings.AUTH_USER_MODEL, verbose_name='Архивировано пользователем')), + ('archived_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='archived_%(class)s_set', to='accounts.customuser', verbose_name='Архивировано пользователем')), ('categories', models.ManyToManyField(blank=True, related_name='kits', to='products.productcategory', verbose_name='Категории')), + ('external_category', models.ForeignKey(blank=True, help_text='Категория для интеграций с внешними площадками (Recommerce, WooCommerce и др.)', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='external_%(class)ss', to='products.productcategory', verbose_name='Внешняя категория')), ('order', models.ForeignKey(blank=True, help_text='Заказ, для которого создан временный комплект', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='temporary_kits', to='orders.order', verbose_name='Заказ')), ('showcase', models.ForeignKey(blank=True, help_text='Витрина, на которой выложен временный комплект', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='temporary_kits', to='inventory.showcase', verbose_name='Витрина')), ], diff --git a/myproject/products/migrations/0002_alter_configurableproduct_archived_by_and_more.py b/myproject/products/migrations/0002_alter_configurableproduct_archived_by_and_more.py deleted file mode 100644 index b799a2c..0000000 --- a/myproject/products/migrations/0002_alter_configurableproduct_archived_by_and_more.py +++ /dev/null @@ -1,68 +0,0 @@ -# Generated by Django 5.0.10 on 2026-01-09 20:42 - -import django.db.models.deletion -from django.db import migrations, models - - -def clear_invalid_user_references(apps, schema_editor): - """ - Очистить ссылки на PlatformAdmin в полях archived_by/deleted_by/user, - т.к. они теперь ссылаются на CustomUser. - - Устанавливаем NULL для всех существующих ссылок на пользователей, - чтобы избежать ошибок referential integrity при изменении типа ForeignKey. - """ - ProductCategory = apps.get_model('products', 'ProductCategory') - Product = apps.get_model('products', 'Product') - ProductKit = apps.get_model('products', 'ProductKit') - ConfigurableProduct = apps.get_model('products', 'ConfigurableProduct') - ProductImportJob = apps.get_model('products', 'ProductImportJob') - - # Очищаем все существующие ссылки (ставим NULL) - ProductCategory.objects.all().update(deleted_by=None) - Product.objects.all().update(archived_by=None) - ProductKit.objects.all().update(archived_by=None) - ConfigurableProduct.objects.all().update(archived_by=None) - - # ProductImportJob.user не может быть NULL (CASCADE), поэтому удаляем записи - # если они были созданы PlatformAdmin (что маловероятно) - ProductImportJob.objects.all().delete() - - -class Migration(migrations.Migration): - - dependencies = [ - ('accounts', '0001_initial'), - ('products', '0001_initial'), - ] - - operations = [ - # Сначала очищаем некорректные ссылки - migrations.RunPython(clear_invalid_user_references, reverse_code=migrations.RunPython.noop), - # Затем изменяем типы полей - migrations.AlterField( - model_name='configurableproduct', - name='archived_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='archived_%(class)s_set', to='accounts.customuser', verbose_name='Архивировано пользователем'), - ), - migrations.AlterField( - model_name='product', - name='archived_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='archived_%(class)s_set', to='accounts.customuser', verbose_name='Архивировано пользователем'), - ), - migrations.AlterField( - model_name='productcategory', - name='deleted_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='deleted_categories', to='accounts.customuser', verbose_name='Удалена пользователем'), - ), - migrations.AlterField( - model_name='productimportjob', - name='user', - field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='product_import_jobs', to='accounts.customuser', verbose_name='Пользователь'), - ), - migrations.AlterField( - model_name='productkit', - name='archived_by', - field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='archived_%(class)s_set', to='accounts.customuser', verbose_name='Архивировано пользователем'), - ), - ] diff --git a/myproject/products/migrations/0003_add_marketing_flags.py b/myproject/products/migrations/0003_add_marketing_flags.py deleted file mode 100644 index be5223d..0000000 --- a/myproject/products/migrations/0003_add_marketing_flags.py +++ /dev/null @@ -1,58 +0,0 @@ -# Generated by Django 5.0.10 on 2026-01-12 21:26 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('products', '0002_alter_configurableproduct_archived_by_and_more'), - ] - - operations = [ - migrations.AddField( - model_name='configurableproduct', - name='is_new', - field=models.BooleanField(default=False, help_text='Отображать как новый товар', verbose_name='Новинка'), - ), - migrations.AddField( - model_name='configurableproduct', - name='is_popular', - field=models.BooleanField(default=False, help_text='Отображать как популярный товар', verbose_name='Популярный'), - ), - migrations.AddField( - model_name='configurableproduct', - name='is_special', - field=models.BooleanField(default=False, help_text='Отображать как спецпредложение (акция)', verbose_name='Спецпредложение'), - ), - migrations.AddField( - model_name='product', - name='is_new', - field=models.BooleanField(default=False, help_text='Отображать как новый товар', verbose_name='Новинка'), - ), - migrations.AddField( - model_name='product', - name='is_popular', - field=models.BooleanField(default=False, help_text='Отображать как популярный товар', verbose_name='Популярный'), - ), - migrations.AddField( - model_name='product', - name='is_special', - field=models.BooleanField(default=False, help_text='Отображать как спецпредложение (акция)', verbose_name='Спецпредложение'), - ), - migrations.AddField( - model_name='productkit', - name='is_new', - field=models.BooleanField(default=False, help_text='Отображать как новый товар', verbose_name='Новинка'), - ), - migrations.AddField( - model_name='productkit', - name='is_popular', - field=models.BooleanField(default=False, help_text='Отображать как популярный товар', verbose_name='Популярный'), - ), - migrations.AddField( - model_name='productkit', - name='is_special', - field=models.BooleanField(default=False, help_text='Отображать как спецпредложение (акция)', verbose_name='Спецпредложение'), - ), - ] diff --git a/myproject/products/migrations/0004_configurableproduct_primary_category_and_more.py b/myproject/products/migrations/0004_configurableproduct_primary_category_and_more.py deleted file mode 100644 index dfad758..0000000 --- a/myproject/products/migrations/0004_configurableproduct_primary_category_and_more.py +++ /dev/null @@ -1,29 +0,0 @@ -# Generated by Django 5.0.10 on 2026-01-13 21:08 - -import django.db.models.deletion -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('products', '0003_add_marketing_flags'), - ] - - operations = [ - migrations.AddField( - model_name='configurableproduct', - name='primary_category', - field=models.ForeignKey(blank=True, help_text='Используется для интеграций с внешними площадками', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_%(class)ss', to='products.productcategory', verbose_name='Основная категория'), - ), - migrations.AddField( - model_name='product', - name='primary_category', - field=models.ForeignKey(blank=True, help_text='Используется для интеграций с внешними площадками', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_%(class)ss', to='products.productcategory', verbose_name='Основная категория'), - ), - migrations.AddField( - model_name='productkit', - name='primary_category', - field=models.ForeignKey(blank=True, help_text='Используется для интеграций с внешними площадками', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_%(class)ss', to='products.productcategory', verbose_name='Основная категория'), - ), - ] diff --git a/myproject/products/migrations/0005_rename_primary_category_to_external_category.py b/myproject/products/migrations/0005_rename_primary_category_to_external_category.py deleted file mode 100644 index ad4a8a2..0000000 --- a/myproject/products/migrations/0005_rename_primary_category_to_external_category.py +++ /dev/null @@ -1,72 +0,0 @@ -# Generated migration to rename primary_category to external_category - -from django.db import migrations, models - - -class Migration(migrations.Migration): - dependencies = [ - ('products', '0004_configurableproduct_primary_category_and_more'), - ] - - operations = [ - # Rename primary_category to external_category for Product - migrations.RenameField( - model_name='product', - old_name='primary_category', - new_name='external_category', - ), - # Update related_name for Product - migrations.AlterField( - model_name='product', - name='external_category', - field=models.ForeignKey( - blank=True, - help_text='Категория для интеграций с внешними площадками (Recommerce, WooCommerce и др.)', - null=True, - on_delete=models.SET_NULL, - related_name='external_products', - to='products.productcategory', - verbose_name='Внешняя категория' - ), - ), - # Rename primary_category to external_category for ProductKit - migrations.RenameField( - model_name='productkit', - old_name='primary_category', - new_name='external_category', - ), - # Update related_name for ProductKit - migrations.AlterField( - model_name='productkit', - name='external_category', - field=models.ForeignKey( - blank=True, - help_text='Категория для интеграций с внешними площадками (Recommerce, WooCommerce и др.)', - null=True, - on_delete=models.SET_NULL, - related_name='external_productkits', - to='products.productcategory', - verbose_name='Внешняя категория' - ), - ), - # Rename primary_category to external_category for ConfigurableProduct - migrations.RenameField( - model_name='configurableproduct', - old_name='primary_category', - new_name='external_category', - ), - # Update related_name for ConfigurableProduct - migrations.AlterField( - model_name='configurableproduct', - name='external_category', - field=models.ForeignKey( - blank=True, - help_text='Категория для интеграций с внешними площадками (Recommerce, WooCommerce и др.)', - null=True, - on_delete=models.SET_NULL, - related_name='external_configurableproducts', - to='products.productcategory', - verbose_name='Внешняя категория' - ), - ), - ] diff --git a/myproject/products/templates/products/products_list.html b/myproject/products/templates/products/products_list.html index 23703f4..9106a66 100644 --- a/myproject/products/templates/products/products_list.html +++ b/myproject/products/templates/products/products_list.html @@ -237,11 +237,13 @@
  • + {% if recommerce_integration_enabled %}
  • Синхронизация с Recommerce
  • + {% endif %}
    @@ -595,4 +597,19 @@ + {% endblock %} diff --git a/myproject/products/views/product_views.py b/myproject/products/views/product_views.py index e0414ba..7b933d8 100644 --- a/myproject/products/views/product_views.py +++ b/myproject/products/views/product_views.py @@ -175,7 +175,7 @@ class ProductDetailView(LoginRequiredMixin, ManagerOwnerRequiredMixin, DetailVie product_photos.sort(key=lambda x: (x.order, x.created_at)) context['product_photos'] = product_photos context['photos_count'] = len(product_photos) - + # Кешируем cost_price_details, чтобы не делать множественные запросы к БД from ..services.cost_calculator import ProductCostCalculator context['cost_price_details'] = ProductCostCalculator.get_cost_details(self.object) @@ -277,7 +277,7 @@ class CombinedProductListView(LoginRequiredMixin, ManagerOwnerRequiredMixin, Lis def get_queryset(self): # Получаем фильтр по типу type_filter = self.request.GET.get('type', 'all') - + # Получаем товары и комплекты (только постоянные комплекты) # Аннотируем товары данными об остатках из агрегированной таблицы Stock total_available = Coalesce(Sum('stocks__quantity_available'), Value(0), output_field=DecimalField()) @@ -331,13 +331,13 @@ class CombinedProductListView(LoginRequiredMixin, ManagerOwnerRequiredMixin, Lis elif is_active_filter == '0': products = products.filter(status__in=['archived', 'discontinued']) kits = kits.filter(status__in=['archived', 'discontinued']) - + # Фильтрация по наличию (только для товаров) if in_stock_filter == '1': products = products.filter(in_stock=True) elif in_stock_filter == '0': products = products.filter(in_stock=False) - + # Фильтрация по тегам if tags: products = products.filter(tags__id__in=tags).distinct() @@ -382,12 +382,12 @@ class CombinedProductListView(LoginRequiredMixin, ManagerOwnerRequiredMixin, Lis # Применяем фильтр по типу products_list = [] kits_list = [] - + if type_filter in ['all', 'products']: products_list = list(products.order_by('-created_at')) for p in products_list: p.item_type = 'product' - + if type_filter in ['all', 'kits']: kits_list = list(kits.order_by('-created_at')) for k in kits_list: @@ -411,6 +411,11 @@ class CombinedProductListView(LoginRequiredMixin, ManagerOwnerRequiredMixin, Lis from ..models.base import BaseProductEntity item_statuses = BaseProductEntity.STATUS_CHOICES + # Получаем состояние интеграции Recommerce + from integrations.models import RecommerceIntegration + recommerce_integration = RecommerceIntegration.objects.first() + context['recommerce_integration_enabled'] = recommerce_integration.is_active if recommerce_integration else False + # Данные для фильтров context['filters'] = { 'categories': ProductCategory.objects.filter(is_active=True), @@ -431,7 +436,7 @@ class CombinedProductListView(LoginRequiredMixin, ManagerOwnerRequiredMixin, Lis 'has_discount': self.request.GET.get('has_discount', ''), } } - + context['item_statuses'] = item_statuses # Кнопки действий diff --git a/myproject/system_settings/migrations/0001_initial.py b/myproject/system_settings/migrations/0001_initial.py deleted file mode 100644 index d00ea2b..0000000 --- a/myproject/system_settings/migrations/0001_initial.py +++ /dev/null @@ -1,30 +0,0 @@ -# Generated by Django 5.0.10 on 2026-01-11 19:36 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - initial = True - - dependencies = [ - ] - - operations = [ - migrations.CreateModel( - name='IntegrationConfig', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('integration_id', models.CharField(choices=[('woocommerce', 'WooCommerce')], max_length=50, unique=True, verbose_name='Интеграция')), - ('is_enabled', models.BooleanField(default=False, help_text='Глобальное включение интеграции для тенанта', verbose_name='Включена')), - ('last_sync_at', models.DateTimeField(blank=True, null=True, verbose_name='Последняя синхронизация')), - ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')), - ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Дата обновления')), - ], - options={ - 'verbose_name': 'Настройка интеграции', - 'verbose_name_plural': 'Настройки интеграций', - 'ordering': ['integration_id'], - }, - ), - ] diff --git a/myproject/system_settings/migrations/0002_delete_integrationconfig.py b/myproject/system_settings/migrations/0002_delete_integrationconfig.py deleted file mode 100644 index f71f27e..0000000 --- a/myproject/system_settings/migrations/0002_delete_integrationconfig.py +++ /dev/null @@ -1,16 +0,0 @@ -# Generated by Django 5.0.10 on 2026-01-11 21:41 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('system_settings', '0001_initial'), - ] - - operations = [ - migrations.DeleteModel( - name='IntegrationConfig', - ), - ] diff --git a/myproject/templates/navbar.html b/myproject/templates/navbar.html index 3ba54ab..ac70a92 100644 --- a/myproject/templates/navbar.html +++ b/myproject/templates/navbar.html @@ -1,4 +1,5 @@ - + +{% if request.tenant %}