From 920dbf42730d040818c86dc344ff92d492d8db22 Mon Sep 17 00:00:00 2001 From: Andrey Smakotin Date: Sun, 30 Nov 2025 22:15:33 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=B7=D0=B0=D1=89=D0=B8=D1=82=D0=B0=20=D0=BE?= =?UTF-8?q?=D1=82=20=D0=BF=D0=BE=D0=B2=D1=82=D0=BE=D1=80=D0=BD=D0=BE=D0=B3?= =?UTF-8?q?=D0=BE=20=D1=81=D0=BF=D0=B8=D1=81=D0=B0=D0=BD=D0=B8=D1=8F=20+?= =?UTF-8?q?=20=D0=BA=D0=BE=D0=BC=D0=B0=D0=BD=D0=B4=D0=B0=20=D0=B8=D1=81?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B4?= =?UTF-8?q?=D1=83=D0=B1=D0=BB=D0=B8=D0=BA=D0=B0=D1=82=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Проблема: Сигнал 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 --- check_order.py | 167 ++++++++++++++++++ myproject/check_order_101.py | 74 ++++++++ myproject/fix_order_119.py | 60 +++++++ myproject/inventory/signals.py | 4 + .../commands/fix_duplicate_sales.py | 76 ++++++++ 5 files changed, 381 insertions(+) create mode 100644 check_order.py create mode 100644 myproject/check_order_101.py create mode 100644 myproject/fix_order_119.py create mode 100644 myproject/orders/management/commands/fix_duplicate_sales.py diff --git a/check_order.py b/check_order.py new file mode 100644 index 0000000..7222045 --- /dev/null +++ b/check_order.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python +""" +Скрипт для проверки заказа и его складских операций +""" +import os +import sys +import django + +# Настройка Django +sys.path.insert(0, '/c/Users/team_/Desktop/test_qwen/myproject') +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'myproject.settings') +django.setup() + +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(f"Создан: {order.created_at}") + print(f"Обновлен: {order.updated_at}") + + print("\n" + "=" * 60) + print("ТОВАРЫ В ЗАКАЗЕ (OrderItem)") + print("=" * 60) + items = order.items.all() + if items: + for item in items: + product = item.product or item.product_kit + print(f" ID: {item.id}") + print(f" Товар: {product.name if product else 'НЕТ ТОВАРА!'}") + print(f" Количество: {item.quantity}") + print(f" Цена: {item.price}") + print() + else: + print(" ⚠️ НЕТ ТОВАРОВ В ЗАКАЗЕ!") + + print("=" * 60) + print("РЕЗЕРВЫ (Reservation)") + print("=" * 60) + reservations = Reservation.objects.filter(order_item__order=order) + if reservations: + for res in reservations: + print(f" ID: {res.id}") + print(f" Товар: {res.product.name}") + print(f" Склад: {res.warehouse.name}") + print(f" Количество: {res.quantity}") + print(f" Статус: {res.status}") + print(f" Создан: {res.reserved_at}") + if res.converted_at: + print(f" Конвертирован: {res.converted_at}") + print() + else: + print(" ⚠️ НЕТ РЕЗЕРВОВ!") + + print("=" * 60) + print("ПРОДАЖИ (Sale)") + print("=" * 60) + sales = Sale.objects.filter(order=order) + if sales: + for sale in sales: + print(f" ID: {sale.id}") + print(f" Товар: {sale.product.name}") + print(f" Склад: {sale.warehouse.name}") + print(f" Количество: {sale.quantity}") + print(f" Цена продажи: {sale.sale_price}") + print(f" Обработано: {sale.processed}") + print(f" Дата: {sale.date}") + print() + else: + print(" ⚠️ НЕТ ПРОДАЖ (Sale не созданы!)") + + print("=" * 60) + print("СКЛАДЫ") + print("=" * 60) + warehouse = order.pickup_warehouse or Warehouse.objects.filter(is_active=True).first() + if warehouse: + print(f" Используется склад: {warehouse.name} (ID: {warehouse.id})") + print(f" Активен: {warehouse.is_active}") + else: + print(" ⚠️ НЕТ ДОСТУПНЫХ СКЛАДОВ!") + + print("\n" + "=" * 60) + print("ОСТАТКИ НА СКЛАДЕ (StockBatch)") + print("=" * 60) + if warehouse and items: + for item in items: + product = item.product or item.product_kit + if product: + print(f"\nТовар: {product.name}") + batches = StockBatch.objects.filter( + product=product, + warehouse=warehouse, + is_active=True + ) + if batches: + total = sum(b.quantity for b in batches) + print(f" Всего доступно: {total}") + for batch in batches: + print(f" Партия ID {batch.id}: {batch.quantity} шт (себестоимость: {batch.cost_price})") + else: + print(f" ⚠️ НЕТ ПАРТИЙ НА СКЛАДЕ!") + + # Проверяем Stock + try: + stock = Stock.objects.get(product=product, warehouse=warehouse) + print(f" Stock.quantity_available: {stock.quantity_available}") + print(f" Stock.quantity_reserved: {stock.quantity_reserved}") + print(f" Stock.quantity_free: {stock.quantity_free}") + except Stock.DoesNotExist: + print(f" ⚠️ Stock запись не существует!") + + print("\n" + "=" * 60) + print("ДИАГНОСТИКА") + print("=" * 60) + + # Проверка условий для срабатывания сигнала + if order.status != 'completed': + print(" ⚠️ СТАТУС НЕ 'completed'! Сигнал НЕ СРАБОТАЕТ") + print(f" Текущий статус: {order.status}") + else: + print(" ✅ Статус 'completed' - условие выполнено") + + if not warehouse: + print(" ⚠️ НЕТ СКЛАДА! Сигнал выйдет на строке 76-77") + else: + print(f" ✅ Склад есть: {warehouse.name}") + + if not items: + print(" ⚠️ НЕТ ТОВАРОВ! Сигнал ничего не сделает") + else: + print(f" ✅ Товаров в заказе: {items.count()}") + + # Проверка наличия товара на складе + if warehouse and items: + 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) + if total < item.quantity: + print(f" ⚠️ НЕДОСТАТОЧНО ТОВАРА '{product.name}'!") + print(f" Нужно: {item.quantity}, доступно: {total}") + else: + print(f" ✅ Товара '{product.name}' достаточно: {total} >= {item.quantity}") + +except Order.DoesNotExist: + print(f"⚠️ ЗАКАЗ С НОМЕРОМ '101' НЕ НАЙДЕН В ТЕНАНТЕ 'buba'!") +except Exception as e: + print(f"❌ ОШИБКА: {e}") + import traceback + traceback.print_exc() diff --git a/myproject/check_order_101.py b/myproject/check_order_101.py new file mode 100644 index 0000000..0f90943 --- /dev/null +++ b/myproject/check_order_101.py @@ -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() diff --git a/myproject/fix_order_119.py b/myproject/fix_order_119.py new file mode 100644 index 0000000..37eff56 --- /dev/null +++ b/myproject/fix_order_119.py @@ -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()} продаж") diff --git a/myproject/inventory/signals.py b/myproject/inventory/signals.py index 23eed9f..e756788 100644 --- a/myproject/inventory/signals.py +++ b/myproject/inventory/signals.py @@ -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() diff --git a/myproject/orders/management/commands/fix_duplicate_sales.py b/myproject/orders/management/commands/fix_duplicate_sales.py new file mode 100644 index 0000000..a0bea32 --- /dev/null +++ b/myproject/orders/management/commands/fix_duplicate_sales.py @@ -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()} продаж")