Консолидация миграций и добавление unit_service
- Обновлены начальные миграции для всех приложений - Удалены устаревшие миграции для единиц измерения и SKU - Добавлен новый сервис unit_service.py для управления единицами - Обновлены команды инициализации данных тенанта
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.0.10 on 2025-12-29 22:19
|
# Generated by Django 5.0.10 on 2026-01-03 08:35
|
||||||
|
|
||||||
import django.contrib.auth.validators
|
import django.contrib.auth.validators
|
||||||
import django.utils.timezone
|
import django.utils.timezone
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.0.10 on 2025-12-29 22:19
|
# Generated by Django 5.0.10 on 2026-01-03 08:35
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import phonenumber_field.modelfields
|
import phonenumber_field.modelfields
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.0.10 on 2025-12-29 22:19
|
# Generated by Django 5.0.10 on 2026-01-03 08:35
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.0.10 on 2025-12-29 22:19
|
# Generated by Django 5.0.10 on 2026-01-03 08:35
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import phonenumber_field.modelfields
|
import phonenumber_field.modelfields
|
||||||
@@ -71,6 +71,7 @@ class Migration(migrations.Migration):
|
|||||||
('converted_at', models.DateTimeField(blank=True, help_text='Дата преобразования в продажу или списание', 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_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 сессии корзины')),
|
('cart_session_id', models.CharField(blank=True, help_text='Дополнительная идентификация сессии для надежности', max_length=100, null=True, verbose_name='ID сессии корзины')),
|
||||||
|
('quantity_base', models.DecimalField(blank=True, decimal_places=6, help_text='Количество в единицах хранения товара', max_digits=10, null=True, verbose_name='Количество в базовых единицах')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': 'Резервирование',
|
'verbose_name': 'Резервирование',
|
||||||
@@ -87,6 +88,8 @@ class Migration(migrations.Migration):
|
|||||||
('document_number', models.CharField(blank=True, max_length=100, null=True, verbose_name='Номер документа')),
|
('document_number', models.CharField(blank=True, max_length=100, null=True, verbose_name='Номер документа')),
|
||||||
('date', models.DateTimeField(auto_now_add=True, verbose_name='Дата операции')),
|
('date', models.DateTimeField(auto_now_add=True, verbose_name='Дата операции')),
|
||||||
('processed', models.BooleanField(default=False, verbose_name='Обработана (FIFO применена)')),
|
('processed', models.BooleanField(default=False, verbose_name='Обработана (FIFO применена)')),
|
||||||
|
('quantity_base', models.DecimalField(blank=True, decimal_places=6, help_text='Количество в единицах хранения товара (для списания со склада)', max_digits=10, null=True, verbose_name='Количество в базовых единицах')),
|
||||||
|
('unit_name_snapshot', models.CharField(blank=True, default='', help_text='Название единицы продажи на момент продажи', max_length=100, verbose_name='Название единицы (snapshot)')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': 'Продажа',
|
'verbose_name': 'Продажа',
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.0.10 on 2025-12-29 22:19
|
# Generated by Django 5.0.10 on 2026-01-03 08:35
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -63,6 +63,11 @@ class Migration(migrations.Migration):
|
|||||||
name='product_kit',
|
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='Комплект'),
|
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='reservation',
|
||||||
|
name='sales_unit',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='reservations', to='products.productsalesunit', verbose_name='Единица продажи'),
|
||||||
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='sale',
|
model_name='sale',
|
||||||
name='order',
|
name='order',
|
||||||
@@ -73,6 +78,11 @@ class Migration(migrations.Migration):
|
|||||||
name='product',
|
name='product',
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sales', to='products.product', verbose_name='Товар'),
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sales', to='products.product', verbose_name='Товар'),
|
||||||
),
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='sale',
|
||||||
|
name='sales_unit',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sales', to='products.productsalesunit', verbose_name='Единица продажи'),
|
||||||
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='salebatchallocation',
|
model_name='salebatchallocation',
|
||||||
name='sale',
|
name='sale',
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
# Generated by Django 5.0.10 on 2026-01-01 21:29
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('inventory', '0002_initial'),
|
|
||||||
('products', '0006_populate_unit_of_measure'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='reservation',
|
|
||||||
name='quantity_base',
|
|
||||||
field=models.DecimalField(blank=True, decimal_places=6, help_text='Количество в единицах хранения товара', max_digits=10, null=True, verbose_name='Количество в базовых единицах'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='reservation',
|
|
||||||
name='sales_unit',
|
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='reservations', to='products.productsalesunit', verbose_name='Единица продажи'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='sale',
|
|
||||||
name='quantity_base',
|
|
||||||
field=models.DecimalField(blank=True, decimal_places=6, help_text='Количество в единицах хранения товара (для списания со склада)', max_digits=10, null=True, verbose_name='Количество в базовых единицах'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='sale',
|
|
||||||
name='sales_unit',
|
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='sales', to='products.productsalesunit', verbose_name='Единица продажи'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='sale',
|
|
||||||
name='unit_name_snapshot',
|
|
||||||
field=models.CharField(blank=True, default='', help_text='Название единицы продажи на момент продажи', max_length=100, verbose_name='Название единицы (snapshot)'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.0.10 on 2025-12-29 22:19
|
# Generated by Django 5.0.10 on 2026-01-03 08:35
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import phonenumber_field.modelfields
|
import phonenumber_field.modelfields
|
||||||
@@ -85,10 +85,13 @@ class Migration(migrations.Migration):
|
|||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('item_name_snapshot', models.CharField(default='', max_length=200, verbose_name='Название на момент заказа')),
|
('item_name_snapshot', models.CharField(default='', max_length=200, verbose_name='Название на момент заказа')),
|
||||||
('item_sku_snapshot', models.CharField(blank=True, max_length=100, verbose_name='Артикул на момент заказа')),
|
('item_sku_snapshot', models.CharField(blank=True, max_length=100, verbose_name='Артикул на момент заказа')),
|
||||||
('quantity', models.PositiveIntegerField(default=1, verbose_name='Количество')),
|
('quantity', models.DecimalField(decimal_places=3, default=1, help_text='Количество в единицах продажи (может быть дробным)', max_digits=10, verbose_name='Количество')),
|
||||||
('price', models.DecimalField(decimal_places=2, help_text='Цена на момент создания заказа (фиксируется)', max_digits=10, verbose_name='Цена за единицу')),
|
('price', models.DecimalField(decimal_places=2, help_text='Цена на момент создания заказа (фиксируется)', max_digits=10, verbose_name='Цена за единицу')),
|
||||||
('is_custom_price', models.BooleanField(default=False, help_text='True если цена была изменена вручную при создании заказа', verbose_name='Цена изменена вручную')),
|
('is_custom_price', models.BooleanField(default=False, help_text='True если цена была изменена вручную при создании заказа', verbose_name='Цена изменена вручную')),
|
||||||
('is_from_showcase', models.BooleanField(default=False, help_text='True если товар продан с витрины', verbose_name='С витрины')),
|
('is_from_showcase', models.BooleanField(default=False, help_text='True если товар продан с витрины', verbose_name='С витрины')),
|
||||||
|
('unit_name_snapshot', models.CharField(blank=True, default='', help_text='Название единицы продажи на момент заказа', max_length=100, verbose_name='Название единицы (snapshot)')),
|
||||||
|
('conversion_factor_snapshot', models.DecimalField(blank=True, decimal_places=6, help_text='Коэффициент конверсии на момент заказа', max_digits=15, null=True, verbose_name='Коэффициент конверсии (snapshot)')),
|
||||||
|
('quantity_in_base_units', models.DecimalField(blank=True, decimal_places=6, help_text='Количество в единицах хранения товара (для списания со склада)', max_digits=10, null=True, verbose_name='Количество в базовых единицах')),
|
||||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата добавления')),
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата добавления')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
@@ -250,10 +253,13 @@ class Migration(migrations.Migration):
|
|||||||
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
|
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
|
||||||
('item_name_snapshot', models.CharField(default='', max_length=200, verbose_name='Название на момент заказа')),
|
('item_name_snapshot', models.CharField(default='', max_length=200, verbose_name='Название на момент заказа')),
|
||||||
('item_sku_snapshot', models.CharField(blank=True, max_length=100, verbose_name='Артикул на момент заказа')),
|
('item_sku_snapshot', models.CharField(blank=True, max_length=100, verbose_name='Артикул на момент заказа')),
|
||||||
('quantity', models.PositiveIntegerField(default=1, verbose_name='Количество')),
|
('quantity', models.DecimalField(decimal_places=3, default=1, help_text='Количество в единицах продажи (может быть дробным)', max_digits=10, verbose_name='Количество')),
|
||||||
('price', models.DecimalField(decimal_places=2, help_text='Цена на момент создания заказа (фиксируется)', max_digits=10, verbose_name='Цена за единицу')),
|
('price', models.DecimalField(decimal_places=2, help_text='Цена на момент создания заказа (фиксируется)', max_digits=10, verbose_name='Цена за единицу')),
|
||||||
('is_custom_price', models.BooleanField(default=False, help_text='True если цена была изменена вручную при создании заказа', verbose_name='Цена изменена вручную')),
|
('is_custom_price', models.BooleanField(default=False, help_text='True если цена была изменена вручную при создании заказа', verbose_name='Цена изменена вручную')),
|
||||||
('is_from_showcase', models.BooleanField(default=False, help_text='True если товар продан с витрины', verbose_name='С витрины')),
|
('is_from_showcase', models.BooleanField(default=False, help_text='True если товар продан с витрины', verbose_name='С витрины')),
|
||||||
|
('unit_name_snapshot', models.CharField(blank=True, default='', help_text='Название единицы продажи на момент заказа', max_length=100, verbose_name='Название единицы (snapshot)')),
|
||||||
|
('conversion_factor_snapshot', models.DecimalField(blank=True, decimal_places=6, help_text='Коэффициент конверсии на момент заказа', max_digits=15, null=True, verbose_name='Коэффициент конверсии (snapshot)')),
|
||||||
|
('quantity_in_base_units', models.DecimalField(blank=True, decimal_places=6, help_text='Количество в единицах хранения товара (для списания со склада)', max_digits=10, null=True, verbose_name='Количество в базовых единицах')),
|
||||||
('created_at', models.DateTimeField(blank=True, editable=False, verbose_name='Дата добавления')),
|
('created_at', models.DateTimeField(blank=True, editable=False, verbose_name='Дата добавления')),
|
||||||
('history_id', models.AutoField(primary_key=True, serialize=False)),
|
('history_id', models.AutoField(primary_key=True, serialize=False)),
|
||||||
('history_date', models.DateTimeField(db_index=True)),
|
('history_date', models.DateTimeField(db_index=True)),
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.0.10 on 2025-12-29 22:19
|
# Generated by Django 5.0.10 on 2026-01-03 08:35
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
@@ -28,6 +28,11 @@ class Migration(migrations.Migration):
|
|||||||
name='product_kit',
|
name='product_kit',
|
||||||
field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='products.productkit', verbose_name='Комплект товаров'),
|
field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='products.productkit', verbose_name='Комплект товаров'),
|
||||||
),
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='historicalorderitem',
|
||||||
|
name='sales_unit',
|
||||||
|
field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='products.productsalesunit', verbose_name='Единица продажи'),
|
||||||
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='historicalorderitem',
|
model_name='historicalorderitem',
|
||||||
name='showcase',
|
name='showcase',
|
||||||
@@ -93,6 +98,11 @@ class Migration(migrations.Migration):
|
|||||||
name='product_kit',
|
name='product_kit',
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='order_items', to='products.productkit', verbose_name='Комплект товаров'),
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='order_items', to='products.productkit', verbose_name='Комплект товаров'),
|
||||||
),
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='orderitem',
|
||||||
|
name='sales_unit',
|
||||||
|
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='order_items', to='products.productsalesunit', verbose_name='Единица продажи'),
|
||||||
|
),
|
||||||
migrations.AddField(
|
migrations.AddField(
|
||||||
model_name='orderitem',
|
model_name='orderitem',
|
||||||
name='showcase',
|
name='showcase',
|
||||||
|
|||||||
@@ -1,55 +0,0 @@
|
|||||||
# Generated by Django 5.0.10 on 2026-01-01 21:29
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('orders', '0002_initial'),
|
|
||||||
('products', '0006_populate_unit_of_measure'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='historicalorderitem',
|
|
||||||
name='conversion_factor_snapshot',
|
|
||||||
field=models.DecimalField(blank=True, decimal_places=6, help_text='Коэффициент конверсии на момент заказа', max_digits=15, null=True, verbose_name='Коэффициент конверсии (snapshot)'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='historicalorderitem',
|
|
||||||
name='quantity_in_base_units',
|
|
||||||
field=models.DecimalField(blank=True, decimal_places=6, help_text='Количество в единицах хранения товара (для списания со склада)', max_digits=10, null=True, verbose_name='Количество в базовых единицах'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='historicalorderitem',
|
|
||||||
name='sales_unit',
|
|
||||||
field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='products.productsalesunit', verbose_name='Единица продажи'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='historicalorderitem',
|
|
||||||
name='unit_name_snapshot',
|
|
||||||
field=models.CharField(blank=True, default='', help_text='Название единицы продажи на момент заказа', max_length=100, verbose_name='Название единицы (snapshot)'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='orderitem',
|
|
||||||
name='conversion_factor_snapshot',
|
|
||||||
field=models.DecimalField(blank=True, decimal_places=6, help_text='Коэффициент конверсии на момент заказа', max_digits=15, null=True, verbose_name='Коэффициент конверсии (snapshot)'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='orderitem',
|
|
||||||
name='quantity_in_base_units',
|
|
||||||
field=models.DecimalField(blank=True, decimal_places=6, help_text='Количество в единицах хранения товара (для списания со склада)', max_digits=10, null=True, verbose_name='Количество в базовых единицах'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='orderitem',
|
|
||||||
name='sales_unit',
|
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='order_items', to='products.productsalesunit', verbose_name='Единица продажи'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='orderitem',
|
|
||||||
name='unit_name_snapshot',
|
|
||||||
field=models.CharField(blank=True, default='', help_text='Название единицы продажи на момент заказа', max_length=100, verbose_name='Название единицы (snapshot)'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# Generated by Django 5.0.10 on 2026-01-02 14:55
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('orders', '0003_add_sales_unit_fields'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='historicalorderitem',
|
|
||||||
name='quantity',
|
|
||||||
field=models.DecimalField(decimal_places=3, default=1, help_text='Количество в единицах продажи (может быть дробным)', max_digits=10, verbose_name='Количество'),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='orderitem',
|
|
||||||
name='quantity',
|
|
||||||
field=models.DecimalField(decimal_places=3, default=1, help_text='Количество в единицах продажи (может быть дробным)', max_digits=10, verbose_name='Количество'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
# Generated by Django 5.0.10 on 2025-12-29 22:19
|
# Generated by Django 5.0.10 on 2026-01-03 08:35
|
||||||
|
|
||||||
|
import django.core.validators
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
import products.models.photos
|
import products.models.photos
|
||||||
|
from decimal import Decimal
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
@@ -76,7 +78,7 @@ class Migration(migrations.Migration):
|
|||||||
name='SKUCounter',
|
name='SKUCounter',
|
||||||
fields=[
|
fields=[
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('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='Тип счетчика')),
|
('counter_type', models.CharField(choices=[('product', 'Product Counter'), ('kit', 'Kit Counter'), ('category', 'Category Counter'), ('configurable', 'Configurable Product Counter')], max_length=20, unique=True, verbose_name='Тип счетчика')),
|
||||||
('current_value', models.IntegerField(default=0, verbose_name='Текущее значение')),
|
('current_value', models.IntegerField(default=0, verbose_name='Текущее значение')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
@@ -84,6 +86,22 @@ class Migration(migrations.Migration):
|
|||||||
'verbose_name_plural': 'Счетчики артикулов',
|
'verbose_name_plural': 'Счетчики артикулов',
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='UnitOfMeasure',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('code', models.CharField(help_text='Короткий код: шт, кг, банч, ветка', max_length=20, unique=True, verbose_name='Код')),
|
||||||
|
('name', models.CharField(help_text='Полное название: Штука, Килограмм, Банч', max_length=100, verbose_name='Название')),
|
||||||
|
('short_name', models.CharField(help_text='Для UI: шт., кг., бч.', max_length=10, verbose_name='Сокращение')),
|
||||||
|
('is_active', models.BooleanField(default=True, verbose_name='Активна')),
|
||||||
|
('position', models.PositiveIntegerField(default=0, verbose_name='Порядок сортировки')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Единица измерения',
|
||||||
|
'verbose_name_plural': 'Единицы измерения',
|
||||||
|
'ordering': ['position', 'name'],
|
||||||
|
},
|
||||||
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='ConfigurableProduct',
|
name='ConfigurableProduct',
|
||||||
fields=[
|
fields=[
|
||||||
@@ -126,6 +144,7 @@ class Migration(migrations.Migration):
|
|||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
('attributes', models.JSONField(blank=True, default=dict, help_text='Структурированные атрибуты. Пример: {"length": "60", "color": "red"}', verbose_name='Атрибуты варианта')),
|
('attributes', models.JSONField(blank=True, default=dict, help_text='Структурированные атрибуты. Пример: {"length": "60", "color": "red"}', verbose_name='Атрибуты варианта')),
|
||||||
('is_default', models.BooleanField(default=False, verbose_name='Вариант по умолчанию')),
|
('is_default', models.BooleanField(default=False, verbose_name='Вариант по умолчанию')),
|
||||||
|
('variant_sku', models.CharField(blank=True, help_text='Дополнительный артикул для внешних площадок. Генерируется автоматически.', max_length=50, verbose_name='Артикул варианта')),
|
||||||
('parent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='options', to='products.configurableproduct', verbose_name='Родитель (вариативный товар)')),
|
('parent', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='options', to='products.configurableproduct', verbose_name='Родитель (вариативный товар)')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
@@ -181,7 +200,7 @@ class Migration(migrations.Migration):
|
|||||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Дата обновления')),
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Дата обновления')),
|
||||||
('archived_at', models.DateTimeField(blank=True, null=True, verbose_name='Время архивирования')),
|
('archived_at', models.DateTimeField(blank=True, null=True, verbose_name='Время архивирования')),
|
||||||
('variant_suffix', models.CharField(blank=True, help_text='Суффикс для артикула (например: 50, 60, S, M). Автоматически извлекается из названия.', max_length=20, null=True, verbose_name='Суффикс варианта')),
|
('variant_suffix', models.CharField(blank=True, help_text='Суффикс для артикула (например: 50, 60, S, M). Автоматически извлекается из названия.', max_length=20, null=True, verbose_name='Суффикс варианта')),
|
||||||
('unit', models.CharField(choices=[('шт', 'Штука'), ('м', 'Метр'), ('г', 'Грамм'), ('л', 'Литр'), ('кг', 'Килограмм')], default='шт', max_length=10, verbose_name='Единица измерения')),
|
('unit', models.CharField(choices=[('шт', 'Штука'), ('м', 'Метр'), ('г', 'Грамм'), ('л', 'Литр'), ('кг', 'Килограмм')], default='шт', max_length=10, verbose_name='Единица измерения (deprecated)')),
|
||||||
('cost_price', models.DecimalField(blank=True, decimal_places=2, default=0, help_text='Автоматически вычисляется из партий (средневзвешенная стоимость по FIFO)', max_digits=10, null=True, verbose_name='Себестоимость')),
|
('cost_price', models.DecimalField(blank=True, decimal_places=2, default=0, help_text='Автоматически вычисляется из партий (средневзвешенная стоимость по FIFO)', max_digits=10, null=True, verbose_name='Себестоимость')),
|
||||||
('price', models.DecimalField(decimal_places=2, help_text='Цена продажи товара (бывшее поле sale_price)', max_digits=10, verbose_name='Основная цена')),
|
('price', models.DecimalField(decimal_places=2, help_text='Цена продажи товара (бывшее поле sale_price)', max_digits=10, verbose_name='Основная цена')),
|
||||||
('sale_price', models.DecimalField(blank=True, decimal_places=2, help_text='Если задана, товар продается по этой цене (дешевле основной)', max_digits=10, null=True, verbose_name='Цена со скидкой')),
|
('sale_price', models.DecimalField(blank=True, decimal_places=2, help_text='Если задана, товар продается по этой цене (дешевле основной)', max_digits=10, null=True, verbose_name='Цена со скидкой')),
|
||||||
@@ -379,6 +398,27 @@ class Migration(migrations.Migration):
|
|||||||
'ordering': ['order', '-created_at'],
|
'ordering': ['order', '-created_at'],
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='ProductSalesUnit',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('name', models.CharField(help_text="Например: 'Ветка большая', 'Ветка средняя'", max_length=100, verbose_name='Название')),
|
||||||
|
('conversion_factor', models.DecimalField(decimal_places=6, help_text='Сколько единиц продажи в 1 базовой единице товара. Например: 15 (из 1 банча получается 15 больших веток)', max_digits=15, validators=[django.core.validators.MinValueValidator(Decimal('0.000001'))], verbose_name='Коэффициент конверсии')),
|
||||||
|
('price', models.DecimalField(decimal_places=2, max_digits=10, validators=[django.core.validators.MinValueValidator(Decimal('0'))], verbose_name='Цена продажи')),
|
||||||
|
('sale_price', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, validators=[django.core.validators.MinValueValidator(Decimal('0'))], verbose_name='Цена со скидкой')),
|
||||||
|
('min_quantity', models.DecimalField(decimal_places=3, default=Decimal('1'), help_text='Минимальное количество для продажи', max_digits=10, validators=[django.core.validators.MinValueValidator(Decimal('0.001'))], verbose_name='Мин. количество')),
|
||||||
|
('quantity_step', models.DecimalField(decimal_places=3, default=Decimal('1'), help_text='С каким шагом можно заказывать (0.1, 0.5, 1)', max_digits=10, validators=[django.core.validators.MinValueValidator(Decimal('0.001'))], verbose_name='Шаг количества')),
|
||||||
|
('is_default', models.BooleanField(default=False, help_text='Единица, выбираемая по умолчанию при добавлении в заказ', verbose_name='Единица по умолчанию')),
|
||||||
|
('is_active', models.BooleanField(default=True, verbose_name='Активна')),
|
||||||
|
('position', models.PositiveIntegerField(default=0, verbose_name='Порядок сортировки')),
|
||||||
|
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sales_units', to='products.product', verbose_name='Товар')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Единица продажи товара',
|
||||||
|
'verbose_name_plural': 'Единицы продажи товаров',
|
||||||
|
'ordering': ['position', 'id'],
|
||||||
|
},
|
||||||
|
),
|
||||||
migrations.CreateModel(
|
migrations.CreateModel(
|
||||||
name='ProductTag',
|
name='ProductTag',
|
||||||
fields=[
|
fields=[
|
||||||
@@ -429,6 +469,16 @@ class Migration(migrations.Migration):
|
|||||||
name='variant_group',
|
name='variant_group',
|
||||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='products.productvariantgroup', verbose_name='Группа вариантов'),
|
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='products.productvariantgroup', verbose_name='Группа вариантов'),
|
||||||
),
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='productsalesunit',
|
||||||
|
name='unit',
|
||||||
|
field=models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='products.unitofmeasure', verbose_name='Единица измерения'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='product',
|
||||||
|
name='base_unit',
|
||||||
|
field=models.ForeignKey(blank=True, help_text="Единица хранения и закупки (банч, кг, шт). Если указана, используется вместо поля 'unit'.", null=True, on_delete=django.db.models.deletion.PROTECT, related_name='products', to='products.unitofmeasure', verbose_name='Базовая единица'),
|
||||||
|
),
|
||||||
migrations.AddIndex(
|
migrations.AddIndex(
|
||||||
model_name='configurableproductoptionattribute',
|
model_name='configurableproductoptionattribute',
|
||||||
index=models.Index(fields=['option'], name='products_co_option__bd950a_idx'),
|
index=models.Index(fields=['option'], name='products_co_option__bd950a_idx'),
|
||||||
@@ -505,6 +555,10 @@ class Migration(migrations.Migration):
|
|||||||
model_name='configurableproductoption',
|
model_name='configurableproductoption',
|
||||||
index=models.Index(fields=['parent', 'is_default'], name='products_co_parent__be8830_idx'),
|
index=models.Index(fields=['parent', 'is_default'], name='products_co_parent__be8830_idx'),
|
||||||
),
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='configurableproductoption',
|
||||||
|
index=models.Index(fields=['variant_sku'], name='products_co_variant_2da938_idx'),
|
||||||
|
),
|
||||||
migrations.AddConstraint(
|
migrations.AddConstraint(
|
||||||
model_name='configurableproductoption',
|
model_name='configurableproductoption',
|
||||||
constraint=models.CheckConstraint(check=models.Q(models.Q(('kit__isnull', False), ('product__isnull', True)), models.Q(('kit__isnull', True), ('product__isnull', False)), _connector='OR'), name='configurable_option_kit_xor_product'),
|
constraint=models.CheckConstraint(check=models.Q(models.Q(('kit__isnull', False), ('product__isnull', True)), models.Q(('kit__isnull', True), ('product__isnull', False)), _connector='OR'), name='configurable_option_kit_xor_product'),
|
||||||
@@ -565,14 +619,6 @@ class Migration(migrations.Migration):
|
|||||||
model_name='productkit',
|
model_name='productkit',
|
||||||
constraint=models.UniqueConstraint(condition=models.Q(('is_temporary', False), ('status', 'active')), fields=('name',), name='unique_active_kit_name'),
|
constraint=models.UniqueConstraint(condition=models.Q(('is_temporary', False), ('status', 'active')), fields=('name',), name='unique_active_kit_name'),
|
||||||
),
|
),
|
||||||
migrations.AddIndex(
|
|
||||||
model_name='product',
|
|
||||||
index=models.Index(fields=['in_stock'], name='products_pr_in_stoc_4fee1a_idx'),
|
|
||||||
),
|
|
||||||
migrations.AddIndex(
|
|
||||||
model_name='product',
|
|
||||||
index=models.Index(fields=['sku'], name='products_pr_sku_ca0cdc_idx'),
|
|
||||||
),
|
|
||||||
migrations.AddIndex(
|
migrations.AddIndex(
|
||||||
model_name='kititem',
|
model_name='kititem',
|
||||||
index=models.Index(fields=['kit'], name='products_ki_kit_id_d28dc9_idx'),
|
index=models.Index(fields=['kit'], name='products_ki_kit_id_d28dc9_idx'),
|
||||||
@@ -605,4 +651,16 @@ class Migration(migrations.Migration):
|
|||||||
name='productvariantgroupitem',
|
name='productvariantgroupitem',
|
||||||
unique_together={('variant_group', 'product')},
|
unique_together={('variant_group', 'product')},
|
||||||
),
|
),
|
||||||
|
migrations.AlterUniqueTogether(
|
||||||
|
name='productsalesunit',
|
||||||
|
unique_together={('product', 'name')},
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='product',
|
||||||
|
index=models.Index(fields=['in_stock'], name='products_pr_in_stoc_4fee1a_idx'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='product',
|
||||||
|
index=models.Index(fields=['sku'], name='products_pr_sku_ca0cdc_idx'),
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 5.0.10 on 2025-12-30 07:11
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('products', '0001_initial'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='skucounter',
|
|
||||||
name='counter_type',
|
|
||||||
field=models.CharField(choices=[('product', 'Product Counter'), ('kit', 'Kit Counter'), ('category', 'Category Counter'), ('configurable', 'Configurable Product Counter')], max_length=20, unique=True, verbose_name='Тип счетчика'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
# Generated by Django 5.0.10 on 2025-12-30 08:17
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('products', '0002_add_configurable_sku_counter'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='configurableproductoption',
|
|
||||||
name='variant_sku',
|
|
||||||
field=models.CharField(blank=True, help_text='Дополнительный артикул для внешних площадок. Генерируется автоматически.', max_length=50, verbose_name='Артикул варианта'),
|
|
||||||
),
|
|
||||||
migrations.AddIndex(
|
|
||||||
model_name='configurableproductoption',
|
|
||||||
index=models.Index(fields=['variant_sku'], name='products_co_variant_2da938_idx'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
# Generated by Django 5.0.10 on 2025-12-30 08:17
|
|
||||||
|
|
||||||
import re
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
def populate_variant_sku(apps, schema_editor):
|
|
||||||
"""
|
|
||||||
Генерируем variant_sku для существующих вариантов.
|
|
||||||
Формат: {parent.sku}-V{counter}
|
|
||||||
"""
|
|
||||||
ConfigurableProductOption = apps.get_model('products', 'ConfigurableProductOption')
|
|
||||||
ConfigurableProduct = apps.get_model('products', 'ConfigurableProduct')
|
|
||||||
|
|
||||||
# Получаем все родительские товары
|
|
||||||
for parent in ConfigurableProduct.objects.all():
|
|
||||||
# Получаем все варианты этого родителя
|
|
||||||
options = ConfigurableProductOption.objects.filter(parent=parent).order_by('id')
|
|
||||||
|
|
||||||
for idx, option in enumerate(options, start=1):
|
|
||||||
# Генерируем variant_sku только если он пустой
|
|
||||||
if not option.variant_sku:
|
|
||||||
option.variant_sku = f"{parent.sku}-V{idx}"
|
|
||||||
option.save(update_fields=['variant_sku'])
|
|
||||||
|
|
||||||
|
|
||||||
def reverse_populate(apps, schema_editor):
|
|
||||||
"""Очистка variant_sku при откате миграции"""
|
|
||||||
ConfigurableProductOption = apps.get_model('products', 'ConfigurableProductOption')
|
|
||||||
ConfigurableProductOption.objects.all().update(variant_sku='')
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('products', '0003_add_variant_sku'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RunPython(populate_variant_sku, reverse_populate),
|
|
||||||
]
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
# Generated by Django 5.0.10 on 2026-01-01 21:28
|
|
||||||
|
|
||||||
import django.core.validators
|
|
||||||
import django.db.models.deletion
|
|
||||||
from decimal import Decimal
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('products', '0004_populate_variant_sku'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='UnitOfMeasure',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('code', models.CharField(help_text='Короткий код: шт, кг, банч, ветка', max_length=20, unique=True, verbose_name='Код')),
|
|
||||||
('name', models.CharField(help_text='Полное название: Штука, Килограмм, Банч', max_length=100, verbose_name='Название')),
|
|
||||||
('short_name', models.CharField(help_text='Для UI: шт., кг., бч.', max_length=10, verbose_name='Сокращение')),
|
|
||||||
('is_active', models.BooleanField(default=True, verbose_name='Активна')),
|
|
||||||
('position', models.PositiveIntegerField(default=0, verbose_name='Порядок сортировки')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'Единица измерения',
|
|
||||||
'verbose_name_plural': 'Единицы измерения',
|
|
||||||
'ordering': ['position', 'name'],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='product',
|
|
||||||
name='unit',
|
|
||||||
field=models.CharField(choices=[('шт', 'Штука'), ('м', 'Метр'), ('г', 'Грамм'), ('л', 'Литр'), ('кг', 'Килограмм')], default='шт', max_length=10, verbose_name='Единица измерения (deprecated)'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='product',
|
|
||||||
name='base_unit',
|
|
||||||
field=models.ForeignKey(blank=True, help_text="Единица хранения и закупки (банч, кг, шт). Если указана, используется вместо поля 'unit'.", null=True, on_delete=django.db.models.deletion.PROTECT, related_name='products', to='products.unitofmeasure', verbose_name='Базовая единица'),
|
|
||||||
),
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='ProductSalesUnit',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('name', models.CharField(help_text="Например: 'Ветка большая', 'Ветка средняя'", max_length=100, verbose_name='Название')),
|
|
||||||
('conversion_factor', models.DecimalField(decimal_places=6, help_text='Сколько единиц продажи в 1 базовой единице товара. Например: 15 (из 1 банча получается 15 больших веток)', max_digits=15, validators=[django.core.validators.MinValueValidator(Decimal('0.000001'))], verbose_name='Коэффициент конверсии')),
|
|
||||||
('price', models.DecimalField(decimal_places=2, max_digits=10, validators=[django.core.validators.MinValueValidator(Decimal('0'))], verbose_name='Цена продажи')),
|
|
||||||
('sale_price', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, validators=[django.core.validators.MinValueValidator(Decimal('0'))], verbose_name='Цена со скидкой')),
|
|
||||||
('min_quantity', models.DecimalField(decimal_places=3, default=Decimal('1'), help_text='Минимальное количество для продажи', max_digits=10, validators=[django.core.validators.MinValueValidator(Decimal('0.001'))], verbose_name='Мин. количество')),
|
|
||||||
('quantity_step', models.DecimalField(decimal_places=3, default=Decimal('1'), help_text='С каким шагом можно заказывать (0.1, 0.5, 1)', max_digits=10, validators=[django.core.validators.MinValueValidator(Decimal('0.001'))], verbose_name='Шаг количества')),
|
|
||||||
('is_default', models.BooleanField(default=False, help_text='Единица, выбираемая по умолчанию при добавлении в заказ', verbose_name='Единица по умолчанию')),
|
|
||||||
('is_active', models.BooleanField(default=True, verbose_name='Активна')),
|
|
||||||
('position', models.PositiveIntegerField(default=0, verbose_name='Порядок сортировки')),
|
|
||||||
('product', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='sales_units', to='products.product', verbose_name='Товар')),
|
|
||||||
('unit', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to='products.unitofmeasure', verbose_name='Единица измерения')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'Единица продажи товара',
|
|
||||||
'verbose_name_plural': 'Единицы продажи товаров',
|
|
||||||
'ordering': ['position', 'id'],
|
|
||||||
'unique_together': {('product', 'name')},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,68 +0,0 @@
|
|||||||
# Generated manually for populating UnitOfMeasure with initial data
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
def populate_units(apps, schema_editor):
|
|
||||||
"""Заполнение справочника единиц измерения начальными данными."""
|
|
||||||
UnitOfMeasure = apps.get_model('products', 'UnitOfMeasure')
|
|
||||||
|
|
||||||
units_data = [
|
|
||||||
# Базовые единицы (из UNIT_CHOICES)
|
|
||||||
{'code': 'шт', 'name': 'Штука', 'short_name': 'шт.', 'position': 1},
|
|
||||||
{'code': 'м', 'name': 'Метр', 'short_name': 'м.', 'position': 2},
|
|
||||||
{'code': 'г', 'name': 'Грамм', 'short_name': 'г.', 'position': 3},
|
|
||||||
{'code': 'л', 'name': 'Литр', 'short_name': 'л.', 'position': 4},
|
|
||||||
{'code': 'кг', 'name': 'Килограмм', 'short_name': 'кг.', 'position': 5},
|
|
||||||
# Флористические единицы
|
|
||||||
{'code': 'банч', 'name': 'Банч', 'short_name': 'банч', 'position': 10},
|
|
||||||
{'code': 'ветка', 'name': 'Ветка', 'short_name': 'вет.', 'position': 11},
|
|
||||||
{'code': 'пучок', 'name': 'Пучок', 'short_name': 'пуч.', 'position': 12},
|
|
||||||
{'code': 'голова', 'name': 'Голова', 'short_name': 'гол.', 'position': 13},
|
|
||||||
{'code': 'стебель', 'name': 'Стебель', 'short_name': 'стеб.', 'position': 14},
|
|
||||||
]
|
|
||||||
|
|
||||||
for data in units_data:
|
|
||||||
UnitOfMeasure.objects.get_or_create(
|
|
||||||
code=data['code'],
|
|
||||||
defaults={
|
|
||||||
'name': data['name'],
|
|
||||||
'short_name': data['short_name'],
|
|
||||||
'position': data['position'],
|
|
||||||
'is_active': True,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def migrate_products_to_base_unit(apps, schema_editor):
|
|
||||||
"""Миграция существующих товаров: связываем поле unit с base_unit."""
|
|
||||||
Product = apps.get_model('products', 'Product')
|
|
||||||
UnitOfMeasure = apps.get_model('products', 'UnitOfMeasure')
|
|
||||||
|
|
||||||
# Создаём маппинг старых кодов на объекты UnitOfMeasure
|
|
||||||
unit_mapping = {}
|
|
||||||
for uom in UnitOfMeasure.objects.all():
|
|
||||||
unit_mapping[uom.code] = uom
|
|
||||||
|
|
||||||
# Обновляем товары
|
|
||||||
for product in Product.objects.filter(base_unit__isnull=True):
|
|
||||||
if product.unit and product.unit in unit_mapping:
|
|
||||||
product.base_unit = unit_mapping[product.unit]
|
|
||||||
product.save(update_fields=['base_unit'])
|
|
||||||
|
|
||||||
|
|
||||||
def reverse_migration(apps, schema_editor):
|
|
||||||
"""Откат миграции - ничего не делаем, данные останутся."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('products', '0005_add_unit_of_measure'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RunPython(populate_units, reverse_migration),
|
|
||||||
migrations.RunPython(migrate_products_to_base_unit, reverse_migration),
|
|
||||||
]
|
|
||||||
@@ -2,3 +2,8 @@
|
|||||||
Сервисы для бизнес-логики products приложения.
|
Сервисы для бизнес-логики products приложения.
|
||||||
Следует принципу "Skinny Models, Fat Services".
|
Следует принципу "Skinny Models, Fat Services".
|
||||||
"""
|
"""
|
||||||
|
from .unit_service import UnitOfMeasureService
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'UnitOfMeasureService',
|
||||||
|
]
|
||||||
|
|||||||
147
myproject/products/services/unit_service.py
Normal file
147
myproject/products/services/unit_service.py
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Сервис для управления единицами измерения.
|
||||||
|
|
||||||
|
Отвечает за создание и управление справочником единиц измерения (UnitOfMeasure).
|
||||||
|
"""
|
||||||
|
import logging
|
||||||
|
from typing import List, Dict, Any
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class UnitOfMeasureService:
|
||||||
|
"""
|
||||||
|
Сервис для управления единицами измерения.
|
||||||
|
|
||||||
|
Предоставляет методы для создания и управления базовыми единицами измерения,
|
||||||
|
которые используются в системе для товаров и продаж.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Базовый набор единиц измерения для новых тенантов
|
||||||
|
DEFAULT_UNITS = [
|
||||||
|
# Базовые единицы (из старых UNIT_CHOICES)
|
||||||
|
{'code': 'шт', 'name': 'Штука', 'short_name': 'шт.', 'position': 1},
|
||||||
|
{'code': 'м', 'name': 'Метр', 'short_name': 'м.', 'position': 2},
|
||||||
|
{'code': 'г', 'name': 'Грамм', 'short_name': 'г.', 'position': 3},
|
||||||
|
{'code': 'л', 'name': 'Литр', 'short_name': 'л.', 'position': 4},
|
||||||
|
{'code': 'кг', 'name': 'Килограмм', 'short_name': 'кг.', 'position': 5},
|
||||||
|
|
||||||
|
# Флористические единицы
|
||||||
|
{'code': 'банч', 'name': 'Банч', 'short_name': 'банч', 'position': 10},
|
||||||
|
{'code': 'ветка', 'name': 'Ветка', 'short_name': 'вет.', 'position': 11},
|
||||||
|
{'code': 'пучок', 'name': 'Пучок', 'short_name': 'пуч.', 'position': 12},
|
||||||
|
]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_default_units(cls) -> List:
|
||||||
|
"""
|
||||||
|
Создает базовый набор единиц измерения для тенанта.
|
||||||
|
|
||||||
|
Использует get_or_create, поэтому безопасно вызывать повторно.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[UnitOfMeasure]: Список созданных/существующих единиц измерения
|
||||||
|
"""
|
||||||
|
from products.models import UnitOfMeasure
|
||||||
|
|
||||||
|
created_units = []
|
||||||
|
|
||||||
|
for unit_data in cls.DEFAULT_UNITS:
|
||||||
|
unit, created = UnitOfMeasure.objects.get_or_create(
|
||||||
|
code=unit_data['code'],
|
||||||
|
defaults={
|
||||||
|
'name': unit_data['name'],
|
||||||
|
'short_name': unit_data['short_name'],
|
||||||
|
'position': unit_data['position'],
|
||||||
|
'is_active': True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
created_units.append(unit)
|
||||||
|
|
||||||
|
if created:
|
||||||
|
logger.debug(f"Создана единица измерения: {unit.code} - {unit.name}")
|
||||||
|
|
||||||
|
logger.info(f"Инициализация единиц измерения завершена: {len(created_units)} единиц")
|
||||||
|
return created_units
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def reset_default_units(cls) -> List:
|
||||||
|
"""
|
||||||
|
Удаляет все единицы измерения и создаёт их заново.
|
||||||
|
|
||||||
|
ВНИМАНИЕ: Используйте только при инициализации тенанта или в тестах!
|
||||||
|
Удаление единиц может нарушить связи с существующими товарами.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[UnitOfMeasure]: Список созданных единиц измерения
|
||||||
|
"""
|
||||||
|
from products.models import UnitOfMeasure
|
||||||
|
|
||||||
|
logger.warning("Удаление всех единиц измерения...")
|
||||||
|
UnitOfMeasure.objects.all().delete()
|
||||||
|
|
||||||
|
return cls.create_default_units()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_or_create_unit(cls, code: str, name: str, short_name: str,
|
||||||
|
position: int = 0) -> tuple:
|
||||||
|
"""
|
||||||
|
Получает или создаёт единицу измерения.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
code: Уникальный код единицы
|
||||||
|
name: Полное название
|
||||||
|
short_name: Короткое название для UI
|
||||||
|
position: Позиция для сортировки
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
tuple: (UnitOfMeasure, created) - единица и флаг создания
|
||||||
|
"""
|
||||||
|
from products.models import UnitOfMeasure
|
||||||
|
|
||||||
|
unit, created = UnitOfMeasure.objects.get_or_create(
|
||||||
|
code=code,
|
||||||
|
defaults={
|
||||||
|
'name': name,
|
||||||
|
'short_name': short_name,
|
||||||
|
'position': position,
|
||||||
|
'is_active': True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
if created:
|
||||||
|
logger.info(f"Создана единица измерения: {code} - {name}")
|
||||||
|
|
||||||
|
return unit, created
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_unit_by_code(cls, code: str):
|
||||||
|
"""
|
||||||
|
Получает единицу измерения по коду.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
code: Код единицы измерения
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
UnitOfMeasure или None, если не найдена
|
||||||
|
"""
|
||||||
|
from products.models import UnitOfMeasure
|
||||||
|
|
||||||
|
try:
|
||||||
|
return UnitOfMeasure.objects.get(code=code, is_active=True)
|
||||||
|
except UnitOfMeasure.DoesNotExist:
|
||||||
|
logger.warning(f"Единица измерения с кодом '{code}' не найдена")
|
||||||
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_active_units(cls) -> List:
|
||||||
|
"""
|
||||||
|
Возвращает все активные единицы измерения.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
List[UnitOfMeasure]: Список активных единиц, отсортированных по position
|
||||||
|
"""
|
||||||
|
from products.models import UnitOfMeasure
|
||||||
|
|
||||||
|
return list(UnitOfMeasure.objects.filter(is_active=True).order_by('position', 'code'))
|
||||||
@@ -8,6 +8,7 @@ Management команда для инициализации всех систе
|
|||||||
- Системные способы оплаты
|
- Системные способы оплаты
|
||||||
- Склад по умолчанию
|
- Склад по умолчанию
|
||||||
- Витрину по умолчанию
|
- Витрину по умолчанию
|
||||||
|
- Единицы измерения
|
||||||
|
|
||||||
Использование:
|
Использование:
|
||||||
# Инициализация для конкретного тенанта
|
# Инициализация для конкретного тенанта
|
||||||
@@ -21,7 +22,7 @@ from django_tenants.utils import get_tenant_model, schema_context
|
|||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
class Command(BaseCommand):
|
||||||
help = 'Инициализация всех системных данных тенанта (клиент, статусы, способы оплаты, склад, витрина)'
|
help = 'Инициализация всех системных данных тенанта (клиент, статусы, способы оплаты, склад, витрина, единицы измерения)'
|
||||||
|
|
||||||
def add_arguments(self, parser):
|
def add_arguments(self, parser):
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.0.10 on 2025-12-29 22:19
|
# Generated by Django 5.0.10 on 2026-01-03 08:35
|
||||||
|
|
||||||
import django.core.validators
|
import django.core.validators
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ class TenantOnboardingService:
|
|||||||
from customers.models import Customer
|
from customers.models import Customer
|
||||||
from orders.services import OrderStatusService, PaymentMethodService
|
from orders.services import OrderStatusService, PaymentMethodService
|
||||||
from inventory.services import WarehouseService, ShowcaseService
|
from inventory.services import WarehouseService, ShowcaseService
|
||||||
|
from products.services import UnitOfMeasureService
|
||||||
|
|
||||||
# 1. Системный клиент
|
# 1. Системный клиент
|
||||||
logger.info("Создание системного клиента...")
|
logger.info("Создание системного клиента...")
|
||||||
@@ -133,6 +134,14 @@ class TenantOnboardingService:
|
|||||||
showcase, created = ShowcaseService.get_or_create_default(warehouse)
|
showcase, created = ShowcaseService.get_or_create_default(warehouse)
|
||||||
logger.info(f"Витрина по умолчанию: {showcase.name}")
|
logger.info(f"Витрина по умолчанию: {showcase.name}")
|
||||||
|
|
||||||
|
# 6. Единицы измерения
|
||||||
|
logger.info("Создание единиц измерения...")
|
||||||
|
if reset:
|
||||||
|
UnitOfMeasureService.reset_default_units()
|
||||||
|
else:
|
||||||
|
UnitOfMeasureService.create_default_units()
|
||||||
|
logger.info("Единицы измерения созданы")
|
||||||
|
|
||||||
# ==================== Приватные методы ====================
|
# ==================== Приватные методы ====================
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.0.10 on 2025-12-29 22:19
|
# Generated by Django 5.0.10 on 2026-01-03 08:35
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|||||||
Reference in New Issue
Block a user