Проблема: при создании тенанта автоматически создаются дефолтные Склад и Витрина. Если пользователь удалит их, система может сломаться: POS, создание заказов и резервирование перестанут работать. Решение: реализована строгая валидация + мягкое удаление для витрин. Изменения в inventory/views/warehouse.py: - Добавлена валидация перед деактивацией склада: * Блокировка деактивации последнего активного склада * Проверка ненулевых остатков товаров * Проверка активных резервов * Предупреждение при деактивации дефолтного склада Изменения в inventory/views/showcase.py: - ShowcaseListView: по умолчанию показывает только активные витрины - ShowcaseDeleteView: изменена логика с жесткого на мягкое удаление - Добавлена валидация перед деактивацией витрины: * Блокировка деактивации последней активной витрины склада * Проверка активных резервов * Проверка физических экземпляров комплектов (ShowcaseItem) * Предупреждение при деактивации дефолтной витрины Изменения в inventory/forms_showcase.py: - Проверка уникальности названия витрины учитывает только активные Изменения в inventory/admin.py: - ShowcaseAdmin: добавлены методы delete_model() и delete_queryset() для блокировки удаления последней витрины через админку - WarehouseAdmin: добавлены методы delete_model() и delete_queryset() для блокировки удаления последнего склада через админку Преимущества: ✅ Система не сломается - всегда есть хотя бы один активный склад/витрина ✅ Данные в безопасности - мягкое удаление для обеих сущностей ✅ Понятные сообщения об ошибках для пользователя ✅ Защита работает как в UI, так и в Django Admin 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
171 lines
7.5 KiB
Python
171 lines
7.5 KiB
Python
# -*- coding: utf-8 -*-
|
||
from django.shortcuts import render, get_object_or_404
|
||
from django.views.generic import ListView, CreateView, UpdateView, DeleteView, View
|
||
from django.urls import reverse_lazy
|
||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||
from django.contrib import messages
|
||
from django.http import JsonResponse, HttpResponseRedirect
|
||
from django.views.decorators.http import require_http_methods
|
||
from django.utils.decorators import method_decorator
|
||
from ..models import Warehouse
|
||
from ..forms import WarehouseForm
|
||
|
||
|
||
class WarehouseListView(LoginRequiredMixin, ListView):
|
||
"""
|
||
Список всех складов тенанта
|
||
Сортирует по is_default (по умолчанию первым), потом по названию
|
||
"""
|
||
model = Warehouse
|
||
template_name = 'inventory/warehouse/warehouse_list.html'
|
||
context_object_name = 'warehouses'
|
||
paginate_by = 20
|
||
|
||
def get_queryset(self):
|
||
# Сортируем: сначала is_default DESC (по умолчанию первый), потом по названию
|
||
return Warehouse.objects.filter(is_active=True).order_by('-is_default', 'name')
|
||
|
||
|
||
class WarehouseCreateView(LoginRequiredMixin, CreateView):
|
||
"""
|
||
Создание нового склада
|
||
"""
|
||
model = Warehouse
|
||
form_class = WarehouseForm
|
||
template_name = 'inventory/warehouse/warehouse_form.html'
|
||
success_url = reverse_lazy('inventory:warehouse-list')
|
||
|
||
def form_valid(self, form):
|
||
messages.success(self.request, f'Склад "{form.instance.name}" успешно создан.')
|
||
return super().form_valid(form)
|
||
|
||
|
||
class WarehouseUpdateView(LoginRequiredMixin, UpdateView):
|
||
"""
|
||
Редактирование склада
|
||
"""
|
||
model = Warehouse
|
||
form_class = WarehouseForm
|
||
template_name = 'inventory/warehouse/warehouse_form.html'
|
||
success_url = reverse_lazy('inventory:warehouse-list')
|
||
|
||
def form_valid(self, form):
|
||
messages.success(self.request, f'Склад "{form.instance.name}" успешно обновлён.')
|
||
return super().form_valid(form)
|
||
|
||
|
||
class WarehouseDeleteView(LoginRequiredMixin, DeleteView):
|
||
"""
|
||
Удаление склада (мягкое удаление - деактивация).
|
||
Вместо физического удаления из БД, устанавливаем is_active=False
|
||
"""
|
||
model = Warehouse
|
||
template_name = 'inventory/warehouse/warehouse_confirm_delete.html'
|
||
success_url = reverse_lazy('inventory:warehouse-list')
|
||
|
||
def post(self, request, *args, **kwargs):
|
||
"""
|
||
Переопределяем POST метод чтобы использовать мягкое удаление
|
||
вместо стандартного физического удаления Django.
|
||
Включает строгую валидацию для защиты от случайного удаления.
|
||
"""
|
||
self.object = self.get_object()
|
||
warehouse_name = self.object.name
|
||
|
||
# 1. Проверка: это последний активный склад?
|
||
active_warehouses_count = Warehouse.objects.filter(is_active=True).count()
|
||
if active_warehouses_count <= 1:
|
||
messages.error(
|
||
request,
|
||
'Невозможно деактивировать последний активный склад. '
|
||
'Система требует наличия хотя бы одного активного склада для работы. '
|
||
'Создайте новый склад перед деактивацией этого.'
|
||
)
|
||
return HttpResponseRedirect(reverse_lazy('inventory:warehouse-list'))
|
||
|
||
# 2. Проверка: есть ли ненулевые остатки товаров?
|
||
from inventory.models import Stock
|
||
has_stock = Stock.objects.filter(
|
||
warehouse=self.object,
|
||
available__gt=0
|
||
).exists()
|
||
if has_stock:
|
||
messages.error(
|
||
request,
|
||
f'Невозможно деактивировать склад "{warehouse_name}": '
|
||
'на складе есть товары с ненулевыми остатками. '
|
||
'Переместите все товары на другой склад перед деактивацией.'
|
||
)
|
||
return HttpResponseRedirect(reverse_lazy('inventory:warehouse-list'))
|
||
|
||
# 3. Проверка: есть ли активные резервы?
|
||
from inventory.models import Reservation
|
||
active_reservations_count = Reservation.objects.filter(
|
||
warehouse=self.object,
|
||
status='reserved'
|
||
).count()
|
||
if active_reservations_count > 0:
|
||
messages.error(
|
||
request,
|
||
f'Невозможно деактивировать склад "{warehouse_name}": '
|
||
f'на складе есть {active_reservations_count} активных резервирований. '
|
||
'Завершите все заказы с резервами перед деактивацией склада.'
|
||
)
|
||
return HttpResponseRedirect(reverse_lazy('inventory:warehouse-list'))
|
||
|
||
# 4. Предупреждение: если это дефолтный склад
|
||
if self.object.is_default:
|
||
messages.warning(
|
||
request,
|
||
f'Внимание: вы деактивируете склад по умолчанию "{warehouse_name}". '
|
||
'Рекомендуется сначала назначить другой склад по умолчанию.'
|
||
)
|
||
|
||
# Мягкое удаление - деактивируем склад
|
||
self.object.is_active = False
|
||
self.object.save()
|
||
|
||
messages.success(
|
||
request,
|
||
f'Склад "{warehouse_name}" успешно архивирован и скрыт из списка. '
|
||
'Все связанные данные сохранены.'
|
||
)
|
||
return HttpResponseRedirect(self.get_success_url())
|
||
|
||
|
||
@method_decorator(require_http_methods(["POST"]), name="dispatch")
|
||
class SetDefaultWarehouseView(LoginRequiredMixin, View):
|
||
"""
|
||
Установка склада по умолчанию
|
||
Обрабатывает POST запрос от AJAX и возвращает JSON ответ
|
||
"""
|
||
|
||
def post(self, request, pk):
|
||
"""
|
||
Установить склад с заданным pk как склад по умолчанию
|
||
"""
|
||
try:
|
||
warehouse = get_object_or_404(Warehouse, pk=pk, is_active=True)
|
||
|
||
# Установить этот склад как по умолчанию
|
||
# (метод save() в модели автоматически снимет флаг с других)
|
||
warehouse.is_default = True
|
||
warehouse.save()
|
||
|
||
return JsonResponse({
|
||
'status': 'success',
|
||
'message': f'Склад "{warehouse.name}" установлен по умолчанию',
|
||
'warehouse_id': warehouse.id,
|
||
'warehouse_name': warehouse.name
|
||
})
|
||
except Warehouse.DoesNotExist:
|
||
return JsonResponse({
|
||
'status': 'error',
|
||
'message': 'Склад не найден'
|
||
}, status=404)
|
||
except Exception as e:
|
||
return JsonResponse({
|
||
'status': 'error',
|
||
'message': str(e)
|
||
}, status=500)
|