From 5d24b1cd6eef180f1be08cc020097e96ba34e693 Mon Sep 17 00:00:00 2001 From: Andrey Smakotin Date: Mon, 8 Dec 2025 17:58:40 +0300 Subject: [PATCH] =?UTF-8?q?=D0=92=D0=B8=D1=82=D1=80=D0=B8=D0=BD=D0=BD?= =?UTF-8?q?=D1=8B=D0=B5=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BB=D0=B5=D0=BA=D1=82?= =?UTF-8?q?=D1=8B=20=D0=BE=D1=81=D1=82=D0=B0=D1=8E=D1=82=D1=81=D1=8F=20?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=B2=D0=B8=D1=82=D1=80=D0=B8=D0=BD=D0=B5=20?= =?UTF-8?q?=D0=BF=D1=80=D0=B8=20=D0=BE=D1=82=D0=BC=D0=B5=D0=BD=D0=B5=20?= =?UTF-8?q?=D0=B7=D0=B0=D0=BA=D0=B0=D0=B7=D0=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Проблема: при отмене заказа с витринным временным комплектом резервы освобождались (status='released'), и букет исчезал с витрины. Это неправильное поведение для временных комплектов на витрине - они должны оставаться доступными для продажи. Решение: - В сигнале rollback_sale_on_status_change добавлено разделение резервов на: * Обычные резервы - работают как раньше (released при отмене, reserved при возврате) * Витринные временные комплекты (is_temporary=True, showcase!=null) - ВСЕГДА возвращаются в статус reserved, независимо от типа отката заказа - Для витринных комплектов сохраняются привязки к showcase и product_kit - Букеты остаются видимыми на витрине и доступны для повторной продажи Бизнес-логика: - При ВОЗВРАТЕ (completed → draft/in_delivery): букет возвращается на витрину - При ОТМЕНЕ (completed → cancelled): букет ТАКЖЕ возвращается на витрину - Букет можно убрать только вручную через функцию разбора комплекта --- myproject/inventory/signals.py | 75 +++++++++++++++++++++++----------- 1 file changed, 52 insertions(+), 23 deletions(-) diff --git a/myproject/inventory/signals.py b/myproject/inventory/signals.py index 3d9095e..3c4f907 100644 --- a/myproject/inventory/signals.py +++ b/myproject/inventory/signals.py @@ -247,13 +247,15 @@ def rollback_sale_on_status_change(sender, instance, created, **kwargs): Процесс: 1. Отслеживаем переход ОТ статуса 'completed' 2. Удаляем Sale и восстанавливаем StockBatch через SaleBatchAllocation - 3. Обновляем резервы (reserved или released в зависимости от сценария) + 3. Обновляем резервы: + - Обычные резервы: reserved или released в зависимости от сценария + - Витринные временные комплекты: ВСЕГДА reserved (остаются на витрине) 4. Обновляем Stock - 5. Устанавливаем is_returned для отмены Сценарии: - - А (ошибка): completed → draft/in_delivery → резервы возвращаются в 'reserved' - - Б (отмена): completed → cancelled → резервы освобождаются в 'released' + - А (ошибка/возврат): completed → draft/in_delivery → обычные резервы в 'reserved' + - Б (отмена): completed → cancelled → обычные резервы в 'released' + - В (витринные комплекты): любой уход от completed → резервы в 'reserved' (букет остаётся на витрине) ПРИМЕЧАНИЕ: Этот сигнал ОБРАБАТЫВАЕТ ТОЛЬКО переход ОТ 'completed'! Для перехода к 'cancelled' из любого статуса см. release_reservations_on_cancellation @@ -414,27 +416,54 @@ def rollback_sale_on_status_change(sender, instance, created, **kwargs): status='converted_to_sale' ) - reservations_count = reservations.count() + # Разделяем резервы на витринные временные комплекты и обычные + # Витринные временные комплекты: is_temporary=True и showcase не null + showcase_kit_reservations = reservations.filter( + product_kit__is_temporary=True, + product_kit__showcase__isnull=False + ) + + # Обычные резервы (все остальные) + normal_reservations = reservations.exclude( + id__in=showcase_kit_reservations.values_list('id', flat=True) + ) + + showcase_count = showcase_kit_reservations.count() + normal_count = normal_reservations.count() + total_count = showcase_count + normal_count - if reservations_count > 0: - # Обновляем резервы через .save() чтобы сработал сигнал обновления Stock - # Сигнал update_stock_on_reservation_change автоматически обновит Stock - for reservation in reservations: - reservation.status = reservation_target_status - if reservation_target_status == 'released': - reservation.released_at = timezone.now() - # converted_at оставляем (для истории) + if total_count > 0: + # Обновляем обычные резервы согласно сценарию (released при отмене, reserved при возврате) + if normal_count > 0: + for reservation in normal_reservations: + reservation.status = reservation_target_status + if reservation_target_status == 'released': + reservation.released_at = timezone.now() + # converted_at оставляем (для истории) + + # Используем save() с указанием измененных полей + update_fields = ['status'] + if reservation_target_status == 'released': + update_fields.append('released_at') + reservation.save(update_fields=update_fields) - # Используем save() с указанием измененных полей - update_fields = ['status'] - if reservation_target_status == 'released': - update_fields.append('released_at') - reservation.save(update_fields=update_fields) - - logger.info( - f"✓ Обновлено {reservations_count} резервов: " - f"converted_to_sale → {reservation_target_status}" - ) + logger.info( + f"✓ Обновлено {normal_count} обычных резервов: " + f"converted_to_sale → {reservation_target_status}" + ) + + # Витринные временные комплекты ВСЕГДА возвращаются в reserved (остаются на витрине) + if showcase_count > 0: + for reservation in showcase_kit_reservations: + reservation.status = 'reserved' + # Не трогаем showcase и product_kit - они остаются привязанными + # converted_at оставляем (для истории) + reservation.save(update_fields=['status']) + + logger.info( + f"✓ Обновлено {showcase_count} резервов витринных комплектов: " + f"converted_to_sale → reserved (возвращены на витрину)" + ) else: logger.warning( f"⚠ Для заказа {instance.order_number} нет резервов в статусе 'converted_to_sale'"