fix(inventory, orders, pos): удалена зависимость от django-simple-history для tenant-моделей

- Добавлен pre_save сигнал для Order вместо django-simple-history
- Переписаны все функции signals.py без использования instance.history
- Заменены .username на .name|default:.email для CustomUser в шаблонах
- Исправлен CSRF-токен в POS для работы с CSRF_USE_SESSIONS=True

Теперь создание заказов работает корректно в мультитенантной архитектуре.
This commit is contained in:
2026-01-10 17:21:00 +03:00
parent 8f3c90c11a
commit 4ea01b8269
12 changed files with 85 additions and 118 deletions

View File

@@ -4,7 +4,7 @@
Подключаются при создании, изменении и удалении заказов.
"""
from django.db.models.signals import post_save, pre_delete, post_delete
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
from django.dispatch import receiver
@@ -20,33 +20,49 @@ from inventory.services.batch_manager import StockBatchManager
# InventoryProcessor больше не используется в сигналах - обработка вызывается явно через view
# ============================================================================
# pre_save сигнал для сохранения предыдущего статуса Order
# ============================================================================
@receiver(pre_save, sender='orders.Order')
def store_previous_order_status(sender, instance, **kwargs):
"""
Сохраняет предыдущий статус перед сохранением заказа.
Используется в post_save сигналах для отслеживания изменений статуса
без использования django-simple-history (который не работает с tenant схемами).
"""
if instance.pk:
try:
instance._previous_status = sender.objects.get(pk=instance.pk).status
except sender.DoesNotExist:
instance._previous_status = None
else:
instance._previous_status = None
def update_is_returned_flag(order):
"""
Обновляет флаг is_returned на основе фактического состояния заказа.
Логика:
- Если есть хотя бы одна Sale по этому заказу → is_returned = False
- Если Sale нет, но заказ когда-либо был в статусе completed → is_returned = True
- Если заказ ни разу не был completed → is_returned = False
Логика (упрощенная без history):
- Если есть хотя бы одна Sale по этому заказу → is_returned = False (заказ активен)
- Если Sale нет → is_returned = True (продажи были, но откачены/удалены)
Это гарантирует что флаг отражает реальность:
- Заказ продан и не возвращён → False
- Заказ был продан, но продажи откачены (возврат) → True
- Новый заказ без продаж → False
- Новый заказ без продаж → False (но при первом создании Sale станет False)
"""
has_sale_now = Sale.objects.filter(order=order).exists()
if has_sale_now:
# Есть актуальные продажи → заказ не возвращён
new_flag = False
else:
# Проверяем историю только если нет Sale (оптимизация производительности)
was_completed_ever = order.history.filter(
status__is_positive_end=True
).exists()
# Продаж нет → возвращён только если был когда-то completed
new_flag = was_completed_ever
# Продаж нет → заказ возвращен (если продажи были, они откачены)
new_flag = True
# Обновляем только если значение изменилось
if order.is_returned != new_flag:
Order.objects.filter(pk=order.pk).update(is_returned=new_flag)
@@ -312,29 +328,19 @@ def create_sale_on_order_completion(sender, instance, created, **kwargs):
# === ЗАЩИТА ОТ RACE CONDITION: Проверяем предыдущий статус ===
# Если уже были в completed и снова переходим в completed (например completed → draft → completed),
# проверяем наличие Sale чтобы избежать дублирования
try:
history_count = instance.history.count()
if history_count >= 2:
previous_record = instance.history.all()[1]
if previous_record.status_id:
from orders.models import OrderStatus
previous_status = OrderStatus.objects.get(id=previous_record.status_id)
# Если предыдущий статус тоже был положительным финальным
if previous_status.is_positive_end:
logger.info(
f"🔄 Заказ {instance.order_number}: повторный переход в положительный статус "
f"({previous_status.name}{instance.status.name}). Проверяем Sale..."
)
# Проверяем есть ли уже Sale
if Sale.objects.filter(order=instance).exists():
logger.info(
f"✓ Заказ {instance.order_number}: Sale уже существуют, пропускаем создание"
)
update_is_returned_flag(instance)
return
except (instance.history.model.DoesNotExist, OrderStatus.DoesNotExist, IndexError):
pass
previous_status = getattr(instance, '_previous_status', None)
if previous_status and previous_status.is_positive_end:
logger.info(
f"🔄 Заказ {instance.order_number}: повторный переход в положительный статус "
f"({previous_status.name}{instance.status.name}). Проверяем Sale..."
)
# Проверяем есть ли уже Sale
if Sale.objects.filter(order=instance).exists():
logger.info(
f"✓ Заказ {instance.order_number}: Sale уже существуют, пропускаем создание"
)
update_is_returned_flag(instance)
return
# Защита от повторного списания: проверяем, не созданы ли уже Sale для этого заказа
if Sale.objects.filter(order=instance).exists():
@@ -602,25 +608,10 @@ def rollback_sale_on_status_change(sender, instance, created, **kwargs):
current_status = instance.status
# === Получаем предыдущий статус через django-simple-history ===
try:
# Получаем предыдущую запись из истории (индекс [1] = предпоследняя)
history_count = instance.history.count()
if history_count < 2:
return # Нет истории для сравнения
previous_record = instance.history.all()[1]
if not previous_record.status_id:
return
# Импортируем OrderStatus если еще не импортирован
from orders.models import OrderStatus
previous_status = OrderStatus.objects.get(id=previous_record.status_id)
except (instance.history.model.DoesNotExist, OrderStatus.DoesNotExist, IndexError):
# Нет истории или статус удалён
return
# === Получаем предыдущий статус из pre_save сигнала ===
previous_status = getattr(instance, '_previous_status', None)
if not previous_status:
return # Нет предыдущего статуса (новый заказ или первое сохранение)
# === Проверяем: был ли переход ОТ 'completed'? ===
if previous_status.code != 'completed':
@@ -990,26 +981,10 @@ def release_reservations_on_cancellation(sender, instance, created, **kwargs):
# Проверяем: это статус отмены?
if not current_status.is_negative_end:
return # Не отмена, выходим
# === Получаем предыдущий статус ===
try:
history_count = instance.history.count()
if history_count < 2:
# Нет истории - значит заказ создан сразу в cancelled (необычно, но возможно)
# Продолжаем обработку
previous_status = None
else:
previous_record = instance.history.all()[1]
if not previous_record.status_id:
previous_status = None
else:
from orders.models import OrderStatus
previous_status = OrderStatus.objects.get(id=previous_record.status_id)
except (instance.history.model.DoesNotExist, OrderStatus.DoesNotExist, IndexError):
previous_status = None
# === Получаем предыдущий статус из pre_save сигнала ===
previous_status = getattr(instance, '_previous_status', None)
# Проверяем: не был ли уже в cancelled?
if previous_status and previous_status.is_negative_end:
return # Уже был в отмене, не обрабатываем повторно
@@ -1147,24 +1122,12 @@ def reserve_stock_on_uncancellation(sender, instance, created, **kwargs):
# Проверяем: текущий статус НЕ отмена?
if current_status.is_negative_end:
return # Всё ещё в отмене, выходим
# === Получаем предыдущий статус ===
try:
history_count = instance.history.count()
if history_count < 2:
return # Нет истории для сравнения
previous_record = instance.history.all()[1]
if not previous_record.status_id:
return
from orders.models import OrderStatus
previous_status = OrderStatus.objects.get(id=previous_record.status_id)
except (instance.history.model.DoesNotExist, OrderStatus.DoesNotExist, IndexError):
return
# === Получаем предыдущий статус из pre_save сигнала ===
previous_status = getattr(instance, '_previous_status', None)
if not previous_status:
return # Нет предыдущего статуса
# Проверяем: был ли предыдущий статус = cancelled?
if not previous_status.is_negative_end:
return # Не было перехода от cancelled, выходим