Files
octopus/myproject/inventory/migrations/0001_initial.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

253 lines
16 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.
# Generated by Django 5.0.10 on 2025-11-15 11:57
import phonenumber_field.modelfields
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='DocumentCounter',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('counter_type', models.CharField(choices=[('transfer', 'Перемещение товара')], max_length=20, unique=True, verbose_name='Тип счетчика')),
('current_value', models.IntegerField(default=0, verbose_name='Текущее значение')),
],
options={
'verbose_name': 'Счетчик документов',
'verbose_name_plural': 'Счетчики документов',
},
),
migrations.CreateModel(
name='Incoming',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('quantity', models.DecimalField(decimal_places=3, max_digits=10, verbose_name='Количество')),
('cost_price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Закупочная цена')),
('notes', models.TextField(blank=True, null=True, verbose_name='Примечания')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
],
options={
'verbose_name': 'Товар в поступлении',
'verbose_name_plural': 'Товары в поступлениях',
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='IncomingBatch',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('document_number', models.CharField(db_index=True, max_length=100, unique=True, verbose_name='Номер документа')),
('supplier_name', models.CharField(blank=True, max_length=200, null=True, verbose_name='Наименование поставщика')),
('notes', models.TextField(blank=True, null=True, verbose_name='Примечания')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Дата обновления')),
],
options={
'verbose_name': 'Партия поступления',
'verbose_name_plural': 'Партии поступлений',
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='Inventory',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('date', models.DateTimeField(auto_now_add=True, verbose_name='Дата инвентаризации')),
('status', models.CharField(choices=[('draft', 'Черновик'), ('processing', 'В обработке'), ('completed', 'Завершена')], default='draft', max_length=20, verbose_name='Статус')),
('conducted_by', models.CharField(blank=True, max_length=200, null=True, verbose_name='Провел инвентаризацию')),
('notes', models.TextField(blank=True, null=True, verbose_name='Примечания')),
],
options={
'verbose_name': 'Инвентаризация',
'verbose_name_plural': 'Инвентаризации',
'ordering': ['-date'],
},
),
migrations.CreateModel(
name='InventoryLine',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('quantity_system', models.DecimalField(decimal_places=3, max_digits=10, verbose_name='Количество в системе')),
('quantity_fact', models.DecimalField(decimal_places=3, max_digits=10, verbose_name='Фактическое количество')),
('difference', models.DecimalField(decimal_places=3, default=0, editable=False, max_digits=10, verbose_name='Разница (факт - система)')),
('processed', models.BooleanField(default=False, verbose_name='Обработана (создана операция)')),
],
options={
'verbose_name': 'Строка инвентаризации',
'verbose_name_plural': 'Строки инвентаризации',
},
),
migrations.CreateModel(
name='Reservation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('quantity', models.DecimalField(decimal_places=3, max_digits=10, verbose_name='Количество')),
('status', models.CharField(choices=[('reserved', 'Зарезервирован'), ('released', 'Освобожден'), ('converted_to_sale', 'Преобразован в продажу')], default='reserved', max_length=20, verbose_name='Статус')),
('reserved_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата резервирования')),
('released_at', models.DateTimeField(blank=True, null=True, verbose_name='Дата освобождения')),
('converted_at', models.DateTimeField(blank=True, null=True, verbose_name='Дата преобразования в продажу')),
],
options={
'verbose_name': 'Резервирование',
'verbose_name_plural': 'Резервирования',
'ordering': ['-reserved_at'],
},
),
migrations.CreateModel(
name='Sale',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('quantity', models.DecimalField(decimal_places=3, max_digits=10, verbose_name='Количество')),
('sale_price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Цена продажи')),
('document_number', models.CharField(blank=True, max_length=100, null=True, verbose_name='Номер документа')),
('date', models.DateTimeField(auto_now_add=True, verbose_name='Дата операции')),
('processed', models.BooleanField(default=False, verbose_name='Обработана (FIFO применена)')),
],
options={
'verbose_name': 'Продажа',
'verbose_name_plural': 'Продажи',
'ordering': ['-date'],
},
),
migrations.CreateModel(
name='SaleBatchAllocation',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('quantity', models.DecimalField(decimal_places=3, max_digits=10, verbose_name='Количество')),
('cost_price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Закупочная цена')),
],
options={
'verbose_name': 'Распределение продажи по партиям',
'verbose_name_plural': 'Распределения продаж по партиям',
},
),
migrations.CreateModel(
name='Stock',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('quantity_available', models.DecimalField(decimal_places=3, default=0, editable=False, max_digits=10, verbose_name='Доступное количество')),
('quantity_reserved', models.DecimalField(decimal_places=3, default=0, editable=False, max_digits=10, verbose_name='Зарезервированное количество')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Дата обновления')),
],
options={
'verbose_name': 'Остаток на складе',
'verbose_name_plural': 'Остатки на складе',
},
),
migrations.CreateModel(
name='StockBatch',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('quantity', models.DecimalField(decimal_places=3, max_digits=10, verbose_name='Количество')),
('cost_price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Закупочная цена')),
('is_active', models.BooleanField(default=True, verbose_name='Активна')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Дата обновления')),
],
options={
'verbose_name': 'Партия товара',
'verbose_name_plural': 'Партии товаров',
'ordering': ['created_at'],
},
),
migrations.CreateModel(
name='StockMovement',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('change', models.DecimalField(decimal_places=3, max_digits=10, verbose_name='Изменение')),
('reason', models.CharField(choices=[('purchase', 'Закупка'), ('sale', 'Продажа'), ('write_off', 'Списание'), ('adjustment', 'Корректировка')], max_length=20, verbose_name='Причина')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
],
options={
'verbose_name': 'Движение товара',
'verbose_name_plural': 'Движения товаров',
},
),
migrations.CreateModel(
name='Transfer',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('quantity', models.DecimalField(decimal_places=3, max_digits=10, verbose_name='Количество')),
('document_number', models.CharField(blank=True, max_length=100, null=True, verbose_name='Номер документа')),
('date', models.DateTimeField(auto_now_add=True, verbose_name='Дата операции')),
],
options={
'verbose_name': 'Перемещение',
'verbose_name_plural': 'Перемещения',
'ordering': ['-date'],
},
),
migrations.CreateModel(
name='TransferBatch',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('document_number', models.CharField(db_index=True, max_length=100, unique=True, verbose_name='Номер документа')),
('notes', models.TextField(blank=True, null=True, verbose_name='Примечания')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Дата обновления')),
],
options={
'verbose_name': 'Документ перемещения',
'verbose_name_plural': 'Документы перемещения',
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='TransferItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('quantity', models.DecimalField(decimal_places=3, max_digits=10, verbose_name='Количество')),
],
options={
'verbose_name': 'Строка перемещения',
'verbose_name_plural': 'Строки перемещения',
'ordering': ['id'],
},
),
migrations.CreateModel(
name='Warehouse',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200, verbose_name='Название')),
('description', models.TextField(blank=True, null=True, verbose_name='Описание')),
('street', models.CharField(blank=True, max_length=255, null=True, verbose_name='Улица')),
('building_number', models.CharField(blank=True, max_length=20, null=True, verbose_name='Номер здания')),
('phone', phonenumber_field.modelfields.PhoneNumberField(blank=True, max_length=128, null=True, region='BY', verbose_name='Телефон')),
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='Email')),
('is_active', models.BooleanField(default=True, verbose_name='Активен')),
('is_default', models.BooleanField(default=False, help_text='Автоматически выбирается при создании новых документов', verbose_name='Склад по умолчанию')),
('is_pickup_point', models.BooleanField(default=True, help_text='Можно ли выбрать этот склад как точку самовывоза заказа', verbose_name='Доступен для самовывоза')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Дата обновления')),
],
options={
'verbose_name': 'Склад',
'verbose_name_plural': 'Склады',
},
),
migrations.CreateModel(
name='WriteOff',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('quantity', models.DecimalField(decimal_places=3, max_digits=10, verbose_name='Количество')),
('reason', models.CharField(choices=[('damage', 'Повреждение'), ('spoilage', 'Порча'), ('shortage', 'Недостача'), ('inventory', 'Инвентаризационная недостача'), ('other', 'Другое')], default='other', max_length=20, verbose_name='Причина')),
('cost_price', models.DecimalField(decimal_places=2, editable=False, max_digits=10, verbose_name='Закупочная цена')),
('document_number', models.CharField(blank=True, max_length=100, null=True, verbose_name='Номер документа')),
('notes', models.TextField(blank=True, null=True, verbose_name='Примечания')),
('date', models.DateTimeField(auto_now_add=True, verbose_name='Дата операции')),
],
options={
'verbose_name': 'Списание',
'verbose_name_plural': 'Списания',
'ordering': ['-date'],
},
),
]