Files
octopus/myproject/products/views/api_views.py
Andrey Smakotin d92045c4c4 refactor: Создать базовый класс BaseProductEntity и реструктурировать Product/ProductKit
Основные изменения:

## Модели (models.py)
- Создан абстрактный класс BaseProductEntity с общими полями:
  * Идентификация: name, sku, slug
  * Описания: description, short_description (новое поле)
  * Статус: is_active, timestamps, soft delete
  * Managers: objects, all_objects, active

- Product:
  * Унаследован от BaseProductEntity
  * sale_price переименован в price (основная цена)
  * Добавлено новое поле sale_price (цена со скидкой, nullable)
  * Добавлено property actual_price

- ProductKit:
  * Унаследован от BaseProductEntity
  * fixed_price переименован в price (ручная цена)
  * pricing_method: 'fixed' → 'manual'
  * Добавлено поле sale_price (цена со скидкой)
  * Добавлено поле cost_price (nullable)
  * Добавлены properties: calculated_price, actual_price
  * Обновлен calculate_price_with_substitutions()

## Forms (forms.py)
- ProductForm: добавлено short_description, price, sale_price
- ProductKitForm: добавлено short_description, cost_price, price, sale_price

## Admin (admin.py)
- ProductAdmin: обновлены list_display и fieldsets с новыми полями
- ProductKitAdmin: добавлены fieldsets, метод get_price_display()

## Templates
- product_form.html: добавлены поля price, sale_price, short_description
- product_detail.html: показывает зачеркнутую цену + скидку + бейджик "Акция"
- product_list.html: отображение цен со скидкой и бейджиком "Акция"
- all_products_list.html: единообразное отображение цен
- productkit_detail.html: отображение скидок с бейджиком "Акция"

## API (api_views.py)
- Обновлены все endpoints для использования поля price вместо sale_price

Результат: единообразная архитектура с поддержкой скидок, DRY-принцип,
логичные названия полей, красивое отображение акций в UI.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-31 00:49:01 +03:00

190 lines
7.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
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,
'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', 'in_stock')
for product in products:
text = product['name']
if product['sku']:
text += f" ({product['sku']})"
results.append({
'id': product['id'],
'text': text,
'sku': product['sku'],
'price': str(product['price']) if product['price'] else None,
'in_stock': product['in_stock']
})
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', 'in_stock')
for product in products:
text = product['name']
if product['sku']:
text += f" ({product['sku']})"
results.append({
'id': product['id'],
'text': text,
'sku': product['sku'],
'price': str(product['price']) if product['price'] else None,
'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}
})