From 4ea01b8269de86a511ff10cbd6868038b55962b5 Mon Sep 17 00:00:00 2001 From: Andrey Smakotin Date: Sat, 10 Jan 2026 17:21:00 +0300 Subject: [PATCH] =?UTF-8?q?fix(inventory,=20orders,=20pos):=20=D1=83=D0=B4?= =?UTF-8?q?=D0=B0=D0=BB=D0=B5=D0=BD=D0=B0=20=D0=B7=D0=B0=D0=B2=D0=B8=D1=81?= =?UTF-8?q?=D0=B8=D0=BC=D0=BE=D1=81=D1=82=D1=8C=20=D0=BE=D1=82=20django-si?= =?UTF-8?q?mple-history=20=D0=B4=D0=BB=D1=8F=20tenant-=D0=BC=D0=BE=D0=B4?= =?UTF-8?q?=D0=B5=D0=BB=D0=B5=D0=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлен pre_save сигнал для Order вместо django-simple-history - Переписаны все функции signals.py без использования instance.history - Заменены .username на .name|default:.email для CustomUser в шаблонах - Исправлен CSRF-токен в POS для работы с CSRF_USE_SESSIONS=True Теперь создание заказов работает корректно в мультитенантной архитектуре. --- myproject/inventory/signals.py | 155 +++++++----------- .../templates/inventory/debug_page.html | 4 +- .../incoming_document_detail.html | 12 +- .../incoming_document_list.html | 2 +- .../inventory/transformation/detail.html | 2 +- .../inventory/transformation/list.html | 2 +- .../inventory/writeoff_document/detail.html | 2 +- .../inventory/writeoff_document/list.html | 2 +- .../orders/templates/orders/order_detail.html | 4 +- .../orders/templates/orders/order_form.html | 2 +- myproject/pos/static/pos/js/terminal.js | 13 +- myproject/pos/templates/pos/terminal.html | 3 + 12 files changed, 85 insertions(+), 118 deletions(-) diff --git a/myproject/inventory/signals.py b/myproject/inventory/signals.py index 4fcb005..a45726d 100644 --- a/myproject/inventory/signals.py +++ b/myproject/inventory/signals.py @@ -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, выходим diff --git a/myproject/inventory/templates/inventory/debug_page.html b/myproject/inventory/templates/inventory/debug_page.html index 2381ac3..8dcbd1b 100644 --- a/myproject/inventory/templates/inventory/debug_page.html +++ b/myproject/inventory/templates/inventory/debug_page.html @@ -688,10 +688,10 @@ {{ doc.get_receipt_type_display }} {{ doc.date|date:"d.m.Y" }} {{ doc.supplier_name|default:"-" }} - {{ doc.created_by.username|default:"-" }} + {{ doc.created_by.name|default:doc.created_by.email|default:"-" }} {% if doc.confirmed_by %} - {{ doc.confirmed_by.username }} ({{ doc.confirmed_at|date:"d.m H:i" }}) + {{ doc.confirmed_by.name|default:doc.confirmed_by.email }} ({{ doc.confirmed_at|date:"d.m H:i" }}) {% else %} - {% endif %} diff --git a/myproject/inventory/templates/inventory/incoming_document/incoming_document_detail.html b/myproject/inventory/templates/inventory/incoming_document/incoming_document_detail.html index 495e431..84c9d18 100644 --- a/myproject/inventory/templates/inventory/incoming_document/incoming_document_detail.html +++ b/myproject/inventory/templates/inventory/incoming_document/incoming_document_detail.html @@ -17,16 +17,6 @@ - - {% if messages %} - {% for message in messages %} - - {% endfor %} - {% endif %} -
@@ -102,7 +92,7 @@

Провёл

-

{% if document.confirmed_by %}{{ document.confirmed_by.username }}{% else %}-{% endif %}

+

{% if document.confirmed_by %}{{ document.confirmed_by.name|default:document.confirmed_by.email }}{% else %}-{% endif %}

{% endif %} diff --git a/myproject/inventory/templates/inventory/incoming_document/incoming_document_list.html b/myproject/inventory/templates/inventory/incoming_document/incoming_document_list.html index 51f46c9..ac29d3a 100644 --- a/myproject/inventory/templates/inventory/incoming_document/incoming_document_list.html +++ b/myproject/inventory/templates/inventory/incoming_document/incoming_document_list.html @@ -67,7 +67,7 @@ {{ doc.items.count }} {{ doc.total_quantity }} - {% if doc.created_by %}{{ doc.created_by.username }}{% else %}-{% endif %} + {% if doc.created_by %}{{ doc.created_by.name|default:doc.created_by.email }}{% else %}-{% endif %} diff --git a/myproject/inventory/templates/inventory/transformation/detail.html b/myproject/inventory/templates/inventory/transformation/detail.html index f3fdb86..477ec5f 100644 --- a/myproject/inventory/templates/inventory/transformation/detail.html +++ b/myproject/inventory/templates/inventory/transformation/detail.html @@ -85,7 +85,7 @@

Сотрудник

-

{% if transformation.employee %}{{ transformation.employee.username }}{% else %}-{% endif %}

+

{% if transformation.employee %}{{ transformation.employee.name|default:transformation.employee.email }}{% else %}-{% endif %}

diff --git a/myproject/inventory/templates/inventory/transformation/list.html b/myproject/inventory/templates/inventory/transformation/list.html index 99ec9e7..8ca0437 100644 --- a/myproject/inventory/templates/inventory/transformation/list.html +++ b/myproject/inventory/templates/inventory/transformation/list.html @@ -75,7 +75,7 @@ {% endfor %} - {% if transformation.employee %}{{ transformation.employee.username }}{% else %}-{% endif %} + {% if transformation.employee %}{{ transformation.employee.name|default:transformation.employee.email }}{% else %}-{% endif %}
diff --git a/myproject/inventory/templates/inventory/writeoff_document/detail.html b/myproject/inventory/templates/inventory/writeoff_document/detail.html index 088c108..7336b24 100644 --- a/myproject/inventory/templates/inventory/writeoff_document/detail.html +++ b/myproject/inventory/templates/inventory/writeoff_document/detail.html @@ -91,7 +91,7 @@

Провёл

-

{% if document.confirmed_by %}{{ document.confirmed_by.username }}{% else %}-{% endif %}

+

{% if document.confirmed_by %}{{ document.confirmed_by.name|default:document.confirmed_by.email }}{% else %}-{% endif %}

{% endif %} diff --git a/myproject/inventory/templates/inventory/writeoff_document/list.html b/myproject/inventory/templates/inventory/writeoff_document/list.html index 1c9a3df..45b3b15 100644 --- a/myproject/inventory/templates/inventory/writeoff_document/list.html +++ b/myproject/inventory/templates/inventory/writeoff_document/list.html @@ -63,7 +63,7 @@ {{ doc.items.count }} {{ doc.total_quantity }} - {% if doc.created_by %}{{ doc.created_by.username }}{% else %}-{% endif %} + {% if doc.created_by %}{{ doc.created_by.name|default:doc.created_by.email }}{% else %}-{% endif %}
diff --git a/myproject/orders/templates/orders/order_detail.html b/myproject/orders/templates/orders/order_detail.html index 3283771..af723af 100644 --- a/myproject/orders/templates/orders/order_detail.html +++ b/myproject/orders/templates/orders/order_detail.html @@ -138,7 +138,7 @@
Изменен:
- {{ order.modified_by.get_short_name|default:order.modified_by.username }} + {{ order.modified_by.name|default:order.modified_by.email }}
{% endif %} @@ -430,7 +430,7 @@
{{ transaction.notes|default:transaction.reason }} {% endif %} {% if transaction.created_by %} -
Кем: {{ transaction.created_by.get_short_name|default:transaction.created_by.username }} +
Кем: {{ transaction.created_by.name|default:transaction.created_by.email }} {% endif %} diff --git a/myproject/orders/templates/orders/order_form.html b/myproject/orders/templates/orders/order_form.html index e605216..43102b4 100644 --- a/myproject/orders/templates/orders/order_form.html +++ b/myproject/orders/templates/orders/order_form.html @@ -910,7 +910,7 @@ {% if transaction.created_by %} - {{ transaction.created_by.get_short_name|default:transaction.created_by.username }} + {{ transaction.created_by.name|default:transaction.created_by.email }} {% else %} — {% endif %} diff --git a/myproject/pos/static/pos/js/terminal.js b/myproject/pos/static/pos/js/terminal.js index 43fd0c6..8aa5ed0 100644 --- a/myproject/pos/static/pos/js/terminal.js +++ b/myproject/pos/static/pos/js/terminal.js @@ -2209,7 +2209,18 @@ function getCookie(name) { } // Алиас для обратной совместимости -const getCsrfToken = () => getCookie('csrftoken'); +// ВАЖНО: При CSRF_USE_SESSIONS=True токен хранится в сессии, а не в cookie +// Извлекаем его из скрытого поля в HTML ({% csrf_token %}) +const getCsrfToken = () => { + // Пытаемся найти токен в DOM (из {% csrf_token %}) + const csrfInput = document.querySelector('[name=csrfmiddlewaretoken]'); + if (csrfInput) { + return csrfInput.value; + } + + // Fallback: пытаемся прочитать из cookie (если CSRF_USE_SESSIONS=False) + return getCookie('csrftoken'); +}; // Сброс режима редактирования при закрытии модального окна document.getElementById('createTempKitModal').addEventListener('hidden.bs.modal', function() { diff --git a/myproject/pos/templates/pos/terminal.html b/myproject/pos/templates/pos/terminal.html index 093fd40..cb70d7a 100644 --- a/myproject/pos/templates/pos/terminal.html +++ b/myproject/pos/templates/pos/terminal.html @@ -8,6 +8,9 @@ {% endblock %} {% block content %} + +{% csrf_token %} +