fix(inventory): создавать Sale после применения скидок в POS checkout

Добавлен механизм skip_sale_creation на базе thread-local storage
для управления моментом создания Sale через сигнал.

Проблема: сигнал create_sale_on_order_completion срабатывал при
Order.objects.create(status=completed) до применения скидок.

Решение: пропускать сигнал во время создания заказа, затем явно
создавать Sale после применения всех скидок через order.save().

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-21 01:05:22 +03:00
parent 1e4b7598ae
commit 2dc36b3d01
2 changed files with 44 additions and 1 deletions

View File

@@ -4,6 +4,7 @@
Подключаются при создании, изменении и удалении заказов.
"""
import threading
from django.db.models.signals import post_save, pre_delete, post_delete, pre_save
from django.db.models import Q
from django.db import transaction
@@ -19,6 +20,26 @@ from inventory.services import SaleProcessor
from inventory.services.batch_manager import StockBatchManager
# InventoryProcessor больше не используется в сигналах - обработка вызывается явно через view
# ============================================================================
# Thread-local storage для временных флагов управления сигналами
# ============================================================================
_skip_sale_creation = threading.local()
def skip_sale_creation():
"""Установить флаг для пропуска создания Sale в сигнале."""
_skip_sale_creation.value = True
def reset_sale_creation():
"""Сбросить флаг пропуска создания Sale."""
_skip_sale_creation.value = False
def is_skip_sale_creation():
"""Проверить, установлен ли флаг пропуска создания Sale."""
return getattr(_skip_sale_creation, 'value', False)
# ============================================================================
# pre_save сигнал для сохранения предыдущего статуса Order
@@ -278,10 +299,22 @@ def create_sale_on_order_completion(sender, instance, created, **kwargs):
3. Для каждого товара создаем Sale (автоматический FIFO-список)
4. ТОЛЬКО после успешного создания Sale обновляем резервы на 'converted_to_sale'
5. Обновляем флаг is_returned
ПРИМЕЧАНИЕ: Если у Order установлен атрибут skip_sale_creation=True,
создание Sale пропускается (используется в POS для создания Sale после применения скидок).
"""
import logging
logger = logging.getLogger(__name__)
# === ПРОВЕРКА: Пропуск создания Sale по флагу ===
# Используется в POS checkout, где Sale создаётся явно после применения скидок
if is_skip_sale_creation():
logger.info(
f" Заказ {instance.order_number}: skip_sale_creation=True (thread-local), "
f"пропускаем автоматическое создание Sale"
)
return
if created:
return # Только для обновлений

View File

@@ -15,6 +15,7 @@ import logging
from products.models import Product, ProductCategory, ProductKit, KitItem
from inventory.models import Showcase, Reservation, Warehouse, Stock
from inventory.services import ShowcaseManager
from inventory.signals import skip_sale_creation, reset_sale_creation
logger = logging.getLogger(__name__)
@@ -1526,6 +1527,10 @@ def pos_checkout(request):
# Атомарная операция
with db_transaction.atomic():
# ВАЖНО: Устанавливаем флаг для пропуска автоматического создания Sale в сигнале.
# Sale будет создан ЯВНО после применения всех скидок.
skip_sale_creation()
# 1. Создаём заказ с текущей датой и временем в локальном часовом поясе (Europe/Minsk)
from django.utils import timezone as tz
from orders.models import Delivery
@@ -1714,6 +1719,11 @@ def pos_checkout(request):
cart_key = f'pos:cart:{request.user.id}:{warehouse_id}'
cache.delete(cart_key)
# 7. Явно создаём Sale после применения всех скидок
# Сбрасываем флаг пропуска и вызываем save() для активации сигнала
reset_sale_creation()
order.save() # Триггерит сигнал create_sale_on_order_completion
return JsonResponse({
'success': True,
'order_number': order.order_number,