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

@@ -69,29 +69,10 @@ class Order(models.Model):
help_text="Общая сумма заказа"
)
# Скидки
applied_discount = models.ForeignKey(
'discounts.Discount',
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='orders',
verbose_name="Примененная скидка"
)
discount_amount = models.DecimalField(
max_digits=10,
decimal_places=2,
default=0,
verbose_name="Сумма скидки"
)
applied_promo_code = models.CharField(
max_length=50,
blank=True,
null=True,
verbose_name="Использованный промокод"
)
# === СКИДКИ ===
# Скидки хранятся в модели DiscountApplication (через related_name='discount_applications')
# Старые поля applied_discount, discount_amount, applied_promo_code УДАЛЕНЫ
# Используйте свойства ниже для доступа к скидкам
# Частичная оплата
amount_paid = models.DecimalField(
@@ -407,6 +388,7 @@ class Order(models.Model):
"""
Пересчитывает итоговую сумму заказа.
total_amount = subtotal + delivery_cost - discount_amount
Скидка берётся из DiscountApplication
"""
from decimal import Decimal
@@ -417,7 +399,7 @@ class Order(models.Model):
if hasattr(self, 'delivery'):
delivery_cost = self.delivery.cost
# Вычитаем скидку на весь заказ (если есть)
# Вычитаем скидку на весь заказ (из DiscountApplication через свойство)
order_discount = Decimal(str(self.discount_amount)) if self.discount_amount else Decimal('0')
self.total_amount = subtotal + delivery_cost - order_discount
@@ -432,6 +414,42 @@ class Order(models.Model):
self.delivery.cost = 0
self.delivery.save(update_fields=['cost'])
# === Свойства для доступа к скидкам (через DiscountApplication) ===
@property
def order_discounts(self):
"""Скидки на заказ (QuerySet DiscountApplication с target='order')"""
from discounts.models import DiscountApplication
return self.discount_applications.filter(target='order').select_related('discount', 'promo_code')
@property
def item_discounts(self):
"""Скидки на позиции (QuerySet DiscountApplication с target='order_item')"""
from discounts.models import DiscountApplication
return self.discount_applications.filter(target='order_item').select_related('discount', 'order_item')
@property
def discount_amount(self):
"""Общая сумма скидки на заказ"""
from decimal import Decimal
return self.order_discounts.aggregate(
total=models.Sum('discount_amount')
)['total'] or Decimal('0')
@property
def applied_discount(self):
"""Первая применённая скидка (для обратной совместимости)"""
first = self.order_discounts.first()
return first.discount if first else None
@property
def applied_promo_code(self):
"""Промокод из первой записи (для обратной совместимости)"""
first = self.order_discounts.first()
if first and first.promo_code:
return first.promo_code.code
return None
# === Свойства обратной совместимости для доступа к полям доставки ===
# Эти свойства обеспечивают доступ к полям Delivery через Order для обратной совместимости
# после рефакторинга, когда поля доставки были перенесены в отдельную модель Delivery

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