Fix Product filtering and add kit disassembly functionality
Fixed: - Replace is_active with status='active' for Product filtering in IncomingModelForm - Product model uses status field instead of is_active Added: - Showcase field to ProductKit for tracking showcase placement - product_kit field to Reservation for tracking kit-specific reservations - Disassemble button in POS terminal for showcase kits - API endpoint for kit disassembly (release reservations, mark discontinued) - Improved reservation filtering when dismantling specific kits Changes: - ShowcaseManager now links reservations to specific kit instances - POS terminal modal shows disassemble button in edit mode - Kit disassembly properly updates stock aggregates 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -894,7 +894,10 @@ async function openEditKitModal(kitId) {
|
||||
// Меняем заголовок и кнопку
|
||||
document.getElementById('createTempKitModalLabel').textContent = 'Редактирование витринного букета';
|
||||
document.getElementById('confirmCreateTempKit').textContent = 'Сохранить изменения';
|
||||
|
||||
|
||||
// Показываем кнопку "Разобрать" в режиме редактирования
|
||||
document.getElementById('disassembleKitBtn').style.display = 'block';
|
||||
|
||||
// Открываем модальное окно
|
||||
const modal = new bootstrap.Modal(document.getElementById('createTempKitModal'));
|
||||
modal.show();
|
||||
@@ -1234,6 +1237,58 @@ document.getElementById('confirmCreateTempKit').onclick = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
// Обработчик кнопки "Разобрать букет"
|
||||
document.getElementById('disassembleKitBtn').addEventListener('click', async () => {
|
||||
if (!isEditMode || !editingKitId) {
|
||||
alert('Ошибка: режим редактирования не активен');
|
||||
return;
|
||||
}
|
||||
|
||||
// Запрос подтверждения
|
||||
const confirmed = confirm(
|
||||
'Вы уверены?\n\n' +
|
||||
'Букет будет разобран:\n' +
|
||||
'• Все резервы компонентов будут освобождены\n' +
|
||||
'• Комплект будет помечен как "Снят"\n\n' +
|
||||
'Это действие нельзя отменить!'
|
||||
);
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`/pos/api/product-kits/${editingKitId}/disassemble/`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-CSRFToken': getCookie('csrftoken')
|
||||
}
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success) {
|
||||
alert(`✅ ${data.message}\n\nОсвобождено резервов: ${data.released_count}`);
|
||||
|
||||
// Закрываем модальное окно
|
||||
const modal = bootstrap.Modal.getInstance(document.getElementById('createTempKitModal'));
|
||||
modal.hide();
|
||||
|
||||
// Обновляем витринные комплекты
|
||||
isShowcaseView = true;
|
||||
currentCategoryId = null;
|
||||
await refreshShowcaseKits();
|
||||
renderCategories();
|
||||
renderProducts();
|
||||
} else {
|
||||
alert(`❌ Ошибка: ${data.error}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error disassembling kit:', error);
|
||||
alert('Произошла ошибка при разборе букета');
|
||||
}
|
||||
});
|
||||
|
||||
// Вспомогательная функция для получения CSRF токена
|
||||
function getCookie(name) {
|
||||
let cookieValue = null;
|
||||
@@ -1259,10 +1314,13 @@ document.getElementById('createTempKitModal').addEventListener('hidden.bs.modal'
|
||||
// Сбрасываем режим редактирования
|
||||
isEditMode = false;
|
||||
editingKitId = null;
|
||||
|
||||
|
||||
// Восстанавливаем заголовок и текст кнопки
|
||||
document.getElementById('createTempKitModalLabel').textContent = 'Создать витринный букет из корзины';
|
||||
document.getElementById('confirmCreateTempKit').innerHTML = '<i class="bi bi-check-circle"></i> Создать и зарезервировать';
|
||||
|
||||
// Скрываем кнопку "Разобрать"
|
||||
document.getElementById('disassembleKitBtn').style.display = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -252,6 +252,12 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<!-- Кнопка "Разобрать" (отображается только в режиме редактирования) -->
|
||||
<button type="button" class="btn btn-danger me-auto" id="disassembleKitBtn" style="display: none;">
|
||||
<i class="bi bi-scissors"></i> Разобрать букет
|
||||
</button>
|
||||
|
||||
<!-- Правая группа кнопок -->
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Отмена</button>
|
||||
<button type="button" class="btn btn-primary" id="confirmCreateTempKit">
|
||||
<i class="bi bi-check-circle"></i> Создать и зарезервировать
|
||||
|
||||
@@ -23,6 +23,8 @@ urlpatterns = [
|
||||
path('api/product-kits/<int:kit_id>/', views.get_product_kit_details, name='get-product-kit-details'),
|
||||
# Обновить временный комплект (состав, фото, цены) [POST]
|
||||
path('api/product-kits/<int:kit_id>/update/', views.update_product_kit, name='update-product-kit'),
|
||||
# Разобрать витринный комплект (освободить резервы, установить статус discontinued) [POST]
|
||||
path('api/product-kits/<int:kit_id>/disassemble/', views.disassemble_product_kit, name='disassemble-product-kit'),
|
||||
# Создать временный комплект и зарезервировать на витрину [POST]
|
||||
path('api/create-temp-kit/', views.create_temp_kit_to_showcase, name='create-temp-kit-api'),
|
||||
]
|
||||
@@ -716,7 +716,8 @@ def create_temp_kit_to_showcase(request):
|
||||
status='active',
|
||||
price_adjustment_type=price_adjustment_type,
|
||||
price_adjustment_value=price_adjustment_value,
|
||||
sale_price=sale_price
|
||||
sale_price=sale_price,
|
||||
showcase=showcase
|
||||
)
|
||||
|
||||
# 2. Создаём KitItem для каждого товара из корзины
|
||||
@@ -911,3 +912,73 @@ def update_product_kit(request, kit_id):
|
||||
return JsonResponse({'success': False, 'error': 'Неверный формат данных'}, status=400)
|
||||
except Exception as e:
|
||||
return JsonResponse({'success': False, 'error': f'Ошибка: {str(e)}'}, status=500)
|
||||
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["POST"])
|
||||
def disassemble_product_kit(request, kit_id):
|
||||
"""
|
||||
Разбирает витринный комплект: освобождает резервы и устанавливает статус 'discontinued'.
|
||||
|
||||
Args:
|
||||
request: HTTP запрос
|
||||
kit_id: ID комплекта для разбора
|
||||
|
||||
Returns:
|
||||
JSON: {
|
||||
'success': bool,
|
||||
'released_count': int,
|
||||
'message': str,
|
||||
'error': str (если failed)
|
||||
}
|
||||
"""
|
||||
try:
|
||||
# Получаем комплект с витриной (только временные комплекты)
|
||||
kit = ProductKit.objects.select_related('showcase').get(id=kit_id, is_temporary=True)
|
||||
|
||||
# Проверяем, что комплект ещё не разобран
|
||||
if kit.status == 'discontinued':
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'Комплект уже разобран (статус: Снят)'
|
||||
}, status=400)
|
||||
|
||||
# Проверяем, что у комплекта есть привязанная витрина
|
||||
if not kit.showcase:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'Комплект не привязан к витрине'
|
||||
}, status=400)
|
||||
|
||||
# Освобождаем резервы и устанавливаем статус
|
||||
# ShowcaseManager.dismantle_from_showcase уже использует transaction.atomic()
|
||||
result = ShowcaseManager.dismantle_from_showcase(
|
||||
showcase=kit.showcase,
|
||||
product_kit=kit
|
||||
)
|
||||
|
||||
if not result['success']:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': result['message']
|
||||
}, status=400)
|
||||
|
||||
# Устанавливаем статус комплекта 'discontinued'
|
||||
kit.discontinue(user=request.user)
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'released_count': result['released_count'],
|
||||
'message': f'Комплект "{kit.name}" разобран. Статус изменён на "Снят".'
|
||||
})
|
||||
|
||||
except ProductKit.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'Комплект не найден'
|
||||
}, status=404)
|
||||
except Exception as e:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': f'Ошибка при разборе: {str(e)}'
|
||||
}, status=500)
|
||||
|
||||
Reference in New Issue
Block a user