Files
octopus/myproject/inventory/tasks.py
Andrey Smakotin 33e33ecbac Добавлена система 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>
2025-11-20 23:45:34 +03:00

82 lines
3.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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)
}