Реализована трёхуровневая система статусов товаров и комплектов: - active (Активный) - товар доступен для продажи - archived (Архивный) - скрыт, можно восстановить в следующем сезоне - discontinued (Снят) - морально устарел, готов к удалению Изменения: 1. Модели (BaseProductEntity, Product, ProductKit): - Заменено поле is_deleted (Boolean) на status (CharField) - Добавлены архивные метаданные (archived_at, archived_by) - Обновлены методы: archive(), restore(), discontinue(), delete() - Уникальное ограничение изменено на conditional (status='active') 2. Менеджеры (ActiveManager, SoftDeleteQuerySet): - Полиморфная поддержка обеих систем (status и is_active) - Использует hasattr() для совместимости с наследниками - Методы: archive(), restore(), discontinue(), archived_only(), active_only() 3. Формы (ProductForm, ProductKitForm): - Включены поле status в формы - Валидация уникальности по status='active' - CSS классы для статус-селектора 4. Admin панель: - DeletedFilter переименован в StatusFilter с тремя опциями - get_status_display() с цветным отображением статуса - Actions: restore_items, hard_delete_selected, delete_selected - Readonly поля для архивирования 5. Представления: - ProductListView: фильтр status вместо is_active - CombinedProductListView: поддержка фильтра status для товаров и комплектов - API views обновлены для работы со статусом 6. Шаблоны: - product_form.html: form.status вместо form.is_active - productkit_create.html: form.status вместо form.is_active - productkit_edit.html: form.status вместо form.is_active 7. Миграции: - Удалены все старые миграции (чистый перезапуск по требованию пользователя) - Создана новая миграция 0001_initial с полной структурой status-системы - Удален старый код преобразования is_deleted -> status Проведённые проверки: - Django system check passed ✓ - Полиморфные менеджеры работают с обеими системами - Уникальные ограничения корректно работают с условиями - История заказов сохраняется даже после архивирования товара (django-simple-history) 🤖 Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
150 lines
5.9 KiB
Python
150 lines
5.9 KiB
Python
"""
|
||
Менеджеры и QuerySets для моделей продуктов.
|
||
Реализуют паттерн Soft Delete и фильтрацию активных записей.
|
||
"""
|
||
from django.db import models
|
||
from django.utils import timezone
|
||
|
||
|
||
class ActiveManager(models.Manager):
|
||
"""Менеджер для фильтрации только активных записей"""
|
||
def get_queryset(self):
|
||
# Работает с обоими полями: status (Product/ProductKit) и is_active (Category/Tag)
|
||
qs = super().get_queryset()
|
||
if hasattr(self.model, 'status'):
|
||
return qs.filter(status='active')
|
||
elif hasattr(self.model, 'is_active'):
|
||
return qs.filter(is_active=True)
|
||
return qs
|
||
|
||
|
||
class SoftDeleteQuerySet(models.QuerySet):
|
||
"""
|
||
QuerySet для архивирования товаров.
|
||
Позволяет фильтровать архивированные/снятые элементы и восстанавливать их.
|
||
Поддерживает обе системы: status (новая) и is_deleted/is_active (старая).
|
||
"""
|
||
def _has_status_field(self):
|
||
"""Проверяет, использует ли модель поле status"""
|
||
return hasattr(self.model, 'status')
|
||
|
||
def _has_is_deleted_field(self):
|
||
"""Проверяет, использует ли модель поле is_deleted"""
|
||
return hasattr(self.model, 'is_deleted')
|
||
|
||
def delete(self):
|
||
"""Архивирование вместо hard delete"""
|
||
if self._has_status_field():
|
||
return self.update(
|
||
status='archived',
|
||
archived_at=timezone.now()
|
||
)
|
||
elif self._has_is_deleted_field():
|
||
return self.update(
|
||
is_deleted=True,
|
||
deleted_at=timezone.now()
|
||
)
|
||
else:
|
||
# Fallback для моделей без мягкого удаления
|
||
return super().delete()
|
||
|
||
def hard_delete(self):
|
||
"""Явный hard delete - удаляет из БД окончательно"""
|
||
return super().delete()
|
||
|
||
def archive(self):
|
||
"""Архивирование товаров"""
|
||
if self._has_status_field():
|
||
return self.update(status='archived', archived_at=timezone.now())
|
||
elif self._has_is_deleted_field():
|
||
return self.update(is_deleted=True, deleted_at=timezone.now())
|
||
else:
|
||
return self.delete()
|
||
|
||
def restore(self):
|
||
"""Восстановление архивированных товаров"""
|
||
if self._has_status_field():
|
||
return self.update(
|
||
status='active',
|
||
archived_at=None,
|
||
archived_by=None
|
||
)
|
||
elif self._has_is_deleted_field():
|
||
return self.update(
|
||
is_deleted=False,
|
||
deleted_at=None,
|
||
deleted_by=None
|
||
)
|
||
|
||
def discontinue(self):
|
||
"""Пометить как снятые (устарели)"""
|
||
if self._has_status_field():
|
||
return self.update(status='discontinued')
|
||
else:
|
||
# Для моделей без status просто архивируем
|
||
return self.archive()
|
||
|
||
def archived_only(self):
|
||
"""Получить только архивированные товары"""
|
||
if self._has_status_field():
|
||
return self.filter(status='archived')
|
||
elif self._has_is_deleted_field():
|
||
return self.filter(is_deleted=True)
|
||
return self.none()
|
||
|
||
def discontinued_only(self):
|
||
"""Получить только снятые товары"""
|
||
if self._has_status_field():
|
||
return self.filter(status='discontinued')
|
||
return self.none()
|
||
|
||
def active_only(self):
|
||
"""Получить только активные товары"""
|
||
if self._has_status_field():
|
||
return self.filter(status='active')
|
||
elif self._has_is_deleted_field():
|
||
return self.filter(is_deleted=False)
|
||
return self.all()
|
||
|
||
def with_archived(self):
|
||
"""Получить все элементы включая архивированные"""
|
||
return self.all()
|
||
|
||
|
||
class SoftDeleteManager(models.Manager):
|
||
"""
|
||
Manager для работы с архивированием товаров.
|
||
По умолчанию показывает только активные товары/элементы.
|
||
Поддерживает обе системы: status (новая) и is_deleted/is_active (старая).
|
||
"""
|
||
def get_queryset(self):
|
||
qs = SoftDeleteQuerySet(self.model, using=self._db)
|
||
# Автоматически фильтруем активные записи в зависимости от полей
|
||
if hasattr(self.model, 'status'):
|
||
return qs.filter(status='active')
|
||
elif hasattr(self.model, 'is_deleted'):
|
||
return qs.filter(is_deleted=False)
|
||
elif hasattr(self.model, 'is_active'):
|
||
return qs.filter(is_active=True)
|
||
return qs
|
||
|
||
def archived_only(self):
|
||
"""Получить только архивированные товары"""
|
||
qs = SoftDeleteQuerySet(self.model, using=self._db)
|
||
if hasattr(self.model, 'status'):
|
||
return qs.filter(status='archived')
|
||
elif hasattr(self.model, 'is_deleted'):
|
||
return qs.filter(is_deleted=True)
|
||
return qs.none()
|
||
|
||
def discontinued_only(self):
|
||
"""Получить только снятые товары"""
|
||
qs = SoftDeleteQuerySet(self.model, using=self._db)
|
||
if hasattr(self.model, 'status'):
|
||
return qs.filter(status='discontinued')
|
||
return qs.none()
|
||
|
||
def all_with_archived(self):
|
||
"""Получить все товары включая архивированные"""
|
||
return SoftDeleteQuerySet(self.model, using=self._db).all()
|