Рефакторинг: отделение Delivery от Order, обязательные поля доставки, исправление доменов

- Отделена модель Delivery от Order (OneToOne связь)
- Добавлены обязательные поля delivery_date, time_from, time_to в Delivery
- Delivery обязательна при создании заказа (кроме черновиков)
- Добавлены методы calculate_total() и reset_delivery_cost() в Order
- Добавлена валидация полей доставки в OrderForm
- Исправлено создание доменов - убран порт из домена в БД
- Исправлен редирект после установки пароля (правильный формат URL)
- Исправлена ошибка NoReverseMatch в navbar для public схемы
- Удалены все старые миграции (база создается с нуля)
- Обновлены views для работы с новой моделью Delivery
This commit is contained in:
2025-12-23 23:52:59 +03:00
parent d29c736252
commit 94fe363cb1
61 changed files with 1342 additions and 2189 deletions

View File

@@ -1,4 +1,4 @@
# Generated by Django 5.0.10 on 2025-11-15 11:57
# Generated by Django 5.0.10 on 2025-12-23 20:38
import phonenumber_field.modelfields
from django.db import migrations, models
@@ -16,7 +16,7 @@ class Migration(migrations.Migration):
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='Тип счетчика')),
('counter_type', models.CharField(choices=[('transfer', 'Перемещение товара'), ('writeoff', 'Списание товара'), ('incoming', 'Поступление товара'), ('inventory', 'Инвентаризация')], max_length=20, unique=True, verbose_name='Тип счетчика')),
('current_value', models.IntegerField(default=0, verbose_name='Текущее значение')),
],
options={
@@ -44,6 +44,7 @@ class Migration(migrations.Migration):
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='Номер документа')),
('receipt_type', models.CharField(choices=[('supplier', 'Поступление от поставщика'), ('inventory', 'Оприходование при инвентаризации'), ('adjustment', 'Оприходование без инвентаризации')], db_index=True, default='supplier', max_length=20, 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='Дата создания')),
@@ -55,13 +56,49 @@ class Migration(migrations.Migration):
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='IncomingDocument',
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='Номер документа')),
('status', models.CharField(choices=[('draft', 'Черновик'), ('confirmed', 'Проведён'), ('cancelled', 'Отменён')], db_index=True, default='draft', max_length=20, verbose_name='Статус')),
('date', models.DateField(help_text='Дата, к которой относится поступление', verbose_name='Дата документа')),
('receipt_type', models.CharField(choices=[('supplier', 'Поступление от поставщика'), ('inventory', 'Оприходование при инвентаризации'), ('adjustment', 'Оприходование без инвентаризации')], db_index=True, default='supplier', max_length=20, verbose_name='Тип поступления')),
('supplier_name', models.CharField(blank=True, help_text="Заполняется для типа 'Поступление от поставщика'", max_length=200, null=True, verbose_name='Наименование поставщика')),
('notes', models.TextField(blank=True, null=True, verbose_name='Примечания')),
('confirmed_at', models.DateTimeField(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': ['-date', '-created_at'],
},
),
migrations.CreateModel(
name='IncomingDocumentItem',
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='Создан')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Обновлён')),
],
options={
'verbose_name': 'Позиция документа поступления',
'verbose_name_plural': 'Позиции документа поступления',
'ordering': ['id'],
},
),
migrations.CreateModel(
name='Inventory',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('document_number', models.CharField(blank=True, db_index=True, max_length=100, null=True, unique=True, verbose_name='Номер документа')),
('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={
@@ -75,9 +112,13 @@ class Migration(migrations.Migration):
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='Разница (факт - система)')),
('quantity_fact', models.DecimalField(decimal_places=3, help_text='Количество свободных товаров, подсчитанных физически', max_digits=10, verbose_name='Подсчитано (факт, свободные)')),
('difference', models.DecimalField(decimal_places=3, default=0, editable=False, help_text='(Подсчитано + Зарезервировано) - Всего на складе', max_digits=10, verbose_name='Итоговая разница')),
('processed', models.BooleanField(default=False, verbose_name='Обработана (создана операция)')),
('snapshot_quantity_available', models.DecimalField(blank=True, decimal_places=3, help_text='Всего на складе на момент завершения инвентаризации', max_digits=10, null=True, verbose_name='Всего на складе (snapshot)')),
('snapshot_quantity_reserved', models.DecimalField(blank=True, decimal_places=3, help_text='В резервах на момент завершения инвентаризации', max_digits=10, null=True, verbose_name='В резервах (snapshot)')),
('snapshot_quantity_system', models.DecimalField(blank=True, decimal_places=3, help_text='В системе свободно на момент завершения инвентаризации', max_digits=10, null=True, verbose_name='В системе свободно (snapshot)')),
('snapshot_difference', models.DecimalField(blank=True, decimal_places=3, help_text='Итоговая разница на момент завершения инвентаризации', max_digits=10, null=True, verbose_name='Итоговая разница (snapshot)')),
],
options={
'verbose_name': 'Строка инвентаризации',
@@ -89,10 +130,12 @@ class Migration(migrations.Migration):
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='Статус')),
('status', models.CharField(choices=[('reserved', 'Зарезервирован'), ('released', 'Освобожден'), ('converted_to_sale', 'Преобразован в продажу'), ('converted_to_writeoff', 'Преобразован в списание')], default='reserved', max_length=25, 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='Дата преобразования в продажу')),
('converted_at', models.DateTimeField(blank=True, help_text='Дата преобразования в продажу или списание', null=True, verbose_name='Дата преобразования')),
('cart_lock_expires_at', models.DateTimeField(blank=True, help_text='Время истечения блокировки в корзине (для витринных комплектов)', null=True, verbose_name='Блокировка корзины истекает')),
('cart_session_id', models.CharField(blank=True, help_text='Дополнительная идентификация сессии для надежности', max_length=100, null=True, verbose_name='ID сессии корзины')),
],
options={
'verbose_name': 'Резервирование',
@@ -128,6 +171,39 @@ class Migration(migrations.Migration):
'verbose_name_plural': 'Распределения продаж по партиям',
},
),
migrations.CreateModel(
name='Showcase',
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='Описание')),
('is_active', models.BooleanField(default=True, verbose_name='Активна')),
('is_default', models.BooleanField(default=False, 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': ['warehouse', 'name'],
},
),
migrations.CreateModel(
name='ShowcaseItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('status', models.CharField(choices=[('available', 'Доступен'), ('in_cart', 'В корзине'), ('sold', 'Продан'), ('dismantled', 'Разобран')], db_index=True, default='available', max_length=20, verbose_name='Статус')),
('sold_at', models.DateTimeField(blank=True, null=True, verbose_name='Дата продажи')),
('cart_lock_expires_at', models.DateTimeField(blank=True, null=True, verbose_name='Блокировка истекает')),
('cart_session_id', models.CharField(blank=True, max_length=100, null=True, verbose_name='ID сессии корзины')),
('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='Stock',
fields=[
@@ -249,4 +325,38 @@ class Migration(migrations.Migration):
'ordering': ['-date'],
},
),
migrations.CreateModel(
name='WriteOffDocument',
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='Номер документа')),
('status', models.CharField(choices=[('draft', 'Черновик'), ('confirmed', 'Проведён'), ('cancelled', 'Отменён')], db_index=True, default='draft', max_length=20, verbose_name='Статус')),
('date', models.DateField(help_text='Дата, к которой относится списание', verbose_name='Дата документа')),
('notes', models.TextField(blank=True, null=True, verbose_name='Примечания')),
('confirmed_at', models.DateTimeField(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': ['-date', '-created_at'],
},
),
migrations.CreateModel(
name='WriteOffDocumentItem',
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='damage', max_length=20, verbose_name='Причина списания')),
('notes', models.TextField(blank=True, null=True, verbose_name='Примечания')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'verbose_name': 'Позиция документа списания',
'verbose_name_plural': 'Позиции документа списания',
'ordering': ['id'],
},
),
]

View File

@@ -1,6 +1,7 @@
# Generated by Django 5.0.10 on 2025-11-15 11:57
# Generated by Django 5.0.10 on 2025-12-23 20:38
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
@@ -12,6 +13,8 @@ class Migration(migrations.Migration):
('inventory', '0001_initial'),
('orders', '0001_initial'),
('products', '0001_initial'),
('user_roles', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
@@ -25,6 +28,36 @@ class Migration(migrations.Migration):
name='batch',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='inventory.incomingbatch', verbose_name='Партия'),
),
migrations.AddField(
model_name='incomingdocument',
name='confirmed_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='confirmed_incoming_documents', to=settings.AUTH_USER_MODEL, verbose_name='Провёл'),
),
migrations.AddField(
model_name='incomingdocument',
name='created_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_incoming_documents', to=settings.AUTH_USER_MODEL, verbose_name='Создал'),
),
migrations.AddField(
model_name='incomingdocumentitem',
name='document',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='inventory.incomingdocument', verbose_name='Документ'),
),
migrations.AddField(
model_name='incomingdocumentitem',
name='product',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='incoming_document_items', to='products.product', verbose_name='Товар'),
),
migrations.AddField(
model_name='inventory',
name='conducted_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='inventories', to='user_roles.userrole', verbose_name='Провел инвентаризацию'),
),
migrations.AddField(
model_name='incomingdocument',
name='inventory',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='incoming_documents', to='inventory.inventory', verbose_name='Инвентаризация'),
),
migrations.AddField(
model_name='inventoryline',
name='inventory',
@@ -35,6 +68,11 @@ class Migration(migrations.Migration):
name='product',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='products.product', verbose_name='Товар'),
),
migrations.AddField(
model_name='reservation',
name='locked_by_user',
field=models.ForeignKey(blank=True, help_text='Кассир, который добавил комплект в корзину', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='cart_locks', to=settings.AUTH_USER_MODEL, verbose_name='Заблокировано пользователем'),
),
migrations.AddField(
model_name='reservation',
name='order_item',
@@ -45,6 +83,11 @@ class Migration(migrations.Migration):
name='product',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='reservations', to='products.product', verbose_name='Товар'),
),
migrations.AddField(
model_name='reservation',
name='product_kit',
field=models.ForeignKey(blank=True, help_text='Временный комплект, для которого создан резерв', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='reservations', to='products.productkit', verbose_name='Комплект'),
),
migrations.AddField(
model_name='sale',
name='order',
@@ -60,6 +103,36 @@ class Migration(migrations.Migration):
name='sale',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='batch_allocations', to='inventory.sale', verbose_name='Продажа'),
),
migrations.AddField(
model_name='reservation',
name='showcase',
field=models.ForeignKey(blank=True, help_text='Витрина, на которой выложен букет', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='reservations', to='inventory.showcase', verbose_name='Витрина'),
),
migrations.AddField(
model_name='showcaseitem',
name='locked_by_user',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='locked_showcase_items', to=settings.AUTH_USER_MODEL, verbose_name='Заблокировано пользователем'),
),
migrations.AddField(
model_name='showcaseitem',
name='product_kit',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='showcase_items', to='products.productkit', verbose_name='Шаблон комплекта'),
),
migrations.AddField(
model_name='showcaseitem',
name='showcase',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='showcase_items', to='inventory.showcase', verbose_name='Витрина'),
),
migrations.AddField(
model_name='showcaseitem',
name='sold_order_item',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sold_showcase_items', to='orders.orderitem', verbose_name='Позиция заказа (продажа)'),
),
migrations.AddField(
model_name='reservation',
name='showcase_item',
field=models.ForeignKey(blank=True, help_text='Для какого физического экземпляра создан резерв', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='reservations', to='inventory.showcaseitem', verbose_name='Экземпляр на витрине'),
),
migrations.AddField(
model_name='stock',
name='product',
@@ -162,6 +235,11 @@ class Migration(migrations.Migration):
name='warehouse',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='stocks', to='inventory.warehouse', verbose_name='Склад'),
),
migrations.AddField(
model_name='showcase',
name='warehouse',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='showcases', to='inventory.warehouse', verbose_name='Склад'),
),
migrations.AddField(
model_name='sale',
name='warehouse',
@@ -177,6 +255,11 @@ class Migration(migrations.Migration):
name='warehouse',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='inventories', to='inventory.warehouse', verbose_name='Склад'),
),
migrations.AddField(
model_name='incomingdocument',
name='warehouse',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='incoming_documents', to='inventory.warehouse', verbose_name='Склад'),
),
migrations.AddField(
model_name='incomingbatch',
name='warehouse',
@@ -187,6 +270,70 @@ class Migration(migrations.Migration):
name='batch',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='writeoffs', to='inventory.stockbatch', verbose_name='Партия'),
),
migrations.AddField(
model_name='writeoffdocument',
name='confirmed_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='confirmed_writeoff_documents', to=settings.AUTH_USER_MODEL, verbose_name='Провёл'),
),
migrations.AddField(
model_name='writeoffdocument',
name='created_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_writeoff_documents', to=settings.AUTH_USER_MODEL, verbose_name='Создал'),
),
migrations.AddField(
model_name='writeoffdocument',
name='inventory',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='writeoff_documents', to='inventory.inventory', verbose_name='Инвентаризация'),
),
migrations.AddField(
model_name='writeoffdocument',
name='warehouse',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='writeoff_documents', to='inventory.warehouse', verbose_name='Склад'),
),
migrations.AddField(
model_name='writeoffdocumentitem',
name='document',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='inventory.writeoffdocument', verbose_name='Документ'),
),
migrations.AddField(
model_name='writeoffdocumentitem',
name='product',
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='writeoff_document_items', to='products.product', verbose_name='Товар'),
),
migrations.AddField(
model_name='writeoffdocumentitem',
name='reservation',
field=models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='writeoff_document_item_reverse', to='inventory.reservation', verbose_name='Резерв'),
),
migrations.AddField(
model_name='reservation',
name='writeoff_document_item',
field=models.ForeignKey(blank=True, help_text='Резерв для документа списания (черновик)', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='reservations', to='inventory.writeoffdocumentitem', verbose_name='Позиция документа списания'),
),
migrations.AddIndex(
model_name='incomingdocumentitem',
index=models.Index(fields=['document'], name='inventory_i_documen_96d470_idx'),
),
migrations.AddIndex(
model_name='incomingdocumentitem',
index=models.Index(fields=['product'], name='inventory_i_product_932432_idx'),
),
migrations.AddIndex(
model_name='showcaseitem',
index=models.Index(fields=['showcase', 'status'], name='inventory_s_showcas_116f7f_idx'),
),
migrations.AddIndex(
model_name='showcaseitem',
index=models.Index(fields=['product_kit', 'status'], name='inventory_s_product_785870_idx'),
),
migrations.AddIndex(
model_name='showcaseitem',
index=models.Index(fields=['status', 'cart_lock_expires_at'], name='inventory_s_status_6acf05_idx'),
),
migrations.AddIndex(
model_name='showcaseitem',
index=models.Index(fields=['locked_by_user', 'status'], name='inventory_s_locked__88eac9_idx'),
),
migrations.AddIndex(
model_name='incoming',
index=models.Index(fields=['batch'], name='inventory_i_batch_i_c50b63_idx'),
@@ -263,6 +410,18 @@ class Migration(migrations.Migration):
name='stock',
unique_together={('product', 'warehouse')},
),
migrations.AddIndex(
model_name='showcase',
index=models.Index(fields=['warehouse'], name='inventory_s_warehou_1e4a8a_idx'),
),
migrations.AddIndex(
model_name='showcase',
index=models.Index(fields=['is_active'], name='inventory_s_is_acti_387bfb_idx'),
),
migrations.AddIndex(
model_name='showcase',
index=models.Index(fields=['is_default'], name='inventory_s_is_defa_bf9a7c_idx'),
),
migrations.AddIndex(
model_name='sale',
index=models.Index(fields=['product', 'warehouse'], name='inventory_s_product_084314_idx'),
@@ -275,6 +434,78 @@ class Migration(migrations.Migration):
model_name='sale',
index=models.Index(fields=['order'], name='inventory_s_order_i_7d13ea_idx'),
),
migrations.AddIndex(
model_name='inventory',
index=models.Index(fields=['document_number'], name='inventory_i_documen_8df782_idx'),
),
migrations.AddIndex(
model_name='incomingdocument',
index=models.Index(fields=['document_number'], name='inventory_i_documen_5b89ad_idx'),
),
migrations.AddIndex(
model_name='incomingdocument',
index=models.Index(fields=['warehouse', 'status'], name='inventory_i_warehou_8f141d_idx'),
),
migrations.AddIndex(
model_name='incomingdocument',
index=models.Index(fields=['date'], name='inventory_i_date_8ace9b_idx'),
),
migrations.AddIndex(
model_name='incomingdocument',
index=models.Index(fields=['receipt_type'], name='inventory_i_receipt_92f322_idx'),
),
migrations.AddIndex(
model_name='incomingdocument',
index=models.Index(fields=['-created_at'], name='inventory_i_created_174930_idx'),
),
migrations.AddIndex(
model_name='incomingbatch',
index=models.Index(fields=['document_number'], name='inventory_i_documen_679096_idx'),
),
migrations.AddIndex(
model_name='incomingbatch',
index=models.Index(fields=['warehouse'], name='inventory_i_warehou_cc3a73_idx'),
),
migrations.AddIndex(
model_name='incomingbatch',
index=models.Index(fields=['receipt_type'], name='inventory_i_receipt_ce70c1_idx'),
),
migrations.AddIndex(
model_name='incomingbatch',
index=models.Index(fields=['-created_at'], name='inventory_i_created_59ee8b_idx'),
),
migrations.AddIndex(
model_name='writeoff',
index=models.Index(fields=['batch'], name='inventory_w_batch_i_b098ce_idx'),
),
migrations.AddIndex(
model_name='writeoff',
index=models.Index(fields=['date'], name='inventory_w_date_70c7e3_idx'),
),
migrations.AddIndex(
model_name='writeoffdocument',
index=models.Index(fields=['document_number'], name='inventory_w_documen_a9ae00_idx'),
),
migrations.AddIndex(
model_name='writeoffdocument',
index=models.Index(fields=['warehouse', 'status'], name='inventory_w_warehou_69fbf6_idx'),
),
migrations.AddIndex(
model_name='writeoffdocument',
index=models.Index(fields=['date'], name='inventory_w_date_a853cb_idx'),
),
migrations.AddIndex(
model_name='writeoffdocument',
index=models.Index(fields=['-created_at'], name='inventory_w_created_02c298_idx'),
),
migrations.AddIndex(
model_name='writeoffdocumentitem',
index=models.Index(fields=['document'], name='inventory_w_documen_d77c5e_idx'),
),
migrations.AddIndex(
model_name='writeoffdocumentitem',
index=models.Index(fields=['product'], name='inventory_w_product_6e32fc_idx'),
),
migrations.AddIndex(
model_name='reservation',
index=models.Index(fields=['product', 'warehouse'], name='inventory_r_product_fa0d33_idx'),
@@ -288,23 +519,27 @@ class Migration(migrations.Migration):
index=models.Index(fields=['order_item'], name='inventory_r_order_i_ae991f_idx'),
),
migrations.AddIndex(
model_name='incomingbatch',
index=models.Index(fields=['document_number'], name='inventory_i_documen_679096_idx'),
model_name='reservation',
index=models.Index(fields=['showcase'], name='inventory_r_showcas_bd3508_idx'),
),
migrations.AddIndex(
model_name='incomingbatch',
index=models.Index(fields=['warehouse'], name='inventory_i_warehou_cc3a73_idx'),
model_name='reservation',
index=models.Index(fields=['product_kit'], name='inventory_r_product_70aed5_idx'),
),
migrations.AddIndex(
model_name='incomingbatch',
index=models.Index(fields=['-created_at'], name='inventory_i_created_59ee8b_idx'),
model_name='reservation',
index=models.Index(fields=['cart_lock_expires_at'], name='inventory_r_cart_lo_e9b52a_idx'),
),
migrations.AddIndex(
model_name='writeoff',
index=models.Index(fields=['batch'], name='inventory_w_batch_i_b098ce_idx'),
model_name='reservation',
index=models.Index(fields=['locked_by_user'], name='inventory_r_locked__706cbf_idx'),
),
migrations.AddIndex(
model_name='writeoff',
index=models.Index(fields=['date'], name='inventory_w_date_70c7e3_idx'),
model_name='reservation',
index=models.Index(fields=['product_kit', 'cart_lock_expires_at'], name='inventory_r_product_5dacdf_idx'),
),
migrations.AddIndex(
model_name='reservation',
index=models.Index(fields=['showcase_item'], name='inventory_r_showcas_8cfff5_idx'),
),
]

View File

@@ -1,50 +0,0 @@
# Generated by Django 5.0.10 on 2025-11-16 18:07
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0002_initial'),
('orders', '0002_initial'),
('products', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Showcase',
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='Описание')),
('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='Дата обновления')),
('warehouse', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='showcases', to='inventory.warehouse', verbose_name='Склад')),
],
options={
'verbose_name': 'Витрина',
'verbose_name_plural': 'Витрины',
'ordering': ['warehouse', 'name'],
},
),
migrations.AddField(
model_name='reservation',
name='showcase',
field=models.ForeignKey(blank=True, help_text='Витрина, на которой выложен букет', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='reservations', to='inventory.showcase', verbose_name='Витрина'),
),
migrations.AddIndex(
model_name='reservation',
index=models.Index(fields=['showcase'], name='inventory_r_showcas_bd3508_idx'),
),
migrations.AddIndex(
model_name='showcase',
index=models.Index(fields=['warehouse'], name='inventory_s_warehou_1e4a8a_idx'),
),
migrations.AddIndex(
model_name='showcase',
index=models.Index(fields=['is_active'], name='inventory_s_is_acti_387bfb_idx'),
),
]

View File

@@ -1,22 +0,0 @@
# Generated by Django 5.0.10 on 2025-11-20 08:08
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0003_showcase_reservation_showcase_and_more'),
]
operations = [
migrations.AddField(
model_name='showcase',
name='is_default',
field=models.BooleanField(default=False, verbose_name='По умолчанию'),
),
migrations.AddIndex(
model_name='showcase',
index=models.Index(fields=['is_default'], name='inventory_s_is_defa_bf9a7c_idx'),
),
]

View File

@@ -1,25 +0,0 @@
# Generated by Django 5.0.10 on 2025-11-20 12:09
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0004_showcase_is_default_and_more'),
('orders', '0003_historicalorderitem_is_from_showcase_and_more'),
('products', '0008_productkit_showcase_and_more'),
]
operations = [
migrations.AddField(
model_name='reservation',
name='product_kit',
field=models.ForeignKey(blank=True, help_text='Временный комплект, для которого создан резерв', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='reservations', to='products.productkit', verbose_name='Комплект'),
),
migrations.AddIndex(
model_name='reservation',
index=models.Index(fields=['product_kit'], name='inventory_r_product_70aed5_idx'),
),
]

View File

@@ -1,45 +0,0 @@
# Generated by Django 5.0.10 on 2025-11-20 20:20
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0005_reservation_product_kit_and_more'),
('orders', '0003_historicalorderitem_is_from_showcase_and_more'),
('products', '0008_productkit_showcase_and_more'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='reservation',
name='cart_lock_expires_at',
field=models.DateTimeField(blank=True, help_text='Время истечения блокировки в корзине (для витринных комплектов)', null=True, verbose_name='Блокировка корзины истекает'),
),
migrations.AddField(
model_name='reservation',
name='cart_session_id',
field=models.CharField(blank=True, help_text='Дополнительная идентификация сессии для надежности', max_length=100, null=True, verbose_name='ID сессии корзины'),
),
migrations.AddField(
model_name='reservation',
name='locked_by_user',
field=models.ForeignKey(blank=True, help_text='Кассир, который добавил комплект в корзину', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='cart_locks', to=settings.AUTH_USER_MODEL, verbose_name='Заблокировано пользователем'),
),
migrations.AddIndex(
model_name='reservation',
index=models.Index(fields=['cart_lock_expires_at'], name='inventory_r_cart_lo_e9b52a_idx'),
),
migrations.AddIndex(
model_name='reservation',
index=models.Index(fields=['locked_by_user'], name='inventory_r_locked__706cbf_idx'),
),
migrations.AddIndex(
model_name='reservation',
index=models.Index(fields=['product_kit', 'cart_lock_expires_at'], name='inventory_r_product_5dacdf_idx'),
),
]

View File

@@ -1,63 +0,0 @@
# Generated by Django 5.0.10 on 2025-12-09 04:19
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0006_reservation_cart_lock_expires_at_and_more'),
('orders', '0006_transaction_delete_payment_and_more'),
('products', '0010_alter_product_cost_price'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='ShowcaseItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('status', models.CharField(choices=[('available', 'Доступен'), ('in_cart', 'В корзине'), ('sold', 'Продан'), ('dismantled', 'Разобран')], db_index=True, default='available', max_length=20, verbose_name='Статус')),
('sold_at', models.DateTimeField(blank=True, null=True, verbose_name='Дата продажи')),
('cart_lock_expires_at', models.DateTimeField(blank=True, null=True, verbose_name='Блокировка истекает')),
('cart_session_id', models.CharField(blank=True, max_length=100, null=True, verbose_name='ID сессии корзины')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Создан')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Обновлен')),
('locked_by_user', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='locked_showcase_items', to=settings.AUTH_USER_MODEL, verbose_name='Заблокировано пользователем')),
('product_kit', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='showcase_items', to='products.productkit', verbose_name='Шаблон комплекта')),
('showcase', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='showcase_items', to='inventory.showcase', verbose_name='Витрина')),
('sold_order_item', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sold_showcase_item', to='orders.orderitem', verbose_name='Позиция заказа (продажа)')),
],
options={
'verbose_name': 'Экземпляр на витрине',
'verbose_name_plural': 'Экземпляры на витрине',
},
),
migrations.AddField(
model_name='reservation',
name='showcase_item',
field=models.ForeignKey(blank=True, help_text='Для какого физического экземпляра создан резерв', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='reservations', to='inventory.showcaseitem', verbose_name='Экземпляр на витрине'),
),
migrations.AddIndex(
model_name='reservation',
index=models.Index(fields=['showcase_item'], name='inventory_r_showcas_8cfff5_idx'),
),
migrations.AddIndex(
model_name='showcaseitem',
index=models.Index(fields=['showcase', 'status'], name='inventory_s_showcas_116f7f_idx'),
),
migrations.AddIndex(
model_name='showcaseitem',
index=models.Index(fields=['product_kit', 'status'], name='inventory_s_product_785870_idx'),
),
migrations.AddIndex(
model_name='showcaseitem',
index=models.Index(fields=['status', 'cart_lock_expires_at'], name='inventory_s_status_6acf05_idx'),
),
migrations.AddIndex(
model_name='showcaseitem',
index=models.Index(fields=['locked_by_user', 'status'], name='inventory_s_locked__88eac9_idx'),
),
]

View File

@@ -1,64 +0,0 @@
# Generated manually - Data migration for ShowcaseItem
from django.db import migrations
def migrate_showcase_kits_to_items(apps, schema_editor):
"""
Для каждого существующего витринного букета (ProductKit с is_temporary=True и showcase):
1. Создать ShowcaseItem
2. Привязать существующие Reservation к этому ShowcaseItem
"""
ProductKit = apps.get_model('products', 'ProductKit')
ShowcaseItem = apps.get_model('inventory', 'ShowcaseItem')
Reservation = apps.get_model('inventory', 'Reservation')
# Находим все витринные комплекты
showcase_kits = ProductKit.objects.filter(
is_temporary=True,
showcase__isnull=False
)
for kit in showcase_kits:
# Создаём ShowcaseItem для каждого существующего витринного букета
showcase_item = ShowcaseItem.objects.create(
showcase=kit.showcase,
product_kit=kit,
status='available'
)
# Привязываем существующие резервы к этому ShowcaseItem
Reservation.objects.filter(
product_kit=kit,
showcase=kit.showcase,
status='reserved'
).update(showcase_item=showcase_item)
def reverse_migration(apps, schema_editor):
"""
Откат: удаляем созданные ShowcaseItem и очищаем связи в Reservation
"""
ShowcaseItem = apps.get_model('inventory', 'ShowcaseItem')
Reservation = apps.get_model('inventory', 'Reservation')
# Очищаем связи в резервах
Reservation.objects.filter(showcase_item__isnull=False).update(showcase_item=None)
# Удаляем все ShowcaseItem
ShowcaseItem.objects.all().delete()
class Migration(migrations.Migration):
dependencies = [
('inventory', '0007_add_showcase_item_model'),
('products', '0001_initial'), # Убедимся что ProductKit существует
]
operations = [
migrations.RunPython(
migrate_showcase_kits_to_items,
reverse_code=reverse_migration
),
]

View File

@@ -1,65 +0,0 @@
# Generated manually - Fix ShowcaseItem status for already sold kits
from django.db import migrations
def fix_showcase_items_status(apps, schema_editor):
"""
Исправляем статус ShowcaseItem для уже проданных комплектов.
Логика:
- Если у ShowcaseItem нет активных резервов (status='reserved') →
это уже проданный/разобранный букет → удаляем ShowcaseItem
"""
ShowcaseItem = apps.get_model('inventory', 'ShowcaseItem')
Reservation = apps.get_model('inventory', 'Reservation')
# Находим все ShowcaseItem в статусе 'available'
available_items = ShowcaseItem.objects.filter(status='available')
items_to_delete = []
for item in available_items:
# Проверяем есть ли активные резервы для этого экземпляра
has_active_reservations = Reservation.objects.filter(
showcase_item=item,
status='reserved'
).exists()
# Если резервы не привязаны к showcase_item, проверяем старым способом
if not has_active_reservations:
has_active_reservations = Reservation.objects.filter(
product_kit=item.product_kit,
showcase=item.showcase,
status='reserved'
).exists()
if not has_active_reservations:
# Нет активных резервов - этот букет уже продан/разобран
items_to_delete.append(item.id)
# Удаляем ShowcaseItem без активных резервов
if items_to_delete:
ShowcaseItem.objects.filter(id__in=items_to_delete).delete()
def reverse_migration(apps, schema_editor):
"""
Откат невозможен - удалённые ShowcaseItem не восстановить.
Но это безопасно - они относились к уже проданным букетам.
"""
pass
class Migration(migrations.Migration):
dependencies = [
('inventory', '0008_migrate_showcase_kits_to_items'),
]
operations = [
migrations.RunPython(
fix_showcase_items_status,
reverse_code=reverse_migration
),
]

View File

@@ -1,91 +0,0 @@
# Generated by Django 5.0.10 on 2025-12-10 19:21
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0009_fix_showcase_items_status'),
('products', '0010_alter_product_cost_price'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name='documentcounter',
name='counter_type',
field=models.CharField(choices=[('transfer', 'Перемещение товара'), ('writeoff', 'Списание товара')], max_length=20, unique=True, verbose_name='Тип счетчика'),
),
migrations.CreateModel(
name='WriteOffDocument',
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='Номер документа')),
('status', models.CharField(choices=[('draft', 'Черновик'), ('confirmed', 'Проведён'), ('cancelled', 'Отменён')], db_index=True, default='draft', max_length=20, verbose_name='Статус')),
('date', models.DateField(help_text='Дата, к которой относится списание', verbose_name='Дата документа')),
('notes', models.TextField(blank=True, null=True, verbose_name='Примечания')),
('confirmed_at', models.DateTimeField(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='Обновлён')),
('confirmed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='confirmed_writeoff_documents', to=settings.AUTH_USER_MODEL, verbose_name='Провёл')),
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_writeoff_documents', to=settings.AUTH_USER_MODEL, verbose_name='Создал')),
('warehouse', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='writeoff_documents', to='inventory.warehouse', verbose_name='Склад')),
],
options={
'verbose_name': 'Документ списания',
'verbose_name_plural': 'Документы списания',
'ordering': ['-date', '-created_at'],
},
),
migrations.CreateModel(
name='WriteOffDocumentItem',
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='damage', max_length=20, verbose_name='Причина списания')),
('notes', models.TextField(blank=True, null=True, verbose_name='Примечания')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='inventory.writeoffdocument', verbose_name='Документ')),
('product', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='writeoff_document_items', to='products.product', verbose_name='Товар')),
('reservation', models.OneToOneField(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='writeoff_document_item_reverse', to='inventory.reservation', verbose_name='Резерв')),
],
options={
'verbose_name': 'Позиция документа списания',
'verbose_name_plural': 'Позиции документа списания',
'ordering': ['id'],
},
),
migrations.AddField(
model_name='reservation',
name='writeoff_document_item',
field=models.ForeignKey(blank=True, help_text='Резерв для документа списания (черновик)', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='reservations', to='inventory.writeoffdocumentitem', verbose_name='Позиция документа списания'),
),
migrations.AddIndex(
model_name='writeoffdocument',
index=models.Index(fields=['document_number'], name='inventory_w_documen_a9ae00_idx'),
),
migrations.AddIndex(
model_name='writeoffdocument',
index=models.Index(fields=['warehouse', 'status'], name='inventory_w_warehou_69fbf6_idx'),
),
migrations.AddIndex(
model_name='writeoffdocument',
index=models.Index(fields=['date'], name='inventory_w_date_a853cb_idx'),
),
migrations.AddIndex(
model_name='writeoffdocument',
index=models.Index(fields=['-created_at'], name='inventory_w_created_02c298_idx'),
),
migrations.AddIndex(
model_name='writeoffdocumentitem',
index=models.Index(fields=['document'], name='inventory_w_documen_d77c5e_idx'),
),
migrations.AddIndex(
model_name='writeoffdocumentitem',
index=models.Index(fields=['product'], name='inventory_w_product_6e32fc_idx'),
),
]

View File

@@ -1,23 +0,0 @@
# Generated by Django 5.0.10 on 2025-12-11 18:50
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0010_writeoff_document'),
]
operations = [
migrations.AlterField(
model_name='reservation',
name='converted_at',
field=models.DateTimeField(blank=True, help_text='Дата преобразования в продажу или списание', null=True, verbose_name='Дата преобразования'),
),
migrations.AlterField(
model_name='reservation',
name='status',
field=models.CharField(choices=[('reserved', 'Зарезервирован'), ('released', 'Освобожден'), ('converted_to_sale', 'Преобразован в продажу'), ('converted_to_writeoff', 'Преобразован в списание')], default='reserved', max_length=25, verbose_name='Статус'),
),
]

View File

@@ -1,20 +0,0 @@
# Generated by Django 5.0.10 on 2025-12-11 19:23
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0011_add_writeoff_status_to_reservation'),
('orders', '0006_transaction_delete_payment_and_more'),
]
operations = [
migrations.AlterField(
model_name='showcaseitem',
name='sold_order_item',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sold_showcase_items', to='orders.orderitem', verbose_name='Позиция заказа (продажа)'),
),
]

View File

@@ -1,22 +0,0 @@
# Generated by Django 5.0.10 on 2025-12-20 20:42
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0012_change_sold_order_item_to_fk'),
]
operations = [
migrations.AddField(
model_name='incomingbatch',
name='receipt_type',
field=models.CharField(choices=[('supplier', 'Поступление от поставщика'), ('inventory', 'Оприходование при инвентаризации'), ('adjustment', 'Оприходование без инвентаризации')], db_index=True, default='supplier', max_length=20, verbose_name='Тип поступления'),
),
migrations.AddIndex(
model_name='incomingbatch',
index=models.Index(fields=['receipt_type'], name='inventory_i_receipt_ce70c1_idx'),
),
]

View File

@@ -1,91 +0,0 @@
# Generated by Django 5.0.10 on 2025-12-20 21:10
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0013_add_receipt_type_to_incomingbatch'),
('products', '0010_alter_product_cost_price'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name='documentcounter',
name='counter_type',
field=models.CharField(choices=[('transfer', 'Перемещение товара'), ('writeoff', 'Списание товара'), ('incoming', 'Поступление товара')], max_length=20, unique=True, verbose_name='Тип счетчика'),
),
migrations.CreateModel(
name='IncomingDocument',
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='Номер документа')),
('status', models.CharField(choices=[('draft', 'Черновик'), ('confirmed', 'Проведён'), ('cancelled', 'Отменён')], db_index=True, default='draft', max_length=20, verbose_name='Статус')),
('date', models.DateField(help_text='Дата, к которой относится поступление', verbose_name='Дата документа')),
('receipt_type', models.CharField(choices=[('supplier', 'Поступление от поставщика'), ('inventory', 'Оприходование при инвентаризации'), ('adjustment', 'Оприходование без инвентаризации')], db_index=True, default='supplier', max_length=20, verbose_name='Тип поступления')),
('supplier_name', models.CharField(blank=True, help_text="Заполняется для типа 'Поступление от поставщика'", max_length=200, null=True, verbose_name='Наименование поставщика')),
('notes', models.TextField(blank=True, null=True, verbose_name='Примечания')),
('confirmed_at', models.DateTimeField(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='Обновлён')),
('confirmed_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='confirmed_incoming_documents', to=settings.AUTH_USER_MODEL, verbose_name='Провёл')),
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_incoming_documents', to=settings.AUTH_USER_MODEL, verbose_name='Создал')),
('warehouse', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='incoming_documents', to='inventory.warehouse', verbose_name='Склад')),
],
options={
'verbose_name': 'Документ поступления',
'verbose_name_plural': 'Документы поступления',
'ordering': ['-date', '-created_at'],
},
),
migrations.CreateModel(
name='IncomingDocumentItem',
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='Создан')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Обновлён')),
('document', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='inventory.incomingdocument', verbose_name='Документ')),
('product', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='incoming_document_items', to='products.product', verbose_name='Товар')),
],
options={
'verbose_name': 'Позиция документа поступления',
'verbose_name_plural': 'Позиции документа поступления',
'ordering': ['id'],
},
),
migrations.AddIndex(
model_name='incomingdocument',
index=models.Index(fields=['document_number'], name='inventory_i_documen_5b89ad_idx'),
),
migrations.AddIndex(
model_name='incomingdocument',
index=models.Index(fields=['warehouse', 'status'], name='inventory_i_warehou_8f141d_idx'),
),
migrations.AddIndex(
model_name='incomingdocument',
index=models.Index(fields=['date'], name='inventory_i_date_8ace9b_idx'),
),
migrations.AddIndex(
model_name='incomingdocument',
index=models.Index(fields=['receipt_type'], name='inventory_i_receipt_92f322_idx'),
),
migrations.AddIndex(
model_name='incomingdocument',
index=models.Index(fields=['-created_at'], name='inventory_i_created_174930_idx'),
),
migrations.AddIndex(
model_name='incomingdocumentitem',
index=models.Index(fields=['document'], name='inventory_i_documen_96d470_idx'),
),
migrations.AddIndex(
model_name='incomingdocumentitem',
index=models.Index(fields=['product'], name='inventory_i_product_932432_idx'),
),
]

View File

@@ -1,24 +0,0 @@
# Generated by Django 5.0.10 on 2025-12-21 18:45
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0014_alter_documentcounter_counter_type_incomingdocument_and_more'),
]
operations = [
migrations.AddField(
model_name='incomingdocument',
name='inventory',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='incoming_documents', to='inventory.inventory', verbose_name='Инвентаризация'),
),
migrations.AddField(
model_name='writeoffdocument',
name='inventory',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='writeoff_documents', to='inventory.inventory', verbose_name='Инвентаризация'),
),
]

View File

@@ -1,27 +0,0 @@
# Generated by Django 5.0.10 on 2025-12-21 19:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0015_add_inventory_foreign_keys'),
]
operations = [
migrations.AddField(
model_name='inventory',
name='document_number',
field=models.CharField(blank=True, db_index=True, max_length=100, null=True, unique=True, verbose_name='Номер документа'),
),
migrations.AlterField(
model_name='documentcounter',
name='counter_type',
field=models.CharField(choices=[('transfer', 'Перемещение товара'), ('writeoff', 'Списание товара'), ('incoming', 'Поступление товара'), ('inventory', 'Инвентаризация')], max_length=20, unique=True, verbose_name='Тип счетчика'),
),
migrations.AddIndex(
model_name='inventory',
index=models.Index(fields=['document_number'], name='inventory_i_documen_8df782_idx'),
),
]

View File

@@ -1,101 +0,0 @@
# Generated manually - Change conducted_by from CharField to ForeignKey
from django.db import migrations, models
import django.db.models.deletion
def migrate_conducted_by_data(apps, schema_editor):
"""
Миграция данных: для существующих записей инвентаризации
найти первого пользователя с ролью "owner" (владелец) в текущем тенанте
и проставить его UserRole. Если владельца нет - оставить NULL.
"""
Inventory = apps.get_model('inventory', 'Inventory')
UserRole = apps.get_model('user_roles', 'UserRole')
Role = apps.get_model('user_roles', 'Role')
# Находим первого владельца в текущем тенанте
try:
owner_role = Role.objects.get(code='owner', is_system=True)
owner_user_role = UserRole.objects.filter(
role=owner_role,
is_active=True
).first()
if owner_user_role:
# Обновляем все существующие инвентаризации, у которых есть старое текстовое значение
# Используем conducted_by_old (переименованное поле) для проверки
Inventory.objects.exclude(conducted_by_old__isnull=True).exclude(conducted_by_old='').update(
conducted_by_new=owner_user_role
)
except Role.DoesNotExist:
# Если нет роли - оставляем NULL
pass
def reverse_migration(apps, schema_editor):
"""
Откат: очищаем новое поле
"""
Inventory = apps.get_model('inventory', 'Inventory')
Inventory.objects.filter(conducted_by_new__isnull=False).update(conducted_by_new=None)
class Migration(migrations.Migration):
dependencies = [
('inventory', '0016_add_document_number_to_inventory'),
('user_roles', '0001_initial'),
]
operations = [
# Шаг 1: Переименовываем старое поле в conducted_by_old
migrations.RenameField(
model_name='inventory',
old_name='conducted_by',
new_name='conducted_by_old',
),
# Шаг 2: Добавляем новое поле как ForeignKey
migrations.AddField(
model_name='inventory',
name='conducted_by_new',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='inventories_new',
to='user_roles.userrole',
verbose_name='Провел инвентаризацию'
),
),
# Шаг 3: Миграция данных
migrations.RunPython(
migrate_conducted_by_data,
reverse_code=reverse_migration
),
# Шаг 4: Удаляем старое поле
migrations.RemoveField(
model_name='inventory',
name='conducted_by_old',
),
# Шаг 5: Переименовываем новое поле в conducted_by
migrations.RenameField(
model_name='inventory',
old_name='conducted_by_new',
new_name='conducted_by',
),
# Шаг 6: Исправляем related_name
migrations.AlterField(
model_name='inventory',
name='conducted_by',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='inventories',
to='user_roles.userrole',
verbose_name='Провел инвентаризацию'
),
),
]

View File

@@ -1,43 +0,0 @@
# Generated by Django 5.0.10 on 2025-12-22 10:11
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('inventory', '0017_change_conducted_by_to_fk'),
]
operations = [
migrations.AddField(
model_name='inventoryline',
name='snapshot_difference',
field=models.DecimalField(blank=True, decimal_places=3, help_text='Итоговая разница на момент завершения инвентаризации', max_digits=10, null=True, verbose_name='Итоговая разница (snapshot)'),
),
migrations.AddField(
model_name='inventoryline',
name='snapshot_quantity_available',
field=models.DecimalField(blank=True, decimal_places=3, help_text='Всего на складе на момент завершения инвентаризации', max_digits=10, null=True, verbose_name='Всего на складе (snapshot)'),
),
migrations.AddField(
model_name='inventoryline',
name='snapshot_quantity_reserved',
field=models.DecimalField(blank=True, decimal_places=3, help_text='В резервах на момент завершения инвентаризации', max_digits=10, null=True, verbose_name='В резервах (snapshot)'),
),
migrations.AddField(
model_name='inventoryline',
name='snapshot_quantity_system',
field=models.DecimalField(blank=True, decimal_places=3, help_text='В системе свободно на момент завершения инвентаризации', max_digits=10, null=True, verbose_name='В системе свободно (snapshot)'),
),
migrations.AlterField(
model_name='inventoryline',
name='difference',
field=models.DecimalField(decimal_places=3, default=0, editable=False, help_text='(Подсчитано + Зарезервировано) - Всего на складе', max_digits=10, verbose_name='Итоговая разница'),
),
migrations.AlterField(
model_name='inventoryline',
name='quantity_fact',
field=models.DecimalField(decimal_places=3, help_text='Количество свободных товаров, подсчитанных физически', max_digits=10, verbose_name='Подсчитано (факт, свободные)'),
),
]