feat: добавлено редактирование витринных комплектов и изолированное состояние tempCart

- Добавлены API endpoints для получения и обновления витринных комплектов
  - GET /pos/api/product-kits/<id>/ - получение деталей комплекта
  - POST /pos/api/product-kits/<id>/update/ - обновление комплекта
- Реализовано редактирование комплектов из POS интерфейса
  - Кнопка редактирования (карандаш) на карточках витринных букетов
  - Модальное окно предзаполняется данными комплекта
  - Поддержка изменения состава, цен, описания и фото
  - Умное управление резервами при изменении состава
- Введено изолированное состояние tempCart для модального окна
  - Основная корзина (cart) больше не затрагивается при редактировании
  - tempCart используется для создания и редактирования комплектов
  - Автоочистка tempCart при закрытии модального окна
- Устранён побочный эффект загрузки состава комплекта в основную корзину
This commit is contained in:
2025-11-16 23:41:27 +03:00
parent 9dff9cc200
commit cefd6c98a2
3 changed files with 364 additions and 19 deletions

View File

@@ -209,6 +209,56 @@ def get_showcase_kits_api(request):
})
@login_required
@require_http_methods(["GET"])
def get_product_kit_details(request, kit_id):
"""
API endpoint для получения полных данных комплекта для редактирования.
"""
try:
kit = ProductKit.objects.prefetch_related('kit_items__product', 'photos').get(id=kit_id)
# Получаем витрину, на которой размещен комплект
showcase_reservation = Reservation.objects.filter(
product__in=kit.kit_items.values_list('product_id', flat=True),
showcase__isnull=False,
showcase__is_active=True,
status='reserved'
).select_related('showcase').first()
showcase_id = showcase_reservation.showcase.id if showcase_reservation else None
# Собираем данные о составе
items = [{
'product_id': ki.product.id,
'name': ki.product.name,
'qty': str(ki.quantity),
'price': str(ki.product.actual_price)
} for ki in kit.kit_items.all()]
# Фото
photo_url = kit.photos.first().image.url if kit.photos.exists() else None
return JsonResponse({
'success': True,
'kit': {
'id': kit.id,
'name': kit.name,
'description': kit.description or '',
'price_adjustment_type': kit.price_adjustment_type,
'price_adjustment_value': str(kit.price_adjustment_value),
'sale_price': str(kit.sale_price) if kit.sale_price else '',
'base_price': str(kit.base_price),
'final_price': str(kit.actual_price),
'showcase_id': showcase_id,
'items': items,
'photo_url': photo_url
}
})
except ProductKit.DoesNotExist:
return JsonResponse({'success': False, 'error': 'Комплект не найден'}, status=404)
@login_required
@require_http_methods(["POST"])
def create_temp_kit_to_showcase(request):
@@ -362,3 +412,145 @@ def create_temp_kit_to_showcase(request):
'success': False,
'error': f'Ошибка при создании комплекта: {str(e)}'
}, status=500)
@login_required
@require_http_methods(["POST"])
def update_product_kit(request, kit_id):
"""
API endpoint для обновления временного комплекта.
Payload (multipart/form-data):
- kit_name: Новое название
- description: Описание
- items: JSON список [{product_id, quantity}, ...]
- price_adjustment_type, price_adjustment_value, sale_price
- photo: Новое фото (опционально)
- remove_photo: '1' для удаления фото
"""
try:
kit = ProductKit.objects.prefetch_related('kit_items__product', 'photos').get(id=kit_id, is_temporary=True)
# Получаем данные
kit_name = request.POST.get('kit_name', '').strip()
description = request.POST.get('description', '').strip()
items_json = request.POST.get('items', '[]')
price_adjustment_type = request.POST.get('price_adjustment_type', 'none')
price_adjustment_value = Decimal(str(request.POST.get('price_adjustment_value', 0)))
sale_price_str = request.POST.get('sale_price', '')
photo_file = request.FILES.get('photo')
remove_photo = request.POST.get('remove_photo', '') == '1'
items = json.loads(items_json)
sale_price = None
if sale_price_str:
try:
sale_price = Decimal(str(sale_price_str))
if sale_price <= 0:
sale_price = None
except (ValueError, InvalidOperation):
sale_price = None
# Валидация
if not kit_name:
return JsonResponse({'success': False, 'error': 'Необходимо указать название'}, status=400)
if not items:
return JsonResponse({'success': False, 'error': 'Состав не может быть пустым'}, status=400)
# Проверяем товары
product_ids = [item['product_id'] for item in items]
products = Product.objects.in_bulk(product_ids)
if len(products) != len(product_ids):
return JsonResponse({'success': False, 'error': 'Некоторые товары не найдены'}, status=400)
# Агрегируем количества
aggregated_items = {}
for item in items:
product_id = item['product_id']
quantity = Decimal(str(item['quantity']))
aggregated_items[product_id] = aggregated_items.get(product_id, Decimal('0')) + quantity
with transaction.atomic():
# Получаем старый состав для сравнения
old_items = {ki.product_id: ki.quantity for ki in kit.kit_items.all()}
# Получаем витрину для резервов
showcase_reservation = Reservation.objects.filter(
product__in=old_items.keys(),
showcase__isnull=False,
status='reserved'
).select_related('showcase').first()
showcase = showcase_reservation.showcase if showcase_reservation else None
# Вычисляем разницу в составе
all_product_ids = set(old_items.keys()) | set(aggregated_items.keys())
for product_id in all_product_ids:
old_qty = old_items.get(product_id, Decimal('0'))
new_qty = aggregated_items.get(product_id, Decimal('0'))
diff = new_qty - old_qty
if diff > 0 and showcase:
# Нужно дозарезервировать
result = ShowcaseManager.reserve_product_to_showcase(
product=products[product_id],
showcase=showcase,
quantity=diff
)
if not result['success']:
raise Exception(f"Недостаточно запасов: {result['message']}")
elif diff < 0 and showcase:
# Нужно освободить резерв
ShowcaseManager.release_showcase_reservation(
product=products[product_id],
showcase=showcase,
quantity=abs(diff)
)
# Обновляем комплект
kit.name = kit_name
kit.description = description
kit.price_adjustment_type = price_adjustment_type
kit.price_adjustment_value = price_adjustment_value
kit.sale_price = sale_price
kit.save()
# Обновляем состав
kit.kit_items.all().delete()
for product_id, quantity in aggregated_items.items():
KitItem.objects.create(
kit=kit,
product=products[product_id],
quantity=quantity
)
kit.recalculate_base_price()
# Обновляем фото
if remove_photo:
kit.photos.all().delete()
if photo_file:
from products.models import ProductKitPhoto
kit.photos.all().delete() # Удаляем старое
ProductKitPhoto.objects.create(kit=kit, image=photo_file, order=0)
return JsonResponse({
'success': True,
'message': f'Комплект "{kit.name}" обновлён',
'kit_id': kit.id,
'kit_name': kit.name,
'kit_price': str(kit.actual_price)
})
except ProductKit.DoesNotExist:
return JsonResponse({'success': False, 'error': 'Комплект не найден'}, status=404)
except json.JSONDecodeError:
return JsonResponse({'success': False, 'error': 'Неверный формат данных'}, status=400)
except Exception as e:
return JsonResponse({'success': False, 'error': f'Ошибка: {str(e)}'}, status=500)