Реализована возможность редактирования состава витринного комплекта с поддержкой отрицательных резервов
- Добавлены методы reserve_product_to_showcase и release_showcase_reservation в ShowcaseManager - Методы работают с резервами для всех активных экземпляров витринного комплекта - НЕ блокируют создание резерва при нехватке товара, возвращают информацию о дефиците (overdraft) - Обновлён API endpoint update_product_kit для корректировки резервов при изменении состава - Добавлено визуальное предупреждение на фронте о нехватке товара на складе - В модалке редактирования комплекта добавлены контролы для изменения количества товаров (+/-, поле ввода, удаление) - Автоматический пересчёт цен при изменении состава - Очистка корзины POS после успешного создания витринного комплекта
This commit is contained in:
@@ -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):
|
||||
"""
|
||||
|
||||
Reference in New Issue
Block a user