diff --git a/myproject/pos/views.py b/myproject/pos/views.py index d52b73f..fd8b278 100644 --- a/myproject/pos/views.py +++ b/myproject/pos/views.py @@ -227,10 +227,25 @@ def pos_terminal(request): # Проверяем что товар существует Product.objects.get(id=item['id']) cart_data[cart_key] = item - elif item['type'] in ('kit', 'showcase_kit'): - # Проверяем что комплект существует + elif item['type'] == 'kit': + # Обычный комплект - только проверяем существование ProductKit.objects.get(id=item['id']) cart_data[cart_key] = item + elif item['type'] == 'showcase_kit': + # Витринный комплект - проверяем существование И актуальность блокировки + kit = ProductKit.objects.get(id=item['id']) + + # Проверяем, что блокировка всё ещё активна для этого пользователя + has_valid_lock = Reservation.objects.filter( + product_kit=kit, + locked_by_user=request.user, + cart_lock_expires_at__gt=timezone.now(), + status='reserved' + ).exists() + + if has_valid_lock: + cart_data[cart_key] = item + # Если блокировка истекла - не добавляем в корзину (товар доступен другим) except (Product.DoesNotExist, ProductKit.DoesNotExist): # Товар или комплект удален - пропускаем continue @@ -468,43 +483,45 @@ def add_showcase_kit_to_cart(request, kit_id): status='active' ) - # Проверяем существующие блокировки - existing_locks = Reservation.objects.filter( - product_kit=kit, - cart_lock_expires_at__gt=timezone.now(), - status='reserved' - ).exclude( - locked_by_user=request.user - ).select_related('locked_by_user') + # Атомарная проверка и создание блокировки (предотвращает race condition) + with transaction.atomic(): + # Блокируем строки резервов для этого комплекта на уровне БД + reservations = Reservation.objects.select_for_update().filter( + product_kit=kit, + status='reserved' + ) - if existing_locks.exists(): - lock = existing_locks.first() - time_left = (lock.cart_lock_expires_at - timezone.now()).total_seconds() / 60 - return JsonResponse({ - 'success': False, - 'error': f'Этот букет уже в корзине кассира "{lock.locked_by_user.username}". ' - f'Блокировка истечет через {int(time_left)} мин.' - }, status=409) # 409 Conflict + # Проверяем существующие блокировки другими пользователями + existing_lock = reservations.filter( + cart_lock_expires_at__gt=timezone.now() + ).exclude( + locked_by_user=request.user + ).select_related('locked_by_user').first() - # Создаем или продлеваем блокировку для текущего пользователя - lock_expires_at = timezone.now() + timedelta(minutes=30) - session_id = request.session.session_key or '' + if existing_lock: + time_left = (existing_lock.cart_lock_expires_at - timezone.now()).total_seconds() / 60 + return JsonResponse({ + 'success': False, + 'error': f'Этот букет уже в корзине кассира "{existing_lock.locked_by_user.username}". ' + f'Блокировка истечет через {int(time_left)} мин.' + }, status=409) # 409 Conflict - # Обновляем все резервы этого комплекта - updated_count = Reservation.objects.filter( - product_kit=kit, - status='reserved' - ).update( - cart_lock_expires_at=lock_expires_at, - locked_by_user=request.user, - cart_session_id=session_id - ) + # Создаем или продлеваем блокировку для текущего пользователя + lock_expires_at = timezone.now() + timedelta(minutes=30) + session_id = request.session.session_key or '' - if updated_count == 0: - return JsonResponse({ - 'success': False, - 'error': 'У комплекта нет активных резервов. Возможно, он уже продан.' - }, status=400) + # Обновляем все резервы этого комплекта (теперь атомарно!) + updated_count = reservations.update( + cart_lock_expires_at=lock_expires_at, + locked_by_user=request.user, + cart_session_id=session_id + ) + + if updated_count == 0: + return JsonResponse({ + 'success': False, + 'error': 'У комплекта нет активных резервов. Возможно, он уже продан.' + }, status=400) return JsonResponse({ 'success': True,