refactor(db): консолидация миграций и рефакторинг кода
Объединены изменения из промежуточных миграций в начальные миграции для упрощения истории базы данных. Удалены миграции: accounts/0002, discounts/0002, orders/0003-0004, products/0002-0005, user_roles/0002, system_settings/0001-0002, integrations/0001-0002. Добавлена автоматическая creation пользователя при установке пароля. Обновлен UI страницы установки пароля с кастомным стилем. Добавлен conditional rendering для кнопки синхронизации Recommerce. Исправлены редиректы с 'index' на '/' в accounts views. Добавлена проверка request.tenant в navbar и authenticate метод в auth backend.
This commit is contained in:
@@ -1,10 +1,9 @@
|
||||
# Generated by Django 5.0.10 on 2026-01-08 15:58
|
||||
# Generated by Django 5.0.10 on 2026-01-14 07:04
|
||||
|
||||
import django.core.validators
|
||||
import django.db.models.deletion
|
||||
import products.models.photos
|
||||
from decimal import Decimal
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
@@ -13,9 +12,9 @@ class Migration(migrations.Migration):
|
||||
initial = True
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0001_initial'),
|
||||
('inventory', '0001_initial'),
|
||||
('orders', '0001_initial'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
@@ -112,10 +111,13 @@ class Migration(migrations.Migration):
|
||||
('description', models.TextField(blank=True, null=True, verbose_name='Описание')),
|
||||
('short_description', models.TextField(blank=True, help_text='Используется для карточек товаров, превью и площадок', null=True, verbose_name='Краткое описание')),
|
||||
('status', models.CharField(choices=[('active', 'Активный'), ('archived', 'Архивный'), ('discontinued', 'Снят')], db_index=True, default='active', max_length=20, verbose_name='Статус')),
|
||||
('is_new', models.BooleanField(default=False, help_text='Отображать как новый товар', verbose_name='Новинка')),
|
||||
('is_popular', models.BooleanField(default=False, help_text='Отображать как популярный товар', verbose_name='Популярный')),
|
||||
('is_special', models.BooleanField(default=False, help_text='Отображать как спецпредложение (акция)', verbose_name='Спецпредложение')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Дата обновления')),
|
||||
('archived_at', models.DateTimeField(blank=True, null=True, verbose_name='Время архивирования')),
|
||||
('archived_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='archived_%(class)s_set', to=settings.AUTH_USER_MODEL, verbose_name='Архивировано пользователем')),
|
||||
('archived_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='archived_%(class)s_set', to='accounts.customuser', verbose_name='Архивировано пользователем')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Вариативный товар',
|
||||
@@ -196,6 +198,9 @@ class Migration(migrations.Migration):
|
||||
('description', models.TextField(blank=True, null=True, verbose_name='Описание')),
|
||||
('short_description', models.TextField(blank=True, help_text='Используется для карточек товаров, превью и площадок', null=True, verbose_name='Краткое описание')),
|
||||
('status', models.CharField(choices=[('active', 'Активный'), ('archived', 'Архивный'), ('discontinued', 'Снят')], db_index=True, default='active', max_length=20, verbose_name='Статус')),
|
||||
('is_new', models.BooleanField(default=False, help_text='Отображать как новый товар', verbose_name='Новинка')),
|
||||
('is_popular', models.BooleanField(default=False, help_text='Отображать как популярный товар', verbose_name='Популярный')),
|
||||
('is_special', models.BooleanField(default=False, help_text='Отображать как спецпредложение (акция)', verbose_name='Спецпредложение')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Дата обновления')),
|
||||
('archived_at', models.DateTimeField(blank=True, null=True, verbose_name='Время архивирования')),
|
||||
@@ -206,7 +211,7 @@ class Migration(migrations.Migration):
|
||||
('sale_price', models.DecimalField(blank=True, decimal_places=2, help_text='Если задана, товар продается по этой цене (дешевле основной)', max_digits=10, null=True, verbose_name='Цена со скидкой')),
|
||||
('in_stock', models.BooleanField(db_index=True, default=False, help_text='Автоматически обновляется при изменении остатков на складе', verbose_name='В наличии')),
|
||||
('search_keywords', models.TextField(blank=True, help_text='Автоматически генерируется из названия, артикула, описания и категории. Можно дополнить вручную.', verbose_name='Ключевые слова для поиска')),
|
||||
('archived_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='archived_%(class)s_set', to=settings.AUTH_USER_MODEL, verbose_name='Архивировано пользователем')),
|
||||
('archived_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='archived_%(class)s_set', to='accounts.customuser', verbose_name='Архивировано пользователем')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Товар',
|
||||
@@ -290,7 +295,7 @@ class Migration(migrations.Migration):
|
||||
('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='Удалена пользователем')),
|
||||
('deleted_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='deleted_categories', to='accounts.customuser', 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={
|
||||
@@ -303,6 +308,16 @@ class Migration(migrations.Migration):
|
||||
name='categories',
|
||||
field=models.ManyToManyField(blank=True, related_name='products', to='products.productcategory', verbose_name='Категории'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='product',
|
||||
name='external_category',
|
||||
field=models.ForeignKey(blank=True, help_text='Категория для интеграций с внешними площадками (Recommerce, WooCommerce и др.)', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='external_%(class)ss', to='products.productcategory', verbose_name='Внешняя категория'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='configurableproduct',
|
||||
name='external_category',
|
||||
field=models.ForeignKey(blank=True, help_text='Категория для интеграций с внешними площадками (Recommerce, WooCommerce и др.)', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='external_%(class)ss', to='products.productcategory', verbose_name='Внешняя категория'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='ProductCategoryPhoto',
|
||||
fields=[
|
||||
@@ -341,7 +356,7 @@ class Migration(migrations.Migration):
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Создано')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Обновлено')),
|
||||
('completed_at', models.DateTimeField(blank=True, null=True, verbose_name='Завершено')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='product_import_jobs', to=settings.AUTH_USER_MODEL, verbose_name='Пользователь')),
|
||||
('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='product_import_jobs', to='accounts.customuser', verbose_name='Пользователь')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': 'Задача импорта товаров',
|
||||
@@ -359,6 +374,9 @@ class Migration(migrations.Migration):
|
||||
('description', models.TextField(blank=True, null=True, verbose_name='Описание')),
|
||||
('short_description', models.TextField(blank=True, help_text='Используется для карточек товаров, превью и площадок', null=True, verbose_name='Краткое описание')),
|
||||
('status', models.CharField(choices=[('active', 'Активный'), ('archived', 'Архивный'), ('discontinued', 'Снят')], db_index=True, default='active', max_length=20, verbose_name='Статус')),
|
||||
('is_new', models.BooleanField(default=False, help_text='Отображать как новый товар', verbose_name='Новинка')),
|
||||
('is_popular', models.BooleanField(default=False, help_text='Отображать как популярный товар', verbose_name='Популярный')),
|
||||
('is_special', models.BooleanField(default=False, help_text='Отображать как спецпредложение (акция)', verbose_name='Спецпредложение')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Дата обновления')),
|
||||
('archived_at', models.DateTimeField(blank=True, null=True, verbose_name='Время архивирования')),
|
||||
@@ -368,8 +386,9 @@ class Migration(migrations.Migration):
|
||||
('price_adjustment_type', models.CharField(choices=[('none', 'Без изменения'), ('increase_percent', 'Увеличить на %'), ('increase_amount', 'Увеличить на сумму'), ('decrease_percent', 'Уменьшить на %'), ('decrease_amount', 'Уменьшить на сумму')], default='none', max_length=20, verbose_name='Тип корректировки цены')),
|
||||
('price_adjustment_value', models.DecimalField(decimal_places=2, default=0, help_text='Процент (%) или сумма (руб) в зависимости от типа корректировки', max_digits=10, verbose_name='Значение корректировки')),
|
||||
('is_temporary', models.BooleanField(default=False, help_text='Временные комплекты не показываются в каталоге и создаются для конкретного заказа', verbose_name='Временный комплект')),
|
||||
('archived_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='archived_%(class)s_set', to=settings.AUTH_USER_MODEL, verbose_name='Архивировано пользователем')),
|
||||
('archived_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='archived_%(class)s_set', to='accounts.customuser', verbose_name='Архивировано пользователем')),
|
||||
('categories', models.ManyToManyField(blank=True, related_name='kits', to='products.productcategory', verbose_name='Категории')),
|
||||
('external_category', models.ForeignKey(blank=True, help_text='Категория для интеграций с внешними площадками (Recommerce, WooCommerce и др.)', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='external_%(class)ss', to='products.productcategory', verbose_name='Внешняя категория')),
|
||||
('order', models.ForeignKey(blank=True, help_text='Заказ, для которого создан временный комплект', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='temporary_kits', to='orders.order', verbose_name='Заказ')),
|
||||
('showcase', models.ForeignKey(blank=True, help_text='Витрина, на которой выложен временный комплект', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='temporary_kits', to='inventory.showcase', verbose_name='Витрина')),
|
||||
],
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
# Generated by Django 5.0.10 on 2026-01-09 20:42
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
def clear_invalid_user_references(apps, schema_editor):
|
||||
"""
|
||||
Очистить ссылки на PlatformAdmin в полях archived_by/deleted_by/user,
|
||||
т.к. они теперь ссылаются на CustomUser.
|
||||
|
||||
Устанавливаем NULL для всех существующих ссылок на пользователей,
|
||||
чтобы избежать ошибок referential integrity при изменении типа ForeignKey.
|
||||
"""
|
||||
ProductCategory = apps.get_model('products', 'ProductCategory')
|
||||
Product = apps.get_model('products', 'Product')
|
||||
ProductKit = apps.get_model('products', 'ProductKit')
|
||||
ConfigurableProduct = apps.get_model('products', 'ConfigurableProduct')
|
||||
ProductImportJob = apps.get_model('products', 'ProductImportJob')
|
||||
|
||||
# Очищаем все существующие ссылки (ставим NULL)
|
||||
ProductCategory.objects.all().update(deleted_by=None)
|
||||
Product.objects.all().update(archived_by=None)
|
||||
ProductKit.objects.all().update(archived_by=None)
|
||||
ConfigurableProduct.objects.all().update(archived_by=None)
|
||||
|
||||
# ProductImportJob.user не может быть NULL (CASCADE), поэтому удаляем записи
|
||||
# если они были созданы PlatformAdmin (что маловероятно)
|
||||
ProductImportJob.objects.all().delete()
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('accounts', '0001_initial'),
|
||||
('products', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
# Сначала очищаем некорректные ссылки
|
||||
migrations.RunPython(clear_invalid_user_references, reverse_code=migrations.RunPython.noop),
|
||||
# Затем изменяем типы полей
|
||||
migrations.AlterField(
|
||||
model_name='configurableproduct',
|
||||
name='archived_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='archived_%(class)s_set', to='accounts.customuser', verbose_name='Архивировано пользователем'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='product',
|
||||
name='archived_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='archived_%(class)s_set', to='accounts.customuser', verbose_name='Архивировано пользователем'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='productcategory',
|
||||
name='deleted_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='deleted_categories', to='accounts.customuser', verbose_name='Удалена пользователем'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='productimportjob',
|
||||
name='user',
|
||||
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='product_import_jobs', to='accounts.customuser', verbose_name='Пользователь'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='productkit',
|
||||
name='archived_by',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='archived_%(class)s_set', to='accounts.customuser', verbose_name='Архивировано пользователем'),
|
||||
),
|
||||
]
|
||||
@@ -1,58 +0,0 @@
|
||||
# Generated by Django 5.0.10 on 2026-01-12 21:26
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0002_alter_configurableproduct_archived_by_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='configurableproduct',
|
||||
name='is_new',
|
||||
field=models.BooleanField(default=False, help_text='Отображать как новый товар', verbose_name='Новинка'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='configurableproduct',
|
||||
name='is_popular',
|
||||
field=models.BooleanField(default=False, help_text='Отображать как популярный товар', verbose_name='Популярный'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='configurableproduct',
|
||||
name='is_special',
|
||||
field=models.BooleanField(default=False, help_text='Отображать как спецпредложение (акция)', verbose_name='Спецпредложение'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='product',
|
||||
name='is_new',
|
||||
field=models.BooleanField(default=False, help_text='Отображать как новый товар', verbose_name='Новинка'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='product',
|
||||
name='is_popular',
|
||||
field=models.BooleanField(default=False, help_text='Отображать как популярный товар', verbose_name='Популярный'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='product',
|
||||
name='is_special',
|
||||
field=models.BooleanField(default=False, help_text='Отображать как спецпредложение (акция)', verbose_name='Спецпредложение'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='productkit',
|
||||
name='is_new',
|
||||
field=models.BooleanField(default=False, help_text='Отображать как новый товар', verbose_name='Новинка'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='productkit',
|
||||
name='is_popular',
|
||||
field=models.BooleanField(default=False, help_text='Отображать как популярный товар', verbose_name='Популярный'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='productkit',
|
||||
name='is_special',
|
||||
field=models.BooleanField(default=False, help_text='Отображать как спецпредложение (акция)', verbose_name='Спецпредложение'),
|
||||
),
|
||||
]
|
||||
@@ -1,29 +0,0 @@
|
||||
# Generated by Django 5.0.10 on 2026-01-13 21:08
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('products', '0003_add_marketing_flags'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='configurableproduct',
|
||||
name='primary_category',
|
||||
field=models.ForeignKey(blank=True, help_text='Используется для интеграций с внешними площадками', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_%(class)ss', to='products.productcategory', verbose_name='Основная категория'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='product',
|
||||
name='primary_category',
|
||||
field=models.ForeignKey(blank=True, help_text='Используется для интеграций с внешними площадками', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_%(class)ss', to='products.productcategory', verbose_name='Основная категория'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='productkit',
|
||||
name='primary_category',
|
||||
field=models.ForeignKey(blank=True, help_text='Используется для интеграций с внешними площадками', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='primary_%(class)ss', to='products.productcategory', verbose_name='Основная категория'),
|
||||
),
|
||||
]
|
||||
@@ -1,72 +0,0 @@
|
||||
# Generated migration to rename primary_category to external_category
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
('products', '0004_configurableproduct_primary_category_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
# Rename primary_category to external_category for Product
|
||||
migrations.RenameField(
|
||||
model_name='product',
|
||||
old_name='primary_category',
|
||||
new_name='external_category',
|
||||
),
|
||||
# Update related_name for Product
|
||||
migrations.AlterField(
|
||||
model_name='product',
|
||||
name='external_category',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text='Категория для интеграций с внешними площадками (Recommerce, WooCommerce и др.)',
|
||||
null=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='external_products',
|
||||
to='products.productcategory',
|
||||
verbose_name='Внешняя категория'
|
||||
),
|
||||
),
|
||||
# Rename primary_category to external_category for ProductKit
|
||||
migrations.RenameField(
|
||||
model_name='productkit',
|
||||
old_name='primary_category',
|
||||
new_name='external_category',
|
||||
),
|
||||
# Update related_name for ProductKit
|
||||
migrations.AlterField(
|
||||
model_name='productkit',
|
||||
name='external_category',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text='Категория для интеграций с внешними площадками (Recommerce, WooCommerce и др.)',
|
||||
null=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='external_productkits',
|
||||
to='products.productcategory',
|
||||
verbose_name='Внешняя категория'
|
||||
),
|
||||
),
|
||||
# Rename primary_category to external_category for ConfigurableProduct
|
||||
migrations.RenameField(
|
||||
model_name='configurableproduct',
|
||||
old_name='primary_category',
|
||||
new_name='external_category',
|
||||
),
|
||||
# Update related_name for ConfigurableProduct
|
||||
migrations.AlterField(
|
||||
model_name='configurableproduct',
|
||||
name='external_category',
|
||||
field=models.ForeignKey(
|
||||
blank=True,
|
||||
help_text='Категория для интеграций с внешними площадками (Recommerce, WooCommerce и др.)',
|
||||
null=True,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name='external_configurableproducts',
|
||||
to='products.productcategory',
|
||||
verbose_name='Внешняя категория'
|
||||
),
|
||||
),
|
||||
]
|
||||
@@ -237,11 +237,13 @@
|
||||
</a>
|
||||
</li>
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
{% if recommerce_integration_enabled %}
|
||||
<li>
|
||||
<a class="dropdown-item" href="#" id="bulk-recommerce-sync">
|
||||
<i class="bi bi-arrow-repeat"></i> Синхронизация с Recommerce
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -595,4 +597,19 @@
|
||||
<script src="{% static 'products/js/batch-selection.js' %}?v=1.5"></script>
|
||||
<script src="{% static 'products/js/bulk-category-modal.js' %}?v=1.6"></script>
|
||||
<script src="{% static 'products/js/recommerce-sync.js' %}?v=1.2"></script>
|
||||
<script>
|
||||
// Проверка состояния интеграции Recommerce
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const syncBtn = document.getElementById('bulk-recommerce-sync');
|
||||
if (syncBtn) {
|
||||
syncBtn.addEventListener('click', function(e) {
|
||||
{% if not recommerce_integration_enabled %}
|
||||
e.preventDefault();
|
||||
alert('Интеграция с Recommerce отключена. Включите её в настройках.');
|
||||
return false;
|
||||
{% endif %}
|
||||
});
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -175,7 +175,7 @@ class ProductDetailView(LoginRequiredMixin, ManagerOwnerRequiredMixin, DetailVie
|
||||
product_photos.sort(key=lambda x: (x.order, x.created_at))
|
||||
context['product_photos'] = product_photos
|
||||
context['photos_count'] = len(product_photos)
|
||||
|
||||
|
||||
# Кешируем cost_price_details, чтобы не делать множественные запросы к БД
|
||||
from ..services.cost_calculator import ProductCostCalculator
|
||||
context['cost_price_details'] = ProductCostCalculator.get_cost_details(self.object)
|
||||
@@ -277,7 +277,7 @@ class CombinedProductListView(LoginRequiredMixin, ManagerOwnerRequiredMixin, Lis
|
||||
def get_queryset(self):
|
||||
# Получаем фильтр по типу
|
||||
type_filter = self.request.GET.get('type', 'all')
|
||||
|
||||
|
||||
# Получаем товары и комплекты (только постоянные комплекты)
|
||||
# Аннотируем товары данными об остатках из агрегированной таблицы Stock
|
||||
total_available = Coalesce(Sum('stocks__quantity_available'), Value(0), output_field=DecimalField())
|
||||
@@ -331,13 +331,13 @@ class CombinedProductListView(LoginRequiredMixin, ManagerOwnerRequiredMixin, Lis
|
||||
elif is_active_filter == '0':
|
||||
products = products.filter(status__in=['archived', 'discontinued'])
|
||||
kits = kits.filter(status__in=['archived', 'discontinued'])
|
||||
|
||||
|
||||
# Фильтрация по наличию (только для товаров)
|
||||
if in_stock_filter == '1':
|
||||
products = products.filter(in_stock=True)
|
||||
elif in_stock_filter == '0':
|
||||
products = products.filter(in_stock=False)
|
||||
|
||||
|
||||
# Фильтрация по тегам
|
||||
if tags:
|
||||
products = products.filter(tags__id__in=tags).distinct()
|
||||
@@ -382,12 +382,12 @@ class CombinedProductListView(LoginRequiredMixin, ManagerOwnerRequiredMixin, Lis
|
||||
# Применяем фильтр по типу
|
||||
products_list = []
|
||||
kits_list = []
|
||||
|
||||
|
||||
if type_filter in ['all', 'products']:
|
||||
products_list = list(products.order_by('-created_at'))
|
||||
for p in products_list:
|
||||
p.item_type = 'product'
|
||||
|
||||
|
||||
if type_filter in ['all', 'kits']:
|
||||
kits_list = list(kits.order_by('-created_at'))
|
||||
for k in kits_list:
|
||||
@@ -411,6 +411,11 @@ class CombinedProductListView(LoginRequiredMixin, ManagerOwnerRequiredMixin, Lis
|
||||
from ..models.base import BaseProductEntity
|
||||
item_statuses = BaseProductEntity.STATUS_CHOICES
|
||||
|
||||
# Получаем состояние интеграции Recommerce
|
||||
from integrations.models import RecommerceIntegration
|
||||
recommerce_integration = RecommerceIntegration.objects.first()
|
||||
context['recommerce_integration_enabled'] = recommerce_integration.is_active if recommerce_integration else False
|
||||
|
||||
# Данные для фильтров
|
||||
context['filters'] = {
|
||||
'categories': ProductCategory.objects.filter(is_active=True),
|
||||
@@ -431,7 +436,7 @@ class CombinedProductListView(LoginRequiredMixin, ManagerOwnerRequiredMixin, Lis
|
||||
'has_discount': self.request.GET.get('has_discount', ''),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
context['item_statuses'] = item_statuses
|
||||
|
||||
# Кнопки действий
|
||||
|
||||
Reference in New Issue
Block a user