Files
octopus/myproject/inventory/views/showcase.py
Andrey Smakotin a1f5557036 Исключены зарезервированные букеты из отображения в POS
- inventory/views/showcase.py: фильтр .exclude(status='reserved')
  * Витринные букеты со статусом 'reserved' не отображаются в POS
  * Защита от конфликтов: один букет - один заказ
- pos/views.py: фильтр .exclude(showcase_items__status='reserved')
  * Showcase комплекты без доступных букетов скрыты в POS
  * Фильтрация на уровне queryset для производительности
- Консистентная видимость витрины для всех кассиров
2026-01-05 01:39:14 +03:00

279 lines
12 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib import messages
from django.shortcuts import render, redirect, get_object_or_404
from django.db.models import Count, Q
from django.views.generic import ListView, CreateView, UpdateView, DeleteView, View
from django.urls import reverse_lazy
from django.utils.decorators import method_decorator
from django.http import JsonResponse
from inventory.models import Showcase, Reservation
from inventory.forms_showcase import ShowcaseForm
@method_decorator(login_required, name='dispatch')
class ShowcaseListView(ListView):
"""
Список всех витрин с группировкой по складам.
Отображает информацию о количестве активных резервов на каждой витрине.
"""
model = Showcase
template_name = 'inventory/showcase/list.html'
context_object_name = 'showcases'
def get_queryset(self):
"""
Получаем витрины с аннотацией количества активных резервов.
Сортируем по складу и названию.
По умолчанию показываем только активные витрины.
"""
# По умолчанию показываем только активные витрины
queryset = Showcase.objects.filter(is_active=True).select_related('warehouse').annotate(
active_reservations_count=Count(
'reservations',
filter=Q(reservations__status='reserved')
)
).order_by('warehouse__name', 'name')
# Фильтрация по складу, если указан GET-параметр
warehouse_id = self.request.GET.get('warehouse')
if warehouse_id:
queryset = queryset.filter(warehouse_id=warehouse_id)
# Фильтрация по статусу активности (переопределяет дефолтный фильтр)
is_active = self.request.GET.get('is_active')
if is_active == '0':
# Показать только неактивные
queryset = Showcase.objects.filter(is_active=False).select_related('warehouse').annotate(
active_reservations_count=Count(
'reservations',
filter=Q(reservations__status='reserved')
)
).order_by('warehouse__name', 'name')
elif is_active == 'all':
# Показать все (и активные, и неактивные)
queryset = Showcase.objects.select_related('warehouse').annotate(
active_reservations_count=Count(
'reservations',
filter=Q(reservations__status='reserved')
)
).order_by('warehouse__name', 'name')
return queryset
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = 'Витрины'
# Добавляем информацию для фильтров
from inventory.models import Warehouse
context['warehouses'] = Warehouse.objects.filter(is_active=True).order_by('name')
return context
@method_decorator(login_required, name='dispatch')
class ShowcaseCreateView(CreateView):
"""
Создание новой витрины.
"""
model = Showcase
form_class = ShowcaseForm
template_name = 'inventory/showcase/form.html'
success_url = reverse_lazy('inventory:showcase-list')
def form_valid(self, form):
"""Сохраняем витрину и показываем сообщение об успехе"""
response = super().form_valid(form)
messages.success(
self.request,
f'Витрина "{self.object.name}" успешно создана на складе "{self.object.warehouse.name}"'
)
return response
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = 'Создание витрины'
context['form_title'] = 'Новая витрина'
context['submit_text'] = 'Создать витрину'
return context
@method_decorator(login_required, name='dispatch')
class ShowcaseUpdateView(UpdateView):
"""
Редактирование существующей витрины.
"""
model = Showcase
form_class = ShowcaseForm
template_name = 'inventory/showcase/form.html'
success_url = reverse_lazy('inventory:showcase-list')
def form_valid(self, form):
"""Сохраняем изменения и показываем сообщение об успехе"""
response = super().form_valid(form)
messages.success(
self.request,
f'Витрина "{self.object.name}" успешно обновлена'
)
return response
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = f'Редактирование витрины: {self.object.name}'
context['form_title'] = f'Редактирование: {self.object.name}'
context['submit_text'] = 'Сохранить изменения'
# Добавляем информацию о резервах на витрине
context['active_reservations_count'] = Reservation.objects.filter(
showcase=self.object,
status='reserved'
).count()
return context
@method_decorator(login_required, name='dispatch')
class ShowcaseDeleteView(DeleteView):
"""
Деактивация витрины (мягкое удаление) с подтверждением.
Проверяет наличие активных резервов и физических экземпляров перед деактивацией.
"""
model = Showcase
template_name = 'inventory/showcase/delete.html'
success_url = reverse_lazy('inventory:showcase-list')
def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['title'] = f'Деактивация витрины: {self.object.name}'
# Проверяем наличие активных резервов
active_reservations = Reservation.objects.filter(
showcase=self.object,
status='reserved'
).select_related('product')
context['active_reservations'] = active_reservations
context['has_active_reservations'] = active_reservations.exists()
# Проверяем наличие физических экземпляров
from inventory.models import ShowcaseItem
showcase_items = ShowcaseItem.objects.filter(showcase=self.object)
context['showcase_items'] = showcase_items
context['has_showcase_items'] = showcase_items.exists()
return context
def post(self, request, *args, **kwargs):
"""
Переопределяем POST метод для мягкого удаления витрины.
Включает строгую валидацию для защиты от случайной деактивации.
"""
self.object = self.get_object()
showcase = self.object
showcase_name = showcase.name
# 1. Проверка: это последняя активная витрина склада?
active_showcases_count = Showcase.objects.filter(
warehouse=showcase.warehouse,
is_active=True
).count()
if active_showcases_count <= 1:
messages.error(
request,
f'Невозможно деактивировать последнюю активную витрину склада "{showcase.warehouse.name}". '
'Система требует наличия хотя бы одной активной витрины на каждом складе. '
'Создайте новую витрину перед деактивацией этой.'
)
return redirect('inventory:showcase-delete', pk=showcase.pk)
# 2. Проверка: есть ли активные резервы?
active_reservations_count = Reservation.objects.filter(
showcase=showcase,
status='reserved'
).count()
if active_reservations_count > 0:
messages.error(
request,
f'Невозможно деактивировать витрину "{showcase_name}": '
f'на ней есть {active_reservations_count} активных резервов. '
'Сначала освободите или продайте зарезервированные товары.'
)
return redirect('inventory:showcase-delete', pk=showcase.pk)
# 3. Проверка: есть ли физические экземпляры комплектов?
from inventory.models import ShowcaseItem
showcase_items_count = ShowcaseItem.objects.filter(
showcase=showcase,
status__in=['available', 'in_cart', 'reserved']
).count()
if showcase_items_count > 0:
messages.error(
request,
f'Невозможно деактивировать витрину "{showcase_name}": '
f'на ней есть {showcase_items_count} физических экземпляров комплектов. '
'Продайте или переместите все комплекты перед деактивацией витрины.'
)
return redirect('inventory:showcase-delete', pk=showcase.pk)
# 4. Предупреждение: если это дефолтная витрина
if showcase.is_default:
messages.warning(
request,
f'Внимание: вы деактивируете витрину по умолчанию для склада "{showcase.warehouse.name}". '
'Рекомендуется сначала назначить другую витрину по умолчанию.'
)
# Мягкое удаление - деактивируем витрину
showcase.is_active = False
showcase.save()
messages.success(
request,
f'Витрина "{showcase_name}" успешно деактивирована и скрыта из списка. '
'Все связанные данные сохранены.'
)
return HttpResponseRedirect(self.get_success_url())
@method_decorator(login_required, name='dispatch')
class SetDefaultShowcaseView(LoginRequiredMixin, View):
"""
Установка витрины по умолчанию для её склада.
Обрабатывает POST запрос от AJAX и возвращает JSON ответ.
"""
def post(self, request, pk):
"""
Установить витрину с заданным pk как витрину по умолчанию для её склада
"""
try:
showcase = get_object_or_404(Showcase, pk=pk, is_active=True)
# Установить эту витрину как по умолчанию
# (метод save() в модели автоматически снимет флаг с других витрин этого склада)
showcase.is_default = True
showcase.save()
return JsonResponse({
'status': 'success',
'message': f'Витрина "{showcase.name}" на складе "{showcase.warehouse.name}" установлена по умолчанию',
'showcase_id': showcase.id,
'showcase_name': showcase.name,
'warehouse_id': showcase.warehouse.id,
'warehouse_name': showcase.warehouse.name
})
except Showcase.DoesNotExist:
return JsonResponse({
'status': 'error',
'message': 'Витрина не найдена'
}, status=404)
except Exception as e:
return JsonResponse({
'status': 'error',
'message': f'Ошибка при установке витрины по умолчанию: {str(e)}'
}, status=500)