diff --git a/myproject/discounts/services/applier.py b/myproject/discounts/services/applier.py index b6bbefe..5bd0574 100644 --- a/myproject/discounts/services/applier.py +++ b/myproject/discounts/services/applier.py @@ -214,6 +214,69 @@ class DiscountApplier: # Пересчитываем order.calculate_total() + @staticmethod + @transaction.atomic + def apply_manual_discount(order, discount, user=None): + """ + Применить скидку вручную к заказу. + + Args: + order: Order + discount: Discount + user: CustomUser (применивший скидку) + + Returns: + dict: { + 'success': bool, + 'discount_amount': Decimal, + 'error': str + } + """ + from discounts.models import DiscountApplication + + # Проверяем scope скидки + if discount.scope != 'order': + return {'success': False, 'error': 'Эта скидка не применяется к заказу'} + + # Проверяем мин. сумму + if discount.min_order_amount and order.subtotal < discount.min_order_amount: + return {'success': False, 'error': f'Мин. сумма заказа: {discount.min_order_amount} руб.'} + + # Удаляем предыдущую скидку на заказ + DiscountApplier._remove_order_discount_only(order) + + # Рассчитываем сумму + discount_amount = discount.calculate_discount_amount(Decimal(order.subtotal)) + + # Применяем к заказу + order.applied_discount = discount + order.discount_amount = discount_amount + order.save(update_fields=['applied_discount', 'discount_amount']) + + # Пересчитываем total_amount + order.calculate_total() + + # Создаем запись о применении + DiscountApplication.objects.create( + order=order, + discount=discount, + target='order', + base_amount=order.subtotal, + discount_amount=discount_amount, + final_amount=order.subtotal - discount_amount, + customer=order.customer, + applied_by=user + ) + + # Увеличиваем счетчик использований скидки + discount.current_usage_count += 1 + discount.save(update_fields=['current_usage_count']) + + return { + 'success': True, + 'discount_amount': discount_amount + } + @staticmethod def _remove_order_discount_only(order): """ diff --git a/myproject/pos/static/pos/js/terminal.js b/myproject/pos/static/pos/js/terminal.js index 23cfdac..ffc47c3 100644 --- a/myproject/pos/static/pos/js/terminal.js +++ b/myproject/pos/static/pos/js/terminal.js @@ -2294,6 +2294,8 @@ let paymentWidget = null; // Переменные состояния скидок let appliedPromoCode = null; // примененный промокод +let appliedManualDiscount = null; // выбранная вручную скидка +let availableDiscounts = []; // список доступных скидок let cartDiscounts = { orderDiscount: null, // скидка на заказ itemDiscounts: [], // скидки на позиции @@ -2326,6 +2328,9 @@ document.getElementById('checkoutModal').addEventListener('show.bs.modal', async // Проверяем автоматические скидки await checkAutoDiscounts(); + // Загружаем доступные скидки для ручного выбора + await loadAvailableDiscounts(); + // Применяем скидки к итоговой сумме const finalTotal = Math.max(0, totalAmount - cartDiscounts.totalDiscount); document.getElementById('checkoutFinalPrice').textContent = formatMoney(finalTotal) + ' руб.'; @@ -2366,6 +2371,8 @@ function reinitPaymentWidget(mode) { // Сброс скидок function resetDiscounts() { appliedPromoCode = null; + appliedManualDiscount = null; + availableDiscounts = []; cartDiscounts = { orderDiscount: null, itemDiscounts: [], @@ -2379,8 +2386,13 @@ function resetDiscounts() { document.getElementById('promoCodeSuccess').style.display = 'none'; document.getElementById('promoCodeSuccess').textContent = ''; document.getElementById('removePromoBtn').style.display = 'none'; - document.getElementById('autoDiscounts').style.display = 'none'; - document.getElementById('autoDiscountsText').textContent = ''; + + // Новые элементы UI + document.getElementById('autoDiscountsContainer').style.display = 'none'; + document.getElementById('autoDiscountsList').innerHTML = ''; + document.getElementById('manualDiscountContainer').style.display = 'none'; + document.getElementById('discountsSummary').style.display = 'none'; + document.getElementById('itemDiscountsBreakdown').innerHTML = ''; } // Проверить автоматические скидки @@ -2403,29 +2415,168 @@ async function checkAutoDiscounts() { }, body: JSON.stringify({ items: items, - customer_id: customer.id !== SYSTEM_CUSTOMER.id ? customer.id : null + customer_id: customer.id !== SYSTEM_CUSTOMER.id ? customer.id : null, + manual_discount_id: appliedManualDiscount?.id || null }) }); const result = await response.json(); if (result.success) { - // Сохраняем скидки cartDiscounts.totalDiscount = result.total_discount || 0; cartDiscounts.orderDiscount = result.order_discount; + cartDiscounts.itemDiscounts = result.item_discounts || []; - // Показываем автоматические скидки - if (result.order_discount && result.order_discount.discount_id) { - document.getElementById('autoDiscounts').style.display = 'block'; - document.getElementById('autoDiscountsText').textContent = - `${result.order_discount.discount_name}: -${result.order_discount.discount_amount.toFixed(2)} руб.`; - } + updateDiscountsUI(result); } } catch (error) { console.error('Ошибка при проверке автоматических скидок:', error); } } +// Обновить UI скидок +function updateDiscountsUI(result) { + const autoContainer = document.getElementById('autoDiscountsContainer'); + const autoList = document.getElementById('autoDiscountsList'); + const summaryDiv = document.getElementById('discountsSummary'); + const itemBreakdown = document.getElementById('itemDiscountsBreakdown'); + + // Очистка + autoList.innerHTML = ''; + itemBreakdown.innerHTML = ''; + + let hasDiscounts = false; + + // 1. Скидка на заказ (автоматическая) + if (result.order_discount && result.order_discount.discount_id) { + hasDiscounts = true; + autoContainer.style.display = 'block'; + const div = document.createElement('div'); + div.className = 'd-flex justify-content-between'; + div.innerHTML = ` + ${result.order_discount.discount_name} + -${result.order_discount.discount_amount.toFixed(2)} руб. + `; + autoList.appendChild(div); + } else { + autoContainer.style.display = 'none'; + } + + // 2. Скидки на позиции + if (result.item_discounts && result.item_discounts.length > 0) { + hasDiscounts = true; + result.item_discounts.forEach(item => { + const div = document.createElement('div'); + div.className = 'text-muted'; + div.innerHTML = `• ${item.discount_name}: -${item.discount_amount.toFixed(2)} руб.`; + itemBreakdown.appendChild(div); + }); + } + + // 3. Ручная скидка + if (appliedManualDiscount) { + hasDiscounts = true; + document.getElementById('manualDiscountContainer').style.display = 'block'; + document.getElementById('manualDiscountName').textContent = appliedManualDiscount.name; + document.getElementById('manualDiscountAmount').textContent = + `-${appliedManualDiscount.amount.toFixed(2)} руб.`; + } else { + document.getElementById('manualDiscountContainer').style.display = 'none'; + } + + // Показываем/скрываем summary + if (hasDiscounts) { + summaryDiv.style.display = 'block'; + document.getElementById('discountsSubtotal').textContent = + (result.cart_subtotal || 0).toFixed(2) + ' руб.'; + document.getElementById('discountsTotalDiscount').textContent = + '-' + (result.total_discount || 0).toFixed(2) + ' руб.'; + } else { + summaryDiv.style.display = 'none'; + } + + // Обновляем итоговую цену + const finalTotal = Math.max(0, (result.cart_subtotal || 0) - (result.total_discount || 0)); + document.getElementById('checkoutFinalPrice').textContent = formatMoney(finalTotal) + ' руб.'; + + // Пересчитываем виджет оплаты + reinitPaymentWidget(document.getElementById('singlePaymentMode').classList.contains('active') ? 'single' : 'mixed'); +} + +// Загрузить доступные скидки +async function loadAvailableDiscounts() { + try { + let cartTotal = 0; + cart.forEach((item) => { + cartTotal += item.qty * item.price; + }); + + const response = await fetch(`/pos/api/discounts/available/?cart_total=${cartTotal}`); + const result = await response.json(); + + if (result.success) { + availableDiscounts = result.order_discounts; + renderDiscountsDropdown(result.order_discounts); + } + } catch (error) { + console.error('Ошибка загрузки скидок:', error); + } +} + +// Отобразить список скидок в dropdown +function renderDiscountsDropdown(discounts) { + const list = document.getElementById('discountsDropdownList'); + list.innerHTML = ''; + + if (discounts.length === 0) { + list.innerHTML = '