Добавлена защита от повторного списания + команда исправления дубликатов

Проблема: Сигнал post_save срабатывает несколько раз,
создавая дубликаты Sale для одного заказа.

Решения:
1. Добавлена проверка Sale.objects.filter(order=instance).exists()
   перед созданием продаж (inventory/signals.py:74-75)
2. Создана management команда fix_duplicate_sales для исправления
   существующих дубликатов

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-30 22:15:33 +03:00
parent 24292b2e47
commit 920dbf4273
5 changed files with 381 additions and 0 deletions

View File

@@ -0,0 +1,74 @@
from django.db import connection
from orders.models import Order
from inventory.models import Reservation, Sale, Stock, StockBatch, Warehouse
# Устанавливаем схему тенанта
connection.set_schema('buba')
# Получаем заказ
try:
order = Order.objects.get(order_number='101')
print("=" * 60)
print(f"ЗАКАЗ: {order.order_number}")
print("=" * 60)
print(f"ID: {order.id}")
print(f"Статус: '{order.status}'")
print(f"Склад самовывоза: {order.pickup_warehouse}")
print("\n" + "=" * 60)
print("ТОВАРЫ В ЗАКАЗЕ")
print("=" * 60)
items = order.items.all()
print(f"Количество товаров: {items.count()}")
for item in items:
product = item.product or item.product_kit
print(f" - {product.name if product else 'НЕТ!'}: {item.quantity} шт, цена: {item.price}")
print("\n" + "=" * 60)
print("РЕЗЕРВЫ")
print("=" * 60)
reservations = Reservation.objects.filter(order_item__order=order)
print(f"Количество резервов: {reservations.count()}")
for res in reservations:
print(f" - {res.product.name}: {res.quantity} шт, статус: '{res.status}'")
print("\n" + "=" * 60)
print("ПРОДАЖИ (Sale)")
print("=" * 60)
sales = Sale.objects.filter(order=order)
print(f"Количество продаж: {sales.count()}")
if sales:
for sale in sales:
print(f" - {sale.product.name}: {sale.quantity} шт, обработано: {sale.processed}")
else:
print(" ⚠️ ПРОДАЖ НЕТ!")
print("\n" + "=" * 60)
print("ДИАГНОСТИКА")
print("=" * 60)
warehouse = order.pickup_warehouse or Warehouse.objects.filter(is_active=True).first()
print(f"Статус заказа: '{order.status}' (тип: {type(order.status).__name__})")
print(f"Условие: order.status != 'completed' = {order.status != 'completed'}")
print(f"Склад: {warehouse.name if warehouse else 'НЕ НАЙДЕН!'}")
# Проверяем наличие товара
if warehouse and items:
print("\nОстатки на складе:")
for item in items:
product = item.product or item.product_kit
if product:
batches = StockBatch.objects.filter(product=product, warehouse=warehouse, is_active=True)
total = sum(b.quantity for b in batches)
print(f" - {product.name}: {total} доступно (нужно {item.quantity})")
if total < item.quantity:
print(f" ⚠️ НЕДОСТАТОЧНО! (не хватает {item.quantity - total})")
except Order.DoesNotExist:
print("⚠️ ЗАКАЗ 101 НЕ НАЙДЕН В ТЕНАНТЕ 'buba'!")
except Exception as e:
print(f"❌ ОШИБКА: {e}")
import traceback
traceback.print_exc()

View File

@@ -0,0 +1,60 @@
"""
Скрипт для исправления дубликатов продаж в заказе 119
"""
from django.db import connection, transaction
from orders.models import Order
from inventory.models import Sale, SaleBatchAllocation, Stock, StockBatch
connection.set_schema('buba')
order = Order.objects.get(order_number='119')
sales = Sale.objects.filter(order=order).order_by('date')
print(f"Заказ {order.order_number}: найдено {sales.count()} продаж")
if sales.count() <= 1:
print("Дубликатов нет, всё в порядке")
else:
# Оставляем первую продажу, остальные удаляем
first_sale = sales.first()
duplicate_sales = sales.exclude(id=first_sale.id)
print(f"\nОставляем продажу ID {first_sale.id}")
print(f"Удаляем {duplicate_sales.count()} дубликатов:")
with transaction.atomic():
for sale in duplicate_sales:
print(f" - Продажа ID {sale.id}: {sale.product.name} x {sale.quantity}")
# Получаем SaleBatchAllocation для восстановления товара
allocations = SaleBatchAllocation.objects.filter(sale=sale)
# Восстанавливаем товар в партиях
for alloc in allocations:
batch = alloc.batch
print(f" Восстанавливаем партию ID {batch.id}: +{alloc.quantity}")
batch.quantity += alloc.quantity
batch.is_active = True
batch.save()
# Удаляем продажу (каскадно удалятся и SaleBatchAllocation)
sale.delete()
# Обновляем Stock
for item in order.items.all():
product = item.product or item.product_kit
if product:
warehouse = order.pickup_warehouse or Warehouse.objects.filter(is_active=True).first()
if warehouse:
stock, _ = Stock.objects.get_or_create(product=product, warehouse=warehouse)
stock.refresh_from_batches()
print(f"\nStock обновлен для {product.name}:")
print(f" quantity_available: {stock.quantity_available}")
print(f" quantity_reserved: {stock.quantity_reserved}")
print(f" quantity_free: {stock.quantity_free}")
print("\n✅ Дубликаты удалены, товар восстановлен на складе")
# Проверяем результат
sales_after = Sale.objects.filter(order=order)
print(f"\nПосле исправления: {sales_after.count()} продаж")

View File

@@ -70,6 +70,10 @@ def create_sale_on_order_completion(sender, instance, created, **kwargs):
if instance.status.code != 'completed':
return # Только для статуса 'completed'
# Защита от повторного списания: проверяем, не созданы ли уже Sale для этого заказа
if Sale.objects.filter(order=instance).exists():
return # Продажи уже созданы, выходим
# Определяем склад (используем склад самовывоза из заказа или первый активный)
warehouse = instance.pickup_warehouse or Warehouse.objects.filter(is_active=True).first()

View File

@@ -0,0 +1,76 @@
from django.core.management.base import BaseCommand
from django.db import connection, transaction
from orders.models import Order
from inventory.models import Sale, SaleBatchAllocation, Stock, StockBatch, Warehouse
class Command(BaseCommand):
help = 'Исправляет дубликаты продаж для указанного заказа'
def add_arguments(self, parser):
parser.add_argument('order_number', type=str, help='Номер заказа')
parser.add_argument('--tenant', type=str, default='buba', help='Схема тенанта')
def handle(self, *args, **options):
order_number = options['order_number']
tenant = options['tenant']
connection.set_schema(tenant)
try:
order = Order.objects.get(order_number=order_number)
except Order.DoesNotExist:
self.stdout.write(self.style.ERROR(f'Заказ {order_number} не найден в тенанте {tenant}'))
return
sales = Sale.objects.filter(order=order).order_by('date')
self.stdout.write(f"Заказ {order.order_number}: найдено {sales.count()} продаж")
if sales.count() <= 1:
self.stdout.write(self.style.SUCCESS("Дубликатов нет, всё в порядке"))
return
# Оставляем первую продажу, остальные удаляем
first_sale = sales.first()
duplicate_sales = sales.exclude(id=first_sale.id)
self.stdout.write(f"\nОставляем продажу ID {first_sale.id}")
self.stdout.write(f"Удаляем {duplicate_sales.count()} дубликатов:")
with transaction.atomic():
for sale in duplicate_sales:
self.stdout.write(f" - Продажа ID {sale.id}: {sale.product.name} x {sale.quantity}")
# Получаем SaleBatchAllocation для восстановления товара
allocations = SaleBatchAllocation.objects.filter(sale=sale)
# Восстанавливаем товар в партиях
for alloc in allocations:
batch = alloc.batch
self.stdout.write(f" Восстанавливаем партию ID {batch.id}: +{alloc.quantity}")
batch.quantity += alloc.quantity
batch.is_active = True
batch.save()
# Удаляем продажу (каскадно удалятся и SaleBatchAllocation)
sale.delete()
# Обновляем Stock
for item in order.items.all():
product = item.product or item.product_kit
if product:
warehouse = order.pickup_warehouse or Warehouse.objects.filter(is_active=True).first()
if warehouse:
stock, _ = Stock.objects.get_or_create(product=product, warehouse=warehouse)
stock.refresh_from_batches()
self.stdout.write(f"\nStock обновлен для {product.name}:")
self.stdout.write(f" quantity_available: {stock.quantity_available}")
self.stdout.write(f" quantity_reserved: {stock.quantity_reserved}")
self.stdout.write(f" quantity_free: {stock.quantity_free}")
self.stdout.write(self.style.SUCCESS("\n✅ Дубликаты удалены, товар восстановлен на складе"))
# Проверяем результат
sales_after = Sale.objects.filter(order=order)
self.stdout.write(f"\nПосле исправления: {sales_after.count()} продаж")