diff --git a/myproject/inventory/templates/inventory/reservation/reservation_list.html b/myproject/inventory/templates/inventory/reservation/reservation_list.html index 5f00072..d7c22e0 100644 --- a/myproject/inventory/templates/inventory/reservation/reservation_list.html +++ b/myproject/inventory/templates/inventory/reservation/reservation_list.html @@ -7,16 +7,30 @@ {% block inventory_content %}
-

Активные резервирования

- Только для просмотра +

+ Активные резервирования + {% if filtered_product %} + — {{ filtered_product.name }} + {% endif %} +

+
+ {% if filtered_product %} + + Сбросить фильтр + + {% endif %} + Только для просмотра +
+ {% if not filtered_product %}
Информация: Резервы создаются автоматически через POS (витринные комплекты) и заказы. Прямое управление резервами отключено для предотвращения несогласованности данных.
+ {% endif %} {% if reservations %}
@@ -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') # Применяем фильтры