Добавлено отображение остатков и резервов в карточках товаров POS
- Аннотация товаров остатками (available_qty) и резервами (reserved_qty) через Subquery - Компактный формат отображения: X(−Y) где X - доступно, Y - зарезервировано - Визуальная стилизация: крупное число для остатков, мелкое для резервов - Цветовая индикация: зелёный (≥5), жёлтый (<5), красный (≤0) - Без дополнительных SQL-запросов, оптимизировано через подзапросы
This commit is contained in:
@@ -173,7 +173,43 @@ function renderProducts() {
|
|||||||
stock.textContent = `🌺 ${item.showcase_name}`;
|
stock.textContent = `🌺 ${item.showcase_name}`;
|
||||||
stock.style.color = '#856404';
|
stock.style.color = '#856404';
|
||||||
stock.style.fontWeight = 'bold';
|
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 {
|
} else {
|
||||||
|
// Fallback для старых данных или комплектов
|
||||||
stock.textContent = item.in_stock ? 'В наличии' : 'Под заказ';
|
stock.textContent = item.in_stock ? 'В наличии' : 'Под заказ';
|
||||||
if (!item.in_stock) {
|
if (!item.in_stock) {
|
||||||
stock.style.color = '#dc3545';
|
stock.style.color = '#dc3545';
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ from django.contrib.auth.decorators import login_required
|
|||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.views.decorators.http import require_http_methods
|
from django.views.decorators.http import require_http_methods
|
||||||
from django.db import transaction
|
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 django.utils import timezone
|
||||||
from decimal import Decimal, InvalidOperation
|
from decimal import Decimal, InvalidOperation
|
||||||
import json
|
import json
|
||||||
|
|
||||||
from products.models import Product, ProductCategory, ProductKit, KitItem
|
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
|
from inventory.services import ShowcaseManager
|
||||||
|
|
||||||
|
|
||||||
@@ -186,8 +187,31 @@ def pos_terminal(request):
|
|||||||
to_attr='first_photo_list'
|
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
|
# Показываем все товары, не только 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',
|
'categories',
|
||||||
first_product_photo
|
first_product_photo
|
||||||
)
|
)
|
||||||
@@ -222,7 +246,9 @@ def pos_terminal(request):
|
|||||||
'in_stock': p.in_stock,
|
'in_stock': p.in_stock,
|
||||||
'sku': p.sku or '',
|
'sku': p.sku or '',
|
||||||
'image': image_url,
|
'image': image_url,
|
||||||
'type': 'product'
|
'type': 'product',
|
||||||
|
'available_qty': str(p.available_qty),
|
||||||
|
'reserved_qty': str(p.reserved_qty)
|
||||||
})
|
})
|
||||||
|
|
||||||
# Сериализация комплектов с оптимизацией фото
|
# Сериализация комплектов с оптимизацией фото
|
||||||
|
|||||||
Reference in New Issue
Block a user