@@ -36,7 +50,9 @@
{% for r in reservations %}
|
- {{ r.product.name }}
+
+ {{ r.product.name }}
+
|
{{ r.quantity|smart_quantity }}
@@ -78,10 +94,10 @@
diff --git a/myproject/inventory/views/reservation.py b/myproject/inventory/views/reservation.py
index 17604b0..508f1c7 100644
--- a/myproject/inventory/views/reservation.py
+++ b/myproject/inventory/views/reservation.py
@@ -6,6 +6,7 @@ Reservation (Резервирование товара) views - READ ONLY
from django.views.generic import ListView
from django.contrib.auth.mixins import LoginRequiredMixin
from ..models import Reservation
+from products.models import Product
class ReservationListView(LoginRequiredMixin, ListView):
@@ -19,7 +20,25 @@ class ReservationListView(LoginRequiredMixin, ListView):
paginate_by = 20
def get_queryset(self):
- """Показываем все резервы со статусом 'reserved'"""
- return Reservation.objects.filter(
+ """Показываем все резервы со статусом 'reserved', с опциональной фильтрацией по товару"""
+ queryset = Reservation.objects.filter(
status='reserved'
- ).select_related('product', 'warehouse', 'order_item', 'showcase').order_by('-reserved_at')
+ ).select_related('product', 'warehouse', 'order_item', 'showcase', 'product_kit').order_by('-reserved_at')
+
+ # Фильтрация по товару
+ product_id = self.request.GET.get('product')
+ if product_id:
+ queryset = queryset.filter(product_id=product_id)
+
+ return queryset
+
+ def get_context_data(self, **kwargs):
+ context = super().get_context_data(**kwargs)
+ # Если фильтруем по товару, добавляем его в контекст
+ product_id = self.request.GET.get('product')
+ if product_id:
+ try:
+ context['filtered_product'] = Product.objects.get(pk=product_id)
+ except Product.DoesNotExist:
+ pass
+ return context
diff --git a/myproject/products/templates/products/product_detail.html b/myproject/products/templates/products/product_detail.html
index 4a33908..5bd363f 100644
--- a/myproject/products/templates/products/product_detail.html
+++ b/myproject/products/templates/products/product_detail.html
@@ -139,6 +139,16 @@
| Артикул: |
{{ product.sku }} |
+
+ | Остаток: |
+
+ {{ product.total_free|floatformat:0 }} свободно
+ {% if product.total_reserved > 0 %}
+ {{ product.total_reserved|floatformat:0 }} в резерве
+ {% endif %}
+ (всего: {{ product.total_available|floatformat:0 }})
+ |
+
| Описание: |
{{ product.description|default:"-" }} |
@@ -266,16 +276,6 @@
{% endif %}
-
- | В наличии: |
-
- {% if product.in_stock %}
- Да, в наличии
- {% else %}
- Нет, закончился
- {% endif %}
- |
-
| Статус: |
diff --git a/myproject/products/templates/products/products_list.html b/myproject/products/templates/products/products_list.html
index fef3130..1fb59f5 100644
--- a/myproject/products/templates/products/products_list.html
+++ b/myproject/products/templates/products/products_list.html
@@ -131,7 +131,7 @@
| Категория |
Теги |
Цена |
- В наличии |
+ Остаток |
Статус |
Действия |
@@ -193,10 +193,8 @@
{% if item.item_type == 'product' %}
- {% if item.in_stock %}
- Да
- {% else %}
- Нет
+ {{ item.total_free|floatformat:0 }}{% if item.total_reserved > 0 %} / {{ item.total_available|floatformat:0 }}
+ {{ item.total_reserved|floatformat:0 }} в резерве
{% endif %}
{% else %}
-
diff --git a/myproject/products/views/product_views.py b/myproject/products/views/product_views.py
index 054f82e..ae32108 100644
--- a/myproject/products/views/product_views.py
+++ b/myproject/products/views/product_views.py
@@ -5,7 +5,8 @@ from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin
from django.views.generic import ListView, CreateView, DetailView, UpdateView, DeleteView
from django.urls import reverse_lazy
-from django.db.models import Q
+from django.db.models import Q, Sum, Value, DecimalField
+from django.db.models.functions import Coalesce
from itertools import chain
from ..models import Product, ProductCategory, ProductTag, ProductKit
@@ -162,8 +163,14 @@ class ProductDetailView(LoginRequiredMixin, PermissionRequiredMixin, DetailView)
permission_required = 'products.view_product'
def get_queryset(self):
- # Предзагрузка фотографий для избежания N+1 запросов
- return super().get_queryset().prefetch_related('photos')
+ # Предзагрузка фотографий и аннотация остатков
+ total_available = Coalesce(Sum('stocks__quantity_available'), Value(0), output_field=DecimalField())
+ total_reserved = Coalesce(Sum('stocks__quantity_reserved'), Value(0), output_field=DecimalField())
+ return super().get_queryset().prefetch_related('photos').annotate(
+ total_available=total_available,
+ total_reserved=total_reserved,
+ total_free=total_available - total_reserved,
+ )
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
@@ -253,7 +260,14 @@ class CombinedProductListView(LoginRequiredMixin, PermissionRequiredMixin, ListV
type_filter = self.request.GET.get('type', 'all')
# Получаем товары и комплекты (только постоянные комплекты)
- products = Product.objects.prefetch_related('categories', 'photos', 'tags')
+ # Аннотируем товары данными об остатках из агрегированной таблицы Stock
+ total_available = Coalesce(Sum('stocks__quantity_available'), Value(0), output_field=DecimalField())
+ total_reserved = Coalesce(Sum('stocks__quantity_reserved'), Value(0), output_field=DecimalField())
+ products = Product.objects.prefetch_related('categories', 'photos', 'tags').annotate(
+ total_available=total_available,
+ total_reserved=total_reserved,
+ total_free=total_available - total_reserved,
+ )
kits = ProductKit.objects.filter(is_temporary=False).prefetch_related('categories', 'photos', 'tags')
# Применяем фильтры
|