refactor(db): консолидация миграций и рефакторинг кода

Объединены изменения из промежуточных миграций в начальные миграции для упрощения истории базы данных.
Удалены миграции: 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.
This commit is contained in:
2026-01-14 16:30:28 +03:00
parent e7672588c6
commit caeb3f80bd
31 changed files with 238 additions and 558 deletions

View File

@@ -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='Витрина')),
],

View File

@@ -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='Архивировано пользователем'),
),
]

View File

@@ -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='Спецпредложение'),
),
]

View File

@@ -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='Основная категория'),
),
]

View File

@@ -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='Внешняя категория'
),
),
]