""" API представления для приложения products. """ from django.http import JsonResponse from django.db import models from django.core.cache import cache from ..models import Product, ProductVariantGroup def search_products_and_variants(request): """ API endpoint для поиска товаров и групп вариантов (совместимость с Select2). Используется для автокомплита при добавлении компонентов в комплект. Параметры GET: - q: строка поиска (term в Select2) - id: ID товара для получения его данных - type: 'product' или 'variant' (опционально) - page: номер страницы для пагинации (по умолчанию 1) Возвращает JSON в формате Select2: { "results": [ { "id": 1, "text": "Роза красная Freedom 50см (PROD-000001)", "sku": "PROD-000001", "price": "150.00", "in_stock": true } ], "pagination": { "more": true } } """ # Если передан ID товара - получаем его данные напрямую product_id = request.GET.get('id', '').strip() if product_id: try: product = Product.objects.get(id=int(product_id), is_active=True) return JsonResponse({ 'results': [{ 'id': product.id, 'text': f"{product.name} ({product.sku})" if product.sku else product.name, 'sku': product.sku, 'price': str(product.price) if product.price else None, 'actual_price': str(product.actual_price) if product.actual_price else '0', 'in_stock': product.in_stock, 'type': 'product' }], 'pagination': {'more': False} }) except (Product.DoesNotExist, ValueError): return JsonResponse({'results': [], 'pagination': {'more': False}}) query = request.GET.get('q', '').strip() search_type = request.GET.get('type', 'all') page = int(request.GET.get('page', 1)) page_size = 30 results = [] # Если поиска нет - показываем популярные товары if not query or len(query) < 2: # Кэшируем популярные товары на 1 час cache_key = f'popular_products_{search_type}' cached_results = cache.get(cache_key) if cached_results: return JsonResponse(cached_results) if search_type in ['all', 'product']: # Показываем последние добавленные активные товары products = Product.objects.filter(is_active=True)\ .order_by('-created_at')[:page_size]\ .values('id', 'name', 'sku', 'price', 'sale_price', 'in_stock') for product in products: text = product['name'] if product['sku']: text += f" ({product['sku']})" # Получаем actual_price: приоритет sale_price > price actual_price = product['sale_price'] if product['sale_price'] else product['price'] results.append({ 'id': product['id'], 'text': text, 'sku': product['sku'], 'price': str(product['price']) if product['price'] else None, 'actual_price': str(actual_price) if actual_price else '0', 'in_stock': product['in_stock'], 'type': 'product' }) response_data = { 'results': results, 'pagination': {'more': False} } cache.set(cache_key, response_data, 3600) return JsonResponse(response_data) # Поиск товаров (регистронезависимый поиск с приоритетом точных совпадений) if search_type in ['all', 'product']: # Нормализуем запрос - убираем лишние пробелы query_normalized = ' '.join(query.split()) from django.db.models import Case, When, IntegerField from django.conf import settings # ВРЕМЕННЫЙ ФИХ для SQLite: удалить когда база данных будет PostgreSQL # SQLite не поддерживает регистронезависимый поиск для кириллицы в LIKE if 'sqlite' in settings.DATABASES['default']['ENGINE']: from django.db.models.functions import Lower query_lower = query_normalized.lower() products_query = Product.objects.annotate( name_lower=Lower('name'), sku_lower=Lower('sku'), description_lower=Lower('description') ).filter( models.Q(name_lower__contains=query_lower) | models.Q(sku_lower__contains=query_lower) | models.Q(description_lower__contains=query_lower), is_active=True ).annotate( relevance=Case( When(name_lower=query_lower, then=3), When(name_lower__startswith=query_lower, then=2), default=1, output_field=IntegerField() ) ).order_by('-relevance', 'name') else: # Основное решение для PostgreSQL (работает корректно с кириллицей) products_query = Product.objects.filter( models.Q(name__icontains=query_normalized) | models.Q(sku__icontains=query_normalized) | models.Q(description__icontains=query_normalized), is_active=True ).annotate( relevance=Case( When(name__iexact=query_normalized, then=3), When(name__istartswith=query_normalized, then=2), default=1, output_field=IntegerField() ) ).order_by('-relevance', 'name') total_products = products_query.count() start = (page - 1) * page_size end = start + page_size products = products_query[start:end].values('id', 'name', 'sku', 'price', 'sale_price', 'in_stock') for product in products: text = product['name'] if product['sku']: text += f" ({product['sku']})" # Получаем actual_price: приоритет sale_price > price actual_price = product['sale_price'] if product['sale_price'] else product['price'] results.append({ 'id': product['id'], 'text': text, 'sku': product['sku'], 'price': str(product['price']) if product['price'] else None, 'actual_price': str(actual_price) if actual_price else '0', 'in_stock': product['in_stock'], 'type': 'product' }) has_more = total_products > end else: has_more = False # Поиск групп вариантов if search_type in ['all', 'variant']: variants = ProductVariantGroup.objects.filter( models.Q(name__icontains=query) | models.Q(description__icontains=query) ).prefetch_related('products')[:page_size] for variant in variants: count = variant.products.filter(is_active=True).count() results.append({ 'id': variant.id, 'text': f"{variant.name} ({count} вариантов)", 'type': 'variant', 'count': count }) return JsonResponse({ 'results': results, 'pagination': {'more': has_more if search_type == 'product' else False} }) def validate_kit_cost(request): """ AJAX endpoint для валидации себестоимости комплекта в реальном времени. Принимает список компонентов и возвращает информацию о валидности себестоимости, доступных методах ценообразования и проблемах. Request (JSON POST): { 'components': [ { 'product_id': int or null, 'variant_group_id': int or null, 'quantity': float }, ... ] } Response (JSON): { 'is_valid': bool, 'total_cost': float or null, 'problems': [ { 'component_name': str, 'reason': str }, ... ], 'available_methods': { 'manual': bool, 'from_sale_prices': bool, 'from_cost_plus_percent': bool, 'from_cost_plus_amount': bool } } """ if request.method != 'POST': return JsonResponse({'error': 'Method not allowed'}, status=405) try: import json from decimal import Decimal data = json.loads(request.body) components = data.get('components', []) if not components: return JsonResponse({ 'is_valid': False, 'total_cost': None, 'problems': [{ 'component_name': 'Комплект', 'reason': 'Комплект не содержит компонентов' }], 'available_methods': { 'manual': True, 'from_sale_prices': True, 'from_cost_plus_percent': False, 'from_cost_plus_amount': False } }) # Валидируем каждый компонент total_cost = Decimal('0.00') problems = [] for idx, component in enumerate(components): product_id = component.get('product_id') variant_group_id = component.get('variant_group_id') quantity = Decimal(str(component.get('quantity', 1))) product = None product_name = '' # Получаем товар if product_id: try: product = Product.objects.get(id=product_id) product_name = product.name except Product.DoesNotExist: problems.append({ 'component_name': f'Товар #{product_id}', 'reason': 'Товар не найден' }) continue elif variant_group_id: try: variant_group = ProductVariantGroup.objects.get(id=variant_group_id) product = variant_group.products.filter(is_active=True).first() if variant_group: product_name = f"[Варианты] {variant_group.name}" except ProductVariantGroup.DoesNotExist: problems.append({ 'component_name': f'Группа вариантов #{variant_group_id}', 'reason': 'Группа не найдена' }) continue if not product: problems.append({ 'component_name': product_name or f'Компонент {idx + 1}', 'reason': 'Товар не выбран или группа пуста' }) continue # Проверяем себестоимость if product.cost_price is None: problems.append({ 'component_name': product_name, 'reason': 'Себестоимость не определена' }) continue if product.cost_price <= 0: problems.append({ 'component_name': product_name, 'reason': 'Себестоимость равна 0' }) continue # Добавляем в сумму if quantity > 0: total_cost += product.cost_price * quantity # Определяем, какие методы доступны is_cost_valid = len(problems) == 0 available_methods = { 'manual': True, 'from_sale_prices': True, 'from_cost_plus_percent': is_cost_valid, 'from_cost_plus_amount': is_cost_valid } return JsonResponse({ 'is_valid': is_cost_valid, 'total_cost': float(total_cost) if is_cost_valid else None, 'problems': problems, 'available_methods': available_methods }) except json.JSONDecodeError: return JsonResponse({ 'error': 'Invalid JSON' }, status=400) except Exception as e: return JsonResponse({ 'error': str(e) }, status=500)