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:
@@ -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] Все резервы пересчитаны!'))
|
||||||
@@ -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 для товара на основе остатков.
|
||||||
|
|||||||
Reference in New Issue
Block a user