Добавлена защита от удаления дефолтных Склада и Витрины
Проблема: при создании тенанта автоматически создаются дефолтные Склад и Витрина. Если пользователь удалит их, система может сломаться: 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:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user