Добавлена защита от удаления дефолтных Склада и Витрины

Проблема: при создании тенанта автоматически создаются дефолтные
Склад и Витрина. Если пользователь удалит их, система может сломаться:
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>
This commit is contained in:
2026-01-03 19:52:01 +03:00
parent e6fb30aa02
commit 0f09702094
4 changed files with 244 additions and 27 deletions

View File

@@ -66,16 +66,70 @@ class WarehouseDeleteView(LoginRequiredMixin, DeleteView):
def post(self, request, *args, **kwargs):
"""
Переопределяем POST метод чтобы использовать мягкое удаление
вместо стандартного физического удаления Django
вместо стандартного физического удаления 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}" архивирован и скрыт из списка.')
messages.success(
request,
f'Склад "{warehouse_name}" успешно архивирован и скрыт из списка. '
'Все связанные данные сохранены.'
)
return HttpResponseRedirect(self.get_success_url())