Реализована возможность редактирования состава витринного комплекта с поддержкой отрицательных резервов
- Добавлены методы reserve_product_to_showcase и release_showcase_reservation в ShowcaseManager - Методы работают с резервами для всех активных экземпляров витринного комплекта - НЕ блокируют создание резерва при нехватке товара, возвращают информацию о дефиците (overdraft) - Обновлён API endpoint update_product_kit для корректировки резервов при изменении состава - Добавлено визуальное предупреждение на фронте о нехватке товара на складе - В модалке редактирования комплекта добавлены контролы для изменения количества товаров (+/-, поле ввода, удаление) - Автоматический пересчёт цен при изменении состава - Очистка корзины POS после успешного создания витринного комплекта
This commit is contained in:
@@ -1440,22 +1440,92 @@ function renderTempKitItems() {
|
||||
if (item.type !== 'product') return;
|
||||
|
||||
const itemDiv = document.createElement('div');
|
||||
itemDiv.className = 'd-flex justify-content-between align-items-center mb-1 pb-1 border-bottom';
|
||||
itemDiv.innerHTML = `
|
||||
<div>
|
||||
<strong class="small">${item.name}</strong>
|
||||
<br>
|
||||
<small class="text-muted">${item.qty} шт × ${formatMoney(item.price)} руб.</small>
|
||||
</div>
|
||||
<div class="text-end">
|
||||
<strong class="small">${formatMoney(item.qty * item.price)} руб.</strong>
|
||||
</div>
|
||||
itemDiv.className = 'd-flex justify-content-between align-items-center mb-2 pb-2 border-bottom';
|
||||
|
||||
// Левая часть: название и цена
|
||||
const leftDiv = document.createElement('div');
|
||||
leftDiv.className = 'flex-grow-1';
|
||||
leftDiv.innerHTML = `
|
||||
<strong class="small">${item.name}</strong>
|
||||
<br>
|
||||
<small class="text-muted">${formatMoney(item.price)} руб. / шт.</small>
|
||||
`;
|
||||
|
||||
// Правая часть: контролы количества и удаление
|
||||
const rightDiv = document.createElement('div');
|
||||
rightDiv.className = 'd-flex align-items-center gap-2';
|
||||
|
||||
// Кнопка минус
|
||||
const minusBtn = document.createElement('button');
|
||||
minusBtn.className = 'btn btn-sm btn-outline-secondary';
|
||||
minusBtn.innerHTML = '<i class="bi bi-dash"></i>';
|
||||
minusBtn.onclick = (e) => {
|
||||
e.preventDefault();
|
||||
if (item.qty > 1) {
|
||||
item.qty--;
|
||||
} else {
|
||||
tempCart.delete(cartKey);
|
||||
}
|
||||
renderTempKitItems();
|
||||
};
|
||||
|
||||
// Поле количества
|
||||
const qtyInput = document.createElement('input');
|
||||
qtyInput.type = 'number';
|
||||
qtyInput.className = 'form-control form-control-sm text-center';
|
||||
qtyInput.style.width = '60px';
|
||||
qtyInput.value = item.qty;
|
||||
qtyInput.min = 1;
|
||||
qtyInput.onchange = (e) => {
|
||||
const newQty = parseInt(e.target.value) || 1;
|
||||
item.qty = Math.max(1, newQty);
|
||||
renderTempKitItems();
|
||||
};
|
||||
|
||||
// Кнопка плюс
|
||||
const plusBtn = document.createElement('button');
|
||||
plusBtn.className = 'btn btn-sm btn-outline-secondary';
|
||||
plusBtn.innerHTML = '<i class="bi bi-plus"></i>';
|
||||
plusBtn.onclick = (e) => {
|
||||
e.preventDefault();
|
||||
item.qty++;
|
||||
renderTempKitItems();
|
||||
};
|
||||
|
||||
// Сумма за товар
|
||||
const totalDiv = document.createElement('div');
|
||||
totalDiv.className = 'text-end ms-2';
|
||||
totalDiv.style.minWidth = '80px';
|
||||
totalDiv.innerHTML = `<strong class="small">${formatMoney(item.qty * item.price)} руб.</strong>`;
|
||||
|
||||
// Кнопка удаления
|
||||
const deleteBtn = document.createElement('button');
|
||||
deleteBtn.className = 'btn btn-sm btn-outline-danger';
|
||||
deleteBtn.innerHTML = '<i class="bi bi-trash"></i>';
|
||||
deleteBtn.onclick = (e) => {
|
||||
e.preventDefault();
|
||||
tempCart.delete(cartKey);
|
||||
renderTempKitItems();
|
||||
};
|
||||
|
||||
rightDiv.appendChild(minusBtn);
|
||||
rightDiv.appendChild(qtyInput);
|
||||
rightDiv.appendChild(plusBtn);
|
||||
rightDiv.appendChild(totalDiv);
|
||||
rightDiv.appendChild(deleteBtn);
|
||||
|
||||
itemDiv.appendChild(leftDiv);
|
||||
itemDiv.appendChild(rightDiv);
|
||||
container.appendChild(itemDiv);
|
||||
|
||||
estimatedTotal += item.qty * item.price;
|
||||
});
|
||||
|
||||
// Если корзина пуста
|
||||
if (tempCart.size === 0) {
|
||||
container.innerHTML = '<p class="text-muted text-center py-3"><i class="bi bi-inbox"></i> Нет товаров</p>';
|
||||
}
|
||||
|
||||
// Обновляем все расчеты цен
|
||||
updatePriceCalculations(estimatedTotal);
|
||||
}
|
||||
@@ -1657,7 +1727,8 @@ document.getElementById('confirmCreateTempKit').onclick = async () => {
|
||||
// Успех!
|
||||
const createdCount = data.available_count || 1;
|
||||
const qtyInfo = createdCount > 1 ? `\nСоздано экземпляров: ${createdCount}` : '';
|
||||
const successMessage = isEditMode
|
||||
|
||||
let successMessage = isEditMode
|
||||
? `✅ ${data.message}\n\nКомплект: ${data.kit_name}\nЦена: ${data.kit_price} руб.`
|
||||
: `✅ ${data.message}
|
||||
|
||||
@@ -1665,6 +1736,15 @@ document.getElementById('confirmCreateTempKit').onclick = async () => {
|
||||
Цена: ${data.kit_price} руб.${qtyInfo}
|
||||
Зарезервировано компонентов: ${data.reservations_count}`;
|
||||
|
||||
// Если есть предупреждение о нехватке товара - добавляем его
|
||||
if (data.stock_warning && data.stock_warnings && data.stock_warnings.length > 0) {
|
||||
successMessage += '\n\n⚠️ ВНИМАНИЕ: Нехватка товара на складе!\n';
|
||||
data.stock_warnings.forEach(warning => {
|
||||
successMessage += `\n• ${warning.product_name}: не хватает ${warning.overdraft} ед.`;
|
||||
});
|
||||
successMessage += '\n\nПроверьте остатки и пополните склад.';
|
||||
}
|
||||
|
||||
alert(successMessage);
|
||||
|
||||
// Очищаем tempCart (изолированное состояние модалки)
|
||||
@@ -1682,6 +1762,9 @@ document.getElementById('confirmCreateTempKit').onclick = async () => {
|
||||
document.getElementById('salePriceBlock').style.display = 'none';
|
||||
document.getElementById('showcaseKitQuantity').value = '1'; // Сброс количества
|
||||
|
||||
// Запоминаем, был ли режим редактирования до сброса
|
||||
const wasEditMode = isEditMode;
|
||||
|
||||
// Сбрасываем режим редактирования
|
||||
isEditMode = false;
|
||||
editingKitId = null;
|
||||
@@ -1690,6 +1773,12 @@ document.getElementById('confirmCreateTempKit').onclick = async () => {
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('createTempKitModal'));
|
||||
modal.hide();
|
||||
|
||||
// Если это было СОЗДАНИЕ витринного комплекта из корзины,
|
||||
// очищаем основную корзину POS
|
||||
if (!wasEditMode) {
|
||||
await clearCart();
|
||||
}
|
||||
|
||||
// Обновляем витринные комплекты и переключаемся на вид витрины
|
||||
isShowcaseView = true;
|
||||
currentCategoryId = null;
|
||||
|
||||
@@ -1188,15 +1188,16 @@ def update_product_kit(request, kit_id):
|
||||
|
||||
# Получаем витрину для резервов
|
||||
showcase_reservation = Reservation.objects.filter(
|
||||
product__in=old_items.keys(),
|
||||
product_kit=kit,
|
||||
showcase__isnull=False,
|
||||
status='reserved'
|
||||
).select_related('showcase').first()
|
||||
|
||||
showcase = showcase_reservation.showcase if showcase_reservation else None
|
||||
showcase = showcase_reservation.showcase if showcase_reservation else kit.showcase
|
||||
|
||||
# Вычисляем разницу в составе
|
||||
# Вычисляем разницу в составе и собираем информацию о дефиците
|
||||
all_product_ids = set(old_items.keys()) | set(aggregated_items.keys())
|
||||
stock_warnings = [] # Список товаров с нехваткой остатков
|
||||
|
||||
for product_id in all_product_ids:
|
||||
old_qty = old_items.get(product_id, Decimal('0'))
|
||||
@@ -1204,21 +1205,27 @@ def update_product_kit(request, kit_id):
|
||||
diff = new_qty - old_qty
|
||||
|
||||
if diff > 0 and showcase:
|
||||
# Нужно дозарезервировать
|
||||
# Нужно дозарезервировать (на каждый экземпляр)
|
||||
result = ShowcaseManager.reserve_product_to_showcase(
|
||||
product=products[product_id],
|
||||
showcase=showcase,
|
||||
quantity=diff
|
||||
product_kit=kit,
|
||||
quantity_per_item=diff,
|
||||
)
|
||||
if not result['success']:
|
||||
raise Exception(f"Недостаточно запасов: {result['message']}")
|
||||
# Собираем информацию о дефиците
|
||||
if result.get('overdraft', Decimal('0')) > 0:
|
||||
stock_warnings.append({
|
||||
'product_name': products[product_id].name,
|
||||
'overdraft': str(result['overdraft'])
|
||||
})
|
||||
|
||||
elif diff < 0 and showcase:
|
||||
# Нужно освободить резерв
|
||||
# Нужно освободить резерв (на каждый экземпляр)
|
||||
ShowcaseManager.release_showcase_reservation(
|
||||
product=products[product_id],
|
||||
showcase=showcase,
|
||||
quantity=abs(diff)
|
||||
product_kit=kit,
|
||||
quantity_per_item=abs(diff),
|
||||
)
|
||||
|
||||
# Обновляем комплект
|
||||
@@ -1254,7 +1261,9 @@ def update_product_kit(request, kit_id):
|
||||
'message': f'Комплект "{kit.name}" обновлён',
|
||||
'kit_id': kit.id,
|
||||
'kit_name': kit.name,
|
||||
'kit_price': str(kit.actual_price)
|
||||
'kit_price': str(kit.actual_price),
|
||||
'stock_warning': len(stock_warnings) > 0,
|
||||
'stock_warnings': stock_warnings
|
||||
})
|
||||
|
||||
except ProductKit.DoesNotExist:
|
||||
|
||||
Reference in New Issue
Block a user