From e23bdef6792a60aa4ee1eeecb4c3e8308c8d4c41 Mon Sep 17 00:00:00 2001 From: Andrey Smakotin Date: Mon, 17 Nov 2025 14:03:31 +0300 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=BE=20=D0=BE=D1=82=D0=BE=D0=B1=D1=80=D0=B0=D0=B6=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=BE=D1=81=D1=82=D0=B0=D1=82=D0=BA=D0=BE?= =?UTF-8?q?=D0=B2=20=D0=B8=20=D1=80=D0=B5=D0=B7=D0=B5=D1=80=D0=B2=D0=BE?= =?UTF-8?q?=D0=B2=20=D0=B2=20=D0=BA=D0=B0=D1=80=D1=82=D0=BE=D1=87=D0=BA?= =?UTF-8?q?=D0=B0=D1=85=20=D1=82=D0=BE=D0=B2=D0=B0=D1=80=D0=BE=D0=B2=20POS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Аннотация товаров остатками (available_qty) и резервами (reserved_qty) через Subquery - Компактный формат отображения: X(−Y) где X - доступно, Y - зарезервировано - Визуальная стилизация: крупное число для остатков, мелкое для резервов - Цветовая индикация: зелёный (≥5), жёлтый (<5), красный (≤0) - Без дополнительных SQL-запросов, оптимизировано через подзапросы --- myproject/pos/static/pos/js/terminal.js | 36 +++++++++++++++++++++++++ myproject/pos/views.py | 34 ++++++++++++++++++++--- 2 files changed, 66 insertions(+), 4 deletions(-) diff --git a/myproject/pos/static/pos/js/terminal.js b/myproject/pos/static/pos/js/terminal.js index 322e2c4..9c79ead 100644 --- a/myproject/pos/static/pos/js/terminal.js +++ b/myproject/pos/static/pos/js/terminal.js @@ -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'; diff --git a/myproject/pos/views.py b/myproject/pos/views.py index 2da73cc..7261fa3 100644 --- a/myproject/pos/views.py +++ b/myproject/pos/views.py @@ -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) }) # Сериализация комплектов с оптимизацией фото