Добавлена система Soft Lock для витринных комплектов в POS-терминале

Реализована элегантная блокировка витринных букетов при добавлении в корзину,
предотвращающая многократную продажу одного физического комплекта.

## Изменения в БД:
- Добавлены поля в Reservation: cart_lock_expires_at, locked_by_user, cart_session_id
- Созданы индексы для оптимизации запросов блокировок
- Миграция 0006: добавление полей Soft Lock

## Backend (pos/views.py):
- add_showcase_kit_to_cart: создание блокировки на 30 минут с проверкой конфликтов
- remove_showcase_kit_from_cart: снятие блокировки при удалении из корзины
- get_showcase_kits_api: возврат статусов блокировок (is_locked, locked_by_me)

## Frontend (terminal.js):
- addToCart: AJAX запрос для создания блокировки, запрет qty > 1
- removeFromCart: автоматическое снятие блокировки
- renderCart: желтый фон, badge "1 шт (витрина)", скрыты кнопки +/−
- UI индикация: зеленый badge "В корзине" (свой), красный "Занят" (чужой)

## Автоматизация (inventory/tasks.py):
- cleanup_expired_cart_locks: Celery periodic task (каждые 5 минут)
- Автоматическое освобождение истекших блокировок (30 минут timeout)
- Логирование очистки для мониторинга

## Маршруты (pos/urls.py):
- POST /api/showcase-kits/<id>/add-to-cart/ - создание блокировки
- POST /api/showcase-kits/<id>/remove-from-cart/ - снятие блокировки

## Документация:
- ЗАПУСК.md: инструкция по запуску Celery Beat

Преимущества:
✓ Предотвращает конфликты между кассирами
✓ Автоматическое освобождение при таймауте
✓ Понятный UX с визуальной индикацией
✓ Совместимость с существующей логикой резервирования

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-20 23:45:34 +03:00
parent ff0756498c
commit 33e33ecbac
8 changed files with 600 additions and 81 deletions

View File

@@ -0,0 +1,81 @@
# -*- coding: utf-8 -*-
from celery import shared_task
from django.utils import timezone
from django.db.models import Q
import logging
logger = logging.getLogger(__name__)
@shared_task
def cleanup_expired_cart_locks():
"""
Периодическая задача для очистки истекших блокировок корзины.
Освобождает витринные комплекты, которые были добавлены в корзину,
но блокировка истекла (timeout 30 минут).
Запускается каждые 5 минут (настроить в celery beat schedule).
Returns:
dict: Статистика очистки {
'released_count': int, # Количество освобожденных блокировок
'affected_kits': list # ID освобожденных комплектов
}
"""
from inventory.models import Reservation
try:
# Находим все резервы с истекшей блокировкой
expired_locks = Reservation.objects.filter(
Q(cart_lock_expires_at__lte=timezone.now()) &
Q(cart_lock_expires_at__isnull=False) &
Q(status='reserved')
).select_related('product_kit', 'locked_by_user')
# Собираем статистику перед очисткой
affected_kits = list(
expired_locks.values_list('product_kit_id', flat=True).distinct()
)
released_count = expired_locks.count()
# Логируем информацию о блокировках
if released_count > 0:
logger.info(
f"Очистка истекших блокировок: {released_count} резервов, "
f"{len(affected_kits)} комплектов"
)
for lock in expired_locks[:10]: # Логируем первые 10 для отладки
kit_name = lock.product_kit.name if lock.product_kit else 'N/A'
user_name = lock.locked_by_user.username if lock.locked_by_user else 'N/A'
logger.debug(
f"Освобождение блокировки: комплект='{kit_name}', "
f"пользователь='{user_name}', "
f"истекла={lock.cart_lock_expires_at}"
)
# Очищаем блокировки
expired_locks.update(
cart_lock_expires_at=None,
locked_by_user=None,
cart_session_id=None
)
result = {
'released_count': released_count,
'affected_kits': affected_kits,
'timestamp': timezone.now().isoformat()
}
if released_count > 0:
logger.info(f"Очистка завершена успешно: {result}")
return result
except Exception as e:
logger.error(f"Ошибка при очистке истекших блокировок: {str(e)}", exc_info=True)
return {
'released_count': 0,
'affected_kits': [],
'error': str(e)
}