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:
BIN
myproject/celerybeat-schedule
Normal file
BIN
myproject/celerybeat-schedule
Normal file
Binary file not shown.
BIN
myproject/celerybeat-schedule-shm
Normal file
BIN
myproject/celerybeat-schedule-shm
Normal file
Binary file not shown.
BIN
myproject/celerybeat-schedule-wal
Normal file
BIN
myproject/celerybeat-schedule-wal
Normal file
Binary file not shown.
@@ -144,7 +144,7 @@ class ShowcaseManager:
|
|||||||
reservations = Reservation.objects.filter(
|
reservations = Reservation.objects.filter(
|
||||||
showcase=showcase,
|
showcase=showcase,
|
||||||
status='reserved'
|
status='reserved'
|
||||||
).select_related('product')
|
).select_related('product', 'locked_by_user')
|
||||||
|
|
||||||
if not reservations.exists():
|
if not reservations.exists():
|
||||||
return {
|
return {
|
||||||
@@ -153,6 +153,26 @@ class ShowcaseManager:
|
|||||||
'message': f'На витрине "{showcase.name}" нет зарезервированных товаров'
|
'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-продаж
|
# Получаем статус "Завершён" для POS-продаж
|
||||||
completed_status = OrderStatus.objects.filter(
|
completed_status = OrderStatus.objects.filter(
|
||||||
code='completed',
|
code='completed',
|
||||||
|
|||||||
@@ -7,13 +7,17 @@ from django.db import transaction
|
|||||||
from django.db.models import Prefetch, OuterRef, Subquery, DecimalField
|
from django.db.models import Prefetch, OuterRef, Subquery, DecimalField
|
||||||
from django.db.models.functions import Coalesce
|
from django.db.models.functions import Coalesce
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
from decimal import Decimal, InvalidOperation
|
from decimal import Decimal, InvalidOperation
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
|
|
||||||
from products.models import Product, ProductCategory, ProductKit, KitItem
|
from products.models import Product, ProductCategory, ProductKit, KitItem
|
||||||
from inventory.models import Showcase, Reservation, Warehouse, Stock
|
from inventory.models import Showcase, Reservation, Warehouse, Stock
|
||||||
from inventory.services import ShowcaseManager
|
from inventory.services import ShowcaseManager
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def get_pos_warehouse(request):
|
def get_pos_warehouse(request):
|
||||||
"""
|
"""
|
||||||
@@ -939,16 +943,59 @@ def create_temp_kit_to_showcase(request):
|
|||||||
'reservations_count': len(result['reservations'])
|
'reservations_count': len(result['reservations'])
|
||||||
})
|
})
|
||||||
|
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError as e:
|
||||||
|
logger.error(f'JSON decode error при создании временного комплекта: {str(e)}')
|
||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
'success': False,
|
'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)
|
}, status=400)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return JsonResponse({
|
# Детальное логирование для диагностики 500 ошибок
|
||||||
'success': False,
|
logger.error(
|
||||||
'error': f'Ошибка при создании комплекта: {str(e)}'
|
f'Непредвиденная ошибка при создании временного комплекта:\n'
|
||||||
}, status=500)
|
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)}'
|
||||||
|
}, 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)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ def create_temporary_kit(
|
|||||||
name=name.strip(),
|
name=name.strip(),
|
||||||
description=description.strip() if description else '',
|
description=description.strip() if description else '',
|
||||||
is_temporary=True,
|
is_temporary=True,
|
||||||
is_active=True,
|
status='active',
|
||||||
order=order,
|
order=order,
|
||||||
price_adjustment_type='none'
|
price_adjustment_type='none'
|
||||||
)
|
)
|
||||||
@@ -72,7 +72,7 @@ def create_temporary_kit(
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
try:
|
try:
|
||||||
product = Product.objects.get(pk=product_id, is_active=True)
|
product = Product.objects.get(pk=product_id, status='active')
|
||||||
KitItem.objects.create(
|
KitItem.objects.create(
|
||||||
kit=kit,
|
kit=kit,
|
||||||
product=product,
|
product=product,
|
||||||
@@ -135,7 +135,7 @@ def duplicate_kit(kit: ProductKit, new_name: Optional[str] = None) -> ProductKit
|
|||||||
price_adjustment_value=kit.price_adjustment_value,
|
price_adjustment_value=kit.price_adjustment_value,
|
||||||
sale_price=kit.sale_price,
|
sale_price=kit.sale_price,
|
||||||
is_temporary=False, # Копия всегда постоянная
|
is_temporary=False, # Копия всегда постоянная
|
||||||
is_active=kit.is_active
|
status=kit.status
|
||||||
)
|
)
|
||||||
|
|
||||||
# Копируем категории
|
# Копируем категории
|
||||||
|
|||||||
@@ -4,9 +4,13 @@ API представления для приложения products.
|
|||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.core.cache import cache
|
from django.core.cache import cache
|
||||||
|
from django.core.exceptions import ValidationError
|
||||||
|
import logging
|
||||||
|
|
||||||
from ..models import Product, ProductVariantGroup, ProductKit
|
from ..models import Product, ProductVariantGroup, ProductKit
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def search_products_and_variants(request):
|
def search_products_and_variants(request):
|
||||||
"""
|
"""
|
||||||
@@ -629,20 +633,68 @@ def create_temporary_kit_api(request):
|
|||||||
})
|
})
|
||||||
|
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
|
logger.warning(f'Validation error при создании временного комплекта: {str(e)}')
|
||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
'success': False,
|
'success': False,
|
||||||
'error': str(e)
|
'error': str(e)
|
||||||
}, status=400)
|
}, status=400)
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError as e:
|
||||||
|
logger.error(f'JSON decode error при создании временного комплекта: {str(e)}')
|
||||||
return JsonResponse({
|
return JsonResponse({
|
||||||
'success': False,
|
'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)
|
}, status=400)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return JsonResponse({
|
# Детальное логирование для диагностики 500 ошибок
|
||||||
'success': False,
|
try:
|
||||||
'error': f'Ошибка при создании комплекта: {str(e)}'
|
data = json.loads(request.body)
|
||||||
}, status=500)
|
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)}'
|
||||||
|
}, 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)
|
||||||
|
|
||||||
|
|
||||||
def create_tag_api(request):
|
def create_tag_api(request):
|
||||||
|
|||||||
Reference in New Issue
Block a user