From 08a5527ba7daa887f7379f78a3168384278783e5 Mon Sep 17 00:00:00 2001 From: Andrey Smakotin Date: Fri, 21 Nov 2025 00:24:59 +0300 Subject: [PATCH] Fix cart lock validation and error handling improvements MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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 --- myproject/celerybeat-schedule | Bin 0 -> 4096 bytes myproject/celerybeat-schedule-shm | Bin 0 -> 32768 bytes myproject/celerybeat-schedule-wal | Bin 0 -> 152472 bytes .../inventory/services/showcase_manager.py | 22 +++++- myproject/pos/views.py | 59 ++++++++++++++-- myproject/products/services/kit_service.py | 6 +- myproject/products/views/api_views.py | 64 ++++++++++++++++-- 7 files changed, 135 insertions(+), 16 deletions(-) create mode 100644 myproject/celerybeat-schedule create mode 100644 myproject/celerybeat-schedule-shm create mode 100644 myproject/celerybeat-schedule-wal diff --git a/myproject/celerybeat-schedule b/myproject/celerybeat-schedule new file mode 100644 index 0000000000000000000000000000000000000000..4410bda55bad27954e72ab65919e742875d57998 GIT binary patch literal 4096 zcmWFz^vNtqRY=P(%1ta$FlG>7U}9o$P*7lCU|@t|AVoG{WYFsp;RR_IAlr;ljiVtj n8UmvsFd71*Aut*OqaiRF0;3@?8UmvsFd71*Aut*O6ovo*Y?%hH literal 0 HcmV?d00001 diff --git a/myproject/celerybeat-schedule-shm b/myproject/celerybeat-schedule-shm new file mode 100644 index 0000000000000000000000000000000000000000..22a30055fa136647c94f2068544957d7ce511604 GIT binary patch literal 32768 zcmeI)yDmgg6b9hm%(xo{gX1>trQXx%^crs&i5xP z>tttV?|r@pSk>!c>MWERNqPsRw@dY3PM^9*orkOE`?H(#>x;+BySL-plR@>v{pG07 z)aSl`%h&R-mAlg_Rp0$z`tWf+2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7eo3I7Iw1s_DbPwM zW0}ZQW-^;znE}(!a;mIlBZF*bH~Yus+`H7i&XEztUkTK?;m?} ztnz93F0&V~%SS{nATF2?KmY**5I_I{1Q0*~0R#|0V6Y2R*&S7Ne*Zc(kqpOUet(Cv zyTsaOy`bJ#=bL2suGtHm4jn(L&HCBLW-nluPut|v6a$0(z_Wh{Ab+~}h0I>SA@5E10^$o30tg_000IagfB*srAb;)Y1iF7X@ zzAzzx00IagfB*srAbsN+qq$aRa@;KaZWLfBC~ly3Ag{DH~QVAZC~l zKmY**5I_I{1Q0*~0R#|0;FbuCbULcaQ(Dlk#*{!*ZRv1s_1&so;DvuEH9KCP95#Ca zr~F5&7Z5W{2q1s}0tg_000IagfB*srATY=U%rGG92?ghk_X4ktI;i|tJ@ueRK5KJ& z#NQDdk@w1mm|;Qy0R#|0009ILKmY**5XeJ;&M5`X%FQb;yN8dXy?Eg`n07q`V@YB;t!E@oGV*}3MdSYF~!#ahF`NK`d88%xAq_WgtV zCdpiCem2?kQfoX(Yar$(f{A!cQv&92bVtp^dPA3LjL5~b_AV!Cudn**4q(wR24d^ z`YdpEmsn@8UQq9=^G*6$EJKI0J z@8XMRK6l9{dV7I0yuQHD`I*2-5I_I{1Q0*~0R#|0009JsmcZ3|0Wngy)@Hu2pgZ%{ z1Vz{C1ztEk{gD%sTUL2wBi##RY5{rA&@y8*2q1s}0tg_000IagfWUAOpclx;UO=3k zW#w6KH`p<<*iqGIfpe=bN4>zkFKbKNv2L7{-YJ0tg_0 z00IagfB*srAb>!A31s&IV$a>y6AG8TH9_%p_5#zYp5J}G;?8*d0tg_000IagfB*sr3=@H@ULb3> zWPrVZE|t4JTT-+-(+j-S(+j*cOfVTH0tg_000IagfB*srAduGroy{(1<(8`67~s&& ziUF>k65Pa2pjsS&h8%VRl3r?c4@oO{FjItJA|BI}fSEn$j+%+}hA!0@p|6-9nkWp~ zjlhImLQ|JOT(HfB*srAbupRQIMGTV{?O{qJ9O9DI*nAnn8j0tg_0 z00IagfB*srAb&qKdW?ioQZFz606JA?oN2q1s}0tg_000IbHQ=qfi zlkdI2G%?6EIqU_T^aAPA=!(67Hsko#iiNWe&RRKmY**5I_I{1Q0*~0R#{j z1Ool{0-gc(0uRqP+5E{5ezKchU=SQyb^!qd5I_I{1Q0*~0R#}ZL4nTZJMz017;O%? z{P`UA0tNH}>67Y;y+G6VetPoaGaIMU3*6v?kDDWa00IagfB*srAb7f=7!d&<+V(F=U3haz`I009ILKmY**5I_I{1Q0;r776s<3&;cP1>Wtt z5E`}ihv(@9ZqeamGYBAn00IagfB*srAb`M46zFU&&G%kF{5SQe=W^H!6wwQ$Pq-`g z0y`&^7Ir;V`vJYcO?+UoDg+Qf009ILKmY**5I_I{1ac5I_I{1Q0*~0R#|0;3f+6-wTvoXD{%7bToNw literal 0 HcmV?d00001 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):