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:
@@ -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,19 +20,39 @@ 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()
|
||||
|
||||
@@ -40,12 +60,8 @@ def update_is_returned_flag(order):
|
||||
# Есть актуальные продажи → заказ не возвращён
|
||||
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:
|
||||
@@ -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':
|
||||
@@ -991,24 +982,8 @@ 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:
|
||||
@@ -1148,22 +1123,10 @@ 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:
|
||||
|
||||
@@ -688,10 +688,10 @@
|
||||
<td><span class="badge bg-info">{{ doc.get_receipt_type_display }}</span></td>
|
||||
<td class="text-muted-small">{{ doc.date|date:"d.m.Y" }}</td>
|
||||
<td>{{ doc.supplier_name|default:"-" }}</td>
|
||||
<td class="text-muted-small">{{ doc.created_by.username|default:"-" }}</td>
|
||||
<td class="text-muted-small">{{ doc.created_by.name|default:doc.created_by.email|default:"-" }}</td>
|
||||
<td class="text-muted-small">
|
||||
{% 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 %}
|
||||
|
||||
@@ -17,16 +17,6 @@
|
||||
</ol>
|
||||
</nav>
|
||||
|
||||
<!-- Messages -->
|
||||
{% if messages %}
|
||||
{% for message in messages %}
|
||||
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
|
||||
{{ message }}
|
||||
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
|
||||
<div class="row g-3">
|
||||
<!-- Основной контент - одна колонка -->
|
||||
<div class="col-12">
|
||||
@@ -102,7 +92,7 @@
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<p class="text-muted small mb-1">Провёл</p>
|
||||
<p class="fw-semibold">{% if document.confirmed_by %}{{ document.confirmed_by.username }}{% else %}-{% endif %}</p>
|
||||
<p class="fw-semibold">{% if document.confirmed_by %}{{ document.confirmed_by.name|default:document.confirmed_by.email }}{% else %}-{% endif %}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -67,7 +67,7 @@
|
||||
<td class="px-3 py-2 text-end">{{ doc.items.count }}</td>
|
||||
<td class="px-3 py-2 text-end">{{ doc.total_quantity }}</td>
|
||||
<td class="px-3 py-2">
|
||||
{% 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 %}
|
||||
</td>
|
||||
<td class="px-3 py-2 text-end">
|
||||
<a href="{% url 'inventory:incoming-detail' doc.pk %}" class="btn btn-sm btn-outline-primary">
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
</div>
|
||||
<div class="col-md-3">
|
||||
<p class="text-muted small mb-1">Сотрудник</p>
|
||||
<p class="fw-semibold">{% if transformation.employee %}{{ transformation.employee.username }}{% else %}-{% endif %}</p>
|
||||
<p class="fw-semibold">{% if transformation.employee %}{{ transformation.employee.name|default:transformation.employee.email }}{% else %}-{% endif %}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
{% endfor %}
|
||||
</td>
|
||||
<td class="px-3 py-2">
|
||||
{% if transformation.employee %}{{ transformation.employee.username }}{% else %}-{% endif %}
|
||||
{% if transformation.employee %}{{ transformation.employee.name|default:transformation.employee.email }}{% else %}-{% endif %}
|
||||
</td>
|
||||
<td class="px-3 py-2 text-end">
|
||||
<a href="{% url 'inventory:transformation-detail' transformation.pk %}" class="btn btn-sm btn-outline-primary">
|
||||
|
||||
@@ -91,7 +91,7 @@
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<p class="text-muted small mb-1">Провёл</p>
|
||||
<p class="fw-semibold">{% if document.confirmed_by %}{{ document.confirmed_by.username }}{% else %}-{% endif %}</p>
|
||||
<p class="fw-semibold">{% if document.confirmed_by %}{{ document.confirmed_by.name|default:document.confirmed_by.email }}{% else %}-{% endif %}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -63,7 +63,7 @@
|
||||
<td class="px-3 py-2 text-end">{{ doc.items.count }}</td>
|
||||
<td class="px-3 py-2 text-end">{{ doc.total_quantity }}</td>
|
||||
<td class="px-3 py-2">
|
||||
{% 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 %}
|
||||
</td>
|
||||
<td class="px-3 py-2 text-end">
|
||||
<a href="{% url 'inventory:writeoff-document-detail' doc.pk %}" class="btn btn-sm btn-outline-primary">
|
||||
|
||||
@@ -138,7 +138,7 @@
|
||||
<div class="row mb-2">
|
||||
<div class="col-md-4"><strong>Изменен:</strong></div>
|
||||
<div class="col-md-8">
|
||||
{{ order.modified_by.get_short_name|default:order.modified_by.username }}
|
||||
{{ order.modified_by.name|default:order.modified_by.email }}
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
@@ -430,7 +430,7 @@
|
||||
<br><em>{{ transaction.notes|default:transaction.reason }}</em>
|
||||
{% endif %}
|
||||
{% if transaction.created_by %}
|
||||
<br>Кем: {{ transaction.created_by.get_short_name|default:transaction.created_by.username }}
|
||||
<br>Кем: {{ transaction.created_by.name|default:transaction.created_by.email }}
|
||||
{% endif %}
|
||||
</small>
|
||||
</li>
|
||||
|
||||
@@ -910,7 +910,7 @@
|
||||
<td>
|
||||
<small class="text-muted">
|
||||
{% 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 %}
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<!-- CSRF Token для AJAX запросов -->
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Main POS Container -->
|
||||
<div class="pos-main-container">
|
||||
<div class="pos-container">
|
||||
|
||||
Reference in New Issue
Block a user