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:
@@ -2294,12 +2294,15 @@ let paymentWidget = null;
|
||||
|
||||
// Переменные состояния скидок
|
||||
let appliedPromoCode = null; // примененный промокод
|
||||
let appliedManualDiscount = null; // выбранная вручную скидка
|
||||
let appliedManualDiscount = null; // выбранная вручную скидка (из списка)
|
||||
let appliedCustomDiscount = null; // произвольная скидка {value: number, isPercent: boolean}
|
||||
let availableDiscounts = []; // список доступных скидок
|
||||
let skipAutoDiscount = false; // флаг отмены авто-скидки
|
||||
let cartDiscounts = {
|
||||
orderDiscount: null, // скидка на заказ
|
||||
orderDiscounts: [], // скидки на заказ (теперь массив)
|
||||
itemDiscounts: [], // скидки на позиции
|
||||
totalDiscount: 0 // общая сумма скидки
|
||||
totalDiscount: 0, // общая сумма скидки
|
||||
excludedBy: null // исключающая скидка
|
||||
};
|
||||
|
||||
// При открытии модалки checkout
|
||||
@@ -2372,7 +2375,9 @@ function reinitPaymentWidget(mode) {
|
||||
function resetDiscounts() {
|
||||
appliedPromoCode = null;
|
||||
appliedManualDiscount = null;
|
||||
appliedCustomDiscount = null;
|
||||
availableDiscounts = [];
|
||||
skipAutoDiscount = false;
|
||||
cartDiscounts = {
|
||||
orderDiscount: null,
|
||||
itemDiscounts: [],
|
||||
@@ -2393,6 +2398,14 @@ function resetDiscounts() {
|
||||
document.getElementById('manualDiscountContainer').style.display = 'none';
|
||||
document.getElementById('discountsSummary').style.display = 'none';
|
||||
document.getElementById('itemDiscountsBreakdown').innerHTML = '';
|
||||
|
||||
// Сбрасываем произвольную скидку
|
||||
document.getElementById('customDiscountInput').value = '';
|
||||
document.getElementById('customDiscountIsPercent').checked = true;
|
||||
document.getElementById('customDiscountError').style.display = 'none';
|
||||
document.getElementById('customDiscountError').textContent = '';
|
||||
document.getElementById('applyCustomDiscountBtn').style.display = 'inline-block';
|
||||
document.getElementById('removeCustomDiscountBtn').style.display = 'none';
|
||||
}
|
||||
|
||||
// Проверить автоматические скидки
|
||||
@@ -2416,7 +2429,8 @@ async function checkAutoDiscounts() {
|
||||
body: JSON.stringify({
|
||||
items: items,
|
||||
customer_id: customer.id !== SYSTEM_CUSTOMER.id ? customer.id : null,
|
||||
manual_discount_id: appliedManualDiscount?.id || null
|
||||
manual_discount_id: appliedManualDiscount?.id || null,
|
||||
skip_auto_discount: skipAutoDiscount
|
||||
})
|
||||
});
|
||||
|
||||
@@ -2424,8 +2438,9 @@ async function checkAutoDiscounts() {
|
||||
|
||||
if (result.success) {
|
||||
cartDiscounts.totalDiscount = result.total_discount || 0;
|
||||
cartDiscounts.orderDiscount = result.order_discount;
|
||||
cartDiscounts.orderDiscounts = result.order_discounts || [];
|
||||
cartDiscounts.itemDiscounts = result.item_discounts || [];
|
||||
cartDiscounts.excludedBy = result.excluded_by || null;
|
||||
|
||||
updateDiscountsUI(result);
|
||||
}
|
||||
@@ -2434,6 +2449,26 @@ async function checkAutoDiscounts() {
|
||||
}
|
||||
}
|
||||
|
||||
// Получить иконку для режима объединения
|
||||
function getCombineModeIcon(mode) {
|
||||
const icons = {
|
||||
'stack': '<i class="bi bi-layers" title="Складывать (суммировать)"></i>',
|
||||
'max_only': '<i class="bi bi-trophy" title="Только максимум"></i>',
|
||||
'exclusive': '<i class="bi bi-x-circle" title="Исключающая (отменяет остальные)"></i>'
|
||||
};
|
||||
return icons[mode] || '';
|
||||
}
|
||||
|
||||
// Получить описание режима объединения
|
||||
function getCombineModeTitle(mode) {
|
||||
const titles = {
|
||||
'stack': 'Складывается с другими скидками',
|
||||
'max_only': 'Применяется только максимальная из этого типа',
|
||||
'exclusive': 'Отменяет все другие скидки'
|
||||
};
|
||||
return titles[mode] || mode;
|
||||
}
|
||||
|
||||
// Обновить UI скидок
|
||||
function updateDiscountsUI(result) {
|
||||
const autoContainer = document.getElementById('autoDiscountsContainer');
|
||||
@@ -2447,33 +2482,57 @@ function updateDiscountsUI(result) {
|
||||
|
||||
let hasDiscounts = false;
|
||||
|
||||
// 1. Скидка на заказ (автоматическая)
|
||||
if (result.order_discount && result.order_discount.discount_id) {
|
||||
// 1. Скидки на заказ (теперь может быть несколько)
|
||||
const orderDiscounts = result.order_discounts || [];
|
||||
if (orderDiscounts.length > 0) {
|
||||
hasDiscounts = true;
|
||||
autoContainer.style.display = 'block';
|
||||
const div = document.createElement('div');
|
||||
div.className = 'd-flex justify-content-between';
|
||||
div.innerHTML = `
|
||||
<span>${result.order_discount.discount_name}</span>
|
||||
<span class="text-success">-${result.order_discount.discount_amount.toFixed(2)} руб.</span>
|
||||
`;
|
||||
autoList.appendChild(div);
|
||||
|
||||
orderDiscounts.forEach(disc => {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'd-flex justify-content-between align-items-center w-100';
|
||||
const modeIcon = getCombineModeIcon(disc.combine_mode);
|
||||
div.innerHTML = `
|
||||
<span>${modeIcon} ${disc.discount_name}</span>
|
||||
<span class="text-success">-${disc.discount_amount.toFixed(2)} руб.</span>
|
||||
`;
|
||||
autoList.appendChild(div);
|
||||
});
|
||||
|
||||
// Показываем информацию о комбинировании
|
||||
if (orderDiscounts.length > 1) {
|
||||
const infoDiv = document.createElement('div');
|
||||
infoDiv.className = 'text-muted small mt-1';
|
||||
infoDiv.innerHTML = '<i class="bi bi-info-circle"></i> Скидки скомбинированы';
|
||||
autoList.appendChild(infoDiv);
|
||||
}
|
||||
|
||||
// Показываем кнопку отмены (только если еще не пропущена)
|
||||
document.getElementById('skipAutoDiscountBtn').style.display = 'block';
|
||||
} else {
|
||||
autoContainer.style.display = 'none';
|
||||
document.getElementById('skipAutoDiscountBtn').style.display = 'none';
|
||||
}
|
||||
|
||||
// 2. Скидки на позиции
|
||||
if (result.item_discounts && result.item_discounts.length > 0) {
|
||||
// 2. Скидки на позиции (новый формат с массивом discounts)
|
||||
const itemDiscounts = result.item_discounts || [];
|
||||
if (itemDiscounts.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);
|
||||
itemDiscounts.forEach(item => {
|
||||
if (item.discounts && item.discounts.length > 0) {
|
||||
const discNames = item.discounts.map(d => {
|
||||
const modeIcon = getCombineModeIcon(d.combine_mode);
|
||||
return `${modeIcon} ${d.discount_name}`;
|
||||
}).join(', ');
|
||||
const div = document.createElement('div');
|
||||
div.className = 'text-muted small';
|
||||
div.innerHTML = `• ${discNames}: <span class="text-success">-${item.total_discount.toFixed(2)} руб.</span>`;
|
||||
itemBreakdown.appendChild(div);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// 3. Ручная скидка
|
||||
// 3. Ручная скидка (из списка)
|
||||
if (appliedManualDiscount) {
|
||||
hasDiscounts = true;
|
||||
document.getElementById('manualDiscountContainer').style.display = 'block';
|
||||
@@ -2484,19 +2543,54 @@ function updateDiscountsUI(result) {
|
||||
document.getElementById('manualDiscountContainer').style.display = 'none';
|
||||
}
|
||||
|
||||
// 4. Произвольная скидка
|
||||
if (appliedCustomDiscount) {
|
||||
hasDiscounts = true;
|
||||
const customDiscountAmount = appliedCustomDiscount.isPercent
|
||||
? (result.cart_subtotal || 0) * appliedCustomDiscount.value / 100
|
||||
: appliedCustomDiscount.value;
|
||||
const discountText = appliedCustomDiscount.isPercent
|
||||
? `-${appliedCustomDiscount.value}% (-${customDiscountAmount.toFixed(2)} руб.)`
|
||||
: `-${customDiscountAmount.toFixed(2)} руб.`;
|
||||
|
||||
// Показываем в summary или добавляем как отдельную строку
|
||||
const customDiv = document.createElement('div');
|
||||
customDiv.className = 'd-flex justify-content-between align-items-center mt-1';
|
||||
customDiv.innerHTML = `
|
||||
<span class="badge bg-primary">Произвольная скидка ${discountText}</span>
|
||||
`;
|
||||
itemBreakdown.appendChild(customDiv);
|
||||
}
|
||||
|
||||
// Показываем/скрываем summary
|
||||
if (hasDiscounts) {
|
||||
summaryDiv.style.display = 'block';
|
||||
document.getElementById('discountsSubtotal').textContent =
|
||||
(result.cart_subtotal || 0).toFixed(2) + ' руб.';
|
||||
|
||||
// Рассчитываем итоговую скидку с учетом произвольной
|
||||
let totalDiscount = result.total_discount || 0;
|
||||
if (appliedCustomDiscount) {
|
||||
const customDiscountAmount = appliedCustomDiscount.isPercent
|
||||
? (result.cart_subtotal || 0) * appliedCustomDiscount.value / 100
|
||||
: appliedCustomDiscount.value;
|
||||
totalDiscount += customDiscountAmount;
|
||||
}
|
||||
document.getElementById('discountsTotalDiscount').textContent =
|
||||
'-' + (result.total_discount || 0).toFixed(2) + ' руб.';
|
||||
'-' + totalDiscount.toFixed(2) + ' руб.';
|
||||
} else {
|
||||
summaryDiv.style.display = 'none';
|
||||
}
|
||||
|
||||
// Обновляем итоговую цену
|
||||
const finalTotal = Math.max(0, (result.cart_subtotal || 0) - (result.total_discount || 0));
|
||||
let totalDiscount = result.total_discount || 0;
|
||||
if (appliedCustomDiscount) {
|
||||
const customDiscountAmount = appliedCustomDiscount.isPercent
|
||||
? (result.cart_subtotal || 0) * appliedCustomDiscount.value / 100
|
||||
: appliedCustomDiscount.value;
|
||||
totalDiscount += customDiscountAmount;
|
||||
}
|
||||
const finalTotal = Math.max(0, (result.cart_subtotal || 0) - totalDiscount);
|
||||
document.getElementById('checkoutFinalPrice').textContent = formatMoney(finalTotal) + ' руб.';
|
||||
|
||||
// Пересчитываем виджет оплаты
|
||||
@@ -2537,12 +2631,15 @@ function renderDiscountsDropdown(discounts) {
|
||||
const li = document.createElement('li');
|
||||
const valueText = d.discount_type === 'percentage' ? `${d.value}%` : `${d.value} руб.`;
|
||||
const minText = d.min_order_amount ? `(от ${d.min_order_amount} руб.)` : '';
|
||||
const modeIcon = getCombineModeIcon(d.combine_mode);
|
||||
const modeTitle = getCombineModeTitle(d.combine_mode);
|
||||
|
||||
const a = document.createElement('a');
|
||||
a.href = '#';
|
||||
a.className = 'dropdown-item d-flex justify-content-between';
|
||||
a.title = modeTitle;
|
||||
a.innerHTML = `
|
||||
<span>${d.name}</span>
|
||||
<span>${modeIcon} ${d.name}</span>
|
||||
<span class="text-success">${valueText} ${minText}</span>
|
||||
`;
|
||||
a.onclick = (e) => {
|
||||
@@ -2577,6 +2674,12 @@ document.getElementById('removeManualDiscountBtn').addEventListener('click', asy
|
||||
await loadAvailableDiscounts();
|
||||
});
|
||||
|
||||
// Отменить автоматическую скидку
|
||||
document.getElementById('skipAutoDiscountBtn').addEventListener('click', async () => {
|
||||
skipAutoDiscount = true;
|
||||
await checkAutoDiscounts();
|
||||
});
|
||||
|
||||
// Применить промокод
|
||||
async function applyPromoCode() {
|
||||
const code = document.getElementById('promoCodeInput').value.trim().toUpperCase();
|
||||
@@ -2657,8 +2760,9 @@ async function recalculateDiscountsWithPromo(promoCode) {
|
||||
|
||||
if (result.success) {
|
||||
cartDiscounts.totalDiscount = result.total_discount || 0;
|
||||
cartDiscounts.orderDiscount = result.order_discount;
|
||||
cartDiscounts.orderDiscounts = result.order_discounts || [];
|
||||
cartDiscounts.itemDiscounts = result.item_discounts || [];
|
||||
cartDiscounts.excludedBy = result.excluded_by || null;
|
||||
updateDiscountsUI(result);
|
||||
}
|
||||
} catch (error) {
|
||||
@@ -2708,6 +2812,90 @@ document.getElementById('promoCodeInput').addEventListener('keypress', (e) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ===== ПРОИЗВОЛЬНАЯ СКИДКА =====
|
||||
|
||||
// Применить произвольную скидку
|
||||
async function applyCustomDiscount() {
|
||||
const input = document.getElementById('customDiscountInput');
|
||||
const isPercent = document.getElementById('customDiscountIsPercent').checked;
|
||||
const errorDiv = document.getElementById('customDiscountError');
|
||||
|
||||
const value = parseFloat(input.value);
|
||||
|
||||
// Валидация
|
||||
if (isNaN(value) || value <= 0) {
|
||||
errorDiv.textContent = 'Введите корректное значение скидки';
|
||||
errorDiv.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
if (isPercent && value > 100) {
|
||||
errorDiv.textContent = 'Процент не может превышать 100%';
|
||||
errorDiv.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем сумму корзины
|
||||
let cartTotal = 0;
|
||||
cart.forEach((item) => {
|
||||
cartTotal += item.qty * item.price;
|
||||
});
|
||||
|
||||
if (!isPercent && value > cartTotal) {
|
||||
errorDiv.textContent = `Скидка не может превышать сумму заказа (${cartTotal.toFixed(2)} руб.)`;
|
||||
errorDiv.style.display = 'block';
|
||||
return;
|
||||
}
|
||||
|
||||
// Сохраняем произвольную скидку
|
||||
appliedCustomDiscount = { value, isPercent };
|
||||
|
||||
// Сбрасываем другие типы скидок (взаимоисключающие)
|
||||
appliedPromoCode = null;
|
||||
appliedManualDiscount = null;
|
||||
|
||||
// Обновляем UI
|
||||
errorDiv.style.display = 'none';
|
||||
document.getElementById('applyCustomDiscountBtn').style.display = 'none';
|
||||
document.getElementById('removeCustomDiscountBtn').style.display = 'inline-block';
|
||||
document.getElementById('promoCodeInput').value = '';
|
||||
document.getElementById('promoCodeSuccess').style.display = 'none';
|
||||
document.getElementById('promoCodeError').style.display = 'none';
|
||||
|
||||
// Пересчитываем скидки
|
||||
await checkAutoDiscounts();
|
||||
}
|
||||
|
||||
// Удалить произвольную скидку
|
||||
async function removeCustomDiscount() {
|
||||
appliedCustomDiscount = null;
|
||||
|
||||
// Обновляем UI
|
||||
document.getElementById('customDiscountInput').value = '';
|
||||
document.getElementById('applyCustomDiscountBtn').style.display = 'inline-block';
|
||||
document.getElementById('removeCustomDiscountBtn').style.display = 'none';
|
||||
document.getElementById('customDiscountError').style.display = 'none';
|
||||
|
||||
// Пересчитываем скидки
|
||||
await checkAutoDiscounts();
|
||||
}
|
||||
|
||||
// Обработчики кнопок произвольной скидки
|
||||
document.getElementById('applyCustomDiscountBtn').addEventListener('click', applyCustomDiscount);
|
||||
|
||||
document.getElementById('removeCustomDiscountBtn').addEventListener('click', removeCustomDiscount);
|
||||
|
||||
document.getElementById('customDiscountInput').addEventListener('keypress', (e) => {
|
||||
if (e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
applyCustomDiscount();
|
||||
}
|
||||
});
|
||||
|
||||
document.getElementById('customDiscountInput').addEventListener('input', () => {
|
||||
document.getElementById('customDiscountError').style.display = 'none';
|
||||
});
|
||||
|
||||
async function initPaymentWidget(mode, data) {
|
||||
const paymentMethods = [
|
||||
{ id: 1, code: 'account_balance', name: 'С баланса счёта' },
|
||||
@@ -2778,7 +2966,11 @@ async function handleCheckoutSubmit(paymentsData) {
|
||||
payments: paymentsData,
|
||||
notes: document.getElementById('orderNote').value.trim(),
|
||||
promo_code: appliedPromoCode?.code || null,
|
||||
manual_discount_id: appliedManualDiscount?.id || null
|
||||
manual_discount_id: appliedManualDiscount?.id || null,
|
||||
custom_discount: appliedCustomDiscount ? {
|
||||
value: appliedCustomDiscount.value,
|
||||
is_percent: appliedCustomDiscount.isPercent
|
||||
} : null
|
||||
};
|
||||
|
||||
// Отправляем на сервер
|
||||
|
||||
@@ -349,12 +349,17 @@
|
||||
<div class="mb-2 pb-2 border-bottom">
|
||||
<small class="text-muted d-block mb-1">Скидки</small>
|
||||
|
||||
<!-- Автоматические скидки (read-only) -->
|
||||
<!-- Автоматические скидки -->
|
||||
<div id="autoDiscountsContainer" class="mb-2" style="display: none;">
|
||||
<div class="alert alert-success py-1 px-2 mb-1" style="font-size: 0.85rem;">
|
||||
<i class="bi bi-magic me-1"></i>
|
||||
<strong>Автоматически:</strong>
|
||||
<div id="autoDiscountsList"></div>
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<div class="alert alert-success py-1 px-2 mb-0" style="font-size: 0.85rem; flex: 1;">
|
||||
<i class="bi bi-magic me-1"></i>
|
||||
<strong>Автоматически:</strong>
|
||||
<div id="autoDiscountsList"></div>
|
||||
</div>
|
||||
<button type="button" class="btn btn-sm btn-link text-danger p-1 ms-2" id="skipAutoDiscountBtn" style="display: none; width: 32px; height: 32px;" title="Отменить скидку">
|
||||
<i class="bi bi-x-lg fs-5"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -383,6 +388,26 @@
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Произвольная скидка -->
|
||||
<div class="input-group input-group-sm mb-2">
|
||||
<span class="input-group-text"><i class="bi bi-percent"></i></span>
|
||||
<input type="number" class="form-control" id="customDiscountInput"
|
||||
placeholder="Сумма или %" min="0" step="0.01">
|
||||
<button class="btn btn-outline-secondary" type="button" id="applyCustomDiscountBtn">
|
||||
<i class="bi bi-check-lg"></i>
|
||||
</button>
|
||||
<button class="btn btn-outline-danger" type="button" id="removeCustomDiscountBtn" style="display: none;">
|
||||
<i class="bi bi-x-lg"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-check form-check-sm mb-2">
|
||||
<input class="form-check-input" type="checkbox" id="customDiscountIsPercent" checked>
|
||||
<label class="form-check-label small" for="customDiscountIsPercent">
|
||||
Процент (снимите галочку для фиксированной суммы в рублях)
|
||||
</label>
|
||||
</div>
|
||||
<div id="customDiscountError" class="text-danger small mb-2" style="display: none;"></div>
|
||||
|
||||
<!-- Промокод -->
|
||||
<div class="input-group input-group-sm">
|
||||
<span class="input-group-text"><i class="bi bi-ticket-perforated"></i></span>
|
||||
|
||||
@@ -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