Реализован функционал создания временных комплектов на витрину из POS

- Добавлен API endpoint для создания временного комплекта из корзины
- Реализован endpoint получения списка активных витрин
- Создано модальное окно для настройки комплекта и выбора витрины
- JavaScript логика: валидация корзины, отправка данных, очистка после успеха
- Автоматическая генерация названия комплекта с датой и временем
- Агрегация дубликатов товаров в корзине перед созданием
- Резервирование компонентов на витрину через ShowcaseManager
- Расчёт и отображение итоговой цены комплекта
This commit is contained in:
2025-11-16 21:24:07 +03:00
parent 8f6acfb364
commit 156f646252
4 changed files with 403 additions and 3 deletions

View File

@@ -285,9 +285,200 @@ document.getElementById('clearCart').onclick = clearCart;
// Кнопка "На витрину" - функционал будет добавлен позже
document.getElementById('addToShowcaseBtn').onclick = () => {
alert('Функционал "На витрину" будет реализован позже');
openCreateTempKitModal();
};
// Функция открытия модального окна для создания временного комплекта
async function openCreateTempKitModal() {
// Проверяем что корзина не пуста
if (cart.size === 0) {
alert('Корзина пуста. Добавьте товары перед созданием комплекта.');
return;
}
// Проверяем что в корзине только товары (не комплекты)
let hasKits = false;
for (const [cartKey, item] of cart) {
if (item.type === 'kit') {
hasKits = true;
break;
}
}
if (hasKits) {
alert('В корзине есть комплекты. Для витрины добавляйте только отдельные товары.');
return;
}
// Генерируем название по умолчанию
const now = new Date();
const defaultName = `Витрина — ${now.toLocaleDateString('ru-RU')} ${now.toLocaleTimeString('ru-RU', {hour: '2-digit', minute: '2-digit'})}`;
document.getElementById('tempKitName').value = defaultName;
// Загружаем список витрин
await loadShowcases();
// Заполняем список товаров из корзины
renderTempKitItems();
// Открываем модальное окно
const modal = new bootstrap.Modal(document.getElementById('createTempKitModal'));
modal.show();
}
// Загрузка списка витрин
async function loadShowcases() {
try {
const response = await fetch('/pos/api/get-showcases/');
const data = await response.json();
const select = document.getElementById('showcaseSelect');
select.innerHTML = '<option value="">Выберите витрину...</option>';
if (data.success && data.showcases.length > 0) {
data.showcases.forEach(showcase => {
const option = document.createElement('option');
option.value = showcase.id;
option.textContent = `${showcase.name} (${showcase.warehouse_name})`;
select.appendChild(option);
});
} else {
select.innerHTML = '<option value="">Нет доступных витрин</option>';
}
} catch (error) {
console.error('Error loading showcases:', error);
alert('Ошибка загрузки витрин');
}
}
// Отображение товаров из корзины в модальном окне
function renderTempKitItems() {
const container = document.getElementById('tempKitItemsList');
container.innerHTML = '';
let estimatedTotal = 0;
cart.forEach((item, cartKey) => {
// Только товары (не комплекты)
if (item.type !== 'product') return;
const itemDiv = document.createElement('div');
itemDiv.className = 'd-flex justify-content-between align-items-center mb-2 pb-2 border-bottom';
itemDiv.innerHTML = `
<div>
<strong>${item.name}</strong>
<br>
<small class="text-muted">${item.qty} шт × ${formatMoney(item.price)} руб.</small>
</div>
<div class="text-end">
<strong>${formatMoney(item.qty * item.price)} руб.</strong>
</div>
`;
container.appendChild(itemDiv);
estimatedTotal += item.qty * item.price;
});
document.getElementById('tempKitEstimatedPrice').textContent = formatMoney(estimatedTotal);
}
// Подтверждение создания временного комплекта
document.getElementById('confirmCreateTempKit').onclick = async () => {
const kitName = document.getElementById('tempKitName').value.trim();
const showcaseId = document.getElementById('showcaseSelect').value;
// Валидация
if (!kitName) {
alert('Введите название комплекта');
return;
}
if (!showcaseId) {
alert('Выберите витрину');
return;
}
// Собираем товары из корзины
const items = [];
cart.forEach((item, cartKey) => {
if (item.type === 'product') {
items.push({
product_id: item.id,
quantity: item.qty
});
}
});
if (items.length === 0) {
alert('Нет товаров для создания комплекта');
return;
}
// Отправляем запрос на сервер
const confirmBtn = document.getElementById('confirmCreateTempKit');
confirmBtn.disabled = true;
confirmBtn.innerHTML = '<span class="spinner-border spinner-border-sm me-2"></span>Создание...';
try {
const response = await fetch('/pos/api/create-temp-kit/', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRFToken': getCookie('csrftoken')
},
body: JSON.stringify({
kit_name: kitName,
showcase_id: parseInt(showcaseId),
items: items,
price_adjustment_type: 'none',
price_adjustment_value: 0
})
});
const data = await response.json();
if (data.success) {
// Успех!
alert(`${data.message}
Комплект: ${data.kit_name}
Цена: ${data.kit_price} руб.
Зарезервировано компонентов: ${data.reservations_count}`);
// Очищаем корзину
clearCart();
// Закрываем модальное окно
const modal = bootstrap.Modal.getInstance(document.getElementById('createTempKitModal'));
modal.hide();
} else {
alert(`Ошибка: ${data.error}`);
}
} catch (error) {
console.error('Error creating temp kit:', error);
alert('Ошибка при создании комплекта');
} finally {
confirmBtn.disabled = false;
confirmBtn.innerHTML = '<i class="bi bi-check-circle"></i> Создать и зарезервировать';
}
};
// Вспомогательная функция для получения CSRF токена
function getCookie(name) {
let cookieValue = null;
if (document.cookie && document.cookie !== '') {
const cookies = document.cookie.split(';');
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i].trim();
if (cookie.substring(0, name.length + 1) === (name + '=')) {
cookieValue = decodeURIComponent(cookie.substring(name.length + 1));
break;
}
}
}
return cookieValue;
}
// Заглушки для функционала (будет реализовано позже)
document.getElementById('checkoutNow').onclick = async () => {
alert('Функционал будет подключен позже: создание заказа и списание со склада.');

View File

@@ -104,6 +104,55 @@
</div>
</div>
</div>
<!-- Modal: Создание временного комплекта на витрину -->
<div class="modal fade" id="createTempKitModal" tabindex="-1" aria-labelledby="createTempKitModalLabel" aria-hidden="true">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="createTempKitModalLabel">
<i class="bi bi-flower1"></i> Создать временный комплект на витрину
</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<!-- Название комплекта -->
<div class="mb-3">
<label for="tempKitName" class="form-label">Название комплекта *</label>
<input type="text" class="form-control" id="tempKitName" placeholder="Введите название" required>
</div>
<!-- Выбор витрины -->
<div class="mb-3">
<label for="showcaseSelect" class="form-label">Витрина *</label>
<select class="form-select" id="showcaseSelect" required>
<option value="">Загрузка...</option>
</select>
</div>
<!-- Список товаров в корзине -->
<div class="mb-3">
<label class="form-label">Товары в комплекте</label>
<div class="border rounded p-3" id="tempKitItemsList" style="max-height: 300px; overflow-y: auto;">
<!-- Динамически заполняется через JS -->
</div>
</div>
<!-- Итоговая цена (расчётная) -->
<div class="alert alert-info mb-0">
<strong>Расчётная цена:</strong> <span id="tempKitEstimatedPrice">0.00</span> руб.
<small class="d-block text-muted">Цена будет пересчитана автоматически после создания</small>
</div>
</div>
<div class="modal-footer">
<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> Создать и зарезервировать
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}

View File

@@ -7,4 +7,6 @@ app_name = 'pos'
urlpatterns = [
path('', views.pos_terminal, name='terminal'),
path('api/showcase-items/', views.showcase_items_api, name='showcase-items-api'),
path('api/get-showcases/', views.get_showcases_api, name='get-showcases-api'),
path('api/create-temp-kit/', views.create_temp_kit_to_showcase, name='create-temp-kit-api'),
]

View File

@@ -3,10 +3,15 @@ from django.shortcuts import render
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse
from django.views.decorators.http import require_http_methods
from products.models import Product, ProductCategory, ProductKit
from inventory.models import Showcase, Reservation
from django.db import transaction
from django.utils import timezone
from decimal import Decimal
import json
from products.models import Product, ProductCategory, ProductKit, KitItem
from inventory.models import Showcase, Reservation, Warehouse
from inventory.services import ShowcaseManager
@login_required
def pos_terminal(request):
@@ -97,3 +102,156 @@ def showcase_items_api(request):
'success': True,
'showcases': showcases_list
})
@login_required
@require_http_methods(["GET"])
def get_showcases_api(request):
"""
API endpoint для получения списка активных витрин.
Используется для выбора витрины при создании временного комплекта.
"""
showcases = Showcase.objects.filter(is_active=True).select_related('warehouse')
showcases_data = [{
'id': s.id,
'name': s.name,
'warehouse_name': s.warehouse.name,
'warehouse_id': s.warehouse.id
} for s in showcases]
return JsonResponse({
'success': True,
'showcases': showcases_data
})
@login_required
@require_http_methods(["POST"])
def create_temp_kit_to_showcase(request):
"""
API endpoint для создания временного комплекта из корзины POS
и резервирования его на витрину.
Ожидаемый payload:
{
"kit_name": "Название комплекта",
"showcase_id": 1,
"items": [
{"product_id": 1, "quantity": 2.0},
{"product_id": 3, "quantity": 1.0}
],
"price_adjustment_type": "none", // optional
"price_adjustment_value": 0 // optional
}
"""
try:
data = json.loads(request.body)
kit_name = data.get('kit_name', '').strip()
showcase_id = data.get('showcase_id')
items = data.get('items', [])
price_adjustment_type = data.get('price_adjustment_type', 'none')
price_adjustment_value = Decimal(str(data.get('price_adjustment_value', 0)))
# Валидация
if not kit_name:
return JsonResponse({
'success': False,
'error': 'Необходимо указать название комплекта'
}, status=400)
if not showcase_id:
return JsonResponse({
'success': False,
'error': 'Необходимо выбрать витрину'
}, status=400)
if not items or len(items) == 0:
return JsonResponse({
'success': False,
'error': 'Корзина пуста. Добавьте товары для создания комплекта'
}, status=400)
# Проверяем что витрина существует и активна
try:
showcase = Showcase.objects.select_related('warehouse').get(id=showcase_id, is_active=True)
except Showcase.DoesNotExist:
return JsonResponse({
'success': False,
'error': 'Витрина не найдена или неактивна'
}, status=404)
# Проверяем что все товары из корзины - это Product (не Kit)
product_ids = [item['product_id'] for item in items]
products = Product.objects.in_bulk(product_ids)
if len(products) != len(product_ids):
return JsonResponse({
'success': False,
'error': 'Некоторые товары не найдены'
}, status=400)
# Агрегируем дубликаты (если один товар добавлен несколько раз)
aggregated_items = {}
for item in items:
product_id = item['product_id']
quantity = Decimal(str(item['quantity']))
if product_id in aggregated_items:
aggregated_items[product_id] += quantity
else:
aggregated_items[product_id] = quantity
# Создаём временный комплект и резервируем на витрину
with transaction.atomic():
# 1. Создаём ProductKit (is_temporary=True)
kit = ProductKit.objects.create(
name=kit_name,
is_temporary=True,
status='active',
price_adjustment_type=price_adjustment_type,
price_adjustment_value=price_adjustment_value
)
# 2. Создаём KitItem для каждого товара из корзины
for product_id, quantity in aggregated_items.items():
KitItem.objects.create(
kit=kit,
product=products[product_id],
quantity=quantity
)
# 3. Пересчитываем цену комплекта
kit.recalculate_base_price()
# 4. Резервируем комплект на витрину
result = ShowcaseManager.reserve_kit_to_showcase(
product_kit=kit,
showcase=showcase,
quantity=1
)
if not result['success']:
# Откатываем транзакцию через raise
raise Exception(result['message'])
return JsonResponse({
'success': True,
'message': f'Временный комплект "{kit_name}" создан и зарезервирован на витрине "{showcase.name}"',
'kit_id': kit.id,
'kit_name': kit.name,
'kit_price': str(kit.actual_price),
'reservations_count': len(result['reservations'])
})
except json.JSONDecodeError:
return JsonResponse({
'success': False,
'error': 'Неверный формат данных'
}, status=400)
except Exception as e:
return JsonResponse({
'success': False,
'error': f'Ошибка при создании комплекта: {str(e)}'
}, status=500)