Files
octopus/myproject/products/migrations/0001_initial.py
Andrey Smakotin 6735be9b08 feat: Реализовать систему поступления товаров с партиями (IncomingBatch)
Основные изменения:
- Создана модель IncomingBatch для группировки товаров по документам
- Каждое поступление (Incoming) связано с одной батчем поступления
- Автоматическое создание StockBatch для каждого товара в приходе
- Реализована система нумерации партий (IN-XXXX-XXXX) с поиском максимума в БД
- Обновлены все представления (views) для работы с новой архитектурой
- Добавлены детальные страницы просмотра партий поступлений
- Обновлены шаблоны для отображения информации о партиях и их товарах
- Исправлена логика сигналов для создания StockBatch при приходе товара
- Обновлены формы для работы с новой структурой IncomingBatch

Архитектура FIFO:
- IncomingBatch: одна партия поступления (номер IN-XXXX-XXXX)
- Incoming: товар в партии поступления
- StockBatch: одна партия товара на складе (создается для каждого товара)

Это позволяет системе правильно применять FIFO при продаже товаров.

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-29 03:26:06 +03:00

291 lines
19 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.1.4 on 2025-10-28 22:47
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.CreateModel(
name='ProductVariantGroup',
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, 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': ['name'],
},
),
migrations.CreateModel(
name='SKUCounter',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('counter_type', models.CharField(choices=[('product', 'Product Counter'), ('kit', 'Kit Counter'), ('category', 'Category Counter')], max_length=20, unique=True, verbose_name='Тип счетчика')),
('current_value', models.IntegerField(default=0, verbose_name='Текущее значение')),
],
options={
'verbose_name': 'Счетчик артикулов',
'verbose_name_plural': 'Счетчики артикулов',
},
),
migrations.CreateModel(
name='ProductCategory',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200, verbose_name='Название')),
('sku', models.CharField(blank=True, db_index=True, max_length=100, null=True, unique=True, verbose_name='Артикул')),
('slug', models.SlugField(blank=True, max_length=200, unique=True, verbose_name='URL-идентификатор')),
('is_active', models.BooleanField(default=True, verbose_name='Активна')),
('created_at', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Дата создания')),
('updated_at', models.DateTimeField(auto_now=True, null=True, verbose_name='Дата обновления')),
('is_deleted', models.BooleanField(db_index=True, default=False, verbose_name='Удалена')),
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='Время удаления')),
('deleted_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='deleted_categories', to=settings.AUTH_USER_MODEL, verbose_name='Удалена пользователем')),
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='children', to='products.productcategory', verbose_name='Родительская категория')),
],
options={
'verbose_name': 'Категория товара',
'verbose_name_plural': 'Категории товаров',
},
),
migrations.CreateModel(
name='Product',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200, verbose_name='Название')),
('sku', models.CharField(blank=True, db_index=True, max_length=100, null=True, verbose_name='Артикул')),
('slug', models.SlugField(blank=True, max_length=200, unique=True, verbose_name='URL-идентификатор')),
('variant_suffix', models.CharField(blank=True, help_text='Суффикс для артикула (например: 50, 60, S, M). Автоматически извлекается из названия.', max_length=20, null=True, verbose_name='Суффикс варианта')),
('description', models.TextField(blank=True, null=True, verbose_name='Описание')),
('unit', models.CharField(choices=[('шт', 'Штука'), ('м', 'Метр'), ('г', 'Грамм'), ('л', 'Литр'), ('кг', 'Килограмм')], default='шт', max_length=10, verbose_name='Единица измерения')),
('cost_price', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Себестоимость')),
('sale_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='Дата обновления')),
('search_keywords', models.TextField(blank=True, help_text='Автоматически генерируется из названия, артикула, описания и категории. Можно дополнить вручную.', verbose_name='Ключевые слова для поиска')),
('is_deleted', models.BooleanField(db_index=True, default=False, verbose_name='Удален')),
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='Время удаления')),
('deleted_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='deleted_products', to=settings.AUTH_USER_MODEL, verbose_name='Удален пользователем')),
('categories', models.ManyToManyField(blank=True, related_name='products', to='products.productcategory', verbose_name='Категории')),
],
options={
'verbose_name': 'Товар',
'verbose_name_plural': 'Товары',
},
),
migrations.CreateModel(
name='ProductCategoryPhoto',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.ImageField(upload_to='categories/temp/', verbose_name='Оригинальное фото')),
('order', models.PositiveIntegerField(default=0, verbose_name='Порядок')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='photos', to='products.productcategory', verbose_name='Категория')),
],
options={
'verbose_name': 'Фото категории',
'verbose_name_plural': 'Фото категорий',
'ordering': ['order', '-created_at'],
},
),
migrations.CreateModel(
name='ProductKit',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200, verbose_name='Название')),
('sku', models.CharField(blank=True, max_length=100, null=True, verbose_name='Артикул')),
('slug', models.SlugField(max_length=200, unique=True, verbose_name='URL-идентификатор')),
('description', models.TextField(blank=True, null=True, verbose_name='Описание')),
('is_active', models.BooleanField(default=True, verbose_name='Активен')),
('pricing_method', models.CharField(choices=[('fixed', 'Фиксированная цена'), ('from_sale_prices', 'По ценам продажи компонентов'), ('from_cost_plus_percent', 'Себестоимость + процент наценки'), ('from_cost_plus_amount', 'Себестоимость + фикс. наценка')], default='from_sale_prices', max_length=30, verbose_name='Метод ценообразования')),
('fixed_price', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='Фиксированная цена')),
('markup_percent', models.DecimalField(blank=True, decimal_places=2, max_digits=5, null=True, verbose_name='Процент наценки')),
('markup_amount', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='Фиксированная наценка')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Дата обновления')),
('is_deleted', models.BooleanField(db_index=True, default=False, verbose_name='Удален')),
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='Время удаления')),
('categories', models.ManyToManyField(blank=True, related_name='kits', to='products.productcategory', verbose_name='Категории')),
('deleted_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='deleted_kits', to=settings.AUTH_USER_MODEL, verbose_name='Удален пользователем')),
],
options={
'verbose_name': 'Комплект',
'verbose_name_plural': 'Комплекты',
},
),
migrations.CreateModel(
name='ProductKitPhoto',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.ImageField(upload_to='kits/temp/', verbose_name='Оригинальное фото')),
('order', models.PositiveIntegerField(default=0, verbose_name='Порядок')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
('kit', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='photos', to='products.productkit', verbose_name='Комплект')),
],
options={
'verbose_name': 'Фото комплекта',
'verbose_name_plural': 'Фото комплектов',
'ordering': ['order', '-created_at'],
},
),
migrations.CreateModel(
name='ProductPhoto',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('image', models.ImageField(upload_to='products/temp/', verbose_name='Оригинальное фото')),
('order', models.PositiveIntegerField(default=0, verbose_name='Порядок')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='photos', to='products.product', verbose_name='Товар')),
],
options={
'verbose_name': 'Фото товара',
'verbose_name_plural': 'Фото товаров',
'ordering': ['order', '-created_at'],
},
),
migrations.CreateModel(
name='ProductTag',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, unique=True, verbose_name='Название')),
('slug', models.SlugField(max_length=100, unique=True, verbose_name='URL-идентификатор')),
('created_at', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Дата создания')),
('updated_at', models.DateTimeField(auto_now=True, null=True, verbose_name='Дата обновления')),
('is_deleted', models.BooleanField(db_index=True, default=False, verbose_name='Удален')),
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='Время удаления')),
('deleted_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='deleted_tags', to=settings.AUTH_USER_MODEL, verbose_name='Удален пользователем')),
],
options={
'verbose_name': 'Тег товара',
'verbose_name_plural': 'Теги товаров',
},
),
migrations.AddField(
model_name='productkit',
name='tags',
field=models.ManyToManyField(blank=True, related_name='kits', to='products.producttag', verbose_name='Теги'),
),
migrations.AddField(
model_name='product',
name='tags',
field=models.ManyToManyField(blank=True, related_name='products', to='products.producttag', verbose_name='Теги'),
),
migrations.AddField(
model_name='product',
name='variant_groups',
field=models.ManyToManyField(blank=True, related_name='products', to='products.productvariantgroup', verbose_name='Группы вариантов'),
),
migrations.CreateModel(
name='KitItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('quantity', models.DecimalField(blank=True, decimal_places=3, max_digits=10, null=True, verbose_name='Количество')),
('notes', models.CharField(blank=True, max_length=200, verbose_name='Примечание')),
('product', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='kit_items_direct', to='products.product', verbose_name='Конкретный товар')),
('kit', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='kit_items', to='products.productkit', verbose_name='Комплект')),
('variant_group', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='kit_items', to='products.productvariantgroup', verbose_name='Группа вариантов')),
],
options={
'verbose_name': 'Компонент комплекта',
'verbose_name_plural': 'Компоненты комплектов',
},
),
migrations.CreateModel(
name='KitItemPriority',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('priority', models.PositiveIntegerField(default=0, help_text='Меньше = выше приоритет (0 - наивысший)')),
('kit_item', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='priorities', to='products.kititem', verbose_name='Позиция в букете')),
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='products.product', verbose_name='Товар')),
],
options={
'verbose_name': 'Приоритет варианта',
'verbose_name_plural': 'Приоритеты вариантов',
'ordering': ['priority', 'id'],
'unique_together': {('kit_item', 'product')},
},
),
migrations.AddIndex(
model_name='productcategory',
index=models.Index(fields=['is_active'], name='products_pr_is_acti_226e74_idx'),
),
migrations.AddIndex(
model_name='productcategory',
index=models.Index(fields=['is_deleted'], name='products_pr_is_dele_2a96d1_idx'),
),
migrations.AddIndex(
model_name='productcategory',
index=models.Index(fields=['is_deleted', 'created_at'], name='products_pr_is_dele_b8cdf3_idx'),
),
migrations.AddIndex(
model_name='producttag',
index=models.Index(fields=['is_deleted'], name='products_pr_is_dele_ea9be0_idx'),
),
migrations.AddIndex(
model_name='producttag',
index=models.Index(fields=['is_deleted', 'created_at'], name='products_pr_is_dele_bc2d9c_idx'),
),
migrations.AddIndex(
model_name='productkit',
index=models.Index(fields=['is_active'], name='products_pr_is_acti_214d4f_idx'),
),
migrations.AddIndex(
model_name='productkit',
index=models.Index(fields=['slug'], name='products_pr_slug_b5e185_idx'),
),
migrations.AddIndex(
model_name='productkit',
index=models.Index(fields=['is_deleted'], name='products_pr_is_dele_e83a83_idx'),
),
migrations.AddIndex(
model_name='productkit',
index=models.Index(fields=['is_deleted', 'created_at'], name='products_pr_is_dele_1e5bec_idx'),
),
migrations.AddIndex(
model_name='product',
index=models.Index(fields=['is_active'], name='products_pr_is_acti_ca4d9a_idx'),
),
migrations.AddIndex(
model_name='product',
index=models.Index(fields=['is_deleted'], name='products_pr_is_dele_3bba04_idx'),
),
migrations.AddIndex(
model_name='product',
index=models.Index(fields=['is_deleted', 'created_at'], name='products_pr_is_dele_f30efb_idx'),
),
migrations.AddIndex(
model_name='kititem',
index=models.Index(fields=['kit'], name='products_ki_kit_id_d28dc9_idx'),
),
migrations.AddIndex(
model_name='kititem',
index=models.Index(fields=['product'], name='products_ki_product_d2ad00_idx'),
),
migrations.AddIndex(
model_name='kititem',
index=models.Index(fields=['variant_group'], name='products_ki_variant_e42628_idx'),
),
migrations.AddIndex(
model_name='kititem',
index=models.Index(fields=['kit', 'product'], name='products_ki_kit_id_14738f_idx'),
),
migrations.AddIndex(
model_name='kititem',
index=models.Index(fields=['kit', 'variant_group'], name='products_ki_kit_id_8199a8_idx'),
),
]