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.signals import post_save, pre_delete, post_delete, pre_save
from django.db.models import Q from django.db.models import Q
from django.db import transaction from django.db import transaction
@@ -19,6 +20,26 @@ from inventory.services import SaleProcessor
from inventory.services.batch_manager import StockBatchManager from inventory.services.batch_manager import StockBatchManager
# InventoryProcessor больше не используется в сигналах - обработка вызывается явно через view # 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 # pre_save сигнал для сохранения предыдущего статуса Order
@@ -278,10 +299,22 @@ def create_sale_on_order_completion(sender, instance, created, **kwargs):
3. Для каждого товара создаем Sale (автоматический FIFO-список) 3. Для каждого товара создаем Sale (автоматический FIFO-список)
4. ТОЛЬКО после успешного создания Sale обновляем резервы на 'converted_to_sale' 4. ТОЛЬКО после успешного создания Sale обновляем резервы на 'converted_to_sale'
5. Обновляем флаг is_returned 5. Обновляем флаг is_returned
ПРИМЕЧАНИЕ: Если у Order установлен атрибут skip_sale_creation=True,
создание Sale пропускается (используется в POS для создания Sale после применения скидок).
""" """
import logging import logging
logger = logging.getLogger(__name__) 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: if created:
return # Только для обновлений return # Только для обновлений

View File

@@ -15,6 +15,7 @@ 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
from inventory.signals import skip_sale_creation, reset_sale_creation
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -1526,13 +1527,17 @@ def pos_checkout(request):
# Атомарная операция # Атомарная операция
with db_transaction.atomic(): with db_transaction.atomic():
# ВАЖНО: Устанавливаем флаг для пропуска автоматического создания Sale в сигнале.
# Sale будет создан ЯВНО после применения всех скидок.
skip_sale_creation()
# 1. Создаём заказ с текущей датой и временем в локальном часовом поясе (Europe/Minsk) # 1. Создаём заказ с текущей датой и временем в локальном часовом поясе (Europe/Minsk)
from django.utils import timezone as tz from django.utils import timezone as tz
from orders.models import Delivery from orders.models import Delivery
now_utc = tz.now() # Текущее время в UTC now_utc = tz.now() # Текущее время в UTC
now_local = tz.localtime(now_utc) # Конвертируем в локальный часовой пояс (Europe/Minsk) now_local = tz.localtime(now_utc) # Конвертируем в локальный часовой пояс (Europe/Minsk)
current_time = now_local.time() # Извлекаем время в минском часовом поясе current_time = now_local.time() # Извлекаем время в минском часовом поясе
order = Order.objects.create( order = Order.objects.create(
customer=customer, customer=customer,
status=completed_status, # Сразу "Выполнен" status=completed_status, # Сразу "Выполнен"
@@ -1714,6 +1719,11 @@ def pos_checkout(request):
cart_key = f'pos:cart:{request.user.id}:{warehouse_id}' cart_key = f'pos:cart:{request.user.id}:{warehouse_id}'
cache.delete(cart_key) cache.delete(cart_key)
# 7. Явно создаём Sale после применения всех скидок
# Сбрасываем флаг пропуска и вызываем save() для активации сигнала
reset_sale_creation()
order.save() # Триггерит сигнал create_sale_on_order_completion
return JsonResponse({ return JsonResponse({
'success': True, 'success': True,
'order_number': order.order_number, 'order_number': order.order_number,