Добавлена система 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

@@ -1,6 +1,7 @@
from django.db import models
from django.utils import timezone
from django.core.exceptions import ValidationError
from django.conf import settings
from decimal import Decimal
from products.models import Product
from phonenumber_field.modelfields import PhoneNumberField
@@ -428,6 +429,27 @@ class Reservation(models.Model):
converted_at = models.DateTimeField(null=True, blank=True,
verbose_name="Дата преобразования в продажу")
# Soft Lock для корзины POS (витринные комплекты)
cart_lock_expires_at = models.DateTimeField(
null=True, blank=True,
verbose_name="Блокировка корзины истекает",
help_text="Время истечения блокировки в корзине (для витринных комплектов)"
)
locked_by_user = models.ForeignKey(
settings.AUTH_USER_MODEL,
on_delete=models.SET_NULL,
null=True, blank=True,
related_name='cart_locks',
verbose_name="Заблокировано пользователем",
help_text="Кассир, который добавил комплект в корзину"
)
cart_session_id = models.CharField(
max_length=100,
null=True, blank=True,
verbose_name="ID сессии корзины",
help_text="Дополнительная идентификация сессии для надежности"
)
class Meta:
verbose_name = "Резервирование"
verbose_name_plural = "Резервирования"
@@ -438,6 +460,9 @@ class Reservation(models.Model):
models.Index(fields=['order_item']),
models.Index(fields=['showcase']),
models.Index(fields=['product_kit']),
models.Index(fields=['cart_lock_expires_at']),
models.Index(fields=['locked_by_user']),
models.Index(fields=['product_kit', 'cart_lock_expires_at']),
]
def __str__(self):