Реализована возможность редактирования состава витринного комплекта с поддержкой отрицательных резервов

- Добавлены методы reserve_product_to_showcase и release_showcase_reservation в ShowcaseManager
- Методы работают с резервами для всех активных экземпляров витринного комплекта
- НЕ блокируют создание резерва при нехватке товара, возвращают информацию о дефиците (overdraft)
- Обновлён API endpoint update_product_kit для корректировки резервов при изменении состава
- Добавлено визуальное предупреждение на фронте о нехватке товара на складе
- В модалке редактирования комплекта добавлены контролы для изменения количества товаров (+/-, поле ввода, удаление)
- Автоматический пересчёт цен при изменении состава
- Очистка корзины POS после успешного создания витринного комплекта
This commit is contained in:
2025-12-14 13:49:13 +03:00
parent 835d6020e2
commit aff25d0317
3 changed files with 292 additions and 21 deletions

View File

@@ -324,6 +324,179 @@ class ShowcaseManager:
'message': f'Ошибка продажи: {str(e)}'
}
@staticmethod
def reserve_product_to_showcase(product, showcase, product_kit, quantity_per_item):
"""
Дозарезервировать товар для всех АКТИВНЫХ экземпляров витринного комплекта.
НЕ блокирует при нехватке товара - допускает резерв "в минус".
Args:
product: Product товар-компонент
showcase: Showcase витрина
product_kit: ProductKit шаблон витринного комплекта
quantity_per_item: Decimal или int насколько увеличить количество
этого товара в ОДНОМ экземпляре букета
Returns:
dict: {
'success': bool,
'overdraft': Decimal, # сколько не хватает (может быть 0)
'message': str
}
"""
from decimal import Decimal
if not showcase or not product_kit:
return {
'success': True,
'overdraft': Decimal('0'),
'message': 'Нет витрины или комплекта резервы не изменены'
}
quantity_per_item = Decimal(str(quantity_per_item))
if quantity_per_item <= 0:
return {
'success': True,
'overdraft': Decimal('0'),
'message': 'Изменение количества не требует дополнительного резерва'
}
# Берём только актуальные экземпляры на витрине
active_items = ShowcaseItem.objects.filter(
showcase=showcase,
product_kit=product_kit,
status__in=['available', 'in_cart'],
)
item_count = active_items.count()
if item_count == 0:
return {
'success': True,
'overdraft': Decimal('0'),
'message': 'Нет активных экземпляров на витрине резервы не изменены'
}
warehouse = showcase.warehouse
total_needed = quantity_per_item * item_count
# Проверяем, хватает ли свободного остатка (НЕ блокируем, только считаем дефицит)
try:
stock = Stock.objects.get(product=product, warehouse=warehouse)
free_qty = stock.quantity_free
except Stock.DoesNotExist:
free_qty = Decimal('0')
overdraft = max(Decimal('0'), total_needed - free_qty)
# Создаём/увеличиваем резервы по каждому экземпляру (даже если не хватает!)
with transaction.atomic():
for showcase_item in active_items:
reservation, created = Reservation.objects.get_or_create(
product=product,
warehouse=warehouse,
showcase=showcase,
product_kit=product_kit,
showcase_item=showcase_item,
status='reserved',
defaults={'quantity': quantity_per_item},
)
if not created:
reservation.quantity = (reservation.quantity or Decimal('0')) + quantity_per_item
reservation.save(update_fields=['quantity'])
return {
'success': True,
'overdraft': overdraft,
'message': (
f'Зарезервировано {total_needed} ед. товара "{product.name}"'
+ (f' (не хватает {overdraft})' if overdraft > 0 else '')
),
}
@staticmethod
def release_showcase_reservation(product, showcase, product_kit, quantity_per_item):
"""
Освободить часть резерва товара для всех АКТИВНЫХ экземпляров витринного комплекта.
Args:
product: Product товар-компонент
showcase: Showcase витрина
product_kit: ProductKit шаблон витринного комплекта
quantity_per_item: Decimal или int насколько уменьшить количество
этого товара в ОДНОМ экземпляре букета
Returns:
dict: {
'success': bool,
'released': Decimal,
'message': str
}
"""
from decimal import Decimal
if not showcase or not product_kit:
return {
'success': True,
'released': Decimal('0'),
'message': 'Нет витрины или комплекта резервы не изменены',
}
quantity_per_item = Decimal(str(quantity_per_item))
if quantity_per_item <= 0:
return {
'success': True,
'released': Decimal('0'),
'message': 'Изменение количества не требует освобождения резерва',
}
active_items = ShowcaseItem.objects.filter(
showcase=showcase,
product_kit=product_kit,
status__in=['available', 'in_cart'],
)
if not active_items.exists():
return {
'success': True,
'released': Decimal('0'),
'message': 'Нет активных экземпляров на витрине резервы не изменены',
}
released_total = Decimal('0')
with transaction.atomic():
reservations = Reservation.objects.filter(
showcase_item__in=active_items,
product=product,
product_kit=product_kit,
showcase=showcase,
warehouse=showcase.warehouse,
status='reserved',
)
for res in reservations:
old_qty = res.quantity or Decimal('0')
new_qty = old_qty - quantity_per_item
if new_qty > 0:
res.quantity = new_qty
res.save(update_fields=['quantity'])
released_amount = quantity_per_item
else:
# Полностью освобождаем резерв
released_amount = old_qty
res.status = 'released'
res.released_at = timezone.now()
res.save(update_fields=['status', 'released_at'])
released_total += released_amount
return {
'success': True,
'released': released_total,
'message': f'Освобождено {released_total} ед. товара "{product.name}"',
}
@staticmethod
def dismantle_showcase_item(showcase_item):
"""