diff --git a/myproject/pos/static/pos/js/terminal.js b/myproject/pos/static/pos/js/terminal.js
index 0ea5409..0b1f92b 100644
--- a/myproject/pos/static/pos/js/terminal.js
+++ b/myproject/pos/static/pos/js/terminal.js
@@ -570,10 +570,18 @@ function renderProducts() {
const stock = document.createElement('div');
stock.className = 'product-stock';
- // Для витринных комплектов показываем название витрины И доступное количество
+ // Для витринных комплектов показываем название витрины И количество (доступно/всего)
if (item.type === 'showcase_kit') {
- const availableCount = item.available_count || 1;
- stock.innerHTML = `🌺 ${item.showcase_name} ${availableCount} шт`;
+ const availableCount = item.available_count || 0;
+ const totalCount = item.total_count || availableCount;
+ const inCart = totalCount - availableCount;
+
+ // Показываем: доступно / всего (и сколько в корзине)
+ let badgeClass = availableCount > 0 ? 'bg-success' : 'bg-secondary';
+ let badgeText = totalCount > 1 ? `${availableCount}/${totalCount}` : `${availableCount}`;
+ let cartInfo = inCart > 0 ? ` 🛒${inCart}` : '';
+
+ stock.innerHTML = `🌺 ${item.showcase_name} ${badgeText}${cartInfo}`;
stock.style.color = '#856404';
stock.style.fontWeight = 'bold';
} else if (item.type === 'product' && item.available_qty !== undefined && item.reserved_qty !== undefined) {
@@ -736,35 +744,10 @@ function setupInfiniteScroll() {
async function addToCart(item) {
const cartKey = `${item.type}-${item.id}`; // Уникальный ключ: "product-1" или "kit-1"
- // СПЕЦИАЛЬНАЯ ЛОГИКА ДЛЯ ВИТРИННЫХ КОМПЛЕКТОВ (Soft Lock + количество)
+ // СПЕЦИАЛЬНАЯ ЛОГИКА ДЛЯ ВИТРИННЫХ КОМПЛЕКТОВ (Soft Lock)
if (item.type === 'showcase_kit') {
- // Определяем сколько доступно и сколько добавить
- const availableCount = item.available_count || 1;
- const currentInCart = cart.has(cartKey) ? cart.get(cartKey).qty : 0;
- const remainingAvailable = availableCount - currentInCart;
-
- if (remainingAvailable <= 0) {
- alert(`Все ${availableCount} экз. этого букета уже в корзине.`);
- return;
- }
-
- // Если доступно > 1, спрашиваем количество
- let quantityToAdd = 1;
- if (remainingAvailable > 1 && !cart.has(cartKey)) {
- const input = prompt(
- `Доступно ${availableCount} экз. букета "${item.name}".\n` +
- `Сколько добавить в корзину? (1-${remainingAvailable})`,
- '1'
- );
- if (input === null) return; // Отмена
- quantityToAdd = parseInt(input, 10);
- if (isNaN(quantityToAdd) || quantityToAdd < 1 || quantityToAdd > remainingAvailable) {
- alert(`Введите число от 1 до ${remainingAvailable}`);
- return;
- }
- }
-
- // Пытаемся создать блокировку через API
+ // Пытаемся заблокировать 1 экземпляр через API
+ // API сам проверит доступность и вернёт ошибку если нет свободных
try {
const response = await fetch(`/pos/api/showcase-kits/${item.id}/add-to-cart/`, {
method: 'POST',
@@ -772,14 +755,18 @@ async function addToCart(item) {
'X-CSRFToken': getCookie('csrftoken'),
'Content-Type': 'application/json'
},
- body: JSON.stringify({ quantity: quantityToAdd })
+ body: JSON.stringify({ quantity: 1 })
});
const data = await response.json();
if (!response.ok || !data.success) {
- // Конфликт - комплект занят другим кассиром
+ // Нет доступных экземпляров или другая ошибка
alert(data.error || 'Не удалось добавить букет в корзину');
+ // Обновляем витрину чтобы показать актуальное состояние
+ if (isShowcaseView) {
+ await loadShowcaseKits();
+ }
return;
}
@@ -791,7 +778,6 @@ async function addToCart(item) {
const existing = cart.get(cartKey);
existing.qty += lockedItemIds.length;
existing.showcase_item_ids = [...(existing.showcase_item_ids || []), ...lockedItemIds];
- existing.max_qty = availableCount;
} else {
// Создаём новую запись
cart.set(cartKey, {
@@ -800,13 +786,12 @@ async function addToCart(item) {
price: Number(item.price),
qty: lockedItemIds.length,
type: item.type,
- max_qty: availableCount, // Максимум = сколько доступно
- showcase_item_ids: lockedItemIds, // ID заблокированных экземпляров
+ showcase_item_ids: lockedItemIds,
lock_expires_at: data.lock_expires_at
});
}
- // Обновляем список витрины (чтобы показать блокировку)
+ // Обновляем список витрины (чтобы показать актуальные available_count)
if (isShowcaseView) {
await loadShowcaseKits();
}
@@ -919,7 +904,7 @@ function renderCart() {
if (isShowcaseKit) {
const badge = document.createElement('span');
badge.className = 'badge bg-warning text-dark';
- badge.textContent = '1 шт (витрина)';
+ badge.textContent = `${item.qty} шт (витрина)`;
badge.style.fontSize = '0.85rem';
badge.style.padding = '0.5rem 0.75rem';
qtyControl.appendChild(badge);
@@ -1040,10 +1025,28 @@ async function removeFromCart(cartKey) {
}
}
-function clearCart() {
+async function clearCart() {
+ // Сбрасываем все свои блокировки витринных букетов
+ try {
+ await fetch('/pos/api/showcase-kits/release-all-my-locks/', {
+ method: 'POST',
+ headers: { 'X-CSRFToken': getCookie('csrftoken') }
+ });
+ } catch (e) {
+ console.error('Ошибка сброса блокировок:', e);
+ }
+
+ // Очищаем корзину
cart.clear();
renderCart();
saveCartToRedis(); // Сохраняем пустую корзину в Redis
+
+ // Обновляем отображение товаров/витрины чтобы показать актуальные остатки
+ if (isShowcaseView) {
+ await loadShowcaseKits();
+ } else {
+ renderProducts(); // Перерисовать карточки товаров с актуальными остатками
+ }
}
document.getElementById('clearCart').onclick = clearCart;
diff --git a/myproject/pos/urls.py b/myproject/pos/urls.py
index 638adf7..4056778 100644
--- a/myproject/pos/urls.py
+++ b/myproject/pos/urls.py
@@ -25,6 +25,8 @@ urlpatterns = [
path('api/create-draft/', views.create_order_draft, name='create-order-draft'),
# Снять блокировку витринного комплекта при удалении из корзины [POST]
path('api/showcase-kits//remove-from-cart/', views.remove_showcase_kit_from_cart, name='remove-showcase-kit-from-cart'),
+ # Сбросить ВСЕ свои блокировки витринных букетов [POST]
+ path('api/showcase-kits/release-all-my-locks/', views.release_all_my_showcase_locks, name='release-all-my-showcase-locks'),
# Получить детали комплекта для редактирования [GET]
path('api/product-kits//', views.get_product_kit_details, name='get-product-kit-details'),
# Обновить временный комплект (состав, фото, цены) [POST]
diff --git a/myproject/pos/views.py b/myproject/pos/views.py
index 0e8b18f..0c5ec48 100644
--- a/myproject/pos/views.py
+++ b/myproject/pos/views.py
@@ -57,16 +57,17 @@ def get_showcase_kits_for_pos():
НОВАЯ АРХИТЕКТУРА с ShowcaseItem:
- Группирует экземпляры по (product_kit, showcase)
- - Возвращает available_count и showcase_item_ids для каждой группы
- - Позволяет продавать несколько экземпляров одного букета
+ - Показывает ВСЕ букеты (available + in_cart), не только доступные
+ - Возвращает available_count (сколько можно добавить) и total_count (всего)
"""
from products.models import ProductKitPhoto
from inventory.models import ShowcaseItem
- from django.db.models import Count, Prefetch as DjangoPrefetch
+ from django.db.models import Count, Q
- # Группируем доступные ShowcaseItem по (product_kit, showcase)
- available_items = ShowcaseItem.objects.filter(
- status='available',
+ # Группируем ShowcaseItem по (product_kit, showcase)
+ # Включаем и available, и in_cart (чтобы видеть букеты в корзине)
+ all_items = ShowcaseItem.objects.filter(
+ status__in=['available', 'in_cart'],
showcase__is_active=True
).select_related(
'product_kit',
@@ -80,14 +81,15 @@ def get_showcase_kits_for_pos():
'showcase_id',
'showcase__name'
).annotate(
- available_count=Count('id')
+ total_count=Count('id'),
+ available_count=Count('id', filter=Q(status='available'))
).order_by('showcase__name', 'product_kit__name')
- if not available_items:
+ if not all_items:
return []
# Получаем ID всех комплектов для загрузки фото
- kit_ids = list(set(item['product_kit_id'] for item in available_items))
+ kit_ids = list(set(item['product_kit_id'] for item in all_items))
# Загружаем первые фото для комплектов
kit_photos = {}
@@ -101,12 +103,12 @@ def get_showcase_kits_for_pos():
# Формируем результат
showcase_kits = []
- for item in available_items:
+ for item in all_items:
kit_id = item['product_kit_id']
showcase_id = item['showcase_id']
- # Получаем IDs всех доступных экземпляров этой группы
- item_ids = list(ShowcaseItem.objects.filter(
+ # Получаем IDs только ДОСТУПНЫХ экземпляров этой группы
+ available_item_ids = list(ShowcaseItem.objects.filter(
product_kit_id=kit_id,
showcase_id=showcase_id,
status='available'
@@ -120,15 +122,16 @@ def get_showcase_kits_for_pos():
'name': item['product_kit__name'],
'price': str(price),
'category_ids': [],
- 'in_stock': True,
+ 'in_stock': item['available_count'] > 0, # Есть ли доступные
'sku': item['product_kit__sku'] or '',
'image': kit_photos.get(kit_id),
'type': 'showcase_kit',
'showcase_name': item['showcase__name'],
'showcase_id': showcase_id,
- # НОВЫЕ ПОЛЯ для поддержки количества
- 'available_count': item['available_count'],
- 'showcase_item_ids': item_ids
+ # Количества
+ 'available_count': item['available_count'], # Сколько можно добавить
+ 'total_count': item['total_count'], # Всего на витрине (включая в корзине)
+ 'showcase_item_ids': available_item_ids # IDs только доступных
})
return showcase_kits
@@ -404,6 +407,21 @@ def get_showcase_kits_api(request):
Включает информацию о блокировках в корзинах.
"""
from datetime import timedelta
+ from inventory.models import ShowcaseItem
+
+ # Очищаем только ИСТЁКШИЕ блокировки (cart_lock_expires_at < now)
+ expired_locks = ShowcaseItem.objects.filter(
+ status='in_cart',
+ cart_lock_expires_at__lt=timezone.now()
+ )
+ expired_count = expired_locks.update(
+ status='available',
+ locked_by_user=None,
+ cart_lock_expires_at=None,
+ cart_session_id=None
+ )
+ if expired_count > 0:
+ logger.info(f'Очищено {expired_count} истёкших блокировок ShowcaseItem')
showcase_kits_data = get_showcase_kits_for_pos()
@@ -630,6 +648,41 @@ def remove_showcase_kit_from_cart(request, kit_id):
}, status=500)
+@login_required
+@require_http_methods(["POST"])
+def release_all_my_showcase_locks(request):
+ """
+ API endpoint для сброса ВСЕХ блокировок витринных букетов текущего пользователя.
+ Используется при загрузке POS если корзина пустая, чтобы освободить зависшие блокировки.
+ """
+ from inventory.models import ShowcaseItem
+
+ try:
+ # Снимаем ВСЕ блокировки текущего пользователя
+ updated_count = ShowcaseItem.objects.filter(
+ status='in_cart',
+ locked_by_user=request.user
+ ).update(
+ status='available',
+ locked_by_user=None,
+ cart_lock_expires_at=None,
+ cart_session_id=None
+ )
+
+ return JsonResponse({
+ 'success': True,
+ 'message': f'Освобождено {updated_count} блокировок',
+ 'released_count': updated_count
+ })
+
+ except Exception as e:
+ logger.error(f'Ошибка сброса блокировок: {str(e)}', exc_info=True)
+ return JsonResponse({
+ 'success': False,
+ 'error': str(e)
+ }, status=500)
+
+
@login_required
@require_http_methods(["GET"])
def get_items_api(request):