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

Проблема: при создании тенанта автоматически создаются дефолтные
Склад и Витрина. Если пользователь удалит их, система может сломаться:
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

@@ -37,6 +37,63 @@ class ShowcaseAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
)
readonly_fields = ('created_at', 'updated_at')
def delete_model(self, request, obj):
"""
Защита от удаления последней активной витрины склада через админку.
Рекомендуется использовать деактивацию (is_active=False) вместо удаления.
"""
from django.contrib import messages
# Проверка: это последняя активная витрина склада?
active_showcases_count = Showcase.objects.filter(
warehouse=obj.warehouse,
is_active=True
).count()
if active_showcases_count <= 1 and obj.is_active:
messages.error(
request,
f'Невозможно удалить последнюю активную витрину склада "{obj.warehouse.name}" через админку. '
'Используйте деактивацию (is_active=False) вместо удаления.'
)
return
super().delete_model(request, obj)
def delete_queryset(self, request, queryset):
"""
Защита от массового удаления всех витрин склада через админку.
"""
from django.contrib import messages
# Группируем витрины по складам
warehouses = {}
for showcase in queryset:
if showcase.warehouse_id not in warehouses:
warehouses[showcase.warehouse_id] = {
'warehouse': showcase.warehouse,
'deleting_active': 0,
'total_active': Showcase.objects.filter(
warehouse=showcase.warehouse,
is_active=True
).count()
}
if showcase.is_active:
warehouses[showcase.warehouse_id]['deleting_active'] += 1
# Проверяем, не удаляем ли мы все витрины какого-либо склада
for wh_data in warehouses.values():
remaining = wh_data['total_active'] - wh_data['deleting_active']
if remaining < 1:
messages.error(
request,
f'Невозможно удалить все витрины склада "{wh_data["warehouse"].name}". '
'Хотя бы одна активная витрина должна остаться на каждом складе.'
)
return
super().delete_queryset(request, queryset)
# ===== WAREHOUSE =====
@admin.register(Warehouse)
@@ -61,6 +118,50 @@ class WarehouseAdmin(TenantAdminOnlyMixin, admin.ModelAdmin):
return '-'
is_default_display.short_description = 'По умолчанию'
def delete_model(self, request, obj):
"""
Защита от удаления последнего активного склада через админку.
Рекомендуется использовать деактивацию (is_active=False) вместо удаления.
"""
from django.contrib import messages
# Проверка: это последний активный склад?
active_warehouses_count = Warehouse.objects.filter(is_active=True).count()
if active_warehouses_count <= 1 and obj.is_active:
messages.error(
request,
'Невозможно удалить последний активный склад через админку. '
'Система требует наличия хотя бы одного активного склада для работы. '
'Используйте деактивацию (is_active=False) вместо удаления.'
)
return
super().delete_model(request, obj)
def delete_queryset(self, request, queryset):
"""
Защита от массового удаления всех складов через админку.
"""
from django.contrib import messages
# Подсчитываем текущее количество активных складов
active_count = Warehouse.objects.filter(is_active=True).count()
# Подсчитываем, сколько активных складов хотим удалить
deleting_active_count = queryset.filter(is_active=True).count()
# Проверяем, останется ли хотя бы один активный склад
if active_count - deleting_active_count < 1:
messages.error(
request,
'Невозможно удалить все активные склады. '
'Система требует наличия хотя бы одного активного склада для работы. '
'Используйте деактивацию (is_active=False) вместо удаления.'
)
return
super().delete_queryset(request, queryset)
# ===== STOCK BATCH =====
@admin.register(StockBatch)