feat(pos): добавлен полноценный интерфейс скидок в модальное окно продажи

- Добавлен API endpoint /pos/api/discounts/available/ для получения списка доступных скидок
- Добавлен метод DiscountApplier.apply_manual_discount() для применения ручных скидок
- Обновлен POS checkout для обработки manual_discount_id
- Расширена секция скидок в модальном окне:
  * Отображение автоматических скидок (read-only)
  * Dropdown для выбора скидки вручную
  * Подробная детализация: подитог, общая скидка, скидки на позиции
  * Поле промокода с иконкой
- Увеличен размер модального окна и изменено соотношение колонок (5/7)
- Убрана вертикальная прокрутка из модального окна

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-11 01:59:43 +03:00
parent 6313b8f6e7
commit 42d8c34e8c
5 changed files with 389 additions and 30 deletions

View File

@@ -1402,6 +1402,7 @@ def pos_checkout(request):
],
"notes": str (optional),
"promo_code": str (optional) - Промокод для скидки
"manual_discount_id": int (optional) - ID выбранной вручную скидки
}
"""
from orders.models import Order, OrderItem, OrderStatus
@@ -1423,6 +1424,7 @@ def pos_checkout(request):
payments_data = body.get('payments', [])
order_notes = body.get('notes', '')
promo_code = body.get('promo_code') # Промокод для скидки
manual_discount_id = body.get('manual_discount_id') # ID выбранной вручную скидки
if not customer_id:
return JsonResponse({'success': False, 'error': 'Не указан клиент'}, status=400)
@@ -1554,7 +1556,22 @@ def pos_checkout(request):
order.calculate_total()
# 4. Применяем скидки
if promo_code:
if manual_discount_id:
from discounts.services.applier import DiscountApplier
from discounts.models import Discount
try:
discount = Discount.objects.get(id=manual_discount_id, is_active=True)
apply_result = DiscountApplier.apply_manual_discount(
order=order,
discount=discount,
user=request.user
)
if not apply_result['success']:
raise ValidationError(apply_result['error'])
except Discount.DoesNotExist:
pass
elif promo_code:
from discounts.services.applier import DiscountApplier
apply_result = DiscountApplier.apply_promo_code(
order=order,
@@ -1815,3 +1832,85 @@ def calculate_cart_discounts(request):
except Exception as e:
logger.error(f'Ошибка при расчете скидок: {str(e)}', exc_info=True)
return JsonResponse({'success': False, 'error': f'Ошибка: {str(e)}'}, status=500)
@login_required
@require_http_methods(["GET"])
def get_available_discounts(request):
"""
Возвращает список доступных скидок для ручного применения в POS.
Query params:
- cart_total: сумма корзины (для фильтрации по min_order_amount)
Returns JSON:
{
'success': true,
'order_discounts': [
{
'id': 1,
'name': 'Скидка 5%',
'discount_type': 'percentage',
'value': 5,
'min_order_amount': 50
},
...
],
'auto_order_discount': {...} # если есть автоскидка на заказ
}
"""
from discounts.services.calculator import DiscountCalculator
try:
cart_total = request.GET.get('cart_total', '0')
cart_total = Decimal(str(cart_total))
# Получаем НЕ автоматические скидки на заказ для ручного применения
order_discounts = DiscountCalculator.get_available_discounts(
scope='order',
auto_only=False
).filter(is_auto=False)
# Фильтруем по мин. сумме заказа
result_discounts = []
for d in order_discounts:
if d.min_order_amount and cart_total < d.min_order_amount:
continue
result_discounts.append({
'id': d.id,
'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
})
# Получаем автоматическую скидку (только одну для отображения)
auto_discounts = DiscountCalculator.get_available_discounts(
scope='order',
auto_only=True
)
auto_discount_data = None
for d in auto_discounts:
if d.min_order_amount and cart_total < d.min_order_amount:
continue
# Рассчитываем сумму
discount_amount = d.calculate_discount_amount(cart_total)
auto_discount_data = {
'id': d.id,
'name': d.name,
'discount_type': d.discount_type,
'value': float(d.value),
'discount_amount': float(discount_amount)
}
break
return JsonResponse({
'success': True,
'order_discounts': result_discounts,
'auto_order_discount': auto_discount_data
})
except Exception as e:
logger.error(f'Ошибка при получении скидок: {str(e)}', exc_info=True)
return JsonResponse({'success': False, 'error': f'Ошибка: {str(e)}'}, status=500)