diff --git a/myproject/celerybeat-schedule b/myproject/celerybeat-schedule new file mode 100644 index 0000000..4410bda Binary files /dev/null and b/myproject/celerybeat-schedule differ diff --git a/myproject/celerybeat-schedule-shm b/myproject/celerybeat-schedule-shm new file mode 100644 index 0000000..22a3005 Binary files /dev/null and b/myproject/celerybeat-schedule-shm differ diff --git a/myproject/celerybeat-schedule-wal b/myproject/celerybeat-schedule-wal new file mode 100644 index 0000000..f2fa960 Binary files /dev/null and b/myproject/celerybeat-schedule-wal differ diff --git a/myproject/inventory/services/showcase_manager.py b/myproject/inventory/services/showcase_manager.py index a60bdc5..61b5017 100644 --- a/myproject/inventory/services/showcase_manager.py +++ b/myproject/inventory/services/showcase_manager.py @@ -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', diff --git a/myproject/pos/views.py b/myproject/pos/views.py index cdc24eb..c8fb9f1 100644 --- a/myproject/pos/views.py +++ b/myproject/pos/views.py @@ -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,16 +943,59 @@ 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: - return JsonResponse({ - 'success': False, - 'error': f'Ошибка при создании комплекта: {str(e)}' - }, status=500) + # Детальное логирование для диагностики 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)}' + }, 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 diff --git a/myproject/products/services/kit_service.py b/myproject/products/services/kit_service.py index 5446606..bce3a5c 100644 --- a/myproject/products/services/kit_service.py +++ b/myproject/products/services/kit_service.py @@ -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 ) # Копируем категории diff --git a/myproject/products/views/api_views.py b/myproject/products/views/api_views.py index 41b8fd7..320a8b9 100644 --- a/myproject/products/views/api_views.py +++ b/myproject/products/views/api_views.py @@ -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,20 +633,68 @@ 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: - return JsonResponse({ - 'success': False, - 'error': f'Ошибка при создании комплекта: {str(e)}' - }, status=500) + # Детальное логирование для диагностики 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)}' + }, 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):