feat(discounts): добавлено комбинирование скидок по режимам
Добавлено поле combine_mode с тремя режимами: - stack - складывать с другими скидками - max_only - применять только максимальную - exclusive - отменяет все остальные скидки Изменения: - Модель Discount: добавлено поле combine_mode - Calculator: новый класс DiscountCombiner, методы возвращают списки скидок - Applier: создание нескольких DiscountApplication записей - Admin: отображение combine_mode с иконками - POS API: возвращает списки применённых скидок - POS UI: отображение нескольких скидок с иконками режимов Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1403,6 +1403,7 @@ def pos_checkout(request):
|
||||
"notes": str (optional),
|
||||
"promo_code": str (optional) - Промокод для скидки
|
||||
"manual_discount_id": int (optional) - ID выбранной вручную скидки
|
||||
"custom_discount": dict (optional) - Произвольная скидка {"value": float, "is_percent": bool}
|
||||
}
|
||||
"""
|
||||
from orders.models import Order, OrderItem, OrderStatus
|
||||
@@ -1425,6 +1426,7 @@ def pos_checkout(request):
|
||||
order_notes = body.get('notes', '')
|
||||
promo_code = body.get('promo_code') # Промокод для скидки
|
||||
manual_discount_id = body.get('manual_discount_id') # ID выбранной вручную скидки
|
||||
custom_discount = body.get('custom_discount') # Произвольная скидка {"value": float, "is_percent": bool}
|
||||
|
||||
if not customer_id:
|
||||
return JsonResponse({'success': False, 'error': 'Не указан клиент'}, status=400)
|
||||
@@ -1556,7 +1558,38 @@ def pos_checkout(request):
|
||||
order.calculate_total()
|
||||
|
||||
# 4. Применяем скидки
|
||||
if manual_discount_id:
|
||||
if custom_discount:
|
||||
# Применяем произвольную скидку
|
||||
from decimal import Decimal
|
||||
discount_value = Decimal(str(custom_discount.get('value', 0)))
|
||||
is_percent = custom_discount.get('is_percent', False)
|
||||
|
||||
if is_percent:
|
||||
# Процентная скидка
|
||||
discount_amount = order.subtotal * discount_value / 100
|
||||
discount_name = f'Произвольная скидка {discount_value}%'
|
||||
else:
|
||||
# Фиксированная скидка
|
||||
discount_amount = min(discount_value, order.subtotal)
|
||||
discount_name = f'Произвольная скидка {discount_value} руб.'
|
||||
|
||||
order.discount_amount = discount_amount
|
||||
order.applied_promo_code = discount_name # Сохраняем название в поле промокода
|
||||
order.save(update_fields=['discount_amount', 'applied_promo_code'])
|
||||
order.calculate_total()
|
||||
|
||||
# Создаем запись о применении в истории скидок
|
||||
from discounts.models import DiscountApplication
|
||||
DiscountApplication.objects.create(
|
||||
order=order,
|
||||
target='order',
|
||||
base_amount=order.subtotal,
|
||||
discount_amount=discount_amount,
|
||||
final_amount=order.subtotal - discount_amount,
|
||||
customer=customer,
|
||||
applied_by=request.user
|
||||
)
|
||||
elif manual_discount_id:
|
||||
from discounts.services.applier import DiscountApplier
|
||||
from discounts.models import Discount
|
||||
|
||||
@@ -1762,22 +1795,40 @@ def validate_promo_code(request):
|
||||
def calculate_cart_discounts(request):
|
||||
"""
|
||||
Рассчитать скидки для корзины POS.
|
||||
Поддерживает комбинирование скидок по combine_mode.
|
||||
|
||||
Payload JSON:
|
||||
{
|
||||
'items': [...],
|
||||
'promo_code': str (optional),
|
||||
'customer_id': int (optional)
|
||||
'customer_id': int (optional),
|
||||
'manual_discount_id': int (optional),
|
||||
'skip_auto_discount': bool (optional)
|
||||
}
|
||||
|
||||
Returns JSON:
|
||||
{
|
||||
'success': true,
|
||||
'cart_subtotal': float,
|
||||
'order_discount': {...},
|
||||
'item_discounts': [...],
|
||||
'order_discounts': [
|
||||
{'discount_id': int, 'discount_name': str, 'discount_amount': float, 'combine_mode': str},
|
||||
...
|
||||
],
|
||||
'total_order_discount': float,
|
||||
'item_discounts': [
|
||||
{
|
||||
'cart_index': int,
|
||||
'discounts': [
|
||||
{'discount_id': int, 'discount_name': str, 'discount_amount': float, 'combine_mode': str},
|
||||
...
|
||||
],
|
||||
'total_discount': float
|
||||
},
|
||||
...
|
||||
],
|
||||
'total_discount': float,
|
||||
'final_total': float
|
||||
'final_total': float,
|
||||
'excluded_by': {'id': int, 'name': str} или None
|
||||
}
|
||||
"""
|
||||
from discounts.services.calculator import DiscountCalculator
|
||||
@@ -1788,6 +1839,7 @@ def calculate_cart_discounts(request):
|
||||
items_data = data.get('items', [])
|
||||
promo_code = data.get('promo_code')
|
||||
customer_id = data.get('customer_id')
|
||||
skip_auto_discount = data.get('skip_auto_discount', False)
|
||||
|
||||
customer = None
|
||||
if customer_id:
|
||||
@@ -1797,32 +1849,45 @@ def calculate_cart_discounts(request):
|
||||
pass
|
||||
|
||||
result = DiscountCalculator.calculate_cart_discounts(
|
||||
items_data, promo_code, customer
|
||||
items_data, promo_code, customer, skip_auto_discount=skip_auto_discount
|
||||
)
|
||||
|
||||
cart_subtotal = Decimal('0')
|
||||
for item in items_data:
|
||||
cart_subtotal += Decimal(str(item['price'])) * Decimal(str(item['quantity']))
|
||||
# Форматируем item_discounts для JSON (Decimal -> float)
|
||||
formatted_item_discounts = []
|
||||
for item in result['item_discounts']:
|
||||
formatted_discounts = []
|
||||
for disc in item['discounts']:
|
||||
formatted_discounts.append({
|
||||
'discount_id': disc['discount_id'],
|
||||
'discount_name': disc['discount_name'],
|
||||
'discount_amount': float(disc['discount_amount']),
|
||||
'combine_mode': disc['combine_mode']
|
||||
})
|
||||
formatted_item_discounts.append({
|
||||
'cart_index': item['cart_index'],
|
||||
'discounts': formatted_discounts,
|
||||
'total_discount': float(item['total_discount'])
|
||||
})
|
||||
|
||||
# Форматируем order_discounts для JSON
|
||||
formatted_order_discounts = []
|
||||
for disc in result['order_discounts']:
|
||||
formatted_order_discounts.append({
|
||||
'discount_id': disc['discount_id'],
|
||||
'discount_name': disc['discount_name'],
|
||||
'discount_amount': float(disc['discount_amount']),
|
||||
'combine_mode': disc['combine_mode']
|
||||
})
|
||||
|
||||
response_data = {
|
||||
'success': True,
|
||||
'cart_subtotal': float(cart_subtotal),
|
||||
'order_discount': {
|
||||
'discount_id': result['order_discount']['discount'].id if result['order_discount'].get('discount') else None,
|
||||
'discount_name': result['order_discount']['discount'].name if result['order_discount'].get('discount') else None,
|
||||
'discount_amount': float(result['order_discount']['discount_amount']),
|
||||
'error': result['order_discount'].get('error'),
|
||||
} if result['order_discount'] else None,
|
||||
'item_discounts': [
|
||||
{
|
||||
'cart_index': i['cart_index'],
|
||||
'discount_id': i['discount'].id,
|
||||
'discount_name': i['discount'].name,
|
||||
'discount_amount': float(i['discount_amount']),
|
||||
} for i in result['item_discounts']
|
||||
],
|
||||
'cart_subtotal': float(result['cart_subtotal']),
|
||||
'order_discounts': formatted_order_discounts,
|
||||
'total_order_discount': float(result['total_order_discount']),
|
||||
'item_discounts': formatted_item_discounts,
|
||||
'total_discount': float(result['total_discount']),
|
||||
'final_total': float(result['final_total']),
|
||||
'excluded_by': result.get('excluded_by')
|
||||
}
|
||||
|
||||
return JsonResponse(response_data)
|
||||
@@ -1881,7 +1946,8 @@ def get_available_discounts(request):
|
||||
'name': d.name,
|
||||
'discount_type': d.discount_type,
|
||||
'value': float(d.value),
|
||||
'min_order_amount': float(d.min_order_amount) if d.min_order_amount else None
|
||||
'min_order_amount': float(d.min_order_amount) if d.min_order_amount else None,
|
||||
'combine_mode': d.combine_mode
|
||||
})
|
||||
|
||||
# Получаем автоматическую скидку (только одну для отображения)
|
||||
|
||||
Reference in New Issue
Block a user