diff --git a/myproject/products/migrations/0001_initial.py b/myproject/products/migrations/0001_initial.py index 9dbaff2..68d1d4c 100644 --- a/myproject/products/migrations/0001_initial.py +++ b/myproject/products/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.0.10 on 2026-01-03 23:23 +# Generated by Django 5.0.10 on 2026-01-08 15:58 import django.core.validators import django.db.models.deletion @@ -307,6 +307,7 @@ class Migration(migrations.Migration): name='ProductCategoryPhoto', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_main', models.BooleanField(db_index=True, default=False, help_text='Главное фото отображается в карточках, каталоге и превью. Может быть только одно.', verbose_name='Главное фото')), ('order', models.PositiveIntegerField(default=0, verbose_name='Порядок')), ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')), ('image', models.ImageField(upload_to=products.models.photos.get_category_photo_upload_path, verbose_name='Оригинальное фото')), @@ -317,7 +318,35 @@ class Migration(migrations.Migration): options={ 'verbose_name': 'Фото категории', 'verbose_name_plural': 'Фото категорий', - 'ordering': ['order', '-created_at'], + 'ordering': ['-is_main', 'order', '-created_at'], + }, + ), + migrations.CreateModel( + name='ProductImportJob', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('task_id', models.CharField(max_length=255, unique=True, verbose_name='ID задачи Celery')), + ('file_name', models.CharField(max_length=255, verbose_name='Имя файла')), + ('file_path', models.CharField(help_text='Временный путь для обработки', max_length=500, verbose_name='Путь к файлу')), + ('update_existing', models.BooleanField(default=False, verbose_name='Обновлять существующие')), + ('status', models.CharField(choices=[('pending', 'Ожидает'), ('processing', 'Обрабатывается'), ('completed', 'Завершён'), ('failed', 'Ошибка')], db_index=True, default='pending', max_length=20, verbose_name='Статус')), + ('total_rows', models.IntegerField(default=0, verbose_name='Всего строк')), + ('processed_rows', models.IntegerField(default=0, verbose_name='Обработано строк')), + ('created_count', models.IntegerField(default=0, verbose_name='Создано товаров')), + ('updated_count', models.IntegerField(default=0, verbose_name='Обновлено товаров')), + ('skipped_count', models.IntegerField(default=0, verbose_name='Пропущено')), + ('errors_count', models.IntegerField(default=0, verbose_name='Ошибок')), + ('errors_json', models.JSONField(blank=True, default=list, verbose_name='Детали ошибок')), + ('error_message', models.TextField(blank=True, verbose_name='Сообщение об ошибке')), + ('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='Пользователь')), + ], + options={ + 'verbose_name': 'Задача импорта товаров', + 'verbose_name_plural': 'Задачи импорта товаров', + 'ordering': ['-created_at'], }, ), migrations.CreateModel( @@ -368,6 +397,7 @@ class Migration(migrations.Migration): name='ProductKitPhoto', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_main', models.BooleanField(db_index=True, default=False, help_text='Главное фото отображается в карточках, каталоге и превью. Может быть только одно.', verbose_name='Главное фото')), ('order', models.PositiveIntegerField(default=0, verbose_name='Порядок')), ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')), ('image', models.ImageField(upload_to=products.models.photos.get_kit_photo_upload_path, verbose_name='Оригинальное фото')), @@ -378,13 +408,14 @@ class Migration(migrations.Migration): options={ 'verbose_name': 'Фото комплекта', 'verbose_name_plural': 'Фото комплектов', - 'ordering': ['order', '-created_at'], + 'ordering': ['-is_main', 'order', '-created_at'], }, ), migrations.CreateModel( name='ProductPhoto', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('is_main', models.BooleanField(db_index=True, default=False, help_text='Главное фото отображается в карточках, каталоге и превью. Может быть только одно.', verbose_name='Главное фото')), ('order', models.PositiveIntegerField(default=0, verbose_name='Порядок')), ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')), ('image', models.ImageField(upload_to=products.models.photos.get_product_photo_upload_path, verbose_name='Оригинальное фото')), @@ -395,7 +426,7 @@ class Migration(migrations.Migration): options={ 'verbose_name': 'Фото товара', 'verbose_name_plural': 'Фото товаров', - 'ordering': ['order', '-created_at'], + 'ordering': ['-is_main', 'order', '-created_at'], }, ), migrations.CreateModel( @@ -539,6 +570,22 @@ class Migration(migrations.Migration): model_name='productcategoryphoto', index=models.Index(fields=['quality_warning', 'category'], name='products_pr_quality_fc505a_idx'), ), + migrations.AddConstraint( + model_name='productcategoryphoto', + constraint=models.UniqueConstraint(condition=models.Q(('is_main', True)), fields=('category',), name='unique_main_photo_per_category'), + ), + migrations.AddIndex( + model_name='productimportjob', + index=models.Index(fields=['task_id'], name='products_pr_task_id_d8dc9f_idx'), + ), + migrations.AddIndex( + model_name='productimportjob', + index=models.Index(fields=['status', '-created_at'], name='products_pr_status_326f2c_idx'), + ), + migrations.AddIndex( + model_name='productimportjob', + index=models.Index(fields=['user', '-created_at'], name='products_pr_user_id_e32ca9_idx'), + ), migrations.AddIndex( model_name='configurableproductoption', index=models.Index(fields=['parent'], name='products_co_parent__36761a_idx'), @@ -591,6 +638,10 @@ class Migration(migrations.Migration): model_name='productkitphoto', index=models.Index(fields=['quality_warning', 'kit'], name='products_pr_quality_867664_idx'), ), + migrations.AddConstraint( + model_name='productkitphoto', + constraint=models.UniqueConstraint(condition=models.Q(('is_main', True)), fields=('kit',), name='unique_main_photo_per_kit'), + ), migrations.AddIndex( model_name='productphoto', index=models.Index(fields=['quality_level'], name='products_pr_quality_d8f85c_idx'), @@ -603,6 +654,10 @@ class Migration(migrations.Migration): model_name='productphoto', index=models.Index(fields=['quality_warning', 'product'], name='products_pr_quality_6e8b51_idx'), ), + migrations.AddConstraint( + model_name='productphoto', + constraint=models.UniqueConstraint(condition=models.Q(('is_main', True)), fields=('product',), name='unique_main_photo_per_product'), + ), migrations.AddIndex( model_name='productkit', index=models.Index(fields=['is_temporary'], name='products_pr_is_temp_e407a2_idx'), diff --git a/myproject/products/migrations/0002_productimportjob.py b/myproject/products/migrations/0002_productimportjob.py deleted file mode 100644 index d474db6..0000000 --- a/myproject/products/migrations/0002_productimportjob.py +++ /dev/null @@ -1,45 +0,0 @@ -# Generated by Django 5.0.10 on 2026-01-06 03:40 - -import django.db.models.deletion -from django.conf import settings -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('products', '0001_initial'), - migrations.swappable_dependency(settings.AUTH_USER_MODEL), - ] - - operations = [ - migrations.CreateModel( - name='ProductImportJob', - fields=[ - ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('task_id', models.CharField(max_length=255, unique=True, verbose_name='ID задачи Celery')), - ('file_name', models.CharField(max_length=255, verbose_name='Имя файла')), - ('file_path', models.CharField(help_text='Временный путь для обработки', max_length=500, verbose_name='Путь к файлу')), - ('update_existing', models.BooleanField(default=False, verbose_name='Обновлять существующие')), - ('status', models.CharField(choices=[('pending', 'Ожидает'), ('processing', 'Обрабатывается'), ('completed', 'Завершён'), ('failed', 'Ошибка')], db_index=True, default='pending', max_length=20, verbose_name='Статус')), - ('total_rows', models.IntegerField(default=0, verbose_name='Всего строк')), - ('processed_rows', models.IntegerField(default=0, verbose_name='Обработано строк')), - ('created_count', models.IntegerField(default=0, verbose_name='Создано товаров')), - ('updated_count', models.IntegerField(default=0, verbose_name='Обновлено товаров')), - ('skipped_count', models.IntegerField(default=0, verbose_name='Пропущено')), - ('errors_count', models.IntegerField(default=0, verbose_name='Ошибок')), - ('errors_json', models.JSONField(blank=True, default=list, verbose_name='Детали ошибок')), - ('error_message', models.TextField(blank=True, verbose_name='Сообщение об ошибке')), - ('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='Пользователь')), - ], - options={ - 'verbose_name': 'Задача импорта товаров', - 'verbose_name_plural': 'Задачи импорта товаров', - 'ordering': ['-created_at'], - 'indexes': [models.Index(fields=['task_id'], name='products_pr_task_id_d8dc9f_idx'), models.Index(fields=['status', '-created_at'], name='products_pr_status_326f2c_idx'), models.Index(fields=['user', '-created_at'], name='products_pr_user_id_e32ca9_idx')], - }, - ), - ] diff --git a/myproject/products/migrations/0003_alter_productcategoryphoto_options_and_more.py b/myproject/products/migrations/0003_alter_productcategoryphoto_options_and_more.py deleted file mode 100644 index 413cc8d..0000000 --- a/myproject/products/migrations/0003_alter_productcategoryphoto_options_and_more.py +++ /dev/null @@ -1,52 +0,0 @@ -# Generated by Django 5.0.10 on 2026-01-06 05:03 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('products', '0002_productimportjob'), - ] - - operations = [ - migrations.AlterModelOptions( - name='productcategoryphoto', - options={'ordering': ['-is_main', 'order', '-created_at'], 'verbose_name': 'Фото категории', 'verbose_name_plural': 'Фото категорий'}, - ), - migrations.AlterModelOptions( - name='productkitphoto', - options={'ordering': ['-is_main', 'order', '-created_at'], 'verbose_name': 'Фото комплекта', 'verbose_name_plural': 'Фото комплектов'}, - ), - migrations.AlterModelOptions( - name='productphoto', - options={'ordering': ['-is_main', 'order', '-created_at'], 'verbose_name': 'Фото товара', 'verbose_name_plural': 'Фото товаров'}, - ), - migrations.AddField( - model_name='productcategoryphoto', - name='is_main', - field=models.BooleanField(db_index=True, default=False, help_text='Главное фото отображается в карточках, каталоге и превью. Может быть только одно.', verbose_name='Главное фото'), - ), - migrations.AddField( - model_name='productkitphoto', - name='is_main', - field=models.BooleanField(db_index=True, default=False, help_text='Главное фото отображается в карточках, каталоге и превью. Может быть только одно.', verbose_name='Главное фото'), - ), - migrations.AddField( - model_name='productphoto', - name='is_main', - field=models.BooleanField(db_index=True, default=False, help_text='Главное фото отображается в карточках, каталоге и превью. Может быть только одно.', verbose_name='Главное фото'), - ), - migrations.AddConstraint( - model_name='productcategoryphoto', - constraint=models.UniqueConstraint(condition=models.Q(('is_main', True)), fields=('category',), name='unique_main_photo_per_category'), - ), - migrations.AddConstraint( - model_name='productkitphoto', - constraint=models.UniqueConstraint(condition=models.Q(('is_main', True)), fields=('kit',), name='unique_main_photo_per_kit'), - ), - migrations.AddConstraint( - model_name='productphoto', - constraint=models.UniqueConstraint(condition=models.Q(('is_main', True)), fields=('product',), name='unique_main_photo_per_product'), - ), - ] diff --git a/myproject/products/migrations/0004_set_main_photo_from_order.py b/myproject/products/migrations/0004_set_main_photo_from_order.py deleted file mode 100644 index b26d10e..0000000 --- a/myproject/products/migrations/0004_set_main_photo_from_order.py +++ /dev/null @@ -1,68 +0,0 @@ -# Generated by Django 5.0.10 on 2026-01-06 05:03 - -from django.db import migrations - - -def set_main_photo_from_order(apps, schema_editor): - """ - Data migration: устанавливает is_main=True для фото с order=0. - - Для каждой сущности (product, kit, category) находит фото с order=0 - и устанавливает ему is_main=True. - - Если у сущности нет фото с order=0, устанавливает is_main=True для первого фото. - """ - ProductPhoto = apps.get_model('products', 'ProductPhoto') - ProductKitPhoto = apps.get_model('products', 'ProductKitPhoto') - ProductCategoryPhoto = apps.get_model('products', 'ProductCategoryPhoto') - Product = apps.get_model('products', 'Product') - ProductKit = apps.get_model('products', 'ProductKit') - ProductCategory = apps.get_model('products', 'ProductCategory') - - # Обрабатываем ProductPhoto - for product in Product.objects.all(): - photos = ProductPhoto.objects.filter(product=product).order_by('order', '-created_at') - if photos.exists(): - main_photo = photos.filter(order=0).first() or photos.first() - main_photo.is_main = True - main_photo.save(update_fields=['is_main']) - - # Обрабатываем ProductKitPhoto - for kit in ProductKit.objects.all(): - photos = ProductKitPhoto.objects.filter(kit=kit).order_by('order', '-created_at') - if photos.exists(): - main_photo = photos.filter(order=0).first() or photos.first() - main_photo.is_main = True - main_photo.save(update_fields=['is_main']) - - # Обрабатываем ProductCategoryPhoto - for category in ProductCategory.objects.all(): - photos = ProductCategoryPhoto.objects.filter(category=category).order_by('order', '-created_at') - if photos.exists(): - main_photo = photos.filter(order=0).first() or photos.first() - main_photo.is_main = True - main_photo.save(update_fields=['is_main']) - - -def reverse_main_photo(apps, schema_editor): - """ - Reverse migration: сбрасывает is_main в False для всех фото. - """ - ProductPhoto = apps.get_model('products', 'ProductPhoto') - ProductKitPhoto = apps.get_model('products', 'ProductKitPhoto') - ProductCategoryPhoto = apps.get_model('products', 'ProductCategoryPhoto') - - ProductPhoto.objects.filter(is_main=True).update(is_main=False) - ProductKitPhoto.objects.filter(is_main=True).update(is_main=False) - ProductCategoryPhoto.objects.filter(is_main=True).update(is_main=False) - - -class Migration(migrations.Migration): - - dependencies = [ - ('products', '0003_alter_productcategoryphoto_options_and_more'), - ] - - operations = [ - migrations.RunPython(set_main_photo_from_order, reverse_main_photo), - ]