- Добавлен API endpoint для создания временного комплекта из корзины - Реализован endpoint получения списка активных витрин - Создано модальное окно для настройки комплекта и выбора витрины - JavaScript логика: валидация корзины, отправка данных, очистка после успеха - Автоматическая генерация названия комплекта с датой и временем - Агрегация дубликатов товаров в корзине перед созданием - Резервирование компонентов на витрину через ShowcaseManager - Расчёт и отображение итоговой цены комплекта
258 lines
9.8 KiB
Python
258 lines
9.8 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.utils import timezone
|
||
from decimal import Decimal
|
||
import json
|
||
|
||
from products.models import Product, ProductCategory, ProductKit, KitItem
|
||
from inventory.models import Showcase, Reservation, Warehouse
|
||
from inventory.services import ShowcaseManager
|
||
|
||
|
||
@login_required
|
||
def pos_terminal(request):
|
||
"""
|
||
Tablet-friendly POS screen prototype.
|
||
Shows categories and all items (products + kits) for quick tap-to-add.
|
||
"""
|
||
categories_qs = ProductCategory.objects.filter(is_active=True)
|
||
# Показываем все товары, не только in_stock
|
||
products_qs = Product.objects.all().prefetch_related('categories', 'photos')
|
||
# Показываем все комплекты (кроме временных)
|
||
kits_qs = ProductKit.objects.filter(is_temporary=False).prefetch_related('categories', 'photos')
|
||
|
||
categories = [{'id': c.id, 'name': c.name} for c in categories_qs]
|
||
|
||
# Сериализация товаров
|
||
products = [{
|
||
'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': p.photos.first().get_thumbnail_url() if p.photos.exists() else None,
|
||
'type': 'product'
|
||
} for p in products_qs]
|
||
|
||
# Сериализация комплектов
|
||
kits = [{
|
||
'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': k.photos.first().get_thumbnail_url() if k.photos.exists() else None,
|
||
'type': 'kit'
|
||
} for k in kits_qs]
|
||
|
||
# Объединяем все позиции
|
||
all_items = products + kits
|
||
|
||
context = {
|
||
'categories_json': json.dumps(categories),
|
||
'items_json': json.dumps(all_items), # Единый массив товаров и комплектов
|
||
'title': 'POS Terminal',
|
||
}
|
||
return render(request, 'pos/terminal.html', context)
|
||
|
||
|
||
@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
|
||
} for s in showcases]
|
||
|
||
return JsonResponse({
|
||
'success': True,
|
||
'showcases': showcases_data
|
||
})
|
||
|
||
|
||
@login_required
|
||
@require_http_methods(["POST"])
|
||
def create_temp_kit_to_showcase(request):
|
||
"""
|
||
API endpoint для создания временного комплекта из корзины POS
|
||
и резервирования его на витрину.
|
||
|
||
Ожидаемый payload:
|
||
{
|
||
"kit_name": "Название комплекта",
|
||
"showcase_id": 1,
|
||
"items": [
|
||
{"product_id": 1, "quantity": 2.0},
|
||
{"product_id": 3, "quantity": 1.0}
|
||
],
|
||
"price_adjustment_type": "none", // optional
|
||
"price_adjustment_value": 0 // optional
|
||
}
|
||
"""
|
||
try:
|
||
data = json.loads(request.body)
|
||
|
||
kit_name = data.get('kit_name', '').strip()
|
||
showcase_id = data.get('showcase_id')
|
||
items = data.get('items', [])
|
||
price_adjustment_type = data.get('price_adjustment_type', 'none')
|
||
price_adjustment_value = Decimal(str(data.get('price_adjustment_value', 0)))
|
||
|
||
# Валидация
|
||
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,
|
||
is_temporary=True,
|
||
status='active',
|
||
price_adjustment_type=price_adjustment_type,
|
||
price_adjustment_value=price_adjustment_value
|
||
)
|
||
|
||
# 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. Резервируем комплект на витрину
|
||
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)
|