Обновили шапку и вывод всехтоваров. Добавили фильтры
This commit is contained in:
@@ -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.
|
|
||||||
|
|
||||||
## Поддержка
|
|
||||||
|
|
||||||
При возникновении проблем или вопросов обратитесь к разработчикам проекта.
|
|
||||||
@@ -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.contrib.auth.validators
|
||||||
import django.utils.timezone
|
import django.utils.timezone
|
||||||
|
|||||||
@@ -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 django.db.models.deletion
|
||||||
import phonenumber_field.modelfields
|
import phonenumber_field.modelfields
|
||||||
@@ -10,7 +10,6 @@ class Migration(migrations.Migration):
|
|||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('products', '0001_initial'),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
@@ -18,26 +17,20 @@ class Migration(migrations.Migration):
|
|||||||
name='Customer',
|
name='Customer',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('first_name', models.CharField(max_length=100, verbose_name='Имя')),
|
('name', models.CharField(blank=True, max_length=200, verbose_name='Имя')),
|
||||||
('last_name', models.CharField(max_length=100, verbose_name='Фамилия')),
|
('email', models.EmailField(blank=True, max_length=254, verbose_name='Email')),
|
||||||
('email', models.EmailField(max_length=254, unique=True, 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='Телефон')),
|
||||||
('phone', phonenumber_field.modelfields.PhoneNumberField(blank=True, help_text='Телефон в международном формате (например, +375291234567)', max_length=128, null=True, region=None, verbose_name='Телефон')),
|
('loyalty_tier', models.CharField(choices=[('no_discount', 'Без скидки'), ('bronze', 'Бронза'), ('silver', 'Серебро'), ('gold', 'Золото'), ('platinum', 'Платина')], default='no_discount', max_length=20, 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='Уровень лояльности')),
|
|
||||||
('total_spent', models.DecimalField(decimal_places=2, default=0, max_digits=10, 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='Заметки')),
|
('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='Дата создания')),
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
|
||||||
('updated_at', models.DateTimeField(auto_now=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={
|
options={
|
||||||
'verbose_name': 'Клиент',
|
'verbose_name': 'Клиент',
|
||||||
'verbose_name_plural': 'Клиенты',
|
'verbose_name_plural': 'Клиенты',
|
||||||
'ordering': ['-created_at'],
|
'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(
|
migrations.CreateModel(
|
||||||
@@ -51,7 +44,6 @@ class Migration(migrations.Migration):
|
|||||||
('district', models.CharField(blank=True, help_text='Район в Минске для удобства доставки', max_length=100, null=True, verbose_name='Район')),
|
('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='Инструкции для доставки')),
|
('delivery_instructions', models.TextField(blank=True, help_text='Дополнительные инструкции для курьера (домофон, подъезд и т.д.)', null=True, verbose_name='Инструкции для доставки')),
|
||||||
('is_default', models.BooleanField(default=False, help_text='Использовать этот адрес для доставки по умолчанию', 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='Дата создания')),
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
|
||||||
('updated_at', models.DateTimeField(auto_now=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='Клиент')),
|
('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': 'Адрес доставки',
|
||||||
'verbose_name_plural': 'Адреса доставки',
|
'verbose_name_plural': 'Адреса доставки',
|
||||||
'ordering': ['-is_default', '-created_at'],
|
'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'),
|
|
||||||
),
|
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -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='Телефон'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -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',
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -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='Телефон'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -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
|
|
||||||
@@ -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()
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
import django.db.models.deletion
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|||||||
@@ -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
|
import django.db.models.deletion
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|||||||
@@ -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
|
import django.db.models.deletion
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -267,4 +267,24 @@ class Migration(migrations.Migration):
|
|||||||
model_name='product',
|
model_name='product',
|
||||||
index=models.Index(fields=['is_deleted', 'created_at'], name='products_pr_is_dele_f30efb_idx'),
|
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'),
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -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'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -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")
|
|
||||||
@@ -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)
|
|
||||||
@@ -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)
|
|
||||||
@@ -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")
|
|
||||||
@@ -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()
|
|
||||||
Reference in New Issue
Block a user