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,6 +1,5 @@
|
|||||||
# 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.contrib.auth.validators
|
|
||||||
import django.utils.timezone
|
import django.utils.timezone
|
||||||
import uuid
|
import uuid
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
@@ -11,7 +10,6 @@ class Migration(migrations.Migration):
|
|||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('auth', '0012_alter_user_first_name_max_length'),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
@@ -21,21 +19,16 @@ 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')),
|
||||||
('password', models.CharField(max_length=128, verbose_name='password')),
|
('password', models.CharField(max_length=128, verbose_name='password')),
|
||||||
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
|
||||||
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
|
|
||||||
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.UnicodeUsernameValidator()], verbose_name='username')),
|
|
||||||
('first_name', models.CharField(blank=True, max_length=150, verbose_name='first name')),
|
|
||||||
('last_name', models.CharField(blank=True, max_length=150, verbose_name='last name')),
|
|
||||||
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
|
|
||||||
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
|
|
||||||
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
|
|
||||||
('email', models.EmailField(max_length=254, unique=True)),
|
('email', models.EmailField(max_length=254, unique=True)),
|
||||||
('name', models.CharField(max_length=100)),
|
('name', models.CharField(max_length=100)),
|
||||||
|
('is_active', models.BooleanField(default=True)),
|
||||||
|
('is_staff', models.BooleanField(default=False)),
|
||||||
|
('is_superuser', models.BooleanField(default=False)),
|
||||||
|
('date_joined', models.DateTimeField(default=django.utils.timezone.now)),
|
||||||
('is_email_confirmed', models.BooleanField(default=False)),
|
('is_email_confirmed', models.BooleanField(default=False)),
|
||||||
('email_confirmation_token', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
|
('email_confirmation_token', models.UUIDField(default=uuid.uuid4, editable=False, unique=True)),
|
||||||
('email_confirmed_at', models.DateTimeField(blank=True, null=True)),
|
('email_confirmed_at', models.DateTimeField(blank=True, null=True)),
|
||||||
('password_reset_token', models.UUIDField(blank=True, editable=False, null=True, unique=True)),
|
('password_reset_token', models.UUIDField(blank=True, editable=False, null=True, unique=True)),
|
||||||
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to.', related_name='custom_user_set', to='auth.group', verbose_name='groups')),
|
|
||||||
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='custom_user_set', to='auth.permission', verbose_name='user permissions')),
|
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
'verbose_name': 'Пользователь магазина',
|
'verbose_name': 'Пользователь магазина',
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
# Generated by Django 5.0.10 on 2026-01-09 21:04
|
|
||||||
|
|
||||||
import django.utils.timezone
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('accounts', '0001_initial'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='customuser',
|
|
||||||
name='first_name',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='customuser',
|
|
||||||
name='groups',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='customuser',
|
|
||||||
name='last_name',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='customuser',
|
|
||||||
name='user_permissions',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='customuser',
|
|
||||||
name='username',
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='customuser',
|
|
||||||
name='date_joined',
|
|
||||||
field=models.DateTimeField(default=django.utils.timezone.now),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='customuser',
|
|
||||||
name='is_active',
|
|
||||||
field=models.BooleanField(default=True),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='customuser',
|
|
||||||
name='is_staff',
|
|
||||||
field=models.BooleanField(default=False),
|
|
||||||
),
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='customuser',
|
|
||||||
name='is_superuser',
|
|
||||||
field=models.BooleanField(default=False),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,35 +1,108 @@
|
|||||||
{% extends 'base.html' %}
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<title>Установка пароля</title>
|
||||||
|
<style>
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
background: white;
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 10px 40px rgba(0,0,0,0.1);
|
||||||
|
padding: 40px;
|
||||||
|
max-width: 400px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
.subtitle {
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
label {
|
||||||
|
display: block;
|
||||||
|
color: #333;
|
||||||
|
font-weight: 500;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
input[type="password"] {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
input[type="password"]:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: #667eea;
|
||||||
|
}
|
||||||
|
.btn {
|
||||||
|
width: 100%;
|
||||||
|
padding: 12px;
|
||||||
|
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 500;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.btn:hover {
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
.messages {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
.messages .error {
|
||||||
|
background: #fee;
|
||||||
|
color: #c33;
|
||||||
|
padding: 10px;
|
||||||
|
border-radius: 6px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1>Установка пароля</h1>
|
||||||
|
<p class="subtitle">для {{ tenant.name }}</p>
|
||||||
|
|
||||||
{% block title %}Установка пароля{% endblock %}
|
{% if messages %}
|
||||||
|
<div class="messages">
|
||||||
|
{% for message in messages %}
|
||||||
|
<div class="{{ message.tags }}">{{ message }}</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="container">
|
|
||||||
<div class="form-container">
|
|
||||||
<h2 class="text-center mb-4">Установка пароля</h2>
|
|
||||||
|
|
||||||
<!-- Приветственное сообщение -->
|
|
||||||
<div class="alert alert-info">
|
|
||||||
<strong>Добро пожаловать!</strong> Ваш магазин <strong>{{ tenant.name }}</strong> активирован.
|
|
||||||
<br>Установите пароль для входа в систему.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="tab-content">
|
|
||||||
<div class="tab-pane fade show active" id="setup-password">
|
|
||||||
<form method="post">
|
<form method="post">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{% include 'accounts/password_input.html' with field_name='password1' field_label='Пароль' required=True %}
|
<div class="form-group">
|
||||||
{% include 'accounts/password_input.html' with field_name='password2' field_label='Подтверждение пароля' required=True %}
|
<label for="id_password1">Пароль</label>
|
||||||
<button type="submit" class="btn btn-primary w-100">Установить пароль и войти</button>
|
<input type="password" name="password1" id="id_password1" required>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label for="id_password2">Подтвердите пароль</label>
|
||||||
|
<input type="password" name="password2" id="id_password2" required>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="btn">Установить пароль</button>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<!-- Информация -->
|
|
||||||
<div class="text-center mt-3">
|
|
||||||
<small class="text-muted">
|
|
||||||
После установки пароля вы автоматически войдете в свой магазин.
|
|
||||||
</small>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</body>
|
||||||
</div>
|
</html>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ def login_view(request):
|
|||||||
|
|
||||||
def logout_view(request):
|
def logout_view(request):
|
||||||
logout(request)
|
logout(request)
|
||||||
return redirect('index')
|
return redirect('/')
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@@ -142,7 +142,7 @@ def password_reset_confirm(request, token):
|
|||||||
user = CustomUser.objects.get(password_reset_token=token)
|
user = CustomUser.objects.get(password_reset_token=token)
|
||||||
except CustomUser.DoesNotExist:
|
except CustomUser.DoesNotExist:
|
||||||
messages.error(request, 'Ссылка для восстановления пароля недействительна.')
|
messages.error(request, 'Ссылка для восстановления пароля недействительна.')
|
||||||
return redirect('index')
|
return redirect('/')
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
password1 = request.POST.get('password1')
|
password1 = request.POST.get('password1')
|
||||||
@@ -178,7 +178,7 @@ def password_setup_confirm(request, token):
|
|||||||
)
|
)
|
||||||
except TenantRegistration.DoesNotExist:
|
except TenantRegistration.DoesNotExist:
|
||||||
messages.error(request, 'Ссылка для настройки пароля недействительна.')
|
messages.error(request, 'Ссылка для настройки пароля недействительна.')
|
||||||
return redirect('index')
|
return redirect('/')
|
||||||
|
|
||||||
# Проверить истечение токена (7 дней)
|
# Проверить истечение токена (7 дней)
|
||||||
if registration.password_setup_token_created_at:
|
if registration.password_setup_token_created_at:
|
||||||
@@ -188,24 +188,30 @@ def password_setup_confirm(request, token):
|
|||||||
request,
|
request,
|
||||||
'Ссылка для настройки пароля истекла. Пожалуйста, свяжитесь с поддержкой.'
|
'Ссылка для настройки пароля истекла. Пожалуйста, свяжитесь с поддержкой.'
|
||||||
)
|
)
|
||||||
return redirect('index')
|
return redirect('/')
|
||||||
|
|
||||||
# Получить тенант и пользователя-владельца
|
# Получить тенант и пользователя-владельца
|
||||||
from django.db import connection
|
from django.db import connection
|
||||||
tenant = registration.tenant
|
tenant = registration.tenant
|
||||||
if not tenant:
|
if not tenant:
|
||||||
messages.error(request, 'Тенант не найден.')
|
messages.error(request, 'Тенант не найден.')
|
||||||
return redirect('index')
|
return redirect('/')
|
||||||
|
|
||||||
# Переключиться на схему тенанта чтобы найти владельца
|
# Переключиться на схему тенанта чтобы найти владельца
|
||||||
connection.set_tenant(tenant)
|
connection.set_tenant(tenant)
|
||||||
User = get_user_model()
|
from accounts.models import CustomUser
|
||||||
try:
|
|
||||||
owner = User.objects.get(email=registration.owner_email)
|
# Создаём пользователя если он не существует (для случаев когда активация прошла без создания пользователя)
|
||||||
except User.DoesNotExist:
|
owner, created = CustomUser.objects.get_or_create(
|
||||||
connection.set_schema_to_public()
|
email=registration.owner_email,
|
||||||
messages.error(request, 'Пользователь не найден.')
|
defaults={
|
||||||
return redirect('index')
|
'name': registration.owner_name,
|
||||||
|
'is_active': False,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
if created:
|
||||||
|
owner.is_email_confirmed = True
|
||||||
|
owner.save()
|
||||||
|
|
||||||
# Обработать POST - установить пароль
|
# Обработать POST - установить пароль
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# 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.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 2026-01-08 15:58
|
# Generated by Django 5.0.10 on 2026-01-14 07:04
|
||||||
|
|
||||||
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 2026-01-10 21:07
|
# Generated by Django 5.0.10 on 2026-01-14 07:04
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
@@ -9,10 +9,10 @@ class Migration(migrations.Migration):
|
|||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
('accounts', '0002_remove_customuser_first_name_and_more'),
|
('accounts', '0001_initial'),
|
||||||
('customers', '0002_initial'),
|
('customers', '0002_initial'),
|
||||||
('orders', '0002_initial'),
|
('orders', '0002_initial'),
|
||||||
('products', '0002_alter_configurableproduct_archived_by_and_more'),
|
('products', '0001_initial'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
@@ -34,6 +34,7 @@ class Migration(migrations.Migration):
|
|||||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
|
||||||
('min_order_amount', models.DecimalField(blank=True, decimal_places=2, help_text='Скидка применяется только если сумма заказа >= этого значения', max_digits=10, null=True, verbose_name='Мин. сумма заказа')),
|
('min_order_amount', models.DecimalField(blank=True, decimal_places=2, help_text='Скидка применяется только если сумма заказа >= этого значения', max_digits=10, null=True, verbose_name='Мин. сумма заказа')),
|
||||||
('is_auto', models.BooleanField(default=False, help_text='Применяется автоматически при выполнении условий', verbose_name='Автоматическая')),
|
('is_auto', models.BooleanField(default=False, help_text='Применяется автоматически при выполнении условий', verbose_name='Автоматическая')),
|
||||||
|
('combine_mode', models.CharField(choices=[('stack', 'Складывать (суммировать)'), ('max_only', 'Только максимум'), ('exclusive', 'Исключающая (отменяет остальные)')], default='max_only', help_text='stack = суммировать с другими, max_only = применить максимальную, exclusive = отменить остальные', max_length=20, verbose_name='Режим объединения')),
|
||||||
('categories', models.ManyToManyField(blank=True, related_name='discounts', to='products.productcategory', verbose_name='Категории')),
|
('categories', models.ManyToManyField(blank=True, related_name='discounts', to='products.productcategory', verbose_name='Категории')),
|
||||||
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_discounts', to='accounts.customuser', verbose_name='Создал')),
|
('created_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_discounts', to='accounts.customuser', verbose_name='Создал')),
|
||||||
('excluded_products', models.ManyToManyField(blank=True, related_name='excluded_from_discounts', to='products.product', verbose_name='Исключенные товары')),
|
('excluded_products', models.ManyToManyField(blank=True, related_name='excluded_from_discounts', to='products.product', verbose_name='Исключенные товары')),
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 5.0.10 on 2026-01-10 23:55
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('discounts', '0001_initial'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='discount',
|
|
||||||
name='combine_mode',
|
|
||||||
field=models.CharField(choices=[('stack', 'Складывать (суммировать)'), ('max_only', 'Только максимум'), ('exclusive', 'Исключающая (отменяет остальные)')], default='max_only', help_text='stack = суммировать с другими, max_only = применить максимальную, exclusive = отменить остальные', max_length=20, verbose_name='Режим объединения'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
# Generated by Django 5.0.10 on 2026-01-11 21:19
|
# Generated by Django 5.0.10 on 2026-01-14 07:04
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
import integrations.fields
|
import integrations.fields
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
@@ -9,6 +10,7 @@ class Migration(migrations.Migration):
|
|||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
('products', '0001_initial'),
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
@@ -54,4 +56,22 @@ class Migration(migrations.Migration):
|
|||||||
'verbose_name_plural': 'WooCommerce',
|
'verbose_name_plural': 'WooCommerce',
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='IntegrationCategoryMapping',
|
||||||
|
fields=[
|
||||||
|
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||||
|
('integration_type', models.CharField(choices=[('recommerce', 'Recommerce'), ('woocommerce', 'WooCommerce')], db_index=True, max_length=20, verbose_name='Интеграция')),
|
||||||
|
('external_category_sku', models.CharField(help_text='SKU или ID категории на внешней площадке', max_length=100, verbose_name='Артикул категории во внешней системе')),
|
||||||
|
('external_category_name', models.CharField(blank=True, help_text='Для справки, не обязательно', max_length=200, verbose_name='Название категории во внешней системе')),
|
||||||
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Создано')),
|
||||||
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Обновлено')),
|
||||||
|
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='integration_mappings', to='products.productcategory', verbose_name='Категория')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'verbose_name': 'Маппинг категории',
|
||||||
|
'verbose_name_plural': 'Маппинги категорий',
|
||||||
|
'indexes': [models.Index(fields=['integration_type', 'external_category_sku'], name='integration_integra_450473_idx')],
|
||||||
|
'unique_together': {('category', 'integration_type')},
|
||||||
|
},
|
||||||
|
),
|
||||||
]
|
]
|
||||||
@@ -1,33 +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 = [
|
|
||||||
('integrations', '0001_add_integration_models'),
|
|
||||||
('products', '0004_configurableproduct_primary_category_and_more'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='IntegrationCategoryMapping',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('integration_type', models.CharField(choices=[('recommerce', 'Recommerce'), ('woocommerce', 'WooCommerce')], db_index=True, max_length=20, verbose_name='Интеграция')),
|
|
||||||
('external_category_sku', models.CharField(help_text='SKU или ID категории на внешней площадке', max_length=100, verbose_name='Артикул категории во внешней системе')),
|
|
||||||
('external_category_name', models.CharField(blank=True, help_text='Для справки, не обязательно', max_length=200, verbose_name='Название категории во внешней системе')),
|
|
||||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Создано')),
|
|
||||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Обновлено')),
|
|
||||||
('category', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='integration_mappings', to='products.productcategory', verbose_name='Категория')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'Маппинг категории',
|
|
||||||
'verbose_name_plural': 'Маппинги категорий',
|
|
||||||
'indexes': [models.Index(fields=['integration_type', 'external_category_sku'], name='integration_integra_450473_idx')],
|
|
||||||
'unique_together': {('category', 'integration_type')},
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# 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.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 2026-01-08 15:58
|
# Generated by Django 5.0.10 on 2026-01-14 07:04
|
||||||
|
|
||||||
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 2026-01-08 15:58
|
# Generated by Django 5.0.10 on 2026-01-14 07:04
|
||||||
|
|
||||||
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 2026-01-08 15:58
|
# Generated by Django 5.0.10 on 2026-01-14 07:04
|
||||||
|
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
# Generated by Django 5.0.10 on 2026-01-10 21:12
|
|
||||||
|
|
||||||
import django.db.models.deletion
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('discounts', '0001_initial'),
|
|
||||||
('orders', '0002_initial'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='order',
|
|
||||||
name='applied_discount',
|
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='orders', to='discounts.discount', verbose_name='Примененная скидка'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='order',
|
|
||||||
name='applied_promo_code',
|
|
||||||
field=models.CharField(blank=True, max_length=50, null=True, verbose_name='Использованный промокод'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='order',
|
|
||||||
name='discount_amount',
|
|
||||||
field=models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Сумма скидки'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='orderitem',
|
|
||||||
name='applied_discount',
|
|
||||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='order_items', to='discounts.discount', verbose_name='Скидка на позицию'),
|
|
||||||
),
|
|
||||||
migrations.AddField(
|
|
||||||
model_name='orderitem',
|
|
||||||
name='discount_amount',
|
|
||||||
field=models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Сумма скидки'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
# Generated by Django 5.0.10 on 2026-01-11 10:39
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('orders', '0003_order_applied_discount_order_applied_promo_code_and_more'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='order',
|
|
||||||
name='applied_discount',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='order',
|
|
||||||
name='applied_promo_code',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='order',
|
|
||||||
name='discount_amount',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='orderitem',
|
|
||||||
name='applied_discount',
|
|
||||||
),
|
|
||||||
migrations.RemoveField(
|
|
||||||
model_name='orderitem',
|
|
||||||
name='discount_amount',
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.0.10 on 2026-01-08 15:56
|
# Generated by Django 5.0.10 on 2026-01-14 07:03
|
||||||
|
|
||||||
import django.utils.timezone
|
import django.utils.timezone
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|||||||
@@ -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.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 decimal import Decimal
|
||||||
from django.conf import settings
|
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
@@ -13,9 +12,9 @@ class Migration(migrations.Migration):
|
|||||||
initial = True
|
initial = True
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
('accounts', '0001_initial'),
|
||||||
('inventory', '0001_initial'),
|
('inventory', '0001_initial'),
|
||||||
('orders', '0001_initial'),
|
('orders', '0001_initial'),
|
||||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
operations = [
|
operations = [
|
||||||
@@ -112,10 +111,13 @@ class Migration(migrations.Migration):
|
|||||||
('description', models.TextField(blank=True, null=True, verbose_name='Описание')),
|
('description', models.TextField(blank=True, null=True, verbose_name='Описание')),
|
||||||
('short_description', models.TextField(blank=True, help_text='Используется для карточек товаров, превью и площадок', 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='Статус')),
|
('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='Дата создания')),
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
|
||||||
('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='Время архивирования')),
|
||||||
('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={
|
options={
|
||||||
'verbose_name': 'Вариативный товар',
|
'verbose_name': 'Вариативный товар',
|
||||||
@@ -196,6 +198,9 @@ class Migration(migrations.Migration):
|
|||||||
('description', models.TextField(blank=True, null=True, verbose_name='Описание')),
|
('description', models.TextField(blank=True, null=True, verbose_name='Описание')),
|
||||||
('short_description', models.TextField(blank=True, help_text='Используется для карточек товаров, превью и площадок', 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='Статус')),
|
('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='Дата создания')),
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
|
||||||
('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='Время архивирования')),
|
||||||
@@ -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='Цена со скидкой')),
|
('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='В наличии')),
|
('in_stock', models.BooleanField(db_index=True, default=False, help_text='Автоматически обновляется при изменении остатков на складе', verbose_name='В наличии')),
|
||||||
('search_keywords', models.TextField(blank=True, 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={
|
options={
|
||||||
'verbose_name': 'Товар',
|
'verbose_name': 'Товар',
|
||||||
@@ -290,7 +295,7 @@ class Migration(migrations.Migration):
|
|||||||
('updated_at', models.DateTimeField(auto_now=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='Удалена')),
|
('is_deleted', models.BooleanField(db_index=True, default=False, verbose_name='Удалена')),
|
||||||
('deleted_at', models.DateTimeField(blank=True, null=True, 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='Родительская категория')),
|
('parent', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='children', to='products.productcategory', verbose_name='Родительская категория')),
|
||||||
],
|
],
|
||||||
options={
|
options={
|
||||||
@@ -303,6 +308,16 @@ class Migration(migrations.Migration):
|
|||||||
name='categories',
|
name='categories',
|
||||||
field=models.ManyToManyField(blank=True, related_name='products', to='products.productcategory', verbose_name='Категории'),
|
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(
|
migrations.CreateModel(
|
||||||
name='ProductCategoryPhoto',
|
name='ProductCategoryPhoto',
|
||||||
fields=[
|
fields=[
|
||||||
@@ -341,7 +356,7 @@ class Migration(migrations.Migration):
|
|||||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Создано')),
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Создано')),
|
||||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Обновлено')),
|
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Обновлено')),
|
||||||
('completed_at', models.DateTimeField(blank=True, null=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={
|
options={
|
||||||
'verbose_name': 'Задача импорта товаров',
|
'verbose_name': 'Задача импорта товаров',
|
||||||
@@ -359,6 +374,9 @@ class Migration(migrations.Migration):
|
|||||||
('description', models.TextField(blank=True, null=True, verbose_name='Описание')),
|
('description', models.TextField(blank=True, null=True, verbose_name='Описание')),
|
||||||
('short_description', models.TextField(blank=True, help_text='Используется для карточек товаров, превью и площадок', 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='Статус')),
|
('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='Дата создания')),
|
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
|
||||||
('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='Время архивирования')),
|
||||||
@@ -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_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='Значение корректировки')),
|
('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='Временный комплект')),
|
('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='Категории')),
|
('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='Заказ')),
|
('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='Витрина')),
|
('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>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li><hr class="dropdown-divider"></li>
|
<li><hr class="dropdown-divider"></li>
|
||||||
|
{% if recommerce_integration_enabled %}
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item" href="#" id="bulk-recommerce-sync">
|
<a class="dropdown-item" href="#" id="bulk-recommerce-sync">
|
||||||
<i class="bi bi-arrow-repeat"></i> Синхронизация с Recommerce
|
<i class="bi bi-arrow-repeat"></i> Синхронизация с Recommerce
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
|
{% endif %}
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -595,4 +597,19 @@
|
|||||||
<script src="{% static 'products/js/batch-selection.js' %}?v=1.5"></script>
|
<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/bulk-category-modal.js' %}?v=1.6"></script>
|
||||||
<script src="{% static 'products/js/recommerce-sync.js' %}?v=1.2"></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 %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -411,6 +411,11 @@ class CombinedProductListView(LoginRequiredMixin, ManagerOwnerRequiredMixin, Lis
|
|||||||
from ..models.base import BaseProductEntity
|
from ..models.base import BaseProductEntity
|
||||||
item_statuses = BaseProductEntity.STATUS_CHOICES
|
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'] = {
|
context['filters'] = {
|
||||||
'categories': ProductCategory.objects.filter(is_active=True),
|
'categories': ProductCategory.objects.filter(is_active=True),
|
||||||
|
|||||||
@@ -1,30 +0,0 @@
|
|||||||
# Generated by Django 5.0.10 on 2026-01-11 19:36
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
initial = True
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.CreateModel(
|
|
||||||
name='IntegrationConfig',
|
|
||||||
fields=[
|
|
||||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
|
||||||
('integration_id', models.CharField(choices=[('woocommerce', 'WooCommerce')], max_length=50, unique=True, verbose_name='Интеграция')),
|
|
||||||
('is_enabled', models.BooleanField(default=False, help_text='Глобальное включение интеграции для тенанта', verbose_name='Включена')),
|
|
||||||
('last_sync_at', models.DateTimeField(blank=True, null=True, verbose_name='Последняя синхронизация')),
|
|
||||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
|
|
||||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Дата обновления')),
|
|
||||||
],
|
|
||||||
options={
|
|
||||||
'verbose_name': 'Настройка интеграции',
|
|
||||||
'verbose_name_plural': 'Настройки интеграций',
|
|
||||||
'ordering': ['integration_id'],
|
|
||||||
},
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
# Generated by Django 5.0.10 on 2026-01-11 21:41
|
|
||||||
|
|
||||||
from django.db import migrations
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('system_settings', '0001_initial'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.DeleteModel(
|
|
||||||
name='IntegrationConfig',
|
|
||||||
),
|
|
||||||
]
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
<!-- navbar.html - Компонент навигационной панели -->
|
<!-- navbar.html - Компонент навигационной панели (только для tenant схем) -->
|
||||||
|
{% if request.tenant %}
|
||||||
<style>
|
<style>
|
||||||
.navbar .dropdown:hover > .dropdown-menu {
|
.navbar .dropdown:hover > .dropdown-menu {
|
||||||
display: block;
|
display: block;
|
||||||
@@ -80,10 +81,16 @@
|
|||||||
<!-- ⚙️ Настройки (только для owner/superuser) -->
|
<!-- ⚙️ Настройки (только для owner/superuser) -->
|
||||||
{% if request.user.is_owner or request.user.is_superuser %}
|
{% if request.user.is_owner or request.user.is_superuser %}
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
|
{% if request.tenant %}
|
||||||
<a class="nav-link {% if request.resolver_match.namespace == 'system_settings' or 'user_roles' in request.resolver_match.app_names %}active{% endif %}"
|
<a class="nav-link {% if request.resolver_match.namespace == 'system_settings' or 'user_roles' in request.resolver_match.app_names %}active{% endif %}"
|
||||||
href="{% url 'system_settings:settings' %}">
|
href="{% url 'system_settings:settings' %}">
|
||||||
⚙️ Настройки
|
⚙️ Настройки
|
||||||
</a>
|
</a>
|
||||||
|
{% else %}
|
||||||
|
<a class="nav-link" href="/platform/dashboard">
|
||||||
|
⚙️ Настройки
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
@@ -125,3 +132,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
{% endif %}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# Generated by Django 5.0.10 on 2026-01-08 15:56
|
# Generated by Django 5.0.10 on 2026-01-14 07:03
|
||||||
|
|
||||||
import django.core.validators
|
import django.core.validators
|
||||||
import django.db.models.deletion
|
import django.db.models.deletion
|
||||||
|
|||||||
@@ -49,6 +49,13 @@ class RoleBasedPermissionBackend:
|
|||||||
4. Никакие данные из public schema не используются!
|
4. Никакие данные из public schema не используются!
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
def authenticate(self, request, username=None, password=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Этот backend НЕ выполняет аутентификацию.
|
||||||
|
Возвращает None, чтобы Django перешёл к следующему backend.
|
||||||
|
"""
|
||||||
|
return None
|
||||||
|
|
||||||
# Маппинг ролей на наборы разрешений
|
# Маппинг ролей на наборы разрешений
|
||||||
# Формат: 'app_label': ['action1', 'action2', ...]
|
# Формат: 'app_label': ['action1', 'action2', ...]
|
||||||
# где action - это префикс permission: add_product -> add, change_order -> change
|
# где action - это префикс permission: add_product -> add, change_order -> change
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
# 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.db.models.deletion
|
import django.db.models.deletion
|
||||||
from django.db import migrations, models
|
from django.db import migrations, models
|
||||||
@@ -17,7 +17,7 @@ class Migration(migrations.Migration):
|
|||||||
name='Role',
|
name='Role',
|
||||||
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')),
|
||||||
('code', models.CharField(choices=[('owner', 'Владелец'), ('manager', 'Менеджер'), ('florist', 'Флорист'), ('courier', 'Курьер')], max_length=20, unique=True, verbose_name='Код роли')),
|
('code', models.CharField(choices=[('platform_support', 'Техподдержка платформы'), ('owner', 'Владелец'), ('manager', 'Менеджер'), ('florist', 'Флорист'), ('courier', 'Курьер')], max_length=20, unique=True, verbose_name='Код роли')),
|
||||||
('name', models.CharField(max_length=100, verbose_name='Название')),
|
('name', models.CharField(max_length=100, verbose_name='Название')),
|
||||||
('description', models.TextField(blank=True, verbose_name='Описание')),
|
('description', models.TextField(blank=True, verbose_name='Описание')),
|
||||||
('is_system', models.BooleanField(default=True, help_text='Системные роли нельзя удалить', verbose_name='Системная роль')),
|
('is_system', models.BooleanField(default=True, help_text='Системные роли нельзя удалить', verbose_name='Системная роль')),
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
# Generated by Django 5.0.10 on 2026-01-11 09:53
|
|
||||||
|
|
||||||
from django.db import migrations, models
|
|
||||||
|
|
||||||
|
|
||||||
class Migration(migrations.Migration):
|
|
||||||
|
|
||||||
dependencies = [
|
|
||||||
('user_roles', '0001_initial'),
|
|
||||||
]
|
|
||||||
|
|
||||||
operations = [
|
|
||||||
migrations.AlterField(
|
|
||||||
model_name='role',
|
|
||||||
name='code',
|
|
||||||
field=models.CharField(choices=[('platform_support', 'Техподдержка платформы'), ('owner', 'Владелец'), ('manager', 'Менеджер'), ('florist', 'Флорист'), ('courier', 'Курьер')], max_length=20, unique=True, verbose_name='Код роли'),
|
|
||||||
),
|
|
||||||
]
|
|
||||||
Reference in New Issue
Block a user