feat(discounts): добавлено приложение скидок
Создано новое Django приложение для управления скидками: Модели: - BaseDiscount: абстрактный базовый класс с общими полями - Discount: основная модель скидки (процент/фикс, на заказ/товар/категорию) - PromoCode: промокоды для активации скидок - DiscountApplication: история применения скидок Сервисы: - DiscountCalculator: расчёт скидок для корзины и заказов - DiscountApplier: применение скидок к заказам (атомарно) - DiscountValidator: валидация промокодов и условий Админ-панель: - DiscountAdmin: управление скидками - PromoCodeAdmin: управление промокодами - DiscountApplicationAdmin: история применения (только чтение) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
132
myproject/discounts/migrations/0001_initial.py
Normal file
132
myproject/discounts/migrations/0001_initial.py
Normal file
@@ -0,0 +1,132 @@
|
||||
# Generated by Django 5.0.10 on 2026-01-10 21:07
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0002_remove_customuser_first_name_and_more'),
|
||||
('customers', '0002_initial'),
|
||||
('orders', '0002_initial'),
|
||||
('products', '0002_alter_configurableproduct_archived_by_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Discount',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(max_length=200, verbose_name='Название скидки')),
|
||||
('description', models.TextField(blank=True, verbose_name='Описание')),
|
||||
('discount_type', models.CharField(choices=[('percentage', 'Процент'), ('fixed_amount', 'Фиксированная сумма')], max_length=20, verbose_name='Тип скидки')),
|
||||
('value', models.DecimalField(decimal_places=2, help_text='Процент (0-100) или сумма в рублях', max_digits=10, verbose_name='Значение')),
|
||||
('scope', models.CharField(choices=[('order', 'На весь заказ'), ('product', 'На товар'), ('category', 'На категорию товаров')], default='order', max_length=20, verbose_name='Уровень применения')),
|
||||
('is_active', models.BooleanField(db_index=True, default=True, verbose_name='Активна')),
|
||||
('start_date', models.DateTimeField(blank=True, null=True, verbose_name='Дата начала действия')),
|
||||
('end_date', models.DateTimeField(blank=True, null=True, verbose_name='Дата окончания действия')),
|
||||
('max_usage_count', models.PositiveIntegerField(blank=True, help_text='Оставьте пустым для безлимитного использования', null=True, verbose_name='Макс. количество использований')),
|
||||
('current_usage_count', models.PositiveIntegerField(default=0, verbose_name='Текущее количество использований')),
|
||||
('priority', models.PositiveIntegerField(default=0, help_text='Более высокий приоритет применяется первым', verbose_name='Приоритет')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
|
||||
('min_order_amount', models.DecimalField(blank=True, decimal_places=2, help_text='Скидка применяется только если сумма заказа >= этого значения', max_digits=10, null=True, verbose_name='Мин. сумма заказа')),
|
||||
('is_auto', models.BooleanField(default=False, help_text='Применяется автоматически при выполнении условий', verbose_name='Автоматическая')),
|
||||
('categories', models.ManyToManyField(blank=True, related_name='discounts', to='products.productcategory', verbose_name='Категории')),
|
||||
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_discounts', to='accounts.customuser', verbose_name='Создал')),
|
||||
('excluded_products', models.ManyToManyField(blank=True, related_name='excluded_from_discounts', to='products.product', verbose_name='Исключенные товары')),
|
||||
('products', models.ManyToManyField(blank=True, related_name='discounts', to='products.product', verbose_name='Товары')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Скидка',
|
||||
'verbose_name_plural': 'Скидки',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='PromoCode',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('code', models.CharField(help_text='Уникальный код (например: SALE2025, WINTER10)', max_length=50, unique=True, verbose_name='Код промокода')),
|
||||
('max_uses_per_user', models.PositiveIntegerField(blank=True, help_text='Оставьте пустым для безлимитного использования', null=True, verbose_name='Макс. использований на клиента')),
|
||||
('max_total_uses', models.PositiveIntegerField(blank=True, null=True, verbose_name='Макс. общее количество использований')),
|
||||
('current_uses', models.PositiveIntegerField(default=0, verbose_name='Текущее количество использований')),
|
||||
('is_active', models.BooleanField(default=True, verbose_name='Активен')),
|
||||
('start_date', models.DateTimeField(blank=True, null=True, verbose_name='Дата начала действия')),
|
||||
('end_date', models.DateTimeField(blank=True, null=True, verbose_name='Дата окончания действия')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
|
||||
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_promo_codes', to='accounts.customuser', verbose_name='Создал')),
|
||||
('discount', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='promo_codes', to='discounts.discount', verbose_name='Скидка')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Промокод',
|
||||
'verbose_name_plural': 'Промокоды',
|
||||
},
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='DiscountApplication',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('target', models.CharField(choices=[('order', 'Заказ'), ('order_item', 'Позиция заказа')], max_length=20, verbose_name='Объект применения')),
|
||||
('base_amount', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Базовая сумма')),
|
||||
('discount_amount', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Сумма скидки')),
|
||||
('final_amount', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Итоговая сумма')),
|
||||
('applied_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата применения')),
|
||||
('applied_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='applied_discounts', to='accounts.customuser', verbose_name='Применен пользователем')),
|
||||
('customer', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='discount_applications', to='customers.customer', verbose_name='Клиент')),
|
||||
('discount', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='applications', to='discounts.discount', verbose_name='Скидка')),
|
||||
('order', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='discount_applications', to='orders.order', verbose_name='Заказ')),
|
||||
('order_item', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='discount_applications', to='orders.orderitem', verbose_name='Позиция заказа')),
|
||||
('promo_code', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='applications', to='discounts.promocode', verbose_name='Промокод')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Применение скидки',
|
||||
'verbose_name_plural': 'Применения скидок',
|
||||
},
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='discount',
|
||||
index=models.Index(fields=['is_active'], name='discounts_d_is_acti_ae32b7_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='discount',
|
||||
index=models.Index(fields=['scope'], name='discounts_d_scope_2c30a7_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='discount',
|
||||
index=models.Index(fields=['discount_type'], name='discounts_d_discoun_f47d7f_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='discount',
|
||||
index=models.Index(fields=['is_auto'], name='discounts_d_is_auto_a4fe48_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='promocode',
|
||||
index=models.Index(fields=['code'], name='discounts_p_code_f0e5a6_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='promocode',
|
||||
index=models.Index(fields=['is_active'], name='discounts_p_is_acti_25d05d_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='discountapplication',
|
||||
index=models.Index(fields=['order'], name='discounts_d_order_i_2b0f24_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='discountapplication',
|
||||
index=models.Index(fields=['discount'], name='discounts_d_discoun_c0cd4d_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='discountapplication',
|
||||
index=models.Index(fields=['promo_code'], name='discounts_d_promo_c_9ce5dd_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='discountapplication',
|
||||
index=models.Index(fields=['customer'], name='discounts_d_custome_d57e7c_idx'),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='discountapplication',
|
||||
index=models.Index(fields=['applied_at'], name='discounts_d_applied_96adbb_idx'),
|
||||
),
|
||||
]
|
||||
Reference in New Issue
Block a user