Files
octopus/myproject/products/models/managers.py
Andrey Smakotin 7132d2c910 feat: Замена is_active на status для архивирования товаров
Реализована трёхуровневая система статусов товаров и комплектов:
- 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>
2025-11-15 15:30:23 +03:00

150 lines
5.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
Менеджеры и 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()