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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user