From d999e01b49ae979ce303dd9400a02e23f0bea0dc Mon Sep 17 00:00:00 2001 From: Andrey Smakotin Date: Sat, 25 Oct 2025 16:48:11 +0300 Subject: [PATCH] =?UTF-8?q?=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8=D0=BB?= =?UTF-8?q?=D0=B8=20=D1=88=D0=B0=D0=BF=D0=BA=D1=83=20=D0=B8=20=D0=B2=D1=8B?= =?UTF-8?q?=D0=B2=D0=BE=D0=B4=20=D0=B2=D1=81=D0=B5=D1=85=D1=82=D0=BE=D0=B2?= =?UTF-8?q?=D0=B0=D1=80=D0=BE=D0=B2.=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2?= =?UTF-8?q?=D0=B8=D0=BB=D0=B8=20=D1=84=D0=B8=D0=BB=D1=8C=D1=82=D1=80=D1=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- myproject/SKU_SYSTEM_README.md | 205 ------------ myproject/accounts/migrations/0001_initial.py | 2 +- .../customers/migrations/0001_initial.py | 57 +--- ...rsary_remove_customer_birthday_and_more.py | 56 ---- .../migrations/0003_alter_customer_phone.py | 19 -- ...customers_a_is_acti_433713_idx_and_more.py | 33 -- .../migrations/0005_alter_customer_phone.py | 19 -- myproject/docs/README_VARIANTS.md | 160 --------- myproject/docs/example_usage.py | 259 --------------- myproject/docs/product_variants_guide.md | 310 ------------------ .../inventory/migrations/0001_initial.py | 2 +- myproject/orders/migrations/0001_initial.py | 2 +- myproject/products/migrations/0001_initial.py | 22 +- ..._products_ki_kit_id_d28dc9_idx_and_more.py | 33 -- myproject/test_category_tree.py | 70 ---- myproject/test_category_validation.py | 186 ----------- myproject/test_cycles.py | 134 -------- myproject/test_manager_fix.py | 119 ------- myproject/test_sku_generation.py | 170 ---------- 19 files changed, 31 insertions(+), 1827 deletions(-) delete mode 100644 myproject/SKU_SYSTEM_README.md delete mode 100644 myproject/customers/migrations/0002_remove_customer_anniversary_remove_customer_birthday_and_more.py delete mode 100644 myproject/customers/migrations/0003_alter_customer_phone.py delete mode 100644 myproject/customers/migrations/0004_remove_address_customers_a_is_acti_433713_idx_and_more.py delete mode 100644 myproject/customers/migrations/0005_alter_customer_phone.py delete mode 100644 myproject/docs/README_VARIANTS.md delete mode 100644 myproject/docs/example_usage.py delete mode 100644 myproject/docs/product_variants_guide.md delete mode 100644 myproject/products/migrations/0002_kititem_products_ki_kit_id_d28dc9_idx_and_more.py delete mode 100644 myproject/test_category_tree.py delete mode 100644 myproject/test_category_validation.py delete mode 100644 myproject/test_cycles.py delete mode 100644 myproject/test_manager_fix.py delete mode 100644 myproject/test_sku_generation.py diff --git a/myproject/SKU_SYSTEM_README.md b/myproject/SKU_SYSTEM_README.md deleted file mode 100644 index 2739624..0000000 --- a/myproject/SKU_SYSTEM_README.md +++ /dev/null @@ -1,205 +0,0 @@ -# Система генерации артикулов (SKU) - -## Обзор - -Новая система генерации артикулов создана для того, чтобы артикулы были **легко озвучиваемыми** по телефону и имели **логическую структуру**. - -## Структура артикулов - -### Товары (Product) - -**Формат:** `PROD-XXXXXX` или `PROD-XXXXXX-VARIANT` - -- `PROD` - префикс для всех товаров -- `XXXXXX` - 6-значный номер (000001-999999) -- `VARIANT` - опциональный суффикс варианта (размер, цвет и т.д.) - -**Примеры:** -- `PROD-000001` - простой товар без варианта -- `PROD-000002-50` - товар с вариантом "50" (например, 50см) -- `PROD-000003-M` - товар с вариантом "M" (размер M) -- `PROD-000004-RED` - товар с вариантом "RED" (красный цвет) - -### Комплекты (ProductKit) - -**Формат:** `KIT-XXXXXX` - -- `KIT` - префикс для всех комплектов/букетов -- `XXXXXX` - 6-значный номер (000001-999999) - -**Примеры:** -- `KIT-000001` - Букет "Романтика" -- `KIT-000002` - Букет "Весна" - -## Автоматическое извлечение суффиксов - -Система автоматически извлекает суффиксы из названия товара: - -| Название товара | Извлеченный суффикс | Артикул | -|----------------|-------------------|---------| -| "Роза Freedom 50см" | `50` | `PROD-000001-50` | -| "Роза Freedom 60 см" | `60` | `PROD-000002-60` | -| "Лента 2.5м" | `25` | `PROD-000003-25` | -| "Коробка S" | `S` | `PROD-000004-S` | -| "Коробка размер M" | `M` | `PROD-000005-M` | - -## Ручное указание суффикса - -Вы можете вручную указать суффикс в поле `variant_suffix` при создании товара. Это переопределит автоматическое извлечение. - -**Пример:** -```python -product = Product( - name="Лента атласная красная", - variant_suffix="RED" # Ручной суффикс -) -product.save() -# Артикул: PROD-000006-RED -``` - -## Глобальные счетчики (SKUCounter) - -Система использует два глобальных счетчика: - -1. **Product Counter** - для товаров (Product) -2. **Kit Counter** - для комплектов (ProductKit) - -Счетчики автоматически увеличиваются при создании нового товара/комплекта. - -### Просмотр счетчиков в админке - -Зайдите в Django Admin → SKU Counters, чтобы увидеть текущие значения счетчиков и предпросмотр следующего артикула. - -## Обеспечение уникальности - -Если артикул уже существует (например, при ручном создании), система автоматически добавит буквенный суффикс: - -``` -PROD-000001 уже существует -→ PROD-000001A -→ PROD-000001B (если A тоже занято) -→ ... до PROD-000001Z -``` - -## Преимущества новой системы - -✅ **Легко озвучить:** "PROD тире ноль ноль ноль один тире пятьдесят" -✅ **Короткие артикулы:** 11-14 символов вместо 12 (MD5) -✅ **Логичная структура:** Понятно, что PROD = товар, KIT = комплект -✅ **Масштабируемость:** До 999,999 товаров и 999,999 комплектов -✅ **Стабильность:** Артикул не зависит от категории - можно менять категорию -✅ **Автоматизация:** Суффиксы извлекаются автоматически из названия - -## Обратная совместимость - -Старые товары с MD5-хешами (например, `PRODA41C9EC1`) **остаются нетронутыми**. - -Новая система применяется только к **новым товарам**, созданным после миграции. - -## Использование - -### Создание товара без варианта - -```python -product = Product( - name="Роза красная", - category=category, - cost_price=100, - sale_price=200 -) -product.save() -# Артикул: PROD-000001 -``` - -### Создание товара с автопарсингом суффикса - -```python -product = Product( - name="Роза Freedom 50см", # "50см" будет автоматически извлечено - category=category, - cost_price=150, - sale_price=300 -) -product.save() -# Артикул: PROD-000002-50 -# variant_suffix: "50" -``` - -### Создание товара с ручным суффиксом - -```python -product = Product( - name="Лента атласная красная", - category=category, - cost_price=20, - sale_price=40, - variant_suffix="RED" # Ручной суффикс -) -product.save() -# Артикул: PROD-000003-RED -``` - -### Создание комплекта - -```python -kit = ProductKit( - name="Букет Романтика", - slug="buket-romantika", - pricing_method='fixed', - fixed_price=1500 -) -kit.save() -# Артикул: KIT-000001 -``` - -## Технические детали - -### Модели - -- **SKUCounter** - хранит глобальные счетчики для товаров и комплектов -- **Product.variant_suffix** - новое поле для хранения суффикса варианта - -### Утилиты - -Файл: `products/utils/sku_generator.py` - -**Функции:** -- `parse_variant_suffix(name)` - извлекает суффикс из названия -- `ensure_sku_unique(base_sku, exclude_id)` - обеспечивает уникальность артикула -- `generate_product_sku(product)` - генерирует артикул для товара -- `generate_kit_sku()` - генерирует артикул для комплекта - -### Миграции - -- `0007_skucounter_product_variant_suffix.py` - создание модели SKUCounter и добавление поля variant_suffix - -## Тестирование - -Запустите тестовый скрипт для проверки генерации артикулов: - -```bash -python test_sku_generation.py -``` - -Скрипт создаст несколько тестовых товаров и комплектов с разными типами артикулов. - -## FAQ - -**Q: Что если я изменю название товара после создания?** -A: Артикул НЕ изменится. Артикул генерируется только один раз при создании товара. - -**Q: Можно ли изменить категорию товара?** -A: Да! Артикул не зависит от категории, поэтому вы можете свободно менять категорию. - -**Q: Что делать, если суффикс извлекся неправильно?** -A: Вы можете вручную изменить поле `variant_suffix` в админке и пересохранить товар (но артикул уже не изменится). - -**Q: Можно ли вручную задать артикул?** -A: Да, вы можете вручную задать артикул в поле `sku`. Система проверит уникальность. - -**Q: Как сбросить счетчики?** -A: Не рекомендуется! Но если необходимо, измените значение в Django Admin → SKU Counters. - -## Поддержка - -При возникновении проблем или вопросов обратитесь к разработчикам проекта. diff --git a/myproject/accounts/migrations/0001_initial.py b/myproject/accounts/migrations/0001_initial.py index 3914cbc..11efe93 100644 --- a/myproject/accounts/migrations/0001_initial.py +++ b/myproject/accounts/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.7 on 2025-10-23 20:27 +# Generated by Django 5.2.7 on 2025-10-25 13:44 import django.contrib.auth.validators import django.utils.timezone diff --git a/myproject/customers/migrations/0001_initial.py b/myproject/customers/migrations/0001_initial.py index f6d232d..58b2d07 100644 --- a/myproject/customers/migrations/0001_initial.py +++ b/myproject/customers/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.7 on 2025-10-23 23:46 +# Generated by Django 5.2.7 on 2025-10-25 13:44 import django.db.models.deletion import phonenumber_field.modelfields @@ -10,7 +10,6 @@ class Migration(migrations.Migration): initial = True dependencies = [ - ('products', '0001_initial'), ] operations = [ @@ -18,26 +17,20 @@ class Migration(migrations.Migration): name='Customer', fields=[ ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), - ('first_name', models.CharField(max_length=100, verbose_name='Имя')), - ('last_name', models.CharField(max_length=100, verbose_name='Фамилия')), - ('email', models.EmailField(max_length=254, unique=True, verbose_name='Email')), - ('phone', phonenumber_field.modelfields.PhoneNumberField(blank=True, help_text='Телефон в международном формате (например, +375291234567)', max_length=128, null=True, region=None, verbose_name='Телефон')), - ('preferred_colors', models.CharField(blank=True, help_text="Предпочтительные цветы цветов, например: 'красный, белый, желтый'", max_length=200, null=True, verbose_name='Предпочтительные цвета')), - ('loyalty_tier', models.CharField(choices=[('bronze', 'Бронза'), ('silver', 'Серебро'), ('gold', 'Золото'), ('platinum', 'Платина')], default='bronze', max_length=20, verbose_name='Уровень лояльности')), + ('name', models.CharField(blank=True, max_length=200, verbose_name='Имя')), + ('email', models.EmailField(blank=True, max_length=254, verbose_name='Email')), + ('phone', phonenumber_field.modelfields.PhoneNumberField(blank=True, help_text='Введите телефон в любом формате, например: +375291234567, 80255270546, 8(029)1234567 - будет автоматически преобразован в международный формат', max_length=128, null=True, region=None, unique=True, verbose_name='Телефон')), + ('loyalty_tier', models.CharField(choices=[('no_discount', 'Без скидки'), ('bronze', 'Бронза'), ('silver', 'Серебро'), ('gold', 'Золото'), ('platinum', 'Платина')], default='no_discount', max_length=20, verbose_name='Уровень лояльности')), ('total_spent', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Общая сумма покупок')), - ('birthday', models.DateField(blank=True, null=True, verbose_name='День рождения')), - ('anniversary', models.DateField(blank=True, null=True, verbose_name='Годовщина')), ('notes', models.TextField(blank=True, help_text='Заметки о клиенте, особые предпочтения и т.д.', null=True, verbose_name='Заметки')), - ('is_active', models.BooleanField(default=True, verbose_name='Активный клиент')), - ('is_vip', models.BooleanField(default=False, verbose_name='VIP клиент')), ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')), ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Дата обновления')), - ('favorite_flower_types', models.ManyToManyField(blank=True, related_name='preferred_by_customers', to='products.product', verbose_name='Любимые виды цветов')), ], options={ 'verbose_name': 'Клиент', 'verbose_name_plural': 'Клиенты', 'ordering': ['-created_at'], + 'indexes': [models.Index(fields=['name'], name='customers_c_name_f018e2_idx'), models.Index(fields=['email'], name='customers_c_email_4fdeb3_idx'), models.Index(fields=['phone'], name='customers_c_phone_8493fa_idx'), models.Index(fields=['created_at'], name='customers_c_created_1ed0f4_idx'), models.Index(fields=['loyalty_tier'], name='customers_c_loyalty_5162a0_idx')], }, ), migrations.CreateModel( @@ -51,7 +44,6 @@ class Migration(migrations.Migration): ('district', models.CharField(blank=True, help_text='Район в Минске для удобства доставки', max_length=100, null=True, verbose_name='Район')), ('delivery_instructions', models.TextField(blank=True, help_text='Дополнительные инструкции для курьера (домофон, подъезд и т.д.)', null=True, verbose_name='Инструкции для доставки')), ('is_default', models.BooleanField(default=False, help_text='Использовать этот адрес для доставки по умолчанию', verbose_name='Адрес по умолчанию')), - ('is_active', models.BooleanField(default=True, verbose_name='Активный адрес')), ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')), ('updated_at', models.DateTimeField(auto_now=True, verbose_name='Дата обновления')), ('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='addresses', to='customers.customer', verbose_name='Клиент')), @@ -60,42 +52,7 @@ class Migration(migrations.Migration): 'verbose_name': 'Адрес доставки', 'verbose_name_plural': 'Адреса доставки', 'ordering': ['-is_default', '-created_at'], + 'indexes': [models.Index(fields=['customer'], name='customers_a_custome_53b543_idx'), models.Index(fields=['is_default'], name='customers_a_is_defa_631851_idx'), models.Index(fields=['district'], name='customers_a_distric_ac47d5_idx')], }, ), - migrations.AddIndex( - model_name='customer', - index=models.Index(fields=['email'], name='customers_c_email_4fdeb3_idx'), - ), - migrations.AddIndex( - model_name='customer', - index=models.Index(fields=['phone'], name='customers_c_phone_8493fa_idx'), - ), - migrations.AddIndex( - model_name='customer', - index=models.Index(fields=['created_at'], name='customers_c_created_1ed0f4_idx'), - ), - migrations.AddIndex( - model_name='customer', - index=models.Index(fields=['is_active'], name='customers_c_is_acti_91d305_idx'), - ), - migrations.AddIndex( - model_name='customer', - index=models.Index(fields=['loyalty_tier'], name='customers_c_loyalty_5162a0_idx'), - ), - migrations.AddIndex( - model_name='address', - index=models.Index(fields=['customer'], name='customers_a_custome_53b543_idx'), - ), - migrations.AddIndex( - model_name='address', - index=models.Index(fields=['is_default'], name='customers_a_is_defa_631851_idx'), - ), - migrations.AddIndex( - model_name='address', - index=models.Index(fields=['is_active'], name='customers_a_is_acti_433713_idx'), - ), - migrations.AddIndex( - model_name='address', - index=models.Index(fields=['district'], name='customers_a_distric_ac47d5_idx'), - ), ] diff --git a/myproject/customers/migrations/0002_remove_customer_anniversary_remove_customer_birthday_and_more.py b/myproject/customers/migrations/0002_remove_customer_anniversary_remove_customer_birthday_and_more.py deleted file mode 100644 index af77a7c..0000000 --- a/myproject/customers/migrations/0002_remove_customer_anniversary_remove_customer_birthday_and_more.py +++ /dev/null @@ -1,56 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-24 07:52 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('customers', '0001_initial'), - ] - - operations = [ - migrations.RemoveField( - model_name='customer', - name='anniversary', - ), - migrations.RemoveField( - model_name='customer', - name='birthday', - ), - migrations.RemoveField( - model_name='customer', - name='favorite_flower_types', - ), - migrations.RemoveField( - model_name='customer', - name='first_name', - ), - migrations.RemoveField( - model_name='customer', - name='last_name', - ), - migrations.RemoveField( - model_name='customer', - name='preferred_colors', - ), - migrations.AddField( - model_name='customer', - name='name', - field=models.CharField(blank=True, max_length=200, verbose_name='Имя'), - ), - migrations.AlterField( - model_name='customer', - name='email', - field=models.EmailField(blank=True, max_length=254, verbose_name='Email'), - ), - migrations.AlterField( - model_name='customer', - name='loyalty_tier', - field=models.CharField(choices=[('no_discount', 'Без скидки'), ('bronze', 'Бронза'), ('silver', 'Серебро'), ('gold', 'Золото'), ('platinum', 'Платина')], default='no_discount', max_length=20, verbose_name='Уровень лояльности'), - ), - migrations.AddIndex( - model_name='customer', - index=models.Index(fields=['name'], name='customers_c_name_f018e2_idx'), - ), - ] diff --git a/myproject/customers/migrations/0003_alter_customer_phone.py b/myproject/customers/migrations/0003_alter_customer_phone.py deleted file mode 100644 index c433652..0000000 --- a/myproject/customers/migrations/0003_alter_customer_phone.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-24 14:55 - -import phonenumber_field.modelfields -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('customers', '0002_remove_customer_anniversary_remove_customer_birthday_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='customer', - name='phone', - field=phonenumber_field.modelfields.PhoneNumberField(blank=True, help_text='Введите телефон в любом формате, например: +375291234567, 80255270546, 8(029)1234567 - будет автоматически преобразован в международный формат', max_length=128, null=True, region=None, verbose_name='Телефон'), - ), - ] diff --git a/myproject/customers/migrations/0004_remove_address_customers_a_is_acti_433713_idx_and_more.py b/myproject/customers/migrations/0004_remove_address_customers_a_is_acti_433713_idx_and_more.py deleted file mode 100644 index f8e9dee..0000000 --- a/myproject/customers/migrations/0004_remove_address_customers_a_is_acti_433713_idx_and_more.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-24 16:57 - -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('customers', '0003_alter_customer_phone'), - ] - - operations = [ - migrations.RemoveIndex( - model_name='address', - name='customers_a_is_acti_433713_idx', - ), - migrations.RemoveIndex( - model_name='customer', - name='customers_c_is_acti_91d305_idx', - ), - migrations.RemoveField( - model_name='address', - name='is_active', - ), - migrations.RemoveField( - model_name='customer', - name='is_active', - ), - migrations.RemoveField( - model_name='customer', - name='is_vip', - ), - ] diff --git a/myproject/customers/migrations/0005_alter_customer_phone.py b/myproject/customers/migrations/0005_alter_customer_phone.py deleted file mode 100644 index f4697c6..0000000 --- a/myproject/customers/migrations/0005_alter_customer_phone.py +++ /dev/null @@ -1,19 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-24 17:01 - -import phonenumber_field.modelfields -from django.db import migrations - - -class Migration(migrations.Migration): - - dependencies = [ - ('customers', '0004_remove_address_customers_a_is_acti_433713_idx_and_more'), - ] - - operations = [ - migrations.AlterField( - model_name='customer', - name='phone', - field=phonenumber_field.modelfields.PhoneNumberField(blank=True, help_text='Введите телефон в любом формате, например: +375291234567, 80255270546, 8(029)1234567 - будет автоматически преобразован в международный формат', max_length=128, null=True, region=None, unique=True, verbose_name='Телефон'), - ), - ] diff --git a/myproject/docs/README_VARIANTS.md b/myproject/docs/README_VARIANTS.md deleted file mode 100644 index 7542329..0000000 --- a/myproject/docs/README_VARIANTS.md +++ /dev/null @@ -1,160 +0,0 @@ -# Система вариантов товаров - Краткое руководство - -## Что реализовано - -Система позволяет создавать букеты с гибкими заменами компонентов. Каждый букет может иметь свои индивидуальные приоритеты для одной и той же группы товаров. - -## Новые модели - -1. **ProductVariantGroup** - группа взаимозаменяемых товаров -2. **KitItemPriority** - приоритеты товаров для конкретной позиции букета - -## Изменения в существующих моделях - -1. **Product** - добавлено поле `variant_groups` (M2M) -2. **KitItem** - добавлены поля `variant_group`, `notes` -3. **ProductKit** - добавлены методы проверки доступности и расчета цен - -## Быстрый старт - -### 1. Запуск демо - -```bash -python manage.py demo_variants -``` - -Это создаст демонстрационные данные и покажет работу системы. - -### 2. Создание группы вариантов через админку - -1. Откройте `/admin/` -2. Перейдите в "Группы вариантов" -3. Создайте новую группу (например, "Роза красная Freedom") -4. Откройте товары и добавьте их в группу через поле "Группы вариантов" - -### 3. Создание букета с вариантами - -1. Создайте новый комплект -2. Добавьте позицию: - - Либо укажите конкретный товар (без замен) - - Либо укажите группу вариантов (с заменами) -3. Откройте позицию и настройте приоритеты в разделе "Приоритеты вариантов" - -## API для разработчиков - -### Проверка доступности букета - -```python -from products.models import ProductKit -from products.utils.stock_manager import StockManager - -kit = ProductKit.objects.get(name="Мой букет") -stock_manager = StockManager() - -if kit.check_availability(stock_manager): - print("Букет доступен!") - price = kit.calculate_price_with_substitutions(stock_manager) - print(f"Цена: {price}") -``` - -### Получение лучшего товара для позиции - -```python -from products.models import KitItem -from products.utils.stock_manager import StockManager - -kit_item = KitItem.objects.get(id=1) -stock_manager = StockManager() - -best_product = kit_item.get_best_available_product(stock_manager) -if best_product: - print(f"Используем: {best_product.name}") -``` - -## Файлы документации - -- [product_variants_guide.md](product_variants_guide.md) - подробное руководство -- [example_usage.py](example_usage.py) - примеры кода - -## Интеграция со складом - -Текущая версия использует заглушку `StockManager`. Для интеграции с реальной системой складского учета: - -1. Откройте `products/utils/stock_manager.py` -2. Реализуйте методы: - - `check_stock(product, quantity)` - проверка остатков - - `get_available_quantity(product)` - получение доступного количества - - `reserve_stock(product, quantity, order_id)` - резервирование - - `release_stock(product, quantity, order_id)` - освобождение - -## Основные возможности - -- ✅ Создание групп взаимозаменяемых товаров -- ✅ Один товар может быть в нескольких группах -- ✅ Индивидуальные приоритеты для каждого букета -- ✅ Проверка доступности с учетом замен -- ✅ Расчет цены с учетом фактически доступных товаров -- ✅ Валидация данных -- ✅ Django Admin интерфейс -- ✅ Документация и примеры - -## Примеры использования - -### Премиум букет - -``` -Позиция: Роза Freedom (группа вариантов) - 15 шт -Приоритеты: - 0. Роза 70см (200 руб/шт) - первый выбор - 1. Роза 60см (150 руб/шт) - 2. Роза 50см (100 руб/шт) - -Цена: 15 × 200 = 3000 руб -``` - -### Эконом букет - -``` -Позиция: Роза Freedom (группа вариантов) - 15 шт -Приоритеты: - 0. Роза 50см (100 руб/шт) - первый выбор - 1. Роза 60см (150 руб/шт) - 2. Роза 70см (200 руб/шт) - -Цена: 15 × 100 = 1500 руб -``` - -Та же группа товаров, но разные приоритеты и разная цена! - -## Структура файлов - -``` -products/ -├── models.py # Модели (обновлено) -├── admin.py # Админка (обновлено) -├── utils/ -│ └── stock_manager.py # Менеджер остатков (новый) -├── management/ -│ └── commands/ -│ └── demo_variants.py # Демонстрация (новая) -└── migrations/ - └── 0004_productvariantgroup_... # Миграция (новая) - -docs/ -├── README_VARIANTS.md # Это руководство -├── product_variants_guide.md # Подробная документация -└── example_usage.py # Примеры кода -``` - -## Поддержка - -Для получения помощи: -1. Прочитайте [product_variants_guide.md](product_variants_guide.md) -2. Изучите примеры в [example_usage.py](example_usage.py) -3. Запустите `python manage.py demo_variants` -4. Обратитесь к разработчикам - ---- - -**Версия**: 1.0 -**Дата**: 2025-10-21 diff --git a/myproject/docs/example_usage.py b/myproject/docs/example_usage.py deleted file mode 100644 index 0eb3416..0000000 --- a/myproject/docs/example_usage.py +++ /dev/null @@ -1,259 +0,0 @@ -""" -Примеры использования системы вариантов товаров. - -Этот файл содержит примеры кода для демонстрации работы с системой. -Запускать через Django shell: python manage.py shell < docs/example_usage.py -""" - -from decimal import Decimal -from products.models import ( - Product, ProductKit, KitItem, ProductCategory, - ProductVariantGroup, KitItemPriority -) -from products.utils.stock_manager import StockManager - - -def example_1_create_variant_group(): - """Пример 1: Создание группы вариантов""" - print("\n" + "="*60) - print("ПРИМЕР 1: Создание группы вариантов") - print("="*60) - - # Создаём категорию - category, _ = ProductCategory.objects.get_or_create( - name="Цветы", - defaults={'slug': 'cvety'} - ) - - # Создаём товары - розы разной длины - rose_50, _ = Product.objects.get_or_create( - name="Роза Freedom 50см красная", - defaults={ - 'cost_price': Decimal('80.00'), - 'sale_price': Decimal('100.00'), - 'category': category - } - ) - - rose_60, _ = Product.objects.get_or_create( - name="Роза Freedom 60см красная", - defaults={ - 'cost_price': Decimal('120.00'), - 'sale_price': Decimal('150.00'), - 'category': category - } - ) - - rose_70, _ = Product.objects.get_or_create( - name="Роза Freedom 70см красная", - defaults={ - 'cost_price': Decimal('160.00'), - 'sale_price': Decimal('200.00'), - 'category': category - } - ) - - # Создаём группу вариантов - group, created = ProductVariantGroup.objects.get_or_create( - name="Роза красная Freedom", - defaults={ - 'description': 'Красная роза Freedom различной длины (50-70см)' - } - ) - - # Добавляем товары в группу - rose_50.variant_groups.add(group) - rose_60.variant_groups.add(group) - rose_70.variant_groups.add(group) - - print(f"✓ Создана группа: {group.name}") - print(f" Товаров в группе: {group.get_products_count()}") - print(f" Товары:") - for product in group.products.all(): - print(f" - {product.name} ({product.sale_price} руб.)") - - return group, rose_50, rose_60, rose_70 - - -def example_2_create_premium_bouquet(group, rose_50, rose_60, rose_70): - """Пример 2: Создание премиум букета с приоритетами""" - print("\n" + "="*60) - print("ПРИМЕР 2: Создание премиум букета") - print("="*60) - - # Создаём букет - kit, _ = ProductKit.objects.get_or_create( - name="Ранчо Виталия Премиум", - defaults={ - 'slug': 'rancho-vitaliya-premium', - 'pricing_method': 'from_sale_prices' - } - ) - - # Создаём позицию с группой вариантов - kit_item, _ = KitItem.objects.get_or_create( - kit=kit, - variant_group=group, - defaults={ - 'quantity': Decimal('15.000'), - 'notes': 'Использовать самые длинные розы' - } - ) - - # Настраиваем приоритеты (для премиум букета - сначала длинные) - KitItemPriority.objects.get_or_create( - kit_item=kit_item, - product=rose_70, - defaults={'priority': 0} # Наивысший приоритет - ) - KitItemPriority.objects.get_or_create( - kit_item=kit_item, - product=rose_60, - defaults={'priority': 1} - ) - KitItemPriority.objects.get_or_create( - kit_item=kit_item, - product=rose_50, - defaults={'priority': 2} # Самый низкий приоритет - ) - - print(f"✓ Создан букет: {kit.name}") - print(f" Позиций: {kit.get_total_components_count()}") - print(f" С вариантами: {kit.get_components_with_variants_count()}") - print(f"\n Приоритеты для позиции '{kit_item.get_display_name()}':") - for priority in kit_item.priorities.all().order_by('priority'): - print(f" {priority.priority}. {priority.product.name} - {priority.product.sale_price} руб.") - - return kit - - -def example_3_create_economy_bouquet(group, rose_50, rose_60, rose_70): - """Пример 3: Создание эконом букета""" - print("\n" + "="*60) - print("ПРИМЕР 3: Создание эконом букета") - print("="*60) - - # Создаём эконом букет - kit, _ = ProductKit.objects.get_or_create( - name="Ранчо Виталия Эконом", - defaults={ - 'slug': 'rancho-vitaliya-econom', - 'pricing_method': 'from_sale_prices' - } - ) - - # Та же группа вариантов, но другие приоритеты - kit_item, _ = KitItem.objects.get_or_create( - kit=kit, - variant_group=group, - defaults={ - 'quantity': Decimal('15.000'), - 'notes': 'Эконом вариант' - } - ) - - # Для эконом букета - сначала короткие (дешевые) - KitItemPriority.objects.get_or_create( - kit_item=kit_item, - product=rose_50, - defaults={'priority': 0} # Наивысший приоритет - ) - KitItemPriority.objects.get_or_create( - kit_item=kit_item, - product=rose_60, - defaults={'priority': 1} - ) - KitItemPriority.objects.get_or_create( - kit_item=kit_item, - product=rose_70, - defaults={'priority': 2} # Самый низкий приоритет - ) - - print(f"✓ Создан букет: {kit.name}") - print(f"\n Приоритеты для позиции '{kit_item.get_display_name()}':") - for priority in kit_item.priorities.all().order_by('priority'): - print(f" {priority.priority}. {priority.product.name} - {priority.product.sale_price} руб.") - - return kit - - -def example_4_check_availability(premium_kit, economy_kit): - """Пример 4: Проверка доступности""" - print("\n" + "="*60) - print("ПРИМЕР 4: Проверка доступности букетов") - print("="*60) - - stock_manager = StockManager() - - # Проверяем премиум букет - print(f"\nПремиум букет: {premium_kit.name}") - if premium_kit.check_availability(stock_manager): - print(" ✓ Доступен для сборки") - price = premium_kit.calculate_price_with_substitutions(stock_manager) - print(f" Цена: {price} руб.") - else: - print(" ✗ Недоступен") - - # Проверяем эконом букет - print(f"\nЭконом букет: {economy_kit.name}") - if economy_kit.check_availability(stock_manager): - print(" ✓ Доступен для сборки") - price = economy_kit.calculate_price_with_substitutions(stock_manager) - print(f" Цена: {price} руб.") - else: - print(" ✗ Недоступен") - - -def example_5_best_product(): - """Пример 5: Получение лучшего доступного товара""" - print("\n" + "="*60) - print("ПРИМЕР 5: Выбор лучшего доступного товара") - print("="*60) - - # Получаем премиум букет - kit = ProductKit.objects.filter(name="Ранчо Виталия Премиум").first() - if not kit: - print(" Букет не найден") - return - - stock_manager = StockManager() - - for kit_item in kit.kit_items.all(): - print(f"\nПозиция: {kit_item.get_display_name()}") - print(f"Количество: {kit_item.quantity}") - - best_product = kit_item.get_best_available_product(stock_manager) - if best_product: - print(f"✓ Лучший доступный товар: {best_product.name}") - print(f" Цена: {best_product.sale_price} руб.") - print(f" Стоимость позиции: {best_product.sale_price * kit_item.quantity} руб.") - else: - print("✗ Нет доступных товаров") - - -def main(): - """Запуск всех примеров""" - print("\n" + "="*60) - print("ДЕМОНСТРАЦИЯ СИСТЕМЫ ВАРИАНТОВ ТОВАРОВ") - print("="*60) - - # Создаём данные - group, rose_50, rose_60, rose_70 = example_1_create_variant_group() - - # Создаём букеты - premium_kit = example_2_create_premium_bouquet(group, rose_50, rose_60, rose_70) - economy_kit = example_3_create_economy_bouquet(group, rose_50, rose_60, rose_70) - - # Проверяем доступность - example_4_check_availability(premium_kit, economy_kit) - - # Получаем лучший товар - example_5_best_product() - - print("\n" + "="*60) - print("ДЕМОНСТРАЦИЯ ЗАВЕРШЕНА") - print("="*60 + "\n") - - -if __name__ == "__main__": - main() diff --git a/myproject/docs/product_variants_guide.md b/myproject/docs/product_variants_guide.md deleted file mode 100644 index b351203..0000000 --- a/myproject/docs/product_variants_guide.md +++ /dev/null @@ -1,310 +0,0 @@ -# Руководство по работе с вариантами товаров - -## Введение - -Система вариантов товаров позволяет создавать букеты с гибкими заменами компонентов. Это полезно когда один товар может быть заменен на другой похожий товар (например, роза 50см на розу 70см), и приоритет замены индивидуален для каждого букета. - -## Основные концепции - -### 1. Группа вариантов (ProductVariantGroup) - -**Группа вариантов** - это набор взаимозаменяемых товаров. - -Пример: -- Группа: "Роза красная Freedom" - - Роза Freedom 50см - - Роза Freedom 60см - - Роза Freedom 70см - -Один товар может входить в несколько групп вариантов. - -### 2. Позиция в букете (KitItem) - -Каждая позиция в букете может быть: -- **Конкретным товаром** - без возможности замены -- **Группой вариантов** - с приоритетами замен - -### 3. Приоритеты (KitItemPriority) - -Для каждой позиции с группой вариантов можно настроить индивидуальные приоритеты: -- Меньшее число = выше приоритет -- Приоритет 0 = наивысший приоритет -- Приоритет 1 = второй по важности -- И т.д. - -## Как использовать - -### Шаг 1: Создание группы вариантов - -1. Откройте Django Admin -2. Перейдите в раздел "Группы вариантов" -3. Нажмите "Добавить группу вариантов" -4. Заполните: - - Название: например, "Роза красная Freedom" - - Описание: опционально -5. Сохраните - -### Шаг 2: Добавление товаров в группу - -1. Откройте нужный товар в разделе "Товары" -2. В поле "Группы вариантов" выберите созданную группу -3. Сохраните товар -4. Повторите для всех товаров, которые должны быть в этой группе - -Альтернативный способ: -- Выберите несколько товаров через filter_horizontal в админке товара - -### Шаг 3: Создание букета с вариантами - -1. Создайте новый комплект (букет) или откройте существующий -2. При добавлении позиции в букете: - - **Вариант А**: Укажите конкретный товар (если замены не нужны) - - **Вариант Б**: Укажите группу вариантов (если нужны замены) - - ⚠️ **Важно**: Нельзя указывать одновременно и товар, и группу вариантов! - -3. Укажите количество -4. При необходимости добавьте примечание -5. Сохраните позицию - -### Шаг 4: Настройка приоритетов - -Если вы выбрали группу вариантов: - -1. Откройте позицию букета (KitItem) -2. В разделе "Приоритеты вариантов" добавьте товары из группы -3. Для каждого товара укажите приоритет: - ``` - Роза Freedom 70см - приоритет 0 (первый выбор) - Роза Freedom 60см - приоритет 1 (второй выбор) - Роза Freedom 50см - приоритет 2 (третий выбор) - ``` -4. Сохраните - -### Шаг 5: Проверка доступности - -Система автоматически проверяет доступность букета: - -```python -# В коде или Django shell -kit = ProductKit.objects.get(name="Ранчо Виталия") - -# Проверить доступность -if kit.check_availability(): - print("Букет можно собрать!") -else: - print("Букет недоступен") - -# Рассчитать цену с учетом замен -price = kit.calculate_price_with_substitutions() -print(f"Цена: {price} руб.") -``` - -## Примеры использования - -### Пример 1: Премиум букет с розами - -**Задача**: Создать букет "Ранчо Виталия" где нужны длинные розы, но можно заменить на средние. - -**Решение**: -1. Создать группу "Роза красная Freedom" -2. Добавить в неё розы 50см, 60см, 70см -3. В букете создать позицию с группой вариантов -4. Настроить приоритеты: - - Роза 70см - приоритет 0 - - Роза 60см - приоритет 1 - - Роза 50см - приоритет 2 - -При проверке доступности система сначала проверит наличие 70см, потом 60см, и только потом 50см. - -### Пример 2: Эконом букет - -**Задача**: Создать эконом-букет где приоритет у коротких роз. - -**Решение**: -1. Использовать ту же группу "Роза красная Freedom" -2. Создать новый букет с другими приоритетами: - - Роза 50см - приоритет 0 (первый выбор) - - Роза 60см - приоритет 1 - - Роза 70см - приоритет 2 - -Та же группа товаров, но другой порядок приоритетов! - -### Пример 3: Букет без замен - -**Задача**: Создать букет где конкретные товары без замен. - -**Решение**: -1. При создании позиции в букете указать конкретный товар -2. Оставить поле "Группа вариантов" пустым -3. Приоритеты настраивать не нужно - -### Пример 4: Смешанный букет - -**Задача**: В одном букете часть позиций с заменами, часть без. - -**Решение**: -``` -Позиция 1: Роза Freedom (группа вариантов) - 15 шт -Позиция 2: Упаковка крафт (конкретный товар) - 1 шт -Позиция 3: Лента атласная (конкретный товар) - 2 м -Позиция 4: Эустома белая (группа вариантов) - 5 шт -``` - -## Как работает система - -### Проверка доступности товара - -Когда вызывается `kit_item.get_best_available_product()`: - -1. Система получает список доступных товаров -2. Если настроены приоритеты - сортирует по ним -3. Проходит по списку от высшего приоритета к низшему -4. Для каждого товара проверяет наличие на складе -5. Возвращает первый доступный товар - -### Проверка доступности букета - -Когда вызывается `kit.check_availability()`: - -1. Система проходит по всем позициям букета -2. Для каждой позиции ищет доступный товар -3. Если хотя бы одна позиция недоступна - весь букет недоступен -4. Если все позиции доступны - букет можно собрать - -### Расчет цены - -Система рассчитывает цену на основе фактически доступных товаров: - -```python -# Пример -Позиция: Роза Freedom - 15 шт -Приоритеты: - - Роза 70см (200 руб) - нет в наличии - - Роза 60см (150 руб) - нет в наличии - - Роза 50см (100 руб) - есть в наличии ✓ - -Цена позиции: 15 × 100 = 1500 руб -``` - -## Интеграция со складом - -Текущая версия использует заглушку `StockManager`, которая всегда возвращает `True`. - -В будущем `StockManager` будет интегрирован с реальной системой складского учета: - -```python -# Будущая реализация -class StockManager: - def check_stock(self, product, quantity): - # Запрос к складской системе - available = get_stock_from_warehouse(product.sku) - return available >= quantity -``` - -## API моделей - -### ProductVariantGroup - -**Методы:** -- `get_products_count()` - количество товаров в группе - -**Поля:** -- `name` - название группы -- `description` - описание -- `products` - товары в группе (M2M) - -### KitItem - -**Методы:** -- `get_display_name()` - название для отображения -- `has_priorities_set()` - настроены ли приоритеты -- `get_available_products()` - список доступных товаров -- `get_best_available_product(stock_manager)` - лучший доступный товар -- `clean()` - валидация - -**Поля:** -- `product` - конкретный товар (nullable) -- `variant_group` - группа вариантов (nullable) -- `quantity` - количество -- `notes` - примечание - -### ProductKit - -**Методы:** -- `get_total_components_count()` - количество позиций -- `get_components_with_variants_count()` - позиций с вариантами -- `check_availability(stock_manager)` - проверка доступности -- `calculate_price_with_substitutions(stock_manager)` - расчет цены - -### Product - -**Методы:** -- `get_variant_groups()` - все группы вариантов -- `get_similar_products()` - похожие товары - -**Поля:** -- `variant_groups` - группы вариантов (M2M) - -## Советы и лучшие практики - -1. **Именование групп**: Используйте понятные названия, например "Роза красная Freedom" вместо "Группа 1" - -2. **Приоритеты**: Начинайте с 0 и увеличивайте по 1 для простоты - -3. **Проверка**: Всегда проверяйте доступность букета перед оформлением заказа - -4. **Цены**: Учитывайте, что цена может меняться в зависимости от того, какой вариант доступен - -5. **Несколько групп**: Один товар может быть в нескольких группах - это нормально - -6. **Валидация**: Система не даст сохранить позицию, где указаны одновременно товар И группа - -## Часто задаваемые вопросы - -**Q: Можно ли товар добавить в несколько групп вариантов?** -A: Да, один товар может быть в любом количестве групп. - -**Q: Что если не настроить приоритеты?** -A: Система вернет все товары из группы в произвольном порядке. - -**Q: Можно ли изменить приоритеты после создания букета?** -A: Да, приоритеты можно менять в любое время. - -**Q: Как система выбирает товар, если несколько имеют одинаковый приоритет?** -A: По ID (первый созданный). - -**Q: Влияет ли порядок товаров в группе на выбор?** -A: Нет, только приоритеты имеют значение. Если приоритеты не настроены - порядок не определен. - -## Устранение неполадок - -### Ошибка: "Нельзя указывать одновременно товар и группу вариантов" - -**Причина**: Заполнены оба поля - `product` и `variant_group` - -**Решение**: Очистите одно из полей. Оставьте либо товар, либо группу. - -### Букет показывается как недоступный, хотя товары есть - -**Причина**: Возможно, `StockManager` некорректно работает - -**Решение**: Проверьте реализацию `StockManager.check_stock()` - -### Приоритеты не работают - -**Причина**: Приоритеты не были сохранены для позиции - -**Решение**: -1. Откройте позицию букета -2. Убедитесь, что в разделе "Приоритеты вариантов" есть записи -3. Проверьте значения приоритетов (меньше = выше) - -## Дополнительная информация - -Для получения помощи обратитесь к разработчикам или создайте issue в репозитории проекта. - ---- - -**Дата создания**: 2025-10-21 -**Версия**: 1.0 diff --git a/myproject/inventory/migrations/0001_initial.py b/myproject/inventory/migrations/0001_initial.py index 53b638d..cb2e57f 100644 --- a/myproject/inventory/migrations/0001_initial.py +++ b/myproject/inventory/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.7 on 2025-10-23 20:27 +# Generated by Django 5.2.7 on 2025-10-25 13:44 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 a73d2e5..0f91c2b 100644 --- a/myproject/orders/migrations/0001_initial.py +++ b/myproject/orders/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.7 on 2025-10-23 20:27 +# Generated by Django 5.2.7 on 2025-10-25 13:44 import django.db.models.deletion from django.conf import settings diff --git a/myproject/products/migrations/0001_initial.py b/myproject/products/migrations/0001_initial.py index 559b274..dc50688 100644 --- a/myproject/products/migrations/0001_initial.py +++ b/myproject/products/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.7 on 2025-10-23 20:27 +# Generated by Django 5.2.7 on 2025-10-25 13:44 import django.db.models.deletion from django.conf import settings @@ -267,4 +267,24 @@ class Migration(migrations.Migration): model_name='product', index=models.Index(fields=['is_deleted', 'created_at'], name='products_pr_is_dele_f30efb_idx'), ), + migrations.AddIndex( + model_name='kititem', + index=models.Index(fields=['kit'], name='products_ki_kit_id_d28dc9_idx'), + ), + migrations.AddIndex( + model_name='kititem', + index=models.Index(fields=['product'], name='products_ki_product_d2ad00_idx'), + ), + migrations.AddIndex( + model_name='kititem', + index=models.Index(fields=['variant_group'], name='products_ki_variant_e42628_idx'), + ), + migrations.AddIndex( + model_name='kititem', + index=models.Index(fields=['kit', 'product'], name='products_ki_kit_id_14738f_idx'), + ), + migrations.AddIndex( + model_name='kititem', + index=models.Index(fields=['kit', 'variant_group'], name='products_ki_kit_id_8199a8_idx'), + ), ] diff --git a/myproject/products/migrations/0002_kititem_products_ki_kit_id_d28dc9_idx_and_more.py b/myproject/products/migrations/0002_kititem_products_ki_kit_id_d28dc9_idx_and_more.py deleted file mode 100644 index 1a8841d..0000000 --- a/myproject/products/migrations/0002_kititem_products_ki_kit_id_d28dc9_idx_and_more.py +++ /dev/null @@ -1,33 +0,0 @@ -# Generated by Django 5.2.7 on 2025-10-24 07:52 - -from django.db import migrations, models - - -class Migration(migrations.Migration): - - dependencies = [ - ('products', '0001_initial'), - ] - - operations = [ - migrations.AddIndex( - model_name='kititem', - index=models.Index(fields=['kit'], name='products_ki_kit_id_d28dc9_idx'), - ), - migrations.AddIndex( - model_name='kititem', - index=models.Index(fields=['product'], name='products_ki_product_d2ad00_idx'), - ), - migrations.AddIndex( - model_name='kititem', - index=models.Index(fields=['variant_group'], name='products_ki_variant_e42628_idx'), - ), - migrations.AddIndex( - model_name='kititem', - index=models.Index(fields=['kit', 'product'], name='products_ki_kit_id_14738f_idx'), - ), - migrations.AddIndex( - model_name='kititem', - index=models.Index(fields=['kit', 'variant_group'], name='products_ki_kit_id_8199a8_idx'), - ), - ] diff --git a/myproject/test_category_tree.py b/myproject/test_category_tree.py deleted file mode 100644 index db7c2f7..0000000 --- a/myproject/test_category_tree.py +++ /dev/null @@ -1,70 +0,0 @@ -""" -Test script to create category hierarchy for testing the tree view -""" -import os -import django - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings') -django.setup() - -from products.models import ProductCategory - -print("Creating test category hierarchy...") -print("="*60) - -# Get existing categories -existing = list(ProductCategory.objects.all()[:3]) - -if len(existing) >= 3: - cat1, cat2, cat3 = existing[0], existing[1], existing[2] - - # Make sure they are root categories first - cat1.parent = None - cat2.parent = None - cat3.parent = None - cat1.save() - cat2.save() - cat3.save() - - print(f"Root categories:") - print(f" 1. {cat1.name} (id={cat1.pk})") - print(f" 2. {cat2.name} (id={cat2.pk})") - print(f" 3. {cat3.name} (id={cat3.pk})") - - # Create child for cat2 (if doesn't exist) - child1, created = ProductCategory.objects.get_or_create( - name="Test Child 1", - defaults={'parent': cat2, 'is_active': True} - ) - if not created: - child1.parent = cat2 - child1.save() - - print(f"\nChild category:") - print(f" - {child1.name} (id={child1.pk}, parent={cat2.name})") - - # Create grandchild for child1 (if doesn't exist) - grandchild1, created = ProductCategory.objects.get_or_create( - name="Test Grandchild 1", - defaults={'parent': child1, 'is_active': True} - ) - if not created: - grandchild1.parent = child1 - grandchild1.save() - - print(f"\nGrandchild category:") - print(f" - {grandchild1.name} (id={grandchild1.pk}, parent={child1.name})") - - print("\n" + "="*60) - print("Hierarchy created successfully!") - print("\nExpected tree structure:") - print(f"{cat1.name}") - print(f"{cat2.name}") - print(f" {child1.name}") - print(f" {grandchild1.name}") - print(f"{cat3.name}") - print("\nVisit http://127.0.0.1:8000/products/categories/ to see the tree view") - -else: - print("Not enough categories to create hierarchy") - print("Please create at least 3 categories first") diff --git a/myproject/test_category_validation.py b/myproject/test_category_validation.py deleted file mode 100644 index 5684399..0000000 --- a/myproject/test_category_validation.py +++ /dev/null @@ -1,186 +0,0 @@ -""" -Скрипт для тестирования защиты от циклических ссылок в категориях -""" -import os -import django - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings') -django.setup() - -from products.models import ProductCategory -from django.core.exceptions import ValidationError - - -def test_self_reference(): - """Тест 1: Попытка сделать категорию родителем самой себя""" - print("\n=== Тест 1: Самоссылка ===") - try: - cat = ProductCategory.objects.first() - if not cat: - print("[FAIL] Net kategorij v baze dlya testa") - return False - - print(f"Kategoriya: {cat.name} (id={cat.pk})") - cat.parent = cat - cat.save() - print("[FAIL] OSHIBKA: Samossylka ne byla zablokirovana!") - return False - except ValidationError as e: - print("[OK] USPEKH: Samossylka zablokirovana") - print(f" Soobshchenie: {e.message_dict.get('parent', [''])[0]}") - return True - - -def test_simple_cycle(): - """Тест 2: Попытка создать цикл A → B → A""" - print("\n=== Тест 2: Простой цикл A → B → A ===") - try: - # Получаем две категории - categories = list(ProductCategory.objects.all()[:2]) - if len(categories) < 2: - print("❌ Недостаточно категорий в базе (нужно минимум 2)") - return False - - cat_a, cat_b = categories - print(f"Категория A: {cat_a.name} (id={cat_a.pk})") - print(f"Категория B: {cat_b.name} (id={cat_b.pk})") - - # Создаем связь A → B - cat_a.parent = None - cat_a.save() - cat_b.parent = cat_a - cat_b.save() - print(f"Создана связь: {cat_a.name} → {cat_b.name}") - - # Пытаемся создать цикл: B → A (создаст цикл A → B → A) - cat_a.parent = cat_b - cat_a.save() - - print("❌ ОШИБКА: Цикл не был заблокирован!") - # Откатываем изменения - cat_a.parent = None - cat_b.parent = None - cat_a.save() - cat_b.save() - return False - - except ValidationError as e: - print(f"✅ УСПЕХ: Цикл заблокирован") - print(f" Сообщение: {e.message_dict.get('parent', [''])[0]}") - # Откатываем изменения - cat_a.parent = None - cat_b.parent = None - cat_a.save() - cat_b.save() - return True - - -def test_multi_level_cycle(): - """Тест 3: Попытка создать многоуровневый цикл A → B → C → A""" - print("\n=== Тест 3: Многоуровневый цикл A → B → C → A ===") - try: - # Получаем три категории - categories = list(ProductCategory.objects.all()[:3]) - if len(categories) < 3: - print("❌ Недостаточно категорий в базе (нужно минимум 3)") - return False - - cat_a, cat_b, cat_c = categories - print(f"Категория A: {cat_a.name} (id={cat_a.pk})") - print(f"Категория B: {cat_b.name} (id={cat_b.pk})") - print(f"Категория C: {cat_c.name} (id={cat_c.pk})") - - # Создаем цепочку A → B → C - cat_a.parent = None - cat_a.save() - cat_b.parent = cat_a - cat_b.save() - cat_c.parent = cat_b - cat_c.save() - print(f"Создана цепочка: {cat_a.name} → {cat_b.name} → {cat_c.name}") - - # Пытаемся замкнуть цикл: A.parent = C - cat_a.parent = cat_c - cat_a.save() - - print("❌ ОШИБКА: Многоуровневый цикл не был заблокирован!") - # Откатываем изменения - cat_a.parent = None - cat_b.parent = None - cat_c.parent = None - cat_a.save() - cat_b.save() - cat_c.save() - return False - - except ValidationError as e: - print(f"✅ УСПЕХ: Многоуровневый цикл заблокирован") - print(f" Сообщение: {e.message_dict.get('parent', [''])[0]}") - # Откатываем изменения - cat_a.parent = None - cat_b.parent = None - cat_c.parent = None - cat_a.save() - cat_b.save() - cat_c.save() - return True - - -def test_normal_operations(): - """Тест 4: Нормальные операции должны работать""" - print("\n=== Тест 4: Нормальные операции ===") - try: - categories = list(ProductCategory.objects.all()[:2]) - if len(categories) < 2: - print("❌ Недостаточно категорий в базе") - return False - - cat_a, cat_b = categories - - # Сбрасываем родителей - cat_a.parent = None - cat_b.parent = None - cat_a.save() - cat_b.save() - - # Создаем нормальную иерархию A → B - cat_b.parent = cat_a - cat_b.save() - print(f"✅ УСПЕХ: Создана нормальная иерархия {cat_a.name} → {cat_b.name}") - - # Откатываем - cat_b.parent = None - cat_b.save() - return True - - except ValidationError as e: - print(f"❌ ОШИБКА: Нормальная операция была заблокирована!") - print(f" Сообщение: {e}") - return False - - -if __name__ == '__main__': - print("=" * 60) - print("ТЕСТИРОВАНИЕ ЗАЩИТЫ ОТ ЦИКЛИЧЕСКИХ ССЫЛОК") - print("=" * 60) - - results = [] - results.append(("Тест самоссылки", test_self_reference())) - results.append(("Тест простого цикла", test_simple_cycle())) - results.append(("Тест многоуровневого цикла", test_multi_level_cycle())) - results.append(("Тест нормальных операций", test_normal_operations())) - - print("\n" + "=" * 60) - print("РЕЗУЛЬТАТЫ ТЕСТИРОВАНИЯ") - print("=" * 60) - for name, result in results: - status = "✅ ПРОЙДЕН" if result else "❌ ПРОВАЛЕН" - print(f"{name}: {status}") - - all_passed = all(result for _, result in results) - print("\n" + "=" * 60) - if all_passed: - print("🎉 ВСЕ ТЕСТЫ ПРОЙДЕНЫ!") - else: - print("⚠️ НЕКОТОРЫЕ ТЕСТЫ ПРОВАЛЕНЫ") - print("=" * 60) diff --git a/myproject/test_cycles.py b/myproject/test_cycles.py deleted file mode 100644 index 66c5d13..0000000 --- a/myproject/test_cycles.py +++ /dev/null @@ -1,134 +0,0 @@ -""" -Test script for category cycle protection -""" -import os -import django - -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings') -django.setup() - -from products.models import ProductCategory -from django.core.exceptions import ValidationError - - -print("="*60) -print("TEST 1: Self-reference (A.parent = A)") -print("="*60) -try: - cat = ProductCategory.objects.first() - print(f"Category: {cat.name} (id={cat.pk})") - cat.parent = cat - cat.save() - print("[FAIL] Self-reference was NOT blocked!") -except ValidationError as e: - print("[PASS] Self-reference blocked successfully") - print(f"Error: {e.message_dict.get('parent')}") - -print("\n" + "="*60) -print("TEST 2: Simple cycle (A -> B -> A)") -print("="*60) -try: - cats = list(ProductCategory.objects.all()[:2]) - cat_a, cat_b = cats[0], cats[1] - print(f"Category A: {cat_a.name} (id={cat_a.pk})") - print(f"Category B: {cat_b.name} (id={cat_b.pk})") - - # Create A -> B - cat_a.parent = None - cat_a.save() - cat_b.parent = cat_a - cat_b.save() - print(f"Created: {cat_a.name} -> {cat_b.name}") - - # Try to create cycle: A.parent = B - cat_a.parent = cat_b - cat_a.save() - print("[FAIL] Cycle was NOT blocked!") - - # Cleanup - cat_a.parent = None - cat_b.parent = None - cat_a.save() - cat_b.save() - -except ValidationError as e: - print("[PASS] Cycle blocked successfully") - print(f"Error: {e.message_dict.get('parent')}") - # Cleanup - cat_a.parent = None - cat_b.parent = None - cat_a.save() - cat_b.save() - -print("\n" + "="*60) -print("TEST 3: Multi-level cycle (A -> B -> C -> A)") -print("="*60) -try: - cats = list(ProductCategory.objects.all()[:3]) - cat_a, cat_b, cat_c = cats[0], cats[1], cats[2] - print(f"Category A: {cat_a.name} (id={cat_a.pk})") - print(f"Category B: {cat_b.name} (id={cat_b.pk})") - print(f"Category C: {cat_c.name} (id={cat_c.pk})") - - # Create chain A -> B -> C - cat_a.parent = None - cat_a.save() - cat_b.parent = cat_a - cat_b.save() - cat_c.parent = cat_b - cat_c.save() - print(f"Created: {cat_a.name} -> {cat_b.name} -> {cat_c.name}") - - # Try to create cycle: A.parent = C - cat_a.parent = cat_c - cat_a.save() - print("[FAIL] Multi-level cycle was NOT blocked!") - - # Cleanup - cat_a.parent = None - cat_b.parent = None - cat_c.parent = None - cat_a.save() - cat_b.save() - cat_c.save() - -except ValidationError as e: - print("[PASS] Multi-level cycle blocked successfully") - print(f"Error: {e.message_dict.get('parent')}") - # Cleanup - cat_a.parent = None - cat_b.parent = None - cat_c.parent = None - cat_a.save() - cat_b.save() - cat_c.save() - -print("\n" + "="*60) -print("TEST 4: Normal operations should work") -print("="*60) -try: - cats = list(ProductCategory.objects.all()[:2]) - cat_a, cat_b = cats[0], cats[1] - - # Reset - cat_a.parent = None - cat_b.parent = None - cat_a.save() - cat_b.save() - - # Create normal hierarchy A -> B - cat_b.parent = cat_a - cat_b.save() - print(f"[PASS] Normal hierarchy created: {cat_a.name} -> {cat_b.name}") - - # Cleanup - cat_b.parent = None - cat_b.save() - -except ValidationError as e: - print("[FAIL] Normal operation was blocked!") - print(f"Error: {e}") - -print("\n" + "="*60) -print("ALL TESTS COMPLETED") -print("="*60) diff --git a/myproject/test_manager_fix.py b/myproject/test_manager_fix.py deleted file mode 100644 index 8f03d77..0000000 --- a/myproject/test_manager_fix.py +++ /dev/null @@ -1,119 +0,0 @@ -#!/usr/bin/env python -""" -Test script to verify that the manager fix works correctly. -Deleted products should be hidden from Product.objects (default manager). -""" -import os -import sys -import django - -sys.path.insert(0, '/c/Users/team_/Desktop/test_qwen/myproject') -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings') -django.setup() - -from products.models import Product, ProductKit, ProductCategory -from django.utils import timezone -from django.contrib.auth.models import User - -print("=" * 70) -print("TESTING MANAGER FIX - DELETED ITEMS VISIBILITY") -print("=" * 70) - -# Get counts before -print("\n1. BEFORE SOFT DELETE:") -print("-" * 70) -products_before = Product.objects.count() -kits_before = ProductKit.objects.count() -categories_before = ProductCategory.objects.count() - -print(f" Products (objects): {products_before}") -print(f" Kits (objects): {kits_before}") -print(f" Categories (objects): {categories_before}") - -print(f"\n Products (all_objects): {Product.all_objects.count()}") -print(f" Kits (all_objects): {ProductKit.all_objects.count()}") -print(f" Categories (all_objects): {ProductCategory.all_objects.count()}") - -# Get a product to soft delete -try: - product_to_delete = Product.objects.first() - if not product_to_delete: - print("\n❌ No products found to test with!") - else: - product_id = product_to_delete.pk - product_name = product_to_delete.name - - print(f"\n2. SOFT DELETING PRODUCT:") - print("-" * 70) - print(f" Product ID: {product_id}") - print(f" Product Name: {product_name}") - - # Soft delete the product - product_to_delete.is_deleted = True - product_to_delete.deleted_at = timezone.now() - try: - user = User.objects.first() - if user: - product_to_delete.deleted_by = user - except: - pass - product_to_delete.save() - print(f" ✓ Marked as is_deleted=True") - - print(f"\n3. AFTER SOFT DELETE:") - print("-" * 70) - - # Check default manager - products_after = Product.objects.count() - try: - product_in_default = Product.objects.get(pk=product_id) - print(f" ❌ ERROR: Product found in Product.objects (should be hidden!)") - print(f" Product.objects count: {products_after} (was {products_before})") - except Product.DoesNotExist: - print(f" ✓ Product correctly HIDDEN from Product.objects") - print(f" Product.objects count: {products_after} (was {products_before})") - if products_after == products_before - 1: - print(f" ✓ Count decreased by 1 ✓") - else: - print(f" ⚠ Count change unexpected: {products_before} → {products_after}") - - # Check all_objects manager - try: - product_in_all = Product.all_objects.get(pk=product_id, is_deleted=True) - print(f"\n ✓ Product found in Product.all_objects.get(..., is_deleted=True)") - print(f" Product.all_objects count: {Product.all_objects.count()}") - except Product.DoesNotExist: - print(f"\n ❌ ERROR: Product NOT found in all_objects (should be there!)") - print(f" Product.all_objects count: {Product.all_objects.count()}") - - print(f"\n4. TESTING RESTORE:") - print("-" * 70) - - # Restore the product - product_to_delete.is_deleted = False - product_to_delete.deleted_at = None - product_to_delete.deleted_by = None - product_to_delete.save() - print(f" ✓ Marked as is_deleted=False") - - # Check if it reappears - try: - product_restored = Product.objects.get(pk=product_id) - print(f" ✓ Product correctly reappears in Product.objects") - print(f" Product.objects count: {Product.objects.count()}") - except Product.DoesNotExist: - print(f" ❌ ERROR: Product not found after restore!") - print(f" Product.objects count: {Product.objects.count()}") - -except Exception as e: - print(f"\n❌ Error during test: {e}") - import traceback - traceback.print_exc() - -print("\n" + "=" * 70) -print("TEST COMPLETE") -print("=" * 70) -print("\nSUMMARY:") -print("✓ Product.objects now filters out deleted items (correct)") -print("✓ Product.all_objects still provides access to all items") -print("✓ Soft delete/restore functionality working as expected") diff --git a/myproject/test_sku_generation.py b/myproject/test_sku_generation.py deleted file mode 100644 index 4a75eb6..0000000 --- a/myproject/test_sku_generation.py +++ /dev/null @@ -1,170 +0,0 @@ -""" -Тестовый скрипт для проверки генерации артикулов -""" -import os -import django - -# Настройка Django -os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings') -django.setup() - -from products.models import Product, ProductKit, ProductCategory, ProductVariantGroup, SKUCounter - -def test_sku_generation(): - print("=" * 80) - print("ТЕСТИРОВАНИЕ СИСТЕМЫ ГЕНЕРАЦИИ АРТИКУЛОВ") - print("=" * 80) - - # Проверяем текущее состояние счетчиков - print("\n1. Текущее состояние счетчиков:") - print("-" * 80) - for counter in SKUCounter.objects.all(): - print(f" {counter.get_counter_type_display()}: {counter.current_value}") - - if not SKUCounter.objects.exists(): - print(" Счетчики еще не созданы. Они будут созданы при первом создании товара.") - - # Создаем тестовую категорию - print("\n2. Создание тестовой категории:") - print("-" * 80) - category, created = ProductCategory.objects.get_or_create( - name="Розы", - defaults={'slug': 'rozy'} - ) - print(f" Категория 'Розы' {'создана' if created else 'уже существует'}") - - # Тест 1: Простой товар без группы вариантов - print("\n3. Тест 1: Простой товар без суффикса:") - print("-" * 80) - product1 = Product( - name="Роза красная", - category=category, - cost_price=100, - sale_price=200 - ) - product1.save() - print(f" Товар: {product1.name}") - print(f" Артикул: {product1.sku}") - print(f" Суффикс варианта: {product1.variant_suffix or 'нет'}") - - # Тест 2: Товар с размером в названии - print("\n4. Тест 2: Товар с размером в названии (автопарсинг):") - print("-" * 80) - product2 = Product( - name="Роза Freedom 50см", - category=category, - cost_price=150, - sale_price=300 - ) - product2.save() - print(f" Товар: {product2.name}") - print(f" Артикул: {product2.sku}") - print(f" Суффикс варианта: {product2.variant_suffix or 'нет'}") - - # Тест 3: Товар с другим размером - print("\n5. Тест 3: Товар с другим размером:") - print("-" * 80) - product3 = Product( - name="Роза Freedom 60 см", - category=category, - cost_price=180, - sale_price=350 - ) - product3.save() - print(f" Товар: {product3.name}") - print(f" Артикул: {product3.sku}") - print(f" Суффикс варианта: {product3.variant_suffix or 'нет'}") - - # Тест 4: Товар с буквенным размером - print("\n6. Тест 4: Товар с буквенным размером:") - print("-" * 80) - product4 = Product( - name="Коробка подарочная размер M", - category=category, - cost_price=50, - sale_price=100 - ) - product4.save() - print(f" Товар: {product4.name}") - print(f" Артикул: {product4.sku}") - print(f" Суффикс варианта: {product4.variant_suffix or 'нет'}") - - # Тест 5: Товар с ручным указанием суффикса - print("\n7. Тест 5: Товар с ручным указанием суффикса:") - print("-" * 80) - product5 = Product( - name="Лента атласная красная", - category=category, - cost_price=20, - sale_price=40, - variant_suffix="RED" - ) - product5.save() - print(f" Товар: {product5.name}") - print(f" Артикул: {product5.sku}") - print(f" Суффикс варианта: {product5.variant_suffix or 'нет'}") - - # Тест 6: Комплект - print("\n8. Тест 6: Комплект (букет):") - print("-" * 80) - kit1 = ProductKit( - name="Букет Романтика", - slug="buket-romantika", - category=category, - pricing_method='fixed', - fixed_price=1500 - ) - kit1.save() - print(f" Комплект: {kit1.name}") - print(f" Артикул: {kit1.sku}") - - # Тест 7: Еще один комплект - print("\n9. Тест 7: Еще один комплект:") - print("-" * 80) - kit2 = ProductKit( - name="Букет Весна", - slug="buket-vesna", - category=category, - pricing_method='fixed', - fixed_price=2000 - ) - kit2.save() - print(f" Комплект: {kit2.name}") - print(f" Артикул: {kit2.sku}") - - # Проверяем финальное состояние счетчиков - print("\n10. Финальное состояние счетчиков:") - print("-" * 80) - for counter in SKUCounter.objects.all(): - print(f" {counter.get_counter_type_display()}: {counter.current_value}") - - # Показываем все созданные товары - print("\n11. Все созданные тестовые товары:") - print("-" * 80) - print(f" {'Название':<40} {'Артикул':<20} {'Суффикс':<10}") - print(" " + "-" * 70) - for p in Product.objects.filter(name__startswith=('Роза', 'Коробка', 'Лента')): - print(f" {p.name:<40} {p.sku:<20} {p.variant_suffix or '-':<10}") - - print("\n12. Все созданные тестовые комплекты:") - print("-" * 80) - print(f" {'Название':<40} {'Артикул':<20}") - print(" " + "-" * 60) - for k in ProductKit.objects.filter(name__startswith='Букет'): - print(f" {k.name:<40} {k.sku:<20}") - - print("\n" + "=" * 80) - print("ТЕСТИРОВАНИЕ ЗАВЕРШЕНО!") - print("=" * 80) - - # Предложение удалить тестовые данные - print("\nВнимание: Тестовые данные НЕ удалены автоматически.") - print("Чтобы удалить их, выполните:") - print(" python manage.py shell") - print(" from products.models import Product, ProductKit, ProductCategory") - print(" Product.objects.filter(name__startswith=('Роза', 'Коробка', 'Лента')).delete()") - print(" ProductKit.objects.filter(name__startswith='Букет').delete()") - print(" ProductCategory.objects.filter(name='Розы').delete()") - -if __name__ == '__main__': - test_sku_generation()