Реализована прогрессивная загрузка товаров в POS с пагинацией и infinite scroll. Первая загрузка только категорий и склада, товары подгружаются по 60 штук при клике на категорию с сортировкой по свободным остаткам (available-reserved) по убыванию. Добавлен API endpoint /pos/api/items/ с фильтрацией по категориям и пагинацией. Infinite scroll для догрузки следующих страниц. Lazy loading для изображений.
This commit is contained in:
@@ -231,47 +231,6 @@ def pos_terminal(request):
|
||||
|
||||
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',
|
||||
'available_qty': str(p.available_qty),
|
||||
'reserved_qty': str(p.reserved_qty)
|
||||
})
|
||||
|
||||
# Сериализация комплектов с оптимизацией фото
|
||||
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 = [{
|
||||
@@ -282,7 +241,7 @@ def pos_terminal(request):
|
||||
|
||||
context = {
|
||||
'categories_json': json.dumps(categories),
|
||||
'items_json': json.dumps(all_items),
|
||||
'items_json': json.dumps([]), # Пустой массив - загрузка по API при клике на категорию
|
||||
'showcase_kits_json': json.dumps([]), # Пустой массив - загрузка по API
|
||||
'current_warehouse': {
|
||||
'id': current_warehouse.id,
|
||||
@@ -397,6 +356,161 @@ def get_showcase_kits_api(request):
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["GET"])
|
||||
def get_items_api(request):
|
||||
"""
|
||||
API endpoint для получения товаров и комплектов с пагинацией.
|
||||
Параметры:
|
||||
- category_id: ID категории (опционально, для фильтрации)
|
||||
- page: номер страницы (по умолчанию 1)
|
||||
- page_size: размер страницы (по умолчанию 60)
|
||||
Сортировка по умолчанию: по свободному остатку (available - reserved) DESC
|
||||
"""
|
||||
from products.models import ProductPhoto, ProductKitPhoto
|
||||
from django.core.paginator import Paginator
|
||||
|
||||
# Получаем текущий склад
|
||||
current_warehouse = get_pos_warehouse(request)
|
||||
if not current_warehouse:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'Нет активного склада'
|
||||
}, status=400)
|
||||
|
||||
# Параметры пагинации
|
||||
category_id = request.GET.get('category_id')
|
||||
page = int(request.GET.get('page', 1))
|
||||
page_size = int(request.GET.get('page_size', 60))
|
||||
|
||||
# Prefetch для первого фото товаров
|
||||
first_product_photo = Prefetch(
|
||||
'photos',
|
||||
queryset=ProductPhoto.objects.order_by('order')[:1],
|
||||
to_attr='first_photo_list'
|
||||
)
|
||||
|
||||
# Подзапросы для остатков по текущему складу
|
||||
stock_available_subquery = Stock.objects.filter(
|
||||
product=OuterRef('pk'),
|
||||
warehouse=current_warehouse
|
||||
).values('quantity_available')[:1]
|
||||
|
||||
stock_reserved_subquery = Stock.objects.filter(
|
||||
product=OuterRef('pk'),
|
||||
warehouse=current_warehouse
|
||||
).values('quantity_reserved')[:1]
|
||||
|
||||
# Фильтруем только активные товары
|
||||
products_qs = Product.objects.filter(status='active').annotate(
|
||||
available_qty=Coalesce(
|
||||
Subquery(stock_available_subquery, output_field=DecimalField()),
|
||||
Decimal('0'),
|
||||
output_field=DecimalField()
|
||||
),
|
||||
reserved_qty=Coalesce(
|
||||
Subquery(stock_reserved_subquery, output_field=DecimalField()),
|
||||
Decimal('0'),
|
||||
output_field=DecimalField()
|
||||
)
|
||||
).prefetch_related(
|
||||
'categories',
|
||||
first_product_photo
|
||||
)
|
||||
|
||||
# Фильтруем по категории, если указана
|
||||
if category_id:
|
||||
products_qs = products_qs.filter(categories__id=category_id)
|
||||
|
||||
# Сериализуем товары
|
||||
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()
|
||||
|
||||
available = p.available_qty
|
||||
reserved = p.reserved_qty
|
||||
free_qty = available - reserved
|
||||
|
||||
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',
|
||||
'available_qty': str(available),
|
||||
'reserved_qty': str(reserved),
|
||||
'free_qty': float(free_qty) # Для сортировки
|
||||
})
|
||||
|
||||
# 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,
|
||||
status='active'
|
||||
).prefetch_related(
|
||||
'categories',
|
||||
first_kit_photo
|
||||
)
|
||||
|
||||
# Фильтруем комплекты по категории, если указана
|
||||
if category_id:
|
||||
kits_qs = kits_qs.filter(categories__id=category_id)
|
||||
|
||||
# Сериализуем комплекты
|
||||
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',
|
||||
'free_qty': 0 # Для сортировки комплекты всегда внизу
|
||||
})
|
||||
|
||||
# Объединяем и сортируем по free_qty DESC
|
||||
all_items = products + kits
|
||||
all_items.sort(key=lambda x: x['free_qty'], reverse=True)
|
||||
|
||||
# Пагинация
|
||||
paginator = Paginator(all_items, page_size)
|
||||
page_obj = paginator.get_page(page)
|
||||
|
||||
# Удаляем временное поле free_qty из результата
|
||||
items_to_return = []
|
||||
for item in page_obj.object_list:
|
||||
item_copy = item.copy()
|
||||
item_copy.pop('free_qty', None)
|
||||
items_to_return.append(item_copy)
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'items': items_to_return,
|
||||
'has_more': page_obj.has_next(),
|
||||
'next_page': page_obj.next_page_number() if page_obj.has_next() else None,
|
||||
'total_pages': paginator.num_pages,
|
||||
'total_items': paginator.count
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
@require_http_methods(["GET"])
|
||||
def get_product_kit_details(request, kit_id):
|
||||
|
||||
Reference in New Issue
Block a user