Добавлено отображение остатков и резервов в карточках товаров POS

- Аннотация товаров остатками (available_qty) и резервами (reserved_qty) через Subquery
- Компактный формат отображения: X(−Y) где X - доступно, Y - зарезервировано
- Визуальная стилизация: крупное число для остатков, мелкое для резервов
- Цветовая индикация: зелёный (≥5), жёлтый (<5), красный (≤0)
- Без дополнительных SQL-запросов, оптимизировано через подзапросы
This commit is contained in:
2025-11-17 14:03:31 +03:00
parent 4961660b7a
commit e23bdef679
2 changed files with 66 additions and 4 deletions

View File

@@ -173,7 +173,43 @@ function renderProducts() {
stock.textContent = `🌺 ${item.showcase_name}`;
stock.style.color = '#856404';
stock.style.fontWeight = 'bold';
} else if (item.type === 'product' && item.available_qty !== undefined && item.reserved_qty !== undefined) {
// Для обычных товаров показываем остатки: X(-Y)
const available = parseFloat(item.available_qty) || 0;
const reserved = parseFloat(item.reserved_qty) || 0;
const free = available - reserved;
// Создаём элементы для стилизации разных размеров
const availableSpan = document.createElement('span');
availableSpan.textContent = available;
availableSpan.style.fontSize = '1.1em';
availableSpan.style.fontWeight = 'bold';
availableSpan.style.fontStyle = 'normal';
// Отображаем резерв только если он есть
if (reserved > 0) {
const reservedSpan = document.createElement('span');
reservedSpan.textContent = `(${reserved})`;
reservedSpan.style.fontSize = '0.85em';
reservedSpan.style.marginLeft = '3px';
reservedSpan.style.fontStyle = 'normal';
stock.appendChild(availableSpan);
stock.appendChild(reservedSpan);
} else {
stock.appendChild(availableSpan);
}
// Цветовая индикация: красный если свободных остатков нет или отрицательные
if (free <= 0) {
stock.style.color = '#dc3545'; // Красный
} else if (free < 5) {
stock.style.color = '#ffc107'; // Жёлтый (мало остатков)
} else {
stock.style.color = '#28a745'; // Зелёный (достаточно)
}
} else {
// Fallback для старых данных или комплектов
stock.textContent = item.in_stock ? 'В наличии' : 'Под заказ';
if (!item.in_stock) {
stock.style.color = '#dc3545';

View File

@@ -4,13 +4,14 @@ 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.db.models import Prefetch, OuterRef, Subquery, DecimalField
from django.db.models.functions import Coalesce
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.models import Showcase, Reservation, Warehouse, Stock
from inventory.services import ShowcaseManager
@@ -186,8 +187,31 @@ def pos_terminal(request):
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]
# Показываем все товары, не только in_stock
products_qs = Product.objects.all().prefetch_related(
# Аннотируем остатками и резервами с текущего склада
products_qs = Product.objects.all().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
)
@@ -222,7 +246,9 @@ def pos_terminal(request):
'in_stock': p.in_stock,
'sku': p.sku or '',
'image': image_url,
'type': 'product'
'type': 'product',
'available_qty': str(p.available_qty),
'reserved_qty': str(p.reserved_qty)
})
# Сериализация комплектов с оптимизацией фото