Add stock reservation auto-update system

- Add management command to recalculate quantity_reserved for all Stock records
- Add signals to automatically update Stock when Reservation changes
- Implement post_save signal for Reservation creation/updates
- Implement post_delete signal for Reservation deletion
- Both signals call Stock.refresh_from_batches() to recalculate quantities

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-08 11:07:35 +03:00
parent fcc7f2263d
commit d2f15b5126
2 changed files with 157 additions and 0 deletions

View File

@@ -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] Все резервы пересчитаны!'))

View File

@@ -337,6 +337,57 @@ def update_stock_on_writeoff(sender, instance, created, **kwargs):
stock.refresh_from_batches() 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): def _update_product_in_stock(product_id):
""" """
Вспомогательная функция: обновить статус in_stock для товара на основе остатков. Вспомогательная функция: обновить статус in_stock для товара на основе остатков.