Fix cart lock validation and error handling improvements

## 1. Add cart lock validation to sell_from_showcase()
- Prevent selling showcase kits locked in another cashier's cart
- Check cart_lock_expires_at before allowing direct sales
- Return clear error message with lock holder's name and time remaining
- File: inventory/services/showcase_manager.py

## 2. Improve error handling in POS create_temp_kit_to_showcase()
- Add detailed logging for all error types (JSON, validation, generic)
- Provide user-friendly error messages instead of generic 500
- Log full context (kit name, showcase ID, items, user) for debugging
- Categorize errors: stock issues, integrity, locks, not found
- File: pos/views.py

## 3. Fix critical bug in create_temporary_kit()
- Replace non-existent is_active field with status='active'
- Affects 3 locations: kit creation, product lookup, kit duplication
- This was causing 500 errors when creating temporary kits from order edit
- File: products/services/kit_service.py

## 4. Improve error handling in create_temporary_kit_api()
- Add comprehensive logging for order creation endpoint
- Provide specific error messages for common failure scenarios
- Help diagnose issues when creating kits from order editing UI
- File: products/views/api_views.py

These changes complete the Soft Lock system and fix the 500 error issue.

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-21 00:24:59 +03:00
parent 33e33ecbac
commit 08a5527ba7
7 changed files with 135 additions and 16 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -144,7 +144,7 @@ class ShowcaseManager:
reservations = Reservation.objects.filter(
showcase=showcase,
status='reserved'
).select_related('product')
).select_related('product', 'locked_by_user')
if not reservations.exists():
return {
@@ -153,6 +153,26 @@ class ShowcaseManager:
'message': f'На витрине "{showcase.name}" нет зарезервированных товаров'
}
# Проверяем блокировки корзины (Soft Lock)
# Если комплект заблокирован в корзине другого кассира, запрещаем продажу
active_locks = reservations.filter(
cart_lock_expires_at__gt=timezone.now(),
cart_lock_expires_at__isnull=False
)
if active_locks.exists():
lock = active_locks.first()
time_left = (lock.cart_lock_expires_at - timezone.now()).total_seconds() / 60
locker_name = lock.locked_by_user.username if lock.locked_by_user else 'неизвестный кассир'
return {
'success': False,
'order': None,
'message': f'Комплект заблокирован в корзине кассира "{locker_name}". '
f'Блокировка истечёт через {int(time_left)} мин. '
f'Дождитесь освобождения или попросите кассира удалить букет из корзины.'
}
# Получаем статус "Завершён" для POS-продаж
completed_status = OrderStatus.objects.filter(
code='completed',

View File

@@ -7,13 +7,17 @@ from django.db import transaction
from django.db.models import Prefetch, OuterRef, Subquery, DecimalField
from django.db.models.functions import Coalesce
from django.utils import timezone
from django.core.exceptions import ValidationError
from decimal import Decimal, InvalidOperation
import json
import logging
from products.models import Product, ProductCategory, ProductKit, KitItem
from inventory.models import Showcase, Reservation, Warehouse, Stock
from inventory.services import ShowcaseManager
logger = logging.getLogger(__name__)
def get_pos_warehouse(request):
"""
@@ -939,15 +943,58 @@ def create_temp_kit_to_showcase(request):
'reservations_count': len(result['reservations'])
})
except json.JSONDecodeError:
except json.JSONDecodeError as e:
logger.error(f'JSON decode error при создании временного комплекта: {str(e)}')
return JsonResponse({
'success': False,
'error': 'Неверный формат данных'
'error': 'Неверный формат данных корзины'
}, status=400)
except Showcase.DoesNotExist:
logger.warning(f'Попытка создать комплект на несуществующей витрине (ID: {request.POST.get("showcase_id")})')
return JsonResponse({
'success': False,
'error': 'Выбранная витрина не найдена'
}, status=404)
except ValidationError as e:
logger.error(f'Validation error при создании временного комплекта: {str(e)}', exc_info=True)
return JsonResponse({
'success': False,
'error': f'Ошибка валидации: {str(e)}'
}, status=400)
except Exception as e:
# Детальное логирование для диагностики 500 ошибок
logger.error(
f'Непредвиденная ошибка при создании временного комплекта:\n'
f' Название: {request.POST.get("kit_name")}\n'
f' Витрина ID: {request.POST.get("showcase_id")}\n'
f' Товары: {request.POST.get("items")}\n'
f' Пользователь: {request.user.username}\n'
f' Ошибка: {str(e)}',
exc_info=True
)
# Проверяем на типичные ошибки и даём понятные сообщения
error_msg = str(e).lower()
if 'недостаточно' in error_msg or 'insufficient' in error_msg or 'stock' in error_msg:
return JsonResponse({
'success': False,
'error': f'Ошибка при создании комплекта: {str(e)}'
'error': f'Недостаточно товара на складе. {str(e)}'
}, status=400)
elif 'integrity' in error_msg or 'constraint' in error_msg:
return JsonResponse({
'success': False,
'error': 'Ошибка целостности данных. Проверьте, что все товары существуют и витрина активна.'
}, status=400)
elif 'lock' in error_msg or 'blocked' in error_msg or 'заблокирован' in error_msg:
return JsonResponse({
'success': False,
'error': f'Конфликт блокировки: {str(e)}'
}, status=409)
else:
return JsonResponse({
'success': False,
'error': f'Не удалось создать комплект: {str(e)}. Проверьте консоль сервера для деталей.'
}, status=500)

View File

@@ -57,7 +57,7 @@ def create_temporary_kit(
name=name.strip(),
description=description.strip() if description else '',
is_temporary=True,
is_active=True,
status='active',
order=order,
price_adjustment_type='none'
)
@@ -72,7 +72,7 @@ def create_temporary_kit(
continue
try:
product = Product.objects.get(pk=product_id, is_active=True)
product = Product.objects.get(pk=product_id, status='active')
KitItem.objects.create(
kit=kit,
product=product,
@@ -135,7 +135,7 @@ def duplicate_kit(kit: ProductKit, new_name: Optional[str] = None) -> ProductKit
price_adjustment_value=kit.price_adjustment_value,
sale_price=kit.sale_price,
is_temporary=False, # Копия всегда постоянная
is_active=kit.is_active
status=kit.status
)
# Копируем категории

View File

@@ -4,9 +4,13 @@ API представления для приложения products.
from django.http import JsonResponse
from django.db import models
from django.core.cache import cache
from django.core.exceptions import ValidationError
import logging
from ..models import Product, ProductVariantGroup, ProductKit
logger = logging.getLogger(__name__)
def search_products_and_variants(request):
"""
@@ -629,19 +633,67 @@ def create_temporary_kit_api(request):
})
except ValueError as e:
logger.warning(f'Validation error при создании временного комплекта: {str(e)}')
return JsonResponse({
'success': False,
'error': str(e)
}, status=400)
except json.JSONDecodeError:
except json.JSONDecodeError as e:
logger.error(f'JSON decode error при создании временного комплекта: {str(e)}')
return JsonResponse({
'success': False,
'error': 'Некорректный JSON'
'error': 'Некорректный JSON в запросе'
}, status=400)
except ValidationError as e:
logger.error(f'Django ValidationError при создании временного комплекта: {str(e)}', exc_info=True)
return JsonResponse({
'success': False,
'error': f'Ошибка валидации: {str(e)}'
}, status=400)
except Exception as e:
# Детальное логирование для диагностики 500 ошибок
try:
data = json.loads(request.body)
name = data.get('name', 'N/A')
order_id = data.get('order_id', 'N/A')
components_count = len(data.get('components', []))
except:
name = 'N/A'
order_id = 'N/A'
components_count = 'N/A'
logger.error(
f'Непредвиденная ошибка при создании временного комплекта:\n'
f' Название: {name}\n'
f' Заказ ID: {order_id}\n'
f' Количество компонентов: {components_count}\n'
f' Пользователь: {request.user.username if request.user.is_authenticated else "Anonymous"}\n'
f' Ошибка: {str(e)}',
exc_info=True
)
# Проверяем на типичные ошибки и даём понятные сообщения
error_msg = str(e).lower()
if 'недостаточно' in error_msg or 'insufficient' in error_msg or 'stock' in error_msg:
return JsonResponse({
'success': False,
'error': f'Ошибка при создании комплекта: {str(e)}'
'error': f'Недостаточно товара на складе. {str(e)}'
}, status=400)
elif 'integrity' in error_msg or 'constraint' in error_msg:
return JsonResponse({
'success': False,
'error': 'Ошибка целостности данных. Проверьте, что все товары существуют.'
}, status=400)
elif 'not found' in error_msg or 'does not exist' in error_msg or 'не найден' in error_msg:
return JsonResponse({
'success': False,
'error': f'Объект не найден: {str(e)}'
}, status=404)
else:
return JsonResponse({
'success': False,
'error': f'Не удалось создать комплект: {str(e)}. Проверьте консоль сервера для деталей.'
}, status=500)