719 lines
29 KiB
Python
719 lines
29 KiB
Python
# -*- coding: utf-8 -*-
|
||
from django.shortcuts import render
|
||
from django.contrib.auth.decorators import login_required
|
||
from django.http import JsonResponse
|
||
from django.views.decorators.http import require_http_methods
|
||
from django.db import transaction
|
||
from django.db.models import Prefetch, OuterRef, Subquery
|
||
from django.utils import timezone
|
||
from decimal import Decimal, InvalidOperation
|
||
import json
|
||
|
||
from products.models import Product, ProductCategory, ProductKit, KitItem
|
||
from inventory.models import Showcase, Reservation, Warehouse
|
||
from inventory.services import ShowcaseManager
|
||
|
||
|
||
def get_pos_warehouse(request):
|
||
"""
|
||
Получить текущий склад для POS из сессии или выбрать дефолтный.
|
||
Логика выбора:
|
||
1. Если в сессии есть pos_warehouse_id - используем его
|
||
2. Иначе берем склад с is_default=True
|
||
3. Если нет is_default - берем первый активный
|
||
4. Если нет активных складов - None
|
||
"""
|
||
warehouse_id = request.session.get('pos_warehouse_id')
|
||
|
||
if warehouse_id:
|
||
try:
|
||
return Warehouse.objects.get(id=warehouse_id, is_active=True)
|
||
except Warehouse.DoesNotExist:
|
||
# Склад был удален или деактивирован - сбрасываем сессию
|
||
request.session.pop('pos_warehouse_id', None)
|
||
|
||
# Ищем склад по умолчанию
|
||
warehouse = Warehouse.objects.filter(is_active=True, is_default=True).first()
|
||
|
||
if not warehouse:
|
||
# Берем любой первый активный
|
||
warehouse = Warehouse.objects.filter(is_active=True).first()
|
||
|
||
# Сохраняем в сессию для следующих запросов
|
||
if warehouse:
|
||
request.session['pos_warehouse_id'] = warehouse.id
|
||
|
||
return warehouse
|
||
|
||
|
||
def get_showcase_kits_for_pos():
|
||
"""
|
||
Получает витринные комплекты для отображения в POS.
|
||
Возвращает список временных комплектов, которые зарезервированы на витринах.
|
||
Оптимизировано: убраны N+1 запросы, используется один проход по данным.
|
||
"""
|
||
from products.models import ProductKitPhoto
|
||
|
||
# Получаем все зарезервированные товары на витринах
|
||
reserved_products = Reservation.objects.filter(
|
||
showcase__isnull=False,
|
||
showcase__is_active=True,
|
||
status='reserved'
|
||
).values_list('product_id', flat=True).distinct()
|
||
|
||
if not reserved_products:
|
||
return []
|
||
|
||
# Prefetch для первого фото (thumbnail)
|
||
first_photo_prefetch = Prefetch(
|
||
'photos',
|
||
queryset=ProductKitPhoto.objects.order_by('order')[:1],
|
||
to_attr='first_photo_list'
|
||
)
|
||
|
||
# Находим комплекты с резервированными компонентами
|
||
kits_with_showcase_items = ProductKit.objects.filter(
|
||
is_temporary=True,
|
||
status='active',
|
||
kit_items__product_id__in=reserved_products
|
||
).prefetch_related(
|
||
first_photo_prefetch,
|
||
Prefetch('kit_items', queryset=KitItem.objects.select_related('product'))
|
||
).distinct()
|
||
|
||
# Получаем все резервы для компонентов комплектов одним запросом
|
||
all_kit_product_ids = set()
|
||
kit_to_product_ids = {} # {kit.id: set(product_ids)}
|
||
|
||
for kit in kits_with_showcase_items:
|
||
# Используем prefetch'енные kit_items (без дополнительного запроса)
|
||
product_ids = {item.product_id for item in kit.kit_items.all()}
|
||
kit_to_product_ids[kit.id] = product_ids
|
||
all_kit_product_ids.update(product_ids)
|
||
|
||
# Один запрос для всех резервов
|
||
all_reservations = Reservation.objects.filter(
|
||
product_id__in=all_kit_product_ids,
|
||
showcase__isnull=False,
|
||
showcase__is_active=True,
|
||
status='reserved'
|
||
).select_related('showcase').values('product_id', 'showcase_id', 'showcase__name')
|
||
|
||
# Группируем резервы по product_id
|
||
product_to_showcases = {} # {product_id: [(showcase_id, showcase_name), ...]}
|
||
for res in all_reservations:
|
||
product_id = res['product_id']
|
||
if product_id not in product_to_showcases:
|
||
product_to_showcases[product_id] = []
|
||
product_to_showcases[product_id].append((res['showcase_id'], res['showcase__name']))
|
||
|
||
# Формируем результат
|
||
showcase_kits = []
|
||
for kit in kits_with_showcase_items:
|
||
product_ids = kit_to_product_ids[kit.id]
|
||
|
||
# Находим общую витрину для всех компонентов
|
||
showcases_for_kit = None
|
||
for product_id in product_ids:
|
||
showcases = product_to_showcases.get(product_id, [])
|
||
if showcases_for_kit is None:
|
||
showcases_for_kit = set(s[0] for s in showcases)
|
||
else:
|
||
showcases_for_kit &= set(s[0] for s in showcases)
|
||
|
||
if showcases_for_kit:
|
||
# Берём первую витрину
|
||
showcase_id = list(showcases_for_kit)[0]
|
||
showcase_name = next(
|
||
(s[1] for pid in product_ids for s in product_to_showcases.get(pid, []) if s[0] == showcase_id),
|
||
'Неизвестно'
|
||
)
|
||
|
||
# Используем prefetch'енное первое фото
|
||
image_url = None
|
||
if hasattr(kit, 'first_photo_list') and kit.first_photo_list:
|
||
image_url = kit.first_photo_list[0].get_thumbnail_url()
|
||
|
||
showcase_kits.append({
|
||
'id': kit.id,
|
||
'name': kit.name,
|
||
'price': str(kit.actual_price),
|
||
'category_ids': [],
|
||
'in_stock': True,
|
||
'sku': kit.sku or '',
|
||
'image': image_url,
|
||
'type': 'showcase_kit',
|
||
'showcase_name': showcase_name,
|
||
'showcase_id': showcase_id
|
||
})
|
||
|
||
return showcase_kits
|
||
|
||
|
||
@login_required
|
||
def pos_terminal(request):
|
||
"""
|
||
Tablet-friendly POS screen prototype.
|
||
Shows categories and all items (products + kits) for quick tap-to-add.
|
||
Оптимизировано: убрана стартовая загрузка витрин, только thumbnail фото.
|
||
Работает только с одним выбранным складом.
|
||
"""
|
||
from products.models import ProductPhoto, ProductKitPhoto
|
||
|
||
# Получаем текущий склад для POS
|
||
current_warehouse = get_pos_warehouse(request)
|
||
|
||
if not current_warehouse:
|
||
# Нет активных складов - показываем ошибку
|
||
from django.contrib import messages
|
||
messages.error(request, 'Нет активных складов. Обратитесь к администратору.')
|
||
context = {
|
||
'categories_json': json.dumps([]),
|
||
'items_json': json.dumps([]),
|
||
'showcase_kits_json': json.dumps([]),
|
||
'current_warehouse': None,
|
||
'warehouses': [],
|
||
'title': 'POS Terminal',
|
||
}
|
||
return render(request, 'pos/terminal.html', context)
|
||
|
||
categories_qs = ProductCategory.objects.filter(is_active=True)
|
||
|
||
# Prefetch для первого фото товаров
|
||
first_product_photo = Prefetch(
|
||
'photos',
|
||
queryset=ProductPhoto.objects.order_by('order')[:1],
|
||
to_attr='first_photo_list'
|
||
)
|
||
|
||
# Показываем все товары, не только in_stock
|
||
products_qs = Product.objects.all().prefetch_related(
|
||
'categories',
|
||
first_product_photo
|
||
)
|
||
|
||
# Prefetch для первого фото комплектов
|
||
first_kit_photo = Prefetch(
|
||
'photos',
|
||
queryset=ProductKitPhoto.objects.order_by('order')[:1],
|
||
to_attr='first_photo_list'
|
||
)
|
||
|
||
# Показываем все комплекты (кроме временных)
|
||
kits_qs = ProductKit.objects.filter(is_temporary=False).prefetch_related(
|
||
'categories',
|
||
first_kit_photo
|
||
)
|
||
|
||
categories = [{'id': c.id, 'name': c.name} for c in categories_qs]
|
||
|
||
# Сериализация товаров с оптимизацией фото
|
||
products = []
|
||
for p in products_qs:
|
||
image_url = None
|
||
if hasattr(p, 'first_photo_list') and p.first_photo_list:
|
||
image_url = p.first_photo_list[0].get_thumbnail_url()
|
||
|
||
products.append({
|
||
'id': p.id,
|
||
'name': p.name,
|
||
'price': str(p.actual_price),
|
||
'category_ids': [c.id for c in p.categories.all()],
|
||
'in_stock': p.in_stock,
|
||
'sku': p.sku or '',
|
||
'image': image_url,
|
||
'type': 'product'
|
||
})
|
||
|
||
# Сериализация комплектов с оптимизацией фото
|
||
kits = []
|
||
for k in kits_qs:
|
||
image_url = None
|
||
if hasattr(k, 'first_photo_list') and k.first_photo_list:
|
||
image_url = k.first_photo_list[0].get_thumbnail_url()
|
||
|
||
kits.append({
|
||
'id': k.id,
|
||
'name': k.name,
|
||
'price': str(k.actual_price),
|
||
'category_ids': [c.id for c in k.categories.all()],
|
||
'in_stock': False, # Комплекты всегда "Под заказ"
|
||
'sku': k.sku or '',
|
||
'image': image_url,
|
||
'type': 'kit'
|
||
})
|
||
|
||
# Объединяем все позиции
|
||
all_items = products + kits
|
||
|
||
# Список всех активных складов для модалки выбора
|
||
warehouses = Warehouse.objects.filter(is_active=True).order_by('-is_default', 'name')
|
||
warehouses_list = [{
|
||
'id': w.id,
|
||
'name': w.name,
|
||
'is_default': w.is_default
|
||
} for w in warehouses]
|
||
|
||
context = {
|
||
'categories_json': json.dumps(categories),
|
||
'items_json': json.dumps(all_items),
|
||
'showcase_kits_json': json.dumps([]), # Пустой массив - загрузка по API
|
||
'current_warehouse': {
|
||
'id': current_warehouse.id,
|
||
'name': current_warehouse.name
|
||
},
|
||
'warehouses': warehouses_list,
|
||
'title': 'POS Terminal',
|
||
}
|
||
return render(request, 'pos/terminal.html', context)
|
||
|
||
|
||
@login_required
|
||
@require_http_methods(["POST"])
|
||
def set_warehouse(request, warehouse_id):
|
||
"""
|
||
Установить текущий склад для POS.
|
||
Сохраняет выбор в сессию.
|
||
"""
|
||
try:
|
||
warehouse = Warehouse.objects.get(id=warehouse_id, is_active=True)
|
||
request.session['pos_warehouse_id'] = warehouse.id
|
||
|
||
return JsonResponse({
|
||
'success': True,
|
||
'warehouse_id': warehouse.id,
|
||
'warehouse_name': warehouse.name
|
||
})
|
||
except Warehouse.DoesNotExist:
|
||
return JsonResponse({
|
||
'success': False,
|
||
'error': 'Склад не найден или неактивен'
|
||
}, status=404)
|
||
|
||
|
||
@login_required
|
||
@require_http_methods(["GET"])
|
||
def showcase_items_api(request):
|
||
"""
|
||
API endpoint для получения витринных букетов.
|
||
Возвращает комплекты, зарезервированные на активных витринах.
|
||
"""
|
||
# Получаем все активные резервы на витринах
|
||
showcase_reservations = Reservation.objects.filter(
|
||
showcase__isnull=False,
|
||
showcase__is_active=True,
|
||
status='reserved'
|
||
).select_related('showcase', 'product').prefetch_related('product__photos')
|
||
|
||
# Группируем по витринам
|
||
showcases_dict = {}
|
||
for res in showcase_reservations:
|
||
showcase_id = res.showcase.id
|
||
if showcase_id not in showcases_dict:
|
||
showcases_dict[showcase_id] = {
|
||
'id': showcase_id,
|
||
'name': res.showcase.name,
|
||
'warehouse': res.showcase.warehouse.name,
|
||
'items': []
|
||
}
|
||
|
||
# Добавляем товар в список
|
||
showcases_dict[showcase_id]['items'].append({
|
||
'product_id': res.product.id,
|
||
'product_name': res.product.name,
|
||
'quantity': str(res.quantity),
|
||
'image': res.product.photos.first().get_thumbnail_url() if res.product.photos.exists() else None,
|
||
})
|
||
|
||
showcases_list = list(showcases_dict.values())
|
||
|
||
return JsonResponse({
|
||
'success': True,
|
||
'showcases': showcases_list
|
||
})
|
||
|
||
|
||
@login_required
|
||
@require_http_methods(["GET"])
|
||
def get_showcases_api(request):
|
||
"""
|
||
API endpoint для получения списка активных витрин.
|
||
Используется для выбора витрины при создании временного комплекта.
|
||
"""
|
||
showcases = Showcase.objects.filter(is_active=True).select_related('warehouse')
|
||
|
||
showcases_data = [{
|
||
'id': s.id,
|
||
'name': s.name,
|
||
'warehouse_name': s.warehouse.name,
|
||
'warehouse_id': s.warehouse.id,
|
||
'is_default_warehouse': s.warehouse.is_default # Для автовыбора
|
||
} for s in showcases]
|
||
|
||
return JsonResponse({
|
||
'success': True,
|
||
'showcases': showcases_data
|
||
})
|
||
|
||
|
||
@login_required
|
||
@require_http_methods(["GET"])
|
||
def get_showcase_kits_api(request):
|
||
"""
|
||
API endpoint для получения актуального списка витринных комплектов.
|
||
Используется для динамического обновления после создания нового букета.
|
||
"""
|
||
showcase_kits_data = get_showcase_kits_for_pos()
|
||
|
||
return JsonResponse({
|
||
'success': True,
|
||
'items': showcase_kits_data
|
||
})
|
||
|
||
|
||
@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):
|
||
"""
|
||
API endpoint для создания временного комплекта из корзины POS
|
||
и резервирования его на витрину.
|
||
|
||
Ожидаемый payload (multipart/form-data):
|
||
- kit_name: Название комплекта
|
||
- showcase_id: ID витрины
|
||
- items: JSON список [{product_id, quantity}, ...]
|
||
- description: Описание (опционально)
|
||
- price_adjustment_type: Тип корректировки (опционально)
|
||
- price_adjustment_value: Значение корректировки (опционально)
|
||
- sale_price: Ручная финальная цена (опционально)
|
||
- photo: Файл изображения (опционально)
|
||
"""
|
||
try:
|
||
# Получаем данные из FormData
|
||
kit_name = request.POST.get('kit_name', '').strip()
|
||
showcase_id = request.POST.get('showcase_id')
|
||
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')
|
||
|
||
# Парсим items из JSON
|
||
items = json.loads(items_json)
|
||
|
||
# Sale price (опционально)
|
||
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 showcase_id:
|
||
return JsonResponse({
|
||
'success': False,
|
||
'error': 'Необходимо выбрать витрину'
|
||
}, status=400)
|
||
|
||
if not items or len(items) == 0:
|
||
return JsonResponse({
|
||
'success': False,
|
||
'error': 'Корзина пуста. Добавьте товары для создания комплекта'
|
||
}, status=400)
|
||
|
||
# Проверяем что витрина существует и активна
|
||
try:
|
||
showcase = Showcase.objects.select_related('warehouse').get(id=showcase_id, is_active=True)
|
||
except Showcase.DoesNotExist:
|
||
return JsonResponse({
|
||
'success': False,
|
||
'error': 'Витрина не найдена или неактивна'
|
||
}, status=404)
|
||
|
||
# Проверяем что все товары из корзины - это Product (не Kit)
|
||
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']))
|
||
|
||
if product_id in aggregated_items:
|
||
aggregated_items[product_id] += quantity
|
||
else:
|
||
aggregated_items[product_id] = quantity
|
||
|
||
# Создаём временный комплект и резервируем на витрину
|
||
with transaction.atomic():
|
||
# 1. Создаём ProductKit (is_temporary=True)
|
||
kit = ProductKit.objects.create(
|
||
name=kit_name,
|
||
description=description,
|
||
is_temporary=True,
|
||
status='active',
|
||
price_adjustment_type=price_adjustment_type,
|
||
price_adjustment_value=price_adjustment_value,
|
||
sale_price=sale_price
|
||
)
|
||
|
||
# 2. Создаём KitItem для каждого товара из корзины
|
||
for product_id, quantity in aggregated_items.items():
|
||
KitItem.objects.create(
|
||
kit=kit,
|
||
product=products[product_id],
|
||
quantity=quantity
|
||
)
|
||
|
||
# 3. Пересчитываем цену комплекта
|
||
kit.recalculate_base_price()
|
||
|
||
# 4. Загружаем фото, если есть
|
||
if photo_file:
|
||
from products.models import ProductKitPhoto
|
||
ProductKitPhoto.objects.create(
|
||
kit=kit,
|
||
image=photo_file,
|
||
order=0
|
||
)
|
||
|
||
# 5. Резервируем комплект на витрину
|
||
result = ShowcaseManager.reserve_kit_to_showcase(
|
||
product_kit=kit,
|
||
showcase=showcase,
|
||
quantity=1
|
||
)
|
||
|
||
if not result['success']:
|
||
# Откатываем транзакцию через raise
|
||
raise Exception(result['message'])
|
||
|
||
return JsonResponse({
|
||
'success': True,
|
||
'message': f'Временный комплект "{kit_name}" создан и зарезервирован на витрине "{showcase.name}"',
|
||
'kit_id': kit.id,
|
||
'kit_name': kit.name,
|
||
'kit_price': str(kit.actual_price),
|
||
'reservations_count': len(result['reservations'])
|
||
})
|
||
|
||
except json.JSONDecodeError:
|
||
return JsonResponse({
|
||
'success': False,
|
||
'error': 'Неверный формат данных'
|
||
}, status=400)
|
||
except Exception as e:
|
||
return JsonResponse({
|
||
'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)
|