feat(discounts, orders): рефакторинг системы скидок - единый источник правды

- Добавлен combine_mode в форму создания/редактирования скидок
- Добавлена колонка "Объединение" в список скидок с иконками
- Добавлен фильтр по режиму объединения скидок
- Добавлена валидация: только одна exclusive скидка на заказ
- Удалены дублирующие поля из Order и OrderItem:
  - applied_discount, applied_promo_code, discount_amount
- Скидки теперь хранятся только в DiscountApplication
- Добавлены свойства для обратной совместимости

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-11 13:46:02 +03:00
parent cd758a0645
commit c070e42cab
9 changed files with 192 additions and 112 deletions

View File

@@ -82,22 +82,10 @@ class OrderItem(models.Model):
help_text="True если цена была изменена вручную при создании заказа"
)
# Скидки
applied_discount = models.ForeignKey(
'discounts.Discount',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='order_items',
verbose_name="Скидка на позицию"
)
discount_amount = models.DecimalField(
max_digits=10,
decimal_places=2,
default=0,
verbose_name="Сумма скидки"
)
# === СКИДКИ ===
# Скидки хранятся в модели DiscountApplication (через related_name='discount_applications')
# Старые поля applied_discount, discount_amount УДАЛЕНЫ
# Используйте свойства ниже для доступа к скидкам
# Витринные продажи
is_from_showcase = models.BooleanField(
@@ -254,3 +242,23 @@ class OrderItem(models.Model):
if self.is_custom_price and self.original_price:
return self.price - self.original_price
return None
# === Свойства для доступа к скидкам (через DiscountApplication) ===
@property
def item_discounts(self):
"""Скидки на эту позицию (QuerySet DiscountApplication)"""
return self.discount_applications.filter(target='order_item').select_related('discount')
@property
def discount_amount(self):
"""Общая сумма скидки на позицию"""
from django.db.models import Sum
total = self.item_discounts.aggregate(total=Sum('discount_amount'))['total']
return total if total else Decimal('0')
@property
def applied_discount(self):
"""Первая применённая скидка (для обратной совместимости)"""
first = self.item_discounts.first()
return first.discount if first else None