diff --git a/myproject/inventory/management/commands/refresh_stock_reservations.py b/myproject/inventory/management/commands/refresh_stock_reservations.py new file mode 100644 index 0000000..9fcf253 --- /dev/null +++ b/myproject/inventory/management/commands/refresh_stock_reservations.py @@ -0,0 +1,106 @@ +""" +Management команда для пересчета quantity_reserved для всех Stock записей +Используется после добавления сигналов для Reservation +""" +from django.core.management.base import BaseCommand +from django.db import connection, transaction + + +class Command(BaseCommand): + help = 'Пересчитывает quantity_reserved для всех Stock записей' + + def add_arguments(self, parser): + parser.add_argument( + '--schema', + type=str, + default='grach', + help='Схема базы данных (tenant) для работы' + ) + + def handle(self, *args, **options): + schema_name = options['schema'] + + self.stdout.write(f'[НАЧАЛО] Пересчет quantity_reserved для всех Stock записей в схеме {schema_name}...') + + updated_count = 0 + reserved_count = 0 + + # Используем прямой SQL для пересчета quantity_reserved + # Это обходит проблему с tenant routing в Django ORM + with connection.cursor() as cursor, transaction.atomic(): + # Устанавливаем схему + cursor.execute(f'SET search_path TO {schema_name}') + + # Получаем общее количество Stock записей + cursor.execute(f'SELECT COUNT(*) FROM {schema_name}.inventory_stock') + total_count = cursor.fetchone()[0] + + self.stdout.write(f'[INFO] Найдено Stock записей: {total_count}') + + # Получаем все Stock записи с информацией о продукте + cursor.execute(f""" + SELECT + s.id as stock_id, + s.product_id, + p.name as product_name, + s.warehouse_id + FROM {schema_name}.inventory_stock s + JOIN {schema_name}.products_product p ON p.id = s.product_id + """) + stocks = cursor.fetchall() + + # Обрабатываем каждый Stock + for stock_id, product_id, product_name, warehouse_id in stocks: + try: + # Пересчитываем quantity_reserved и quantity_available + # Логика из Stock.refresh_from_batches() + + # 1. Считаем общее количество из активных партий + cursor.execute(f""" + SELECT COALESCE(SUM(quantity), 0) + FROM {schema_name}.inventory_stockbatch + WHERE product_id = %s + AND warehouse_id = %s + AND is_active = true + """, [product_id, warehouse_id]) + total_qty = cursor.fetchone()[0] + + # 2. Считаем зарезервированное количество + cursor.execute(f""" + SELECT COALESCE(SUM(quantity), 0) + FROM {schema_name}.inventory_reservation + WHERE product_id = %s + AND warehouse_id = %s + AND status = 'reserved' + """, [product_id, warehouse_id]) + total_reserved = cursor.fetchone()[0] + + # 3. Обновляем Stock + cursor.execute(f""" + UPDATE {schema_name}.inventory_stock + SET quantity_available = %s, + quantity_reserved = %s, + updated_at = CURRENT_TIMESTAMP + WHERE id = %s + """, [total_qty, total_reserved, stock_id]) + + updated_count += 1 + + if total_reserved > 0: + reserved_count += 1 + self.stdout.write( + f' [OK] Stock #{stock_id}: {product_name} - ' + f'зарезервировано: {total_reserved}' + ) + + except Exception as e: + self.stdout.write( + self.style.ERROR(f' [ОШИБКА] Stock #{stock_id}: {str(e)}') + ) + + self.stdout.write('\n' + '=' * 60) + self.stdout.write(self.style.SUCCESS('[ЗАВЕРШЕНО] Результаты:')) + self.stdout.write(f' Всего Stock записей: {total_count}') + self.stdout.write(f' Обновлено записей: {updated_count}') + self.stdout.write(f' Записей с резервами: {reserved_count}') + self.stdout.write(self.style.SUCCESS('\n[OK] Все резервы пересчитаны!')) diff --git a/myproject/inventory/signals.py b/myproject/inventory/signals.py index 592fa94..24e4ff4 100644 --- a/myproject/inventory/signals.py +++ b/myproject/inventory/signals.py @@ -337,6 +337,57 @@ def update_stock_on_writeoff(sender, instance, created, **kwargs): stock.refresh_from_batches() +@receiver(post_save, sender=Reservation) +def update_stock_on_reservation_change(sender, instance, created, **kwargs): + """ + Сигнал: При создании или изменении резерва (Reservation) обновляем Stock. + + Процесс: + 1. При создании или изменении резерва пересчитываем quantity_reserved + 2. Обновляем запись Stock для этого товара + """ + if not instance.product or not instance.warehouse: + return + + # Получаем или создаем Stock запись + stock, _ = Stock.objects.get_or_create( + product=instance.product, + warehouse=instance.warehouse + ) + + # Пересчитываем остатки из всех активных партий и резервов + # refresh_from_batches() уже вызывает save() + stock.refresh_from_batches() + + +@receiver(post_delete, sender=Reservation) +def update_stock_on_reservation_delete(sender, instance, **kwargs): + """ + Сигнал: При удалении резерва (Reservation) обновляем Stock. + + Процесс: + 1. После удаления резерва пересчитываем quantity_reserved + 2. Обновляем запись Stock для этого товара + """ + if not instance.product or not instance.warehouse: + return + + try: + # Получаем Stock запись (не создаем новую при удалении) + stock = Stock.objects.get( + product=instance.product, + warehouse=instance.warehouse + ) + + # Пересчитываем остатки из всех активных партий и резервов + # refresh_from_batches() уже вызывает save() + stock.refresh_from_batches() + + except Stock.DoesNotExist: + # Если Stock записи нет - ничего не делаем + pass + + def _update_product_in_stock(product_id): """ Вспомогательная функция: обновить статус in_stock для товара на основе остатков.