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>
This commit is contained in:
2025-11-15 15:30:23 +03:00
parent 079bd23829
commit 7132d2c910
26 changed files with 529 additions and 354 deletions

View File

@@ -404,7 +404,7 @@ class TransferLineForm(forms.Form):
Используется в динамической таблице для ввода нескольких товаров.
"""
product = forms.ModelChoiceField(
queryset=Product.objects.filter(is_active=True).order_by('name'),
queryset=Product.objects.filter(status='active').order_by('name'),
widget=forms.Select(attrs={'class': 'form-control'}),
label="Товар",
required=True

View File

@@ -1,4 +1,4 @@
# Generated by Django 5.0.10 on 2025-11-14 20:45
# Generated by Django 5.0.10 on 2025-11-15 11:57
import phonenumber_field.modelfields
from django.db import migrations, models

View File

@@ -1,4 +1,4 @@
# Generated by Django 5.0.10 on 2025-11-14 20:45
# Generated by Django 5.0.10 on 2025-11-15 11:57
import django.db.models.deletion
from django.db import migrations, models

View File

@@ -88,7 +88,7 @@ class IncomingCreateView(LoginRequiredMixin, View):
"""Отображение формы ввода товаров."""
form = IncomingForm()
# Django-tenants автоматически фильтрует по текущей схеме
products = Product.objects.filter(is_active=True).order_by('name')
products = Product.objects.filter(status='active').order_by('name')
# Генерируем номер документа автоматически
generated_document_number = generate_incoming_document_number()
@@ -106,7 +106,7 @@ class IncomingCreateView(LoginRequiredMixin, View):
if not form.is_valid():
# Django-tenants автоматически фильтрует по текущей схеме
products = Product.objects.filter(is_active=True).order_by('name')
products = Product.objects.filter(status='active').order_by('name')
context = {
'form': form,
'products': products,
@@ -128,7 +128,7 @@ class IncomingCreateView(LoginRequiredMixin, View):
if not products_data:
messages.error(request, 'Пожалуйста, добавьте хотя бы один товар.')
# Django-tenants автоматически фильтрует по текущей схеме
products = Product.objects.filter(is_active=True).order_by('name')
products = Product.objects.filter(status='active').order_by('name')
context = {
'form': form,
'products': products,
@@ -186,7 +186,7 @@ class IncomingCreateView(LoginRequiredMixin, View):
messages.error(request, f'Ошибка при создании партии: {str(e)}')
# Восстанавливаем данные на форме
products = Product.objects.filter(is_active=True).order_by('name')
products = Product.objects.filter(status='active').order_by('name')
context = {
'form': form,
'products': products,
@@ -200,7 +200,7 @@ class IncomingCreateView(LoginRequiredMixin, View):
f'❌ Ошибка при создании приходов: {str(e)}'
)
# Django-tenants автоматически фильтрует по текущей схеме
products = Product.objects.filter(is_active=True).order_by('name')
products = Product.objects.filter(status='active').order_by('name')
context = {
'form': form,
'products': products,

View File

@@ -49,7 +49,7 @@ class TransferBulkCreateView(LoginRequiredMixin, View):
def get(self, request):
from products.models import Product
form = TransferBulkForm()
products = Product.objects.filter(is_active=True).values('id', 'name', 'sku').order_by('name')
products = Product.objects.filter(status='active').values('id', 'name', 'sku').order_by('name')
return render(request, self.template_name, {
'form': form,
'products': products