Fix: Implement proper deletion of order items with confirmation dialog

Fixes deletion functionality for order items across frontend and backend:
- Remove restriction preventing deletion of last item
- Add confirmation dialog before deletion
- Properly track and send deleted item IDs to backend via autosave
- Update backend to handle item deletion by ID instead of index
- Fix visual feedback: deleted items are hidden immediately
- Auto-recalculate total sum after deletion

Technical changes:
- order_form.html: Add confirmation dialog, trigger autosave on delete
- autosave.js: Collect deleted item IDs, send to backend
- draft_service.py: Process deleted_item_ids, update items by ID

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-26 06:27:27 +03:00
parent 5df182e030
commit 3b4785e2ad
4 changed files with 98 additions and 52 deletions

View File

@@ -208,6 +208,13 @@ class DraftOrderService:
setattr(order, field, value) setattr(order, field, value)
# Обрабатываем удаление позиций заказа
if 'deleted_item_ids' in data:
deleted_ids = data['deleted_item_ids']
if deleted_ids:
from ..models import OrderItem
OrderItem.objects.filter(id__in=deleted_ids, order=order).delete()
# Обрабатываем позиции заказа (items) # Обрабатываем позиции заказа (items)
if 'items' in data: if 'items' in data:
# Импортируем модели # Импортируем модели
@@ -216,18 +223,9 @@ class DraftOrderService:
items_data = data['items'] items_data = data['items']
# Получаем существующие позиции # Обрабатываем каждую позицию
existing_items = list(order.items.all()) for item_data in items_data:
item_id = item_data.get('id') # ID существующей позиции (если есть)
# Удаляем все существующие позиции, которых нет в новых данных
items_to_keep_count = len(items_data)
for i, existing_item in enumerate(existing_items):
if i >= items_to_keep_count:
# Удаляем лишние позиции
existing_item.delete()
# Обновляем или создаём позиции
for index, item_data in enumerate(items_data):
product_id = item_data.get('product_id') product_id = item_data.get('product_id')
product_kit_id = item_data.get('product_kit_id') product_kit_id = item_data.get('product_kit_id')
quantity = item_data.get('quantity', 1) quantity = item_data.get('quantity', 1)
@@ -275,17 +273,28 @@ class DraftOrderService:
is_custom_price = False is_custom_price = False
# Обновляем существующую позицию или создаём новую # Обновляем существующую позицию или создаём новую
if index < len(existing_items): if item_id:
# Обновляем существующую # Обновляем существующую позицию
item = existing_items[index] try:
item.product = product item = OrderItem.objects.get(id=item_id, order=order)
item.product_kit = product_kit item.product = product
item.quantity = quantity item.product_kit = product_kit
item.price = price item.quantity = quantity
item.is_custom_price = is_custom_price item.price = price
item.save() item.is_custom_price = is_custom_price
item.save()
except OrderItem.DoesNotExist:
# Если позиция не найдена, создаём новую
OrderItem.objects.create(
order=order,
product=product,
product_kit=product_kit,
quantity=quantity,
price=price,
is_custom_price=is_custom_price
)
else: else:
# Создаём новую # Создаём новую позицию
OrderItem.objects.create( OrderItem.objects.create(
order=order, order=order,
product=product, product=product,

View File

@@ -213,7 +213,7 @@
return; return;
} }
const fields = form.querySelectorAll('select, input[type="number"], input[type="checkbox"]'); const fields = form.querySelectorAll('select, input[type="number"], input[type="text"], input[type="checkbox"]');
fields.forEach(field => { fields.forEach(field => {
if (field.tagName === 'SELECT' || field.type === 'checkbox') { if (field.tagName === 'SELECT' || field.type === 'checkbox') {
@@ -421,7 +421,9 @@
} }
// Собираем позиции заказа // Собираем позиции заказа
data.items = collectOrderItems(); const orderItemsData = collectOrderItems();
data.items = orderItemsData.items;
data.deleted_item_ids = orderItemsData.deletedItemIds;
// Флаг для пересчета итоговой суммы // Флаг для пересчета итоговой суммы
data.recalculate = true; data.recalculate = true;
@@ -434,13 +436,20 @@
*/ */
function collectOrderItems() { function collectOrderItems() {
const items = []; const items = [];
const deletedItemIds = [];
const itemForms = document.querySelectorAll('.order-item-form'); const itemForms = document.querySelectorAll('.order-item-form');
itemForms.forEach(form => { itemForms.forEach(form => {
// Пропускаем удаленные формы // Проверяем, помечена ли форма на удаление
const deleteCheckbox = form.querySelector('input[name$="-DELETE"]'); const deleteCheckbox = form.querySelector('input[name$="-DELETE"]');
const idField = form.querySelector('input[name$="-id"]');
if (deleteCheckbox && deleteCheckbox.checked) { if (deleteCheckbox && deleteCheckbox.checked) {
return; // Если форма помечена на удаление и имеет ID, добавляем в список удалённых
if (idField && idField.value) {
deletedItemIds.push(parseInt(idField.value));
}
return; // Не добавляем в items
} }
// Получаем выбранный товар/комплект // Получаем выбранный товар/комплект
@@ -459,9 +468,14 @@
const item = { const item = {
quantity: quantityInput.value || '1', quantity: quantityInput.value || '1',
price: priceInput.value || '0' price: (priceInput.value || '0').replace(',', '.')
}; };
// Если есть ID (существующий товар), добавляем его
if (idField && idField.value) {
item.id = parseInt(idField.value);
}
// Определяем тип: товар или комплект // Определяем тип: товар или комплект
if (itemValue.startsWith('product_')) { if (itemValue.startsWith('product_')) {
item.product_id = parseInt(itemValue.replace('product_', '')); item.product_id = parseInt(itemValue.replace('product_', ''));
@@ -472,7 +486,7 @@
items.push(item); items.push(item);
}); });
return items; return { items, deletedItemIds };
} }
/** /**
@@ -544,4 +558,9 @@
init(); init();
} }
// Экспортируем функцию scheduleAutosave в глобальную область
window.orderAutosave = {
scheduleAutosave: scheduleAutosave
};
})(); })();

View File

@@ -1,5 +1,6 @@
{% extends 'base.html' %} {% extends 'base.html' %}
{% load static %} {% load static %}
{% load l10n %}
{% block title %}{{ title }}{% endblock %} {% block title %}{{ title }}{% endblock %}
@@ -227,11 +228,11 @@
<label class="form-label">Цена</label> <label class="form-label">Цена</label>
<div class="position-relative"> <div class="position-relative">
{% if item_form.instance.product %} {% if item_form.instance.product %}
<input type="number" name="{{ item_form.price.name }}" step="0.01" min="0" class="form-control" id="{{ item_form.price.id_for_label }}" value="{% if item_form.instance.price %}{{ item_form.instance.price }}{% else %}{{ item_form.instance.product.actual_price }}{% endif %}" data-original-price="{{ item_form.instance.product.actual_price }}"> <input type="text" name="{{ item_form.prefix }}-price" step="0.01" min="0" class="form-control" id="{{ item_form.price.id_for_label }}" value="{% if item_form.instance.price and item_form.instance.price != 0 %}{{ item_form.instance.price }}{% else %}{{ item_form.instance.product.actual_price }}{% endif %}" data-original-price="{{ item_form.instance.product.actual_price }}">
{% elif item_form.instance.product_kit %} {% elif item_form.instance.product_kit %}
<input type="number" name="{{ item_form.price.name }}" step="0.01" min="0" class="form-control" id="{{ item_form.price.id_for_label }}" value="{% if item_form.instance.price %}{{ item_form.instance.price }}{% else %}{{ item_form.instance.product_kit.actual_price }}{% endif %}" data-original-price="{{ item_form.instance.product_kit.actual_price }}"> <input type="text" name="{{ item_form.prefix }}-price" step="0.01" min="0" class="form-control" id="{{ item_form.price.id_for_label }}" value="{% if item_form.instance.price and item_form.instance.price != 0 %}{{ item_form.instance.price }}{% else %}{{ item_form.instance.product_kit.actual_price }}{% endif %}" data-original-price="{{ item_form.instance.product_kit.actual_price }}">
{% else %} {% else %}
{{ item_form.price }} <input type="text" name="{{ item_form.prefix }}-price" step="0.01" min="0" class="form-control" id="{{ item_form.price.id_for_label }}" value="{{ item_form.instance.price|default:'' }}" data-original-price="0">
{% endif %} {% endif %}
<span class="custom-price-badge badge bg-warning position-absolute top-0 end-0 mt-1 me-1" style="display: none;"> <span class="custom-price-badge badge bg-warning position-absolute top-0 end-0 mt-1 me-1" style="display: none;">
Изменена Изменена
@@ -302,7 +303,7 @@
<div class="mb-2"> <div class="mb-2">
<label class="form-label">Цена</label> <label class="form-label">Цена</label>
<div class="position-relative"> <div class="position-relative">
<input type="number" name="items-__prefix__-price" step="0.01" min="0" class="form-control" id="id_items-__prefix__-price"> <input type="text" name="items-__prefix__-price" step="0.01" min="0" class="form-control" id="id_items-__prefix__-price">
<span class="custom-price-badge badge bg-warning position-absolute top-0 end-0 mt-1 me-1" style="display: none;"> <span class="custom-price-badge badge bg-warning position-absolute top-0 end-0 mt-1 me-1" style="display: none;">
Изменена Изменена
</span> </span>
@@ -1001,18 +1002,34 @@ document.addEventListener('DOMContentLoaded', function() {
let total = 0; let total = 0;
console.log('[TOTAL] Calculating total for', visibleForms.length, 'forms');
// Для каждого товара: количество × цена // Для каждого товара: количество × цена
visibleForms.forEach(form => { visibleForms.forEach((form, index) => {
const quantityField = form.querySelector('[name$="-quantity"]'); const quantityField = form.querySelector('[name$="-quantity"]');
const priceField = form.querySelector('[name$="-price"]'); const priceField = form.querySelector('[name$="-price"]');
console.log(`[TOTAL] Form ${index}:`, form);
console.log(`[TOTAL] Form ${index}: quantityField=${quantityField}, priceField=${priceField}`);
const allInputs = form.querySelectorAll('input');
console.log(`[TOTAL] Form ${index}: All inputs:`, allInputs);
allInputs.forEach((input, i) => {
console.log(` Input ${i}: name="${input.name}", id="${input.id}", type="${input.type}"`);
});
if (quantityField && priceField) { if (quantityField && priceField) {
const quantity = parseFloat(quantityField.value) || 0; const quantity = parseFloat(quantityField.value) || 0;
const price = parseFloat(priceField.value) || 0; // Заменяем запятую на точку для корректного парсинга
const priceValue = priceField.value.replace(',', '.');
const price = parseFloat(priceValue) || 0;
console.log(`[TOTAL] Form ${index}: quantity=${quantityField.value} (parsed: ${quantity}), price="${priceField.value}" (parsed: ${price}), subtotal=${quantity * price}`);
total += quantity * price; total += quantity * price;
} else {
console.log(`[TOTAL] Form ${index}: SKIPPED - missing fields!`);
} }
}); });
console.log('[TOTAL] Final total:', total);
return total; return total;
} }
@@ -1129,25 +1146,26 @@ document.addEventListener('DOMContentLoaded', function() {
// Функция для удаления формы // Функция для удаления формы
function removeForm(form) { function removeForm(form) {
// Показываем диалог подтверждения
if (!confirm('Вы действительно хотите удалить этот товар из заказа?')) {
return; // Пользователь отменил удаление
}
const deleteCheckbox = form.querySelector('input[name$="-DELETE"]'); const deleteCheckbox = form.querySelector('input[name$="-DELETE"]');
const idField = form.querySelector('input[name$="-id"]'); const idField = form.querySelector('input[name$="-id"]');
// Проверяем, не последняя ли это форма
const visibleForms = Array.from(container.querySelectorAll('.order-item-form'))
.filter(f => !f.classList.contains('deleted'));
if (visibleForms.length <= 1) {
alert('Нельзя удалить единственную позицию. Добавьте новую позицию перед удалением этой.');
return;
}
// Если форма уже сохранена (есть ID), помечаем на удаление // Если форма уже сохранена (есть ID), помечаем на удаление
if (idField && idField.value) { if (idField && idField.value) {
deleteCheckbox.checked = true; deleteCheckbox.checked = true;
form.classList.add('deleted'); form.classList.add('deleted');
console.log('Form marked for deletion'); form.style.display = 'none'; // Скрываем форму визуально
console.log('Form marked for deletion, id:', idField.value);
// Обновляем итоговую сумму после удаления // Обновляем итоговую сумму после удаления
updateOrderItemsTotal(); updateOrderItemsTotal();
// Триггерим автосохранение для отправки изменений
if (typeof window.orderAutosave !== 'undefined' && window.orderAutosave.scheduleAutosave) {
window.orderAutosave.scheduleAutosave();
}
} else { } else {
// Если форма новая, просто удаляем из DOM // Если форма новая, просто удаляем из DOM
form.remove(); form.remove();

View File

@@ -73,7 +73,7 @@ def search_products_and_variants(request):
numeric_id = int(item_id) numeric_id = int(item_id)
if item_type == 'product': if item_type == 'product':
product = Product.objects.get(id=numeric_id, is_active=True) product = Product.objects.get(id=numeric_id, status='active')
return JsonResponse({ return JsonResponse({
'results': [{ 'results': [{
'id': f'product_{product.id}', 'id': f'product_{product.id}',
@@ -89,7 +89,7 @@ def search_products_and_variants(request):
elif item_type == 'kit': elif item_type == 'kit':
# Для комплектов: временные комплекты можно получать по ID (для заказов) # Для комплектов: временные комплекты можно получать по ID (для заказов)
# но не показываем их в общем поиске # но не показываем их в общем поиске
kit = ProductKit.objects.get(id=numeric_id, is_active=True) kit = ProductKit.objects.get(id=numeric_id, status='active')
return JsonResponse({ return JsonResponse({
'results': [{ 'results': [{
'id': f'kit_{kit.id}', 'id': f'kit_{kit.id}',
@@ -170,7 +170,7 @@ def search_products_and_variants(request):
if search_type in ['all', 'kit']: if search_type in ['all', 'kit']:
# Показываем последние добавленные активные комплекты (только постоянные) # Показываем последние добавленные активные комплекты (только постоянные)
kits = ProductKit.objects.filter(is_active=True, is_temporary=False)\ kits = ProductKit.objects.filter(status='active', is_temporary=False)\
.order_by('-created_at')[:page_size]\ .order_by('-created_at')[:page_size]\
.values('id', 'name', 'sku', 'price', 'sale_price') .values('id', 'name', 'sku', 'price', 'sale_price')
@@ -244,7 +244,7 @@ def search_products_and_variants(request):
models.Q(name_lower__contains=query_lower) | models.Q(name_lower__contains=query_lower) |
models.Q(sku_lower__contains=query_lower) | models.Q(sku_lower__contains=query_lower) |
models.Q(description_lower__contains=query_lower), models.Q(description_lower__contains=query_lower),
is_active=True status='active'
).annotate( ).annotate(
relevance=Case( relevance=Case(
When(name_lower=query_lower, then=3), When(name_lower=query_lower, then=3),
@@ -259,7 +259,7 @@ def search_products_and_variants(request):
models.Q(name__icontains=query_normalized) | models.Q(name__icontains=query_normalized) |
models.Q(sku__icontains=query_normalized) | models.Q(sku__icontains=query_normalized) |
models.Q(description__icontains=query_normalized), models.Q(description__icontains=query_normalized),
is_active=True status='active'
).annotate( ).annotate(
relevance=Case( relevance=Case(
When(name__iexact=query_normalized, then=3), When(name__iexact=query_normalized, then=3),
@@ -310,7 +310,7 @@ def search_products_and_variants(request):
models.Q(name_lower__contains=query_lower) | models.Q(name_lower__contains=query_lower) |
models.Q(sku_lower__contains=query_lower) | models.Q(sku_lower__contains=query_lower) |
models.Q(description_lower__contains=query_lower), models.Q(description_lower__contains=query_lower),
is_active=True, status='active',
is_temporary=False is_temporary=False
).annotate( ).annotate(
relevance=Case( relevance=Case(
@@ -325,7 +325,7 @@ def search_products_and_variants(request):
models.Q(name__icontains=query_normalized) | models.Q(name__icontains=query_normalized) |
models.Q(sku__icontains=query_normalized) | models.Q(sku__icontains=query_normalized) |
models.Q(description__icontains=query_normalized), models.Q(description__icontains=query_normalized),
is_active=True, status='active',
is_temporary=False is_temporary=False
).annotate( ).annotate(
relevance=Case( relevance=Case(
@@ -498,7 +498,7 @@ def validate_kit_cost(request):
elif variant_group_id: elif variant_group_id:
try: try:
variant_group = ProductVariantGroup.objects.get(id=variant_group_id) variant_group = ProductVariantGroup.objects.get(id=variant_group_id)
product = variant_group.products.filter(is_active=True).first() product = variant_group.products.filter(status='active').first()
if variant_group: if variant_group:
product_name = f"[Варианты] {variant_group.name}" product_name = f"[Варианты] {variant_group.name}"
except ProductVariantGroup.DoesNotExist: except ProductVariantGroup.DoesNotExist: