Implement flexible order status management system

Features:
- Created OrderStatus model for managing statuses per tenant
- Added system-level statuses: draft, new, confirmed, in_assembly, in_delivery, completed, return, cancelled
- Implemented CRUD views for managing order statuses
- Created OrderStatusService with status transitions and business logic hooks
- Updated Order model to use ForeignKey to OrderStatus
- Added is_returned flag for tracking returned orders
- Updated filters to work with new OrderStatus model
- Created management command for status initialization
- Added HTML templates for status list, form, and confirmation
- Fixed views.py to use OrderStatus instead of removed STATUS_CHOICES

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-13 16:29:50 +03:00
parent 0d5f0d2015
commit c7875f147c
28 changed files with 1337 additions and 390 deletions

View File

@@ -4,7 +4,8 @@
"Bash(dir /b /s settings.py)", "Bash(dir /b /s settings.py)",
"Bash(git add:*)", "Bash(git add:*)",
"Bash(..venvScriptspython.exe manage.py check)", "Bash(..venvScriptspython.exe manage.py check)",
"Bash(python:*)" "Bash(python:*)",
"Bash(dir:*)"
], ],
"deny": [], "deny": [],
"ask": [] "ask": []

View File

@@ -1,4 +1,4 @@
# Generated by Django 5.0.10 on 2025-11-09 22:18 # Generated by Django 5.0.10 on 2025-11-13 13:12
import django.contrib.auth.validators import django.contrib.auth.validators
import django.utils.timezone import django.utils.timezone

View File

@@ -1,6 +1,5 @@
# Generated by Django 5.0.10 on 2025-11-09 22:18 # Generated by Django 5.0.10 on 2025-11-13 13:12
import django.db.models.deletion
import phonenumber_field.modelfields import phonenumber_field.modelfields
from django.db import migrations, models from django.db import migrations, models
@@ -18,7 +17,7 @@ class Migration(migrations.Migration):
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')),
('name', models.CharField(blank=True, max_length=200, verbose_name='Имя')), ('name', models.CharField(blank=True, max_length=200, verbose_name='Имя')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='Email')), ('email', models.EmailField(blank=True, max_length=254, null=True, unique=True, verbose_name='Email')),
('phone', phonenumber_field.modelfields.PhoneNumberField(blank=True, help_text='Введите телефон в любом формате, например: +375291234567, 80255270546, 8(029)1234567 - будет автоматически преобразован в международный формат', max_length=128, null=True, region=None, unique=True, verbose_name='Телефон')), ('phone', phonenumber_field.modelfields.PhoneNumberField(blank=True, help_text='Введите телефон в любом формате, например: +375291234567, 80255270546, 8(029)1234567 - будет автоматически преобразован в международный формат', max_length=128, null=True, region=None, unique=True, verbose_name='Телефон')),
('loyalty_tier', models.CharField(choices=[('no_discount', 'Без скидки'), ('bronze', 'Бронза'), ('silver', 'Серебро'), ('gold', 'Золото'), ('platinum', 'Платина')], default='no_discount', max_length=20, verbose_name='Уровень лояльности')), ('loyalty_tier', models.CharField(choices=[('no_discount', 'Без скидки'), ('bronze', 'Бронза'), ('silver', 'Серебро'), ('gold', 'Золото'), ('platinum', 'Платина')], default='no_discount', max_length=20, verbose_name='Уровень лояльности')),
('total_spent', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Общая сумма покупок')), ('total_spent', models.DecimalField(decimal_places=2, default=0, max_digits=10, verbose_name='Общая сумма покупок')),
@@ -33,28 +32,4 @@ class Migration(migrations.Migration):
'indexes': [models.Index(fields=['name'], name='customers_c_name_f018e2_idx'), models.Index(fields=['email'], name='customers_c_email_4fdeb3_idx'), models.Index(fields=['phone'], name='customers_c_phone_8493fa_idx'), models.Index(fields=['created_at'], name='customers_c_created_1ed0f4_idx'), models.Index(fields=['loyalty_tier'], name='customers_c_loyalty_5162a0_idx')], 'indexes': [models.Index(fields=['name'], name='customers_c_name_f018e2_idx'), models.Index(fields=['email'], name='customers_c_email_4fdeb3_idx'), models.Index(fields=['phone'], name='customers_c_phone_8493fa_idx'), models.Index(fields=['created_at'], name='customers_c_created_1ed0f4_idx'), models.Index(fields=['loyalty_tier'], name='customers_c_loyalty_5162a0_idx')],
}, },
), ),
migrations.CreateModel(
name='Address',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('recipient_name', models.CharField(help_text='Имя человека, которому будет доставлен заказ', max_length=200, verbose_name='Имя получателя')),
('recipient_phone', phonenumber_field.modelfields.PhoneNumberField(blank=True, help_text='Контактный телефон получателя для уточнения адреса', max_length=128, null=True, region=None, verbose_name='Телефон получателя')),
('street', models.CharField(max_length=255, verbose_name='Улица')),
('building_number', models.CharField(max_length=20, verbose_name='Номер здания')),
('apartment_number', models.CharField(blank=True, max_length=20, null=True, verbose_name='Номер квартиры/офиса')),
('district', models.CharField(blank=True, help_text='Район в Минске для удобства доставки', max_length=100, null=True, verbose_name='Район')),
('delivery_instructions', models.TextField(blank=True, help_text='Дополнительные инструкции для курьера (домофон, подъезд и т.д.)', null=True, verbose_name='Инструкции для доставки')),
('confirm_address_with_recipient', models.BooleanField(default=False, help_text='Курьер должен уточнить адрес у получателя перед доставкой', verbose_name='Уточнить адрес у получателя')),
('is_default', 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='Дата обновления')),
('customer', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='addresses', to='customers.customer', verbose_name='Клиент')),
],
options={
'verbose_name': 'Адрес доставки',
'verbose_name_plural': 'Адреса доставки',
'ordering': ['-is_default', '-created_at'],
'indexes': [models.Index(fields=['customer'], name='customers_a_custome_53b543_idx'), models.Index(fields=['is_default'], name='customers_a_is_defa_631851_idx'), models.Index(fields=['district'], name='customers_a_distric_ac47d5_idx')],
},
),
] ]

View File

@@ -1,17 +0,0 @@
# Generated by Django 5.0.10 on 2025-11-10 23:09
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('customers', '0001_initial'),
('orders', '0003_remove_address_model'),
]
operations = [
migrations.DeleteModel(
name='Address',
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.0.10 on 2025-11-11 14:34
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('customers', '0002_remove_address_model'),
]
operations = [
migrations.AlterField(
model_name='customer',
name='email',
field=models.EmailField(blank=True, max_length=254, null=True, verbose_name='Email'),
),
]

View File

@@ -1,18 +0,0 @@
# Generated by Django 5.0.10 on 2025-11-11 14:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('customers', '0003_alter_customer_email'),
]
operations = [
migrations.AlterField(
model_name='customer',
name='email',
field=models.EmailField(blank=True, max_length=254, null=True, unique=True, verbose_name='Email'),
),
]

View File

@@ -1,4 +1,4 @@
# Generated by Django 5.0.10 on 2025-11-09 22:18 # Generated by Django 5.0.10 on 2025-11-13 13:12
from django.db import migrations, models from django.db import migrations, models

View File

@@ -1,4 +1,4 @@
# Generated by Django 5.0.10 on 2025-11-09 22:18 # Generated by Django 5.0.10 on 2025-11-13 13:12
import django.db.models.deletion import django.db.models.deletion
from django.db import migrations, models from django.db import migrations, models

View File

@@ -6,7 +6,7 @@
import django_filters import django_filters
from django import forms from django import forms
from django.db.models import Q from django.db.models import Q
from .models import Order from .models import Order, OrderStatus
class OrderFilter(django_filters.FilterSet): class OrderFilter(django_filters.FilterSet):
@@ -74,8 +74,8 @@ class OrderFilter(django_filters.FilterSet):
) )
# Фильтр по статусу # Фильтр по статусу
status = django_filters.ChoiceFilter( status = django_filters.ModelChoiceFilter(
choices=Order.STATUS_CHOICES, queryset=OrderStatus.objects.all().order_by('order', 'name'),
empty_label='Все статусы', empty_label='Все статусы',
label='Статус', label='Статус',
widget=forms.Select(attrs={'class': 'form-select'}) widget=forms.Select(attrs={'class': 'form-select'})

View File

@@ -1,7 +1,7 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
from django import forms from django import forms
from django.forms import inlineformset_factory from django.forms import inlineformset_factory
from .models import Order, OrderItem, Address from .models import Order, OrderItem, Address, OrderStatus
from customers.models import Customer from customers.models import Customer
from shops.models import Shop from shops.models import Shop
from products.models import Product, ProductKit from products.models import Product, ProductKit
@@ -281,6 +281,81 @@ OrderItemFormSet = inlineformset_factory(
) )
# === СТАТУСЫ ЗАКАЗОВ ===
class OrderStatusForm(forms.ModelForm):
"""Форма для создания и редактирования статусов заказов"""
class Meta:
model = OrderStatus
fields = [
'name',
'code',
'label',
'color',
'description',
'is_positive_end',
'is_negative_end',
]
widgets = {
'name': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Например: Выполнен, В процессе'
}),
'code': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Например: completed, in_progress'
}),
'label': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': 'Метка для отображения (опционально)'
}),
'color': forms.TextInput(attrs={
'class': 'form-control',
'type': 'color'
}),
'description': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': 'Описание статуса (опционально)'
}),
'is_positive_end': forms.CheckboxInput(attrs={
'class': 'form-check-input'
}),
'is_negative_end': forms.CheckboxInput(attrs={
'class': 'form-check-input'
}),
}
def clean(self):
cleaned_data = super().clean()
# Нельзя быть одновременно положительным и отрицательным концом
if cleaned_data.get('is_positive_end') and cleaned_data.get('is_negative_end'):
raise forms.ValidationError(
"Статус не может быть одновременно положительным и отрицательным концом"
)
# Системные статусы нельзя редактировать код
if self.instance.pk and self.instance.is_system:
original_code = OrderStatus.objects.get(pk=self.instance.pk).code
new_code = cleaned_data.get('code')
if original_code != new_code:
raise forms.ValidationError(
"Нельзя менять код системного статуса"
)
return cleaned_data
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Если редактируем системный статус - делаем код readonly
if self.instance.pk and self.instance.is_system:
self.fields['code'].widget.attrs['readonly'] = True
self.fields['code'].help_text = "Код системного статуса нельзя менять"
# === ВРЕМЕННЫЕ КОМПЛЕКТЫ === # === ВРЕМЕННЫЕ КОМПЛЕКТЫ ===
class TemporaryKitForm(forms.ModelForm): class TemporaryKitForm(forms.ModelForm):

View File

@@ -0,0 +1,89 @@
"""
Management command для создания системных статусов заказов.
Использование:
python manage.py create_order_statuses
python manage.py create_order_statuses --reset
python manage.py create_order_statuses --schema grach
"""
from django.core.management.base import BaseCommand
from django.db import connection
from django_tenants.utils import get_tenant_model
from orders.services.order_status_service import OrderStatusService
from orders.models import OrderStatus
class Command(BaseCommand):
"""
Создает системные статусы заказов для текущего тенанта.
"""
help = 'Создает системные статусы заказов для текущего тенанта'
def add_arguments(self, parser):
"""Добавляем опциональные аргументы"""
parser.add_argument(
'--reset',
action='store_true',
help='Удалить все статусы перед созданием (осторожно!)',
)
parser.add_argument(
'--schema',
type=str,
help='Имя схемы тенанта (по умолчанию текущий тенант)',
)
def handle(self, *args, **options):
"""Основной обработчик команды"""
# Если указана схема, переходим на нее
schema_name = options.get('schema')
if schema_name:
Tenant = get_tenant_model()
try:
tenant = Tenant.objects.get(schema_name=schema_name)
connection.set_tenant(tenant)
self.stdout.write(
self.style.SUCCESS(f'Переключился на тенант: {tenant.name}')
)
except Tenant.DoesNotExist:
self.stdout.write(
self.style.ERROR(f'Тенант со схемой {schema_name} не найден')
)
return
if options['reset']:
count = OrderStatus.objects.count()
self.stdout.write(
self.style.WARNING(f'Удаляю {count} статусов...')
)
OrderStatus.objects.all().delete()
self.stdout.write('Создаю системные статусы...')
try:
OrderStatusService.create_default_statuses()
self.stdout.write(
self.style.SUCCESS('Системные статусы успешно созданы')
)
except Exception as e:
self.stdout.write(
self.style.ERROR(f'Ошибка при создании статусов: {str(e)}')
)
return
# Выводим информацию о созданных статусах
statuses = OrderStatus.objects.all().order_by('order')
self.stdout.write(
self.style.SUCCESS(f'\nВсего создано {statuses.count()} статусов:')
)
for status in statuses:
status_type = 'Системный' if status.is_system else 'Пользовательский'
end_type = ''
if status.is_positive_end:
end_type = ' [Успешный]'
elif status.is_negative_end:
end_type = ' [Отрицательный]'
self.stdout.write(
f' - {status.name:<20} ({status.code:<15}) - {status_type}{end_type}'
)

View File

@@ -1,4 +1,4 @@
# Generated by Django 5.0.10 on 2025-11-09 22:18 # Generated by Django 5.0.10 on 2025-11-13 13:12
import django.db.models.deletion import django.db.models.deletion
import simple_history.models import simple_history.models
@@ -17,6 +17,28 @@ class Migration(migrations.Migration):
] ]
operations = [ operations = [
migrations.CreateModel(
name='OrderStatus',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=100, verbose_name='Название статуса')),
('code', models.SlugField(help_text="Уникальный идентификатор (например: 'completed', 'cancelled')", unique=True, verbose_name='Код статуса')),
('label', models.CharField(blank=True, max_length=100, verbose_name='Метка для отображения')),
('is_system', models.BooleanField(default=False, help_text='True для встроенных статусов (draft, completed, cancelled)', verbose_name='Системный статус')),
('is_positive_end', models.BooleanField(default=False, help_text='True если это финальный успешный статус (Выполнен)', verbose_name='Положительный конец')),
('is_negative_end', models.BooleanField(default=False, help_text='True если это финальный отрицательный статус (Отменен)', verbose_name='Отрицательный конец')),
('order', models.PositiveIntegerField(default=0, verbose_name='Порядок отображения')),
('color', models.CharField(blank=True, default='#808080', help_text='Например: #FF5733', max_length=7, verbose_name='Цвет (hex)')),
('description', models.TextField(blank=True, verbose_name='Описание')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'verbose_name': 'Статус заказа',
'verbose_name_plural': 'Статусы заказов',
'ordering': ['order', 'name'],
},
),
migrations.CreateModel( migrations.CreateModel(
name='Payment', name='Payment',
fields=[ fields=[
@@ -32,6 +54,30 @@ class Migration(migrations.Migration):
'ordering': ['-payment_date'], 'ordering': ['-payment_date'],
}, },
), ),
migrations.CreateModel(
name='Address',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('recipient_name', models.CharField(blank=True, help_text='Имя человека, которому будет доставлен заказ', max_length=200, null=True, verbose_name='Имя получателя')),
('recipient_phone', models.CharField(blank=True, help_text='Контактный телефон получателя для уточнения адреса', max_length=20, null=True, verbose_name='Телефон получателя')),
('street', models.CharField(blank=True, max_length=255, null=True, verbose_name='Улица')),
('building_number', models.CharField(blank=True, max_length=20, null=True, verbose_name='Номер здания')),
('apartment_number', models.CharField(blank=True, max_length=20, null=True, verbose_name='Номер квартиры/офиса')),
('entrance', models.CharField(blank=True, help_text='Номер подъезда/входа', max_length=20, null=True, verbose_name='Подъезд')),
('floor', models.CharField(blank=True, max_length=20, null=True, verbose_name='Этаж')),
('intercom_code', models.CharField(blank=True, help_text='Код домофона для входа в здание', max_length=100, null=True, verbose_name='Код домофона')),
('delivery_instructions', models.TextField(blank=True, help_text='Дополнительные инструкции для курьера', null=True, verbose_name='Инструкции для доставки')),
('confirm_address_with_recipient', 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='Дата обновления')),
],
options={
'verbose_name': 'Адрес доставки',
'verbose_name_plural': 'Адреса доставки',
'ordering': ['-created_at'],
'indexes': [models.Index(fields=['created_at'], name='orders_addr_created_98ad97_idx')],
},
),
migrations.CreateModel( migrations.CreateModel(
name='HistoricalOrder', name='HistoricalOrder',
fields=[ fields=[
@@ -42,7 +88,8 @@ class Migration(migrations.Migration):
('delivery_time_start', models.TimeField(blank=True, help_text='Начало временного интервала', null=True, verbose_name='Время от')), ('delivery_time_start', models.TimeField(blank=True, help_text='Начало временного интервала', null=True, verbose_name='Время от')),
('delivery_time_end', models.TimeField(blank=True, help_text='Конец временного интервала', null=True, verbose_name='Время до')), ('delivery_time_end', models.TimeField(blank=True, help_text='Конец временного интервала', null=True, verbose_name='Время до')),
('delivery_cost', models.DecimalField(decimal_places=2, default=0, help_text='0 для самовывоза', max_digits=10, verbose_name='Стоимость доставки')), ('delivery_cost', models.DecimalField(decimal_places=2, default=0, help_text='0 для самовывоза', max_digits=10, verbose_name='Стоимость доставки')),
('status', models.CharField(choices=[('draft', 'Черновик'), ('new', 'Новый'), ('confirmed', 'Подтвержден'), ('in_assembly', 'В сборке'), ('in_delivery', 'В доставке'), ('delivered', 'Доставлен'), ('cancelled', 'Отменен')], default='new', max_length=20, verbose_name='Статус заказа')), ('is_custom_delivery_cost', models.BooleanField(default=False, help_text='True если стоимость доставки была изменена вручную', verbose_name='Стоимость доставки установлена вручную')),
('is_returned', models.BooleanField(default=False, help_text='True если заказ был выполнен, но потом отменен или возвращен клиентом', verbose_name='Возвращен')),
('last_autosave_at', models.DateTimeField(blank=True, help_text='Время последнего автоматического сохранения черновика', null=True, verbose_name='Последнее автосохранение')), ('last_autosave_at', models.DateTimeField(blank=True, help_text='Время последнего автоматического сохранения черновика', null=True, verbose_name='Последнее автосохранение')),
('payment_method', models.CharField(choices=[('cash_to_courier', 'Наличные курьеру'), ('card_to_courier', 'Карта курьеру'), ('online', 'Онлайн оплата'), ('bank_transfer', 'Банковский перевод')], default='cash_to_courier', max_length=20, verbose_name='Способ оплаты')), ('payment_method', models.CharField(choices=[('cash_to_courier', 'Наличные курьеру'), ('card_to_courier', 'Карта курьеру'), ('online', 'Онлайн оплата'), ('bank_transfer', 'Банковский перевод')], default='cash_to_courier', max_length=20, verbose_name='Способ оплаты')),
('is_paid', models.BooleanField(default=False, verbose_name='Оплачен')), ('is_paid', models.BooleanField(default=False, verbose_name='Оплачен')),
@@ -62,7 +109,7 @@ class Migration(migrations.Migration):
('history_change_reason', models.CharField(max_length=100, null=True)), ('history_change_reason', models.CharField(max_length=100, null=True)),
('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)), ('history_type', models.CharField(choices=[('+', 'Created'), ('~', 'Changed'), ('-', 'Deleted')], max_length=1)),
('customer', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='customers.customer', verbose_name='Клиент')), ('customer', models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='customers.customer', verbose_name='Клиент')),
('delivery_address', models.ForeignKey(blank=True, db_constraint=False, help_text='Обязательно для курьерской доставки', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='customers.address', verbose_name='Адрес доставки')), ('delivery_address', models.ForeignKey(blank=True, db_constraint=False, help_text='Обязательно для курьерской доставки', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='orders.address', verbose_name='Адрес доставки')),
('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)), ('history_user', models.ForeignKey(null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to=settings.AUTH_USER_MODEL)),
('modified_by', models.ForeignKey(blank=True, db_constraint=False, help_text='Последний пользователь, изменивший заказ', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='Изменен пользователем')), ('modified_by', models.ForeignKey(blank=True, db_constraint=False, help_text='Последний пользователь, изменивший заказ', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to=settings.AUTH_USER_MODEL, verbose_name='Изменен пользователем')),
('pickup_shop', models.ForeignKey(blank=True, db_constraint=False, help_text='Обязательно для самовывоза', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='shops.shop', verbose_name='Точка самовывоза')), ('pickup_shop', models.ForeignKey(blank=True, db_constraint=False, help_text='Обязательно для самовывоза', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='shops.shop', verbose_name='Точка самовывоза')),
@@ -85,7 +132,8 @@ class Migration(migrations.Migration):
('delivery_time_start', models.TimeField(blank=True, help_text='Начало временного интервала', null=True, verbose_name='Время от')), ('delivery_time_start', models.TimeField(blank=True, help_text='Начало временного интервала', null=True, verbose_name='Время от')),
('delivery_time_end', models.TimeField(blank=True, help_text='Конец временного интервала', null=True, verbose_name='Время до')), ('delivery_time_end', models.TimeField(blank=True, help_text='Конец временного интервала', null=True, verbose_name='Время до')),
('delivery_cost', models.DecimalField(decimal_places=2, default=0, help_text='0 для самовывоза', max_digits=10, verbose_name='Стоимость доставки')), ('delivery_cost', models.DecimalField(decimal_places=2, default=0, help_text='0 для самовывоза', max_digits=10, verbose_name='Стоимость доставки')),
('status', models.CharField(choices=[('draft', 'Черновик'), ('new', 'Новый'), ('confirmed', 'Подтвержден'), ('in_assembly', 'В сборке'), ('in_delivery', 'В доставке'), ('delivered', 'Доставлен'), ('cancelled', 'Отменен')], default='new', max_length=20, verbose_name='Статус заказа')), ('is_custom_delivery_cost', models.BooleanField(default=False, help_text='True если стоимость доставки была изменена вручную', verbose_name='Стоимость доставки установлена вручную')),
('is_returned', models.BooleanField(default=False, help_text='True если заказ был выполнен, но потом отменен или возвращен клиентом', verbose_name='Возвращен')),
('last_autosave_at', models.DateTimeField(blank=True, help_text='Время последнего автоматического сохранения черновика', null=True, verbose_name='Последнее автосохранение')), ('last_autosave_at', models.DateTimeField(blank=True, help_text='Время последнего автоматического сохранения черновика', null=True, verbose_name='Последнее автосохранение')),
('payment_method', models.CharField(choices=[('cash_to_courier', 'Наличные курьеру'), ('card_to_courier', 'Карта курьеру'), ('online', 'Онлайн оплата'), ('bank_transfer', 'Банковский перевод')], default='cash_to_courier', max_length=20, verbose_name='Способ оплаты')), ('payment_method', models.CharField(choices=[('cash_to_courier', 'Наличные курьеру'), ('card_to_courier', 'Карта курьеру'), ('online', 'Онлайн оплата'), ('bank_transfer', 'Банковский перевод')], default='cash_to_courier', max_length=20, verbose_name='Способ оплаты')),
('is_paid', models.BooleanField(default=False, verbose_name='Оплачен')), ('is_paid', models.BooleanField(default=False, verbose_name='Оплачен')),
@@ -101,7 +149,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='Дата обновления')),
('customer', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='orders', to='customers.customer', verbose_name='Клиент')), ('customer', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, related_name='orders', to='customers.customer', verbose_name='Клиент')),
('delivery_address', models.ForeignKey(blank=True, help_text='Обязательно для курьерской доставки', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='orders', to='customers.address', verbose_name='Адрес доставки')), ('delivery_address', models.OneToOneField(blank=True, help_text='Обязательно для курьерской доставки', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='order', to='orders.address', verbose_name='Адрес доставки')),
('modified_by', models.ForeignKey(blank=True, help_text='Последний пользователь, изменивший заказ', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='modified_orders', to=settings.AUTH_USER_MODEL, verbose_name='Изменен пользователем')), ('modified_by', models.ForeignKey(blank=True, help_text='Последний пользователь, изменивший заказ', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='modified_orders', to=settings.AUTH_USER_MODEL, verbose_name='Изменен пользователем')),
('pickup_shop', models.ForeignKey(blank=True, help_text='Обязательно для самовывоза', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='pickup_orders', to='shops.shop', verbose_name='Точка самовывоза')), ('pickup_shop', models.ForeignKey(blank=True, help_text='Обязательно для самовывоза', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='pickup_orders', to='shops.shop', verbose_name='Точка самовывоза')),
], ],

View File

@@ -1,4 +1,4 @@
# Generated by Django 5.0.10 on 2025-11-09 22:18 # Generated by Django 5.0.10 on 2025-11-13 13:12
import django.db.models.deletion import django.db.models.deletion
from django.conf import settings from django.conf import settings
@@ -26,6 +26,26 @@ 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='orderstatus',
name='created_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='created_order_statuses', to=settings.AUTH_USER_MODEL, verbose_name='Создано'),
),
migrations.AddField(
model_name='orderstatus',
name='updated_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='updated_order_statuses', to=settings.AUTH_USER_MODEL, verbose_name='Последнее изменение'),
),
migrations.AddField(
model_name='order',
name='status',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.PROTECT, related_name='orders', to='orders.orderstatus', verbose_name='Статус заказа'),
),
migrations.AddField(
model_name='historicalorder',
name='status',
field=models.ForeignKey(blank=True, db_constraint=False, null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='orders.orderstatus', verbose_name='Статус заказа'),
),
migrations.AddField( migrations.AddField(
model_name='payment', model_name='payment',
name='created_by', name='created_by',
@@ -36,13 +56,37 @@ class Migration(migrations.Migration):
name='order', name='order',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to='orders.order', verbose_name='Заказ'), field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='payments', to='orders.order', verbose_name='Заказ'),
), ),
migrations.AddIndex(
model_name='orderitem',
index=models.Index(fields=['order'], name='orders_orde_order_i_5d347b_idx'),
),
migrations.AddIndex(
model_name='orderitem',
index=models.Index(fields=['product'], name='orders_orde_product_32ff41_idx'),
),
migrations.AddIndex(
model_name='orderitem',
index=models.Index(fields=['product_kit'], name='orders_orde_product_925b51_idx'),
),
migrations.AddIndex(
model_name='orderstatus',
index=models.Index(fields=['code'], name='orders_orde_code_5e1ef7_idx'),
),
migrations.AddIndex(
model_name='orderstatus',
index=models.Index(fields=['is_system'], name='orders_orde_is_syst_2f5b85_idx'),
),
migrations.AddIndex(
model_name='orderstatus',
index=models.Index(fields=['order'], name='orders_orde_order_2e2930_idx'),
),
migrations.AddIndex( migrations.AddIndex(
model_name='order', model_name='order',
index=models.Index(fields=['customer'], name='orders_orde_custome_59b6fb_idx'), index=models.Index(fields=['customer'], name='orders_orde_custome_59b6fb_idx'),
), ),
migrations.AddIndex( migrations.AddIndex(
model_name='order', model_name='order',
index=models.Index(fields=['status'], name='orders_orde_status_c6dd84_idx'), index=models.Index(fields=['status'], name='orders_orde_status__eb4f00_idx'),
), ),
migrations.AddIndex( migrations.AddIndex(
model_name='order', model_name='order',
@@ -65,16 +109,8 @@ class Migration(migrations.Migration):
index=models.Index(fields=['order_number'], name='orders_orde_order_n_f3ada5_idx'), index=models.Index(fields=['order_number'], name='orders_orde_order_n_f3ada5_idx'),
), ),
migrations.AddIndex( migrations.AddIndex(
model_name='orderitem', model_name='order',
index=models.Index(fields=['order'], name='orders_orde_order_i_5d347b_idx'), index=models.Index(fields=['is_custom_delivery_cost'], name='orders_orde_is_cust_108e98_idx'),
),
migrations.AddIndex(
model_name='orderitem',
index=models.Index(fields=['product'], name='orders_orde_product_32ff41_idx'),
),
migrations.AddIndex(
model_name='orderitem',
index=models.Index(fields=['product_kit'], name='orders_orde_product_925b51_idx'),
), ),
migrations.AddIndex( migrations.AddIndex(
model_name='payment', model_name='payment',

View File

@@ -1,46 +0,0 @@
# Generated by Django 5.0.10 on 2025-11-10 23:09
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('orders', '0002_initial'),
]
operations = [
migrations.CreateModel(
name='Address',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('recipient_name', models.CharField(help_text='Имя человека, которому будет доставлен заказ', max_length=200, verbose_name='Имя получателя')),
('recipient_phone', models.CharField(blank=True, help_text='Контактный телефон получателя для уточнения адреса', max_length=20, null=True, verbose_name='Телефон получателя')),
('street', models.CharField(max_length=255, verbose_name='Улица')),
('building_number', models.CharField(max_length=20, verbose_name='Номер здания')),
('apartment_number', models.CharField(blank=True, max_length=20, null=True, verbose_name='Номер квартиры/офиса')),
('district', models.CharField(blank=True, help_text='Район в Минске для удобства доставки', max_length=100, null=True, verbose_name='Район')),
('delivery_instructions', models.TextField(blank=True, help_text='Дополнительные инструкции для курьера (домофон, подъезд и т.д.)', null=True, verbose_name='Инструкции для доставки')),
('confirm_address_with_recipient', 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='Дата обновления')),
],
options={
'verbose_name': 'Адрес доставки',
'verbose_name_plural': 'Адреса доставки',
'ordering': ['-created_at'],
'indexes': [models.Index(fields=['district'], name='orders_addr_distric_fd94e9_idx')],
},
),
migrations.AlterField(
model_name='historicalorder',
name='delivery_address',
field=models.ForeignKey(blank=True, db_constraint=False, help_text='Обязательно для курьерской доставки', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='orders.address', verbose_name='Адрес доставки'),
),
migrations.AlterField(
model_name='order',
name='delivery_address',
field=models.OneToOneField(blank=True, help_text='Обязательно для курьерской доставки', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='order', to='orders.address', verbose_name='Адрес доставки'),
),
]

View File

@@ -1,45 +0,0 @@
# Generated by Django 5.0.10 on 2025-11-10 23:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('orders', '0003_remove_address_model'),
]
operations = [
migrations.RemoveIndex(
model_name='address',
name='orders_addr_distric_fd94e9_idx',
),
migrations.RemoveField(
model_name='address',
name='district',
),
migrations.AddField(
model_name='address',
name='entrance',
field=models.CharField(blank=True, help_text='Номер подъезда/входа', max_length=20, null=True, verbose_name='Подъезд'),
),
migrations.AddField(
model_name='address',
name='floor',
field=models.CharField(blank=True, max_length=20, null=True, verbose_name='Этаж'),
),
migrations.AddField(
model_name='address',
name='intercom_code',
field=models.CharField(blank=True, help_text='Код домофона для входа в здание', max_length=100, null=True, verbose_name='Код домофона'),
),
migrations.AlterField(
model_name='address',
name='delivery_instructions',
field=models.TextField(blank=True, help_text='Дополнительные инструкции для курьера', null=True, verbose_name='Инструкции для доставки'),
),
migrations.AddIndex(
model_name='address',
index=models.Index(fields=['created_at'], name='orders_addr_created_98ad97_idx'),
),
]

View File

@@ -1,46 +0,0 @@
# Generated by Django 5.0.10 on 2025-11-11 09:52
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('customers', '0002_remove_address_model'),
('orders', '0004_remove_address_orders_addr_distric_fd94e9_idx_and_more'),
('shops', '0001_initial'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='historicalorder',
name='is_custom_delivery_cost',
field=models.BooleanField(default=False, help_text='True если стоимость доставки была изменена вручную', verbose_name='Стоимость доставки установлена вручную'),
),
migrations.AddField(
model_name='order',
name='is_custom_delivery_cost',
field=models.BooleanField(default=False, help_text='True если стоимость доставки была изменена вручную', verbose_name='Стоимость доставки установлена вручную'),
),
migrations.AlterField(
model_name='address',
name='building_number',
field=models.CharField(blank=True, max_length=20, null=True, verbose_name='Номер здания'),
),
migrations.AlterField(
model_name='address',
name='recipient_name',
field=models.CharField(blank=True, help_text='Имя человека, которому будет доставлен заказ', max_length=200, null=True, verbose_name='Имя получателя'),
),
migrations.AlterField(
model_name='address',
name='street',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Улица'),
),
migrations.AddIndex(
model_name='order',
index=models.Index(fields=['is_custom_delivery_cost'], name='orders_orde_is_cust_108e98_idx'),
),
]

View File

@@ -7,6 +7,99 @@ from shops.models import Shop
from simple_history.models import HistoricalRecords from simple_history.models import HistoricalRecords
class OrderStatus(models.Model):
"""
Статус заказа, управляется отдельно для каждого тенанта.
Благодаря django-tenants в TENANT_APPS, данные изолированы по схемам.
"""
name = models.CharField(
max_length=100,
verbose_name="Название статуса"
)
code = models.SlugField(
unique=True,
verbose_name="Код статуса",
help_text="Уникальный идентификатор (например: 'completed', 'cancelled')"
)
label = models.CharField(
max_length=100,
verbose_name="Метка для отображения",
blank=True
)
is_system = models.BooleanField(
default=False,
verbose_name="Системный статус",
help_text="True для встроенных статусов (draft, completed, cancelled)"
)
is_positive_end = models.BooleanField(
default=False,
verbose_name="Положительный конец",
help_text="True если это финальный успешный статус (Выполнен)"
)
is_negative_end = models.BooleanField(
default=False,
verbose_name="Отрицательный конец",
help_text="True если это финальный отрицательный статус (Отменен)"
)
order = models.PositiveIntegerField(
default=0,
verbose_name="Порядок отображения"
)
color = models.CharField(
max_length=7,
blank=True,
default='#808080',
verbose_name="Цвет (hex)",
help_text="Например: #FF5733"
)
description = models.TextField(
blank=True,
verbose_name="Описание"
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
created_by = models.ForeignKey(
CustomUser,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='created_order_statuses',
verbose_name="Создано"
)
updated_by = models.ForeignKey(
CustomUser,
on_delete=models.SET_NULL,
null=True,
blank=True,
related_name='updated_order_statuses',
verbose_name="Последнее изменение"
)
class Meta:
verbose_name = "Статус заказа"
verbose_name_plural = "Статусы заказов"
ordering = ['order', 'name']
indexes = [
models.Index(fields=['code']),
models.Index(fields=['is_system']),
models.Index(fields=['order']),
]
def __str__(self):
return self.name
class Address(models.Model): class Address(models.Model):
""" """
Модель адреса доставки для заказа цветочного магазина в Минске. Модель адреса доставки для заказа цветочного магазина в Минске.
@@ -233,23 +326,22 @@ class Order(models.Model):
) )
# Статус заказа # Статус заказа
STATUS_CHOICES = [ status = models.ForeignKey(
('draft', 'Черновик'), 'OrderStatus',
('new', 'Новый'), on_delete=models.PROTECT,
('confirmed', 'Подтвержден'), related_name='orders',
('in_assembly', 'В сборке'), null=True,
('in_delivery', 'В доставке'), blank=True,
('delivered', 'Доставлен'),
('cancelled', 'Отменен'),
]
status = models.CharField(
max_length=20,
choices=STATUS_CHOICES,
default='new',
verbose_name="Статус заказа" verbose_name="Статус заказа"
) )
# Флаг для отслеживания возвратов
is_returned = models.BooleanField(
default=False,
verbose_name="Возвращен",
help_text="True если заказ был выполнен, но потом отменен или возвращен клиентом"
)
# Автосохранение (для черновиков) # Автосохранение (для черновиков)
last_autosave_at = models.DateTimeField( last_autosave_at = models.DateTimeField(
null=True, null=True,
@@ -496,7 +588,7 @@ class Order(models.Model):
def is_draft(self): def is_draft(self):
"""Проверяет, является ли заказ черновиком""" """Проверяет, является ли заказ черновиком"""
return self.status == 'draft' return self.status and self.status.code == 'draft'
@property @property
def amount_due(self): def amount_due(self):

View File

@@ -0,0 +1,220 @@
"""
Сервис для управления статусами заказов.
Содержит бизнес-логику для работы со статусами и их переходами.
"""
from django.db import transaction
from orders.models import OrderStatus, Order
class OrderStatusService:
"""Сервис для работы со статусами заказов"""
@staticmethod
def get_default_status():
"""
Возвращает статус по умолчанию для новых заказов ('new')
"""
try:
return OrderStatus.objects.get(code='new', is_system=True)
except OrderStatus.DoesNotExist:
return None
@staticmethod
def get_draft_status():
"""Возвращает системный статус 'draft' (черновик)"""
try:
return OrderStatus.objects.get(code='draft', is_system=True)
except OrderStatus.DoesNotExist:
return None
@staticmethod
def get_system_status(code):
"""Получить системный статус по коду"""
try:
return OrderStatus.objects.get(code=code, is_system=True)
except OrderStatus.DoesNotExist:
return None
@staticmethod
def create_default_statuses():
"""
Создает системные статусы для тенанта.
Вызывается при первом использовании или миграции.
"""
default_statuses = [
{
'code': 'draft',
'name': 'Черновик',
'label': 'Черновик',
'is_system': True,
'order': 0,
'color': '#9E9E9E',
'description': 'Заказ находится в процессе создания/редактирования'
},
{
'code': 'new',
'name': 'Новый',
'label': 'Новый',
'is_system': True,
'order': 10,
'color': '#2196F3',
'description': 'Новый заказ, ожидающий обработки'
},
{
'code': 'confirmed',
'name': 'Подтвержден',
'label': 'Подтвержден',
'is_system': True,
'order': 20,
'color': '#FF9800',
'description': 'Заказ подтвержден и одобрен'
},
{
'code': 'in_assembly',
'name': 'В сборке',
'label': 'В сборке',
'is_system': True,
'order': 30,
'color': '#FF9800',
'description': 'Заказ находится в процессе сборки/подготовки'
},
{
'code': 'in_delivery',
'name': 'В доставке',
'label': 'В доставке',
'is_system': True,
'order': 40,
'color': '#9C27B0',
'description': 'Заказ в пути к клиенту'
},
{
'code': 'completed',
'name': 'Выполнен',
'label': 'Выполнен',
'is_system': True,
'is_positive_end': True,
'order': 50,
'color': '#4CAF50',
'description': 'Заказ успешно доставлен/выполнен'
},
{
'code': 'return',
'name': 'Возврат',
'label': 'Возврат',
'is_system': True,
'order': 60,
'color': '#FF5722',
'description': 'Заказ возвращен клиентом'
},
{
'code': 'cancelled',
'name': 'Отменен',
'label': 'Отменен',
'is_system': True,
'is_negative_end': True,
'order': 70,
'color': '#F44336',
'description': 'Заказ отменен'
},
]
for status_data in default_statuses:
OrderStatus.objects.get_or_create(
code=status_data['code'],
defaults=status_data
)
@staticmethod
@transaction.atomic
def change_order_status(order, new_status, user, notes=""):
"""
Меняет статус заказа и выполняет соответствующую бизнес-логику.
Args:
order (Order): Экземпляр заказа
new_status (OrderStatus): Новый статус
user (CustomUser): Пользователь, делающий изменение
notes (str): Опциональные заметки
Returns:
Order: Обновленный экземпляр заказа
Raises:
ValueError: Если статус не может быть применен
"""
old_status = order.status
order.status = new_status
order.modified_by = user
order.save()
# Запустить бизнес-логику в зависимости от нового статуса
if new_status.code == 'completed':
_handle_order_completion(order, user)
elif new_status.code == 'cancelled':
_handle_order_cancellation(order, old_status, user)
elif new_status.code == 'return':
_handle_order_return(order, user)
return order
@staticmethod
def get_all_statuses():
"""Возвращает все статусы, отсортированные по порядку"""
return OrderStatus.objects.all().order_by('order', 'name')
@staticmethod
def get_system_statuses():
"""Возвращает только системные статусы"""
return OrderStatus.objects.filter(is_system=True).order_by('order')
@staticmethod
def get_custom_statuses():
"""Возвращает только пользовательские статусы"""
return OrderStatus.objects.filter(is_system=False).order_by('order', 'name')
def _handle_order_completion(order, user):
"""
Обработка при переводе в статус 'Выполнен'.
Здесь происходит списание товаров со склада.
TODO: Интеграция с inventory приложением
"""
# from inventory.services import InventoryService
# InventoryService.process_order_completion(order)
pass
def _handle_order_cancellation(order, old_status, user):
"""
Обработка при переводе в статус 'Отменен'.
Если заказ был выполнен - возвращаем товары и деньги.
"""
if old_status and old_status.code == 'completed':
# Заказ был выполнен - нужно вернуть товары и деньги
order.is_returned = True
order.save()
# TODO: Интеграция с inventory - возврат товаров
# InventoryService.process_order_return(order)
# TODO: Интеграция с платежами - создать возврат
# PaymentService.create_refund(order)
else:
# Заказ отменен до выполнения - просто отменить резервы
# TODO: InventoryService.cancel_order_reservation(order)
pass
def _handle_order_return(order, user):
"""
Обработка при переводе в статус 'Возврат'.
Это промежуточный статус перед окончательной отменой.
"""
order.is_returned = True
order.save()
# TODO: Интеграция с inventory - вернуть товары на склад
# InventoryService.process_order_return(order)

View File

@@ -0,0 +1,129 @@
{% extends "base.html" %}
{% load static %}
{% block title %}Удалить статус{% endblock %}
{% block content %}
<div class="container-fluid mt-4">
<div class="row mb-4">
<div class="col-md-8">
<h1>Удалить статус</h1>
</div>
<div class="col-md-4 text-end">
<a href="{% url 'orders:status_list' %}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Вернуться к статусам
</a>
</div>
</div>
<div class="row">
<div class="col-md-8">
<div class="card border-danger">
<div class="card-header bg-danger text-white">
<h5 class="mb-0">
<i class="fas fa-exclamation-triangle"></i> Подтвердите удаление
</h5>
</div>
<div class="card-body">
<div class="alert alert-warning mb-4">
<i class="fas fa-exclamation-circle"></i>
<strong>Внимание!</strong> Это действие необратимо.
</div>
<p>Вы собираетесь удалить статус:</p>
<div class="mb-3">
<div class="card">
<div class="card-body">
<h5>
<span style="display: inline-block; width: 20px; height: 20px; background-color: {{ object.color }}; border-radius: 3px; margin-right: 10px; vertical-align: middle;"></span>
{{ object.name }}
</h5>
<p class="text-muted mb-2">
<strong>Код:</strong> <code>{{ object.code }}</code>
</p>
{% if object.description %}
<p class="mb-0">
<strong>Описание:</strong><br>
{{ object.description }}
</p>
{% endif %}
</div>
</div>
</div>
{% if orders_count > 0 %}
<div class="alert alert-danger">
<i class="fas fa-ban"></i>
<strong>Невозможно удалить!</strong>
В этом статусе находится {{ orders_count }} заказ(ов).
Пожалуйста, измените статус этих заказов перед удалением.
</div>
<a href="{% url 'orders:status_list' %}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Вернуться к статусам
</a>
{% else %}
<form method="post" class="mt-4">
{% csrf_token %}
<div class="d-flex gap-2">
<button type="submit" class="btn btn-danger">
<i class="fas fa-trash"></i> Да, удалить статус
</button>
<a href="{% url 'orders:status_list' %}" class="btn btn-secondary">
<i class="fas fa-times"></i> Отменить
</a>
</div>
</form>
{% endif %}
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header bg-light">
<h6 class="mb-0">Информация о статусе</h6>
</div>
<div class="card-body">
<dl class="row mb-0">
<dt class="col-sm-5">Название:</dt>
<dd class="col-sm-7">{{ object.name }}</dd>
<dt class="col-sm-5">Код:</dt>
<dd class="col-sm-7"><code>{{ object.code }}</code></dd>
<dt class="col-sm-5">Тип:</dt>
<dd class="col-sm-7">
{% if object.is_system %}
<span class="badge bg-info">Системный</span>
{% else %}
<span class="badge bg-success">Пользовательский</span>
{% endif %}
</dd>
<dt class="col-sm-5">Статус:</dt>
<dd class="col-sm-7">
{% if object.is_positive_end %}
<span class="badge bg-success">✓ Успешный</span>
{% elif object.is_negative_end %}
<span class="badge bg-danger">✗ Отрицательный</span>
{% else %}
<span class="badge bg-secondary">Промежуточный</span>
{% endif %}
</dd>
<dt class="col-sm-5">Заказов:</dt>
<dd class="col-sm-7"><span class="badge bg-light text-dark">{{ orders_count }}</span></dd>
<dt class="col-sm-5">Создано:</dt>
<dd class="col-sm-7"><small>{{ object.created_at|date:"d.m.Y H:i" }}</small></dd>
<dt class="col-sm-5">Изменено:</dt>
<dd class="col-sm-7"><small>{{ object.updated_at|date:"d.m.Y H:i" }}</small></dd>
</dl>
</div>
</div>
</div>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,295 @@
{% extends "base.html" %}
{% load static %}
{% block title %}{% if form.instance.pk %}Редактировать{% else %}Создать{% endif %} статус{% endblock %}
{% block content %}
<div class="container-fluid mt-4">
<div class="row mb-4">
<div class="col-md-8">
<h1>
{% if form.instance.pk %}
Редактировать статус: {{ form.instance.name }}
{% else %}
Создать новый статус
{% endif %}
</h1>
{% if is_system %}
<div class="alert alert-info mt-2">
<i class="fas fa-info-circle"></i> Это системный статус. Некоторые поля не могут быть изменены.
</div>
{% endif %}
</div>
<div class="col-md-4 text-end">
<a href="{% url 'orders:status_list' %}" class="btn btn-secondary">
<i class="fas fa-arrow-left"></i> Вернуться к статусам
</a>
</div>
</div>
{% if form.non_field_errors %}
<div class="alert alert-danger" role="alert">
<h4 class="alert-heading">Ошибка!</h4>
{% for error in form.non_field_errors %}
<p>{{ error }}</p>
{% endfor %}
</div>
{% endif %}
<div class="row">
<div class="col-md-8">
<div class="card">
<div class="card-body">
<form method="post" novalidate>
{% csrf_token %}
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.name.id_for_label }}" class="form-label">
{{ form.name.label }}
<span class="text-danger">*</span>
</label>
{{ form.name }}
{% if form.name.errors %}
<div class="invalid-feedback d-block">
{% for error in form.name.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
<small class="form-text text-muted">
Например: Выполнен, В процессе, Возврат
</small>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.code.id_for_label }}" class="form-label">
{{ form.code.label }}
<span class="text-danger">*</span>
</label>
{{ form.code }}
{% if form.code.errors %}
<div class="invalid-feedback d-block">
{% for error in form.code.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
<small class="form-text text-muted">
{% if form.code.field.help_text %}
{{ form.code.field.help_text|safe }}
{% else %}
Латинские буквы, цифры и подчеркивания
{% endif %}
</small>
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.label.id_for_label }}" class="form-label">
{{ form.label.label }}
</label>
{{ form.label }}
{% if form.label.errors %}
<div class="invalid-feedback d-block">
{% for error in form.label.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
<small class="form-text text-muted">
Для отображения в интерфейсе (опционально)
</small>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.color.id_for_label }}" class="form-label">
{{ form.color.label }}
</label>
<div class="input-group">
{{ form.color }}
<span class="input-group-text" id="color-preview" style="width: 60px; background-color: #808080;"></span>
</div>
{% if form.color.errors %}
<div class="invalid-feedback d-block">
{% for error in form.color.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
</div>
</div>
</div>
<div class="mb-3">
<label for="{{ form.description.id_for_label }}" class="form-label">
{{ form.description.label }}
</label>
{{ form.description }}
{% if form.description.errors %}
<div class="invalid-feedback d-block">
{% for error in form.description.errors %}
{{ error }}
{% endfor %}
</div>
{% endif %}
<small class="form-text text-muted">
Описание для пользователей (опционально)
</small>
</div>
<div class="row">
<div class="col-md-6">
<div class="form-check mb-3">
{{ form.is_positive_end }}
<label class="form-check-label" for="{{ form.is_positive_end.id_for_label }}">
{{ form.is_positive_end.label }}
</label>
<small class="d-block text-muted mt-1">
Отметьте, если это успешный финальный статус (Выполнен)
</small>
</div>
</div>
<div class="col-md-6">
<div class="form-check mb-3">
{{ form.is_negative_end }}
<label class="form-check-label" for="{{ form.is_negative_end.id_for_label }}">
{{ form.is_negative_end.label }}
</label>
<small class="d-block text-muted mt-1">
Отметьте, если это отрицательный финальный статус (Отменен)
</small>
</div>
</div>
</div>
<div class="d-flex gap-2 mt-4">
<button type="submit" class="btn btn-primary">
<i class="fas fa-save"></i>
{% if form.instance.pk %}
Сохранить изменения
{% else %}
Создать статус
{% endif %}
</button>
<a href="{% url 'orders:status_list' %}" class="btn btn-secondary">
<i class="fas fa-times"></i> Отменить
</a>
</div>
</form>
</div>
</div>
</div>
<div class="col-md-4">
<div class="card">
<div class="card-header bg-light">
<h6 class="mb-0">Предпросмотр</h6>
</div>
<div class="card-body">
<div class="mb-3">
<label class="form-label">Название статуса</label>
<p class="lead">
<span id="preview-name">{{ form.instance.name|default:"Название" }}</span>
</p>
</div>
<div class="mb-3">
<label class="form-label">Код статуса</label>
<code id="preview-code">{{ form.instance.code|default:"код" }}</code>
</div>
<div class="mb-3">
<label class="form-label">Внешний вид</label>
<div style="padding: 10px; border-radius: 4px; background-color: {{ form.instance.color|default:'#808080' }}; color: white; text-align: center;" id="preview-color">
<strong id="preview-color-text">{{ form.instance.name|default:"Статус" }}</strong>
</div>
</div>
<div class="mb-3">
<label class="form-label">Тип статуса</label>
<p id="preview-type">
{% if form.instance.is_system %}
<span class="badge bg-info">Системный</span>
{% else %}
<span class="badge bg-success">Пользовательский</span>
{% endif %}
</p>
</div>
<div class="mb-3">
<label class="form-label">Финальный статус</label>
<p id="preview-end">
{% if form.instance.is_positive_end %}
<span class="badge bg-success">✓ Успешный конец</span>
{% elif form.instance.is_negative_end %}
<span class="badge bg-danger">✗ Отрицательный конец</span>
{% else %}
<span class="badge bg-secondary">Промежуточный</span>
{% endif %}
</p>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const nameInput = document.getElementById('{{ form.name.id_for_label }}');
const codeInput = document.getElementById('{{ form.code.id_for_label }}');
const colorInput = document.getElementById('{{ form.color.id_for_label }}');
const positiveEndCheckbox = document.getElementById('{{ form.is_positive_end.id_for_label }}');
const negativeEndCheckbox = document.getElementById('{{ form.is_negative_end.id_for_label }}');
const previewName = document.getElementById('preview-name');
const previewCode = document.getElementById('preview-code');
const previewColor = document.getElementById('preview-color');
const previewColorText = document.getElementById('preview-color-text');
const colorPreview = document.getElementById('color-preview');
const previewEnd = document.getElementById('preview-end');
function updatePreview() {
// Обновляем название
previewName.textContent = nameInput.value || 'Название';
previewColorText.textContent = nameInput.value || 'Статус';
// Обновляем код
previewCode.textContent = codeInput.value || 'код';
// Обновляем цвет
const color = colorInput.value || '#808080';
previewColor.style.backgroundColor = color;
colorPreview.style.backgroundColor = color;
// Обновляем тип конца
if (positiveEndCheckbox.checked) {
previewEnd.innerHTML = '<span class="badge bg-success">✓ Успешный конец</span>';
} else if (negativeEndCheckbox.checked) {
previewEnd.innerHTML = '<span class="badge bg-danger">✗ Отрицательный конец</span>';
} else {
previewEnd.innerHTML = '<span class="badge bg-secondary">Промежуточный</span>';
}
}
nameInput.addEventListener('input', updatePreview);
codeInput.addEventListener('input', updatePreview);
colorInput.addEventListener('change', updatePreview);
positiveEndCheckbox.addEventListener('change', updatePreview);
negativeEndCheckbox.addEventListener('change', updatePreview);
// Инициальное обновление
updatePreview();
});
</script>
{% endblock %}

View File

@@ -0,0 +1,173 @@
{% extends "base.html" %}
{% load static %}
{% block title %}Статусы заказов{% endblock %}
{% block content %}
<div class="container-fluid mt-4">
<div class="row mb-4">
<div class="col-md-6">
<h1>Статусы заказов</h1>
<p class="text-muted">Управление статусами для заказов вашего магазина</p>
</div>
<div class="col-md-6 text-end">
<a href="{% url 'orders:status_create' %}" class="btn btn-primary">
<i class="fas fa-plus"></i> Создать новый статус
</a>
</div>
</div>
{% if messages %}
<div class="row">
<div class="col-md-12">
{% for message in messages %}
<div class="alert alert-{{ message.tags }} alert-dismissible fade show" role="alert">
{{ message }}
<button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button>
</div>
{% endfor %}
</div>
</div>
{% endif %}
<div class="row">
<div class="col-md-12">
<div class="card">
<div class="table-responsive">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th style="width: 50px;"></th>
<th style="width: 200px;">Название</th>
<th style="width: 150px;">Код</th>
<th style="width: 150px;">Тип</th>
<th style="width: 100px;">Конец</th>
<th style="width: 80px;">Цвет</th>
<th style="width: 80px;">Заказов</th>
<th style="width: 150px;">Действия</th>
</tr>
</thead>
<tbody>
{% for status in statuses %}
<tr>
<td>
<span class="badge bg-secondary">{{ status.order }}</span>
</td>
<td>
<strong>{{ status.name }}</strong>
{% if status.description %}
<br>
<small class="text-muted">{{ status.description|truncatewords:10 }}</small>
{% endif %}
</td>
<td>
<code>{{ status.code }}</code>
</td>
<td>
{% if status.is_system %}
<span class="badge bg-info">Системный</span>
{% else %}
<span class="badge bg-success">Пользовательский</span>
{% endif %}
</td>
<td>
{% if status.is_positive_end %}
<span class="badge bg-success">✓ Успешный</span>
{% elif status.is_negative_end %}
<span class="badge bg-danger">✗ Отрицательный</span>
{% else %}
<span class="badge bg-secondary"></span>
{% endif %}
</td>
<td>
<div style="width: 40px; height: 30px; background-color: {{ status.color }}; border-radius: 4px; border: 1px solid #ccc;" title="{{ status.color }}"></div>
</td>
<td>
<span class="badge bg-light text-dark">{{ status.orders_count }}</span>
</td>
<td>
<div class="btn-group btn-group-sm" role="group">
<a href="{% url 'orders:status_edit' status.pk %}" class="btn btn-outline-primary" title="Редактировать">
<i class="fas fa-edit"></i>
</a>
{% if not status.is_system and status.orders_count == 0 %}
<a href="{% url 'orders:status_delete' status.pk %}" class="btn btn-outline-danger" title="Удалить">
<i class="fas fa-trash"></i>
</a>
{% else %}
<button class="btn btn-outline-secondary" disabled title="{% if status.is_system %}Системный статус{% else %}В статусе есть заказы{% endif %}">
<i class="fas fa-trash"></i>
</button>
{% endif %}
{% if not forloop.first %}
<form method="post" action="{% url 'orders:status_move' status.pk %}" class="d-inline" style="display: none;" id="move-up-form-{{ status.pk }}">
{% csrf_token %}
<input type="hidden" name="direction" value="up">
</form>
<button class="btn btn-outline-secondary" onclick="document.getElementById('move-up-form-{{ status.pk }}').submit();" title="Вверх">
<i class="fas fa-arrow-up"></i>
</button>
{% endif %}
{% if not forloop.last %}
<form method="post" action="{% url 'orders:status_move' status.pk %}" class="d-inline" style="display: none;" id="move-down-form-{{ status.pk }}">
{% csrf_token %}
<input type="hidden" name="direction" value="down">
</form>
<button class="btn btn-outline-secondary" onclick="document.getElementById('move-down-form-{{ status.pk }}').submit();" title="Вниз">
<i class="fas fa-arrow-down"></i>
</button>
{% endif %}
</div>
</td>
</tr>
{% empty %}
<tr>
<td colspan="8" class="text-center text-muted py-4">
<i class="fas fa-inbox"></i> Статусы не найдены
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% if is_paginated %}
<nav aria-label="Page navigation" class="mt-3">
<ul class="pagination justify-content-center">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?page=1">Первая</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}">Предыдущая</a>
</li>
{% endif %}
{% for num in page_obj.paginator.page_range %}
{% if page_obj.number == num %}
<li class="page-item active">
<span class="page-link">{{ num }}</span>
</li>
{% else %}
<li class="page-item">
<a class="page-link" href="?page={{ num }}">{{ num }}</a>
</li>
{% endif %}
{% endfor %}
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}">Следующая</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}">Последняя</a>
</li>
{% endif %}
</ul>
</nav>
{% endif %}
</div>
</div>
</div>
{% endblock %}

View File

@@ -15,4 +15,10 @@ urlpatterns = [
path('<int:pk>/autosave/', views.autosave_draft_order, name='order-autosave'), path('<int:pk>/autosave/', views.autosave_draft_order, name='order-autosave'),
path('create-draft/', views.create_draft_from_form, name='order-create-draft'), path('create-draft/', views.create_draft_from_form, name='order-create-draft'),
path('api/customer-address-history/', views.get_customer_address_history, name='api-customer-address-history'), path('api/customer-address-history/', views.get_customer_address_history, name='api-customer-address-history'),
# Order Status Management URLs
path('statuses/', views.order_status_list, name='status_list'),
path('statuses/create/', views.order_status_create, name='status_create'),
path('statuses/<int:pk>/edit/', views.order_status_update, name='status_edit'),
path('statuses/<int:pk>/delete/', views.order_status_delete, name='status_delete'),
] ]

View File

@@ -6,8 +6,9 @@ from django.http import JsonResponse
from django.views.decorators.http import require_http_methods from django.views.decorators.http import require_http_methods
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from .models import Order, OrderItem, Address from django.db import models
from .forms import OrderForm, OrderItemFormSet from .models import Order, OrderItem, Address, OrderStatus
from .forms import OrderForm, OrderItemFormSet, OrderStatusForm
from .filters import OrderFilter from .filters import OrderFilter
from .services import DraftOrderService from .services import DraftOrderService
from .services.address_service import AddressService from .services.address_service import AddressService
@@ -38,7 +39,7 @@ def order_list(request):
context = { context = {
'filter': order_filter, 'filter': order_filter,
'page_obj': page_obj, 'page_obj': page_obj,
'status_choices': Order.STATUS_CHOICES, 'status_choices': OrderStatus.objects.all().order_by('order'),
} }
return render(request, 'orders/order_list.html', context) return render(request, 'orders/order_list.html', context)
@@ -79,7 +80,8 @@ def order_create(request):
# Если нажата кнопка "Сохранить как черновик", создаем черновик # Если нажата кнопка "Сохранить как черновик", создаем черновик
if 'save_as_draft' in request.POST: if 'save_as_draft' in request.POST:
order.status = 'draft' from .services.order_status_service import OrderStatusService
order.status = OrderStatusService.get_draft_status()
order.modified_by = request.user order.modified_by = request.user
order.save() order.save()
@@ -471,6 +473,121 @@ def get_customer_address_history(request):
}, status=500) }, status=500)
# === УПРАВЛЕНИЕ СТАТУСАМИ ЗАКАЗОВ ===
@login_required
def order_status_list(request):
"""Список всех статусов заказов"""
statuses = OrderStatus.objects.all().order_by('order', 'name')
# Добавляем информацию о количестве заказов для каждого статуса
for status in statuses:
status.orders_count = Order.objects.filter(status=status).count()
context = {
'statuses': statuses,
'title': 'Статусы заказов'
}
return render(request, 'orders/status_list.html', context)
@login_required
def order_status_create(request):
"""Создание нового статуса"""
if request.method == 'POST':
form = OrderStatusForm(request.POST)
if form.is_valid():
status = form.save(commit=False)
status.created_by = request.user
status.updated_by = request.user
# Если не указан порядок - делаем его последним
if not status.order:
max_order = OrderStatus.objects.aggregate(models.Max('order'))['order__max'] or 0
status.order = max_order + 10
status.save()
messages.success(request, f'Статус "{status.name}" успешно создан')
return redirect('orders:status_list')
else:
form = OrderStatusForm()
context = {
'form': form,
'title': 'Создать новый статус',
'button_text': 'Создать'
}
return render(request, 'orders/status_form.html', context)
@login_required
def order_status_update(request, pk):
"""Редактирование статуса"""
status = get_object_or_404(OrderStatus, pk=pk)
if request.method == 'POST':
form = OrderStatusForm(request.POST, instance=status)
if form.is_valid():
status = form.save(commit=False)
status.updated_by = request.user
status.save()
messages.success(request, f'Статус "{status.name}" успешно обновлен')
return redirect('orders:status_list')
else:
form = OrderStatusForm(instance=status)
context = {
'form': form,
'status': status,
'title': f'Редактировать статус: {status.name}',
'button_text': 'Сохранить',
'is_system': status.is_system
}
return render(request, 'orders/status_form.html', context)
@login_required
def order_status_delete(request, pk):
"""Удаление статуса"""
status = get_object_or_404(OrderStatus, pk=pk)
if status.is_system:
messages.error(request, f'Нельзя удалить системный статус "{status.name}"')
return redirect('orders:status_list')
if request.method == 'POST':
# Проверяем, что статус не используется в заказах
orders_count = Order.objects.filter(status=status).count()
if orders_count > 0:
messages.error(
request,
f'Невозможно удалить статус. Есть {orders_count} заказов с этим статусом.'
)
return redirect('orders:status_list')
status_name = status.name
status.delete()
messages.success(request, f'Статус "{status_name}" успешно удален')
return redirect('orders:status_list')
# Информация для подтверждения удаления
orders_count = Order.objects.filter(status=status).count()
context = {
'status': status,
'orders_count': orders_count,
'title': 'Удалить статус'
}
return render(request, 'orders/status_confirm_delete.html', context)
# === ВРЕМЕННЫЕ КОМПЛЕКТЫ === # === ВРЕМЕННЫЕ КОМПЛЕКТЫ ===
# УДАЛЕНО: Логика создания временных комплектов перенесена в products.services.kit_service # УДАЛЕНО: Логика создания временных комплектов перенесена в products.services.kit_service
# Используйте API endpoint: products:api-temporary-kit-create # Используйте API endpoint: products:api-temporary-kit-create

View File

@@ -1,4 +1,4 @@
# Generated by Django 5.0.10 on 2025-11-09 22:18 # Generated by Django 5.0.10 on 2025-11-13 13:12
import django.db.models.deletion import django.db.models.deletion
from django.conf import settings from django.conf import settings
@@ -177,15 +177,14 @@ 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')),
('name', models.CharField(max_length=100, unique=True, verbose_name='Название')), ('name', models.CharField(max_length=100, unique=True, verbose_name='Название')),
('slug', models.SlugField(max_length=100, unique=True, verbose_name='URL-идентификатор')), ('slug', models.SlugField(max_length=100, unique=True, verbose_name='URL-идентификатор')),
('is_active', models.BooleanField(db_index=True, default=True, verbose_name='Активен')),
('created_at', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Дата создания')), ('created_at', models.DateTimeField(auto_now_add=True, null=True, verbose_name='Дата создания')),
('updated_at', models.DateTimeField(auto_now=True, null=True, verbose_name='Дата обновления')), ('updated_at', models.DateTimeField(auto_now=True, null=True, verbose_name='Дата обновления')),
('is_deleted', models.BooleanField(db_index=True, default=False, verbose_name='Удален')),
('deleted_at', models.DateTimeField(blank=True, null=True, verbose_name='Время удаления')),
('deleted_by', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='deleted_tags', to=settings.AUTH_USER_MODEL, verbose_name='Удален пользователем')),
], ],
options={ options={
'verbose_name': 'Тег товара', 'verbose_name': 'Тег товара',
'verbose_name_plural': 'Теги товаров', 'verbose_name_plural': 'Теги товаров',
'indexes': [models.Index(fields=['is_active'], name='products_pr_is_acti_7f288f_idx')],
}, },
), ),
migrations.AddField( migrations.AddField(
@@ -294,14 +293,6 @@ class Migration(migrations.Migration):
model_name='productphoto', model_name='productphoto',
index=models.Index(fields=['quality_warning', 'product'], name='products_pr_quality_6e8b51_idx'), index=models.Index(fields=['quality_warning', 'product'], name='products_pr_quality_6e8b51_idx'),
), ),
migrations.AddIndex(
model_name='producttag',
index=models.Index(fields=['is_deleted'], name='products_pr_is_dele_ea9be0_idx'),
),
migrations.AddIndex(
model_name='producttag',
index=models.Index(fields=['is_deleted', 'created_at'], name='products_pr_is_dele_bc2d9c_idx'),
),
migrations.AddIndex( migrations.AddIndex(
model_name='productkit', model_name='productkit',
index=models.Index(fields=['is_temporary'], name='products_pr_is_temp_e407a2_idx'), index=models.Index(fields=['is_temporary'], name='products_pr_is_temp_e407a2_idx'),

View File

@@ -1,42 +0,0 @@
# Generated by Django 5.0.10 on 2025-11-11 18:31
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('products', '0001_initial'),
]
operations = [
migrations.RemoveIndex(
model_name='producttag',
name='products_pr_is_dele_ea9be0_idx',
),
migrations.RemoveIndex(
model_name='producttag',
name='products_pr_is_dele_bc2d9c_idx',
),
migrations.RemoveField(
model_name='producttag',
name='deleted_at',
),
migrations.RemoveField(
model_name='producttag',
name='deleted_by',
),
migrations.RemoveField(
model_name='producttag',
name='is_deleted',
),
migrations.AddField(
model_name='producttag',
name='is_active',
field=models.BooleanField(db_index=True, default=True, verbose_name='Активен'),
),
migrations.AddIndex(
model_name='producttag',
index=models.Index(fields=['is_active'], name='products_pr_is_acti_7f288f_idx'),
),
]

View File

@@ -1,4 +1,4 @@
# Generated by Django 5.0.10 on 2025-11-09 22:18 # Generated by Django 5.0.10 on 2025-11-13 13:12
import phonenumber_field.modelfields import phonenumber_field.modelfields
from django.db import migrations, models from django.db import migrations, models
@@ -17,20 +17,13 @@ class Migration(migrations.Migration):
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')),
('name', models.CharField(max_length=200, verbose_name='Название магазина')), ('name', models.CharField(max_length=200, verbose_name='Название магазина')),
('street', models.CharField(max_length=255, verbose_name='Улица')), ('description', models.TextField(blank=True, help_text='Дополнительная информация о магазине', null=True, verbose_name='Описание')),
('building_number', models.CharField(max_length=20, verbose_name='Номер здания')), ('street', models.CharField(blank=True, max_length=255, null=True, verbose_name='Улица')),
('district', models.CharField(blank=True, help_text='Район в Минске', max_length=100, null=True, verbose_name='Район')), ('building_number', models.CharField(blank=True, max_length=20, null=True, verbose_name='Номер здания')),
('phone', phonenumber_field.modelfields.PhoneNumberField(help_text='Контактный телефон магазина', max_length=128, region=None, verbose_name='Телефон')), ('phone', phonenumber_field.modelfields.PhoneNumberField(blank=True, help_text='Контактный телефон магазина', max_length=128, null=True, region=None, verbose_name='Телефон')),
('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='Email')), ('email', models.EmailField(blank=True, max_length=254, null=True, verbose_name='Email')),
('opening_time', models.TimeField(help_text='Время начала работы магазина', verbose_name='Время открытия')),
('closing_time', models.TimeField(help_text='Время окончания работы магазина', verbose_name='Время закрытия')),
('working_days', models.CharField(default='Пн-Вс', help_text='Например: Пн-Пт, Пн-Вс, Пн-Сб', max_length=100, verbose_name='Рабочие дни')),
('is_active', models.BooleanField(default=True, help_text='Работает ли магазин в данный момент', verbose_name='Активен')), ('is_active', models.BooleanField(default=True, help_text='Работает ли магазин в данный момент', verbose_name='Активен')),
('is_pickup_point', models.BooleanField(default=True, help_text='Доступен ли магазин для самовывоза заказов', verbose_name='Пункт самовывоза')), ('is_pickup_point', models.BooleanField(default=True, help_text='Доступен ли магазин для самовывоза заказов', verbose_name='Пункт самовывоза')),
('description', models.TextField(blank=True, help_text='Дополнительная информация о магазине', null=True, verbose_name='Описание')),
('delivery_instructions', models.TextField(blank=True, help_text='Как найти магазин, где припарковаться и т.д.', null=True, verbose_name='Инструкции для клиентов')),
('latitude', models.DecimalField(blank=True, decimal_places=6, help_text='Координаты для отображения на карте', max_digits=9, null=True, verbose_name='Широта')),
('longitude', models.DecimalField(blank=True, decimal_places=6, help_text='Координаты для отображения на карте', max_digits=9, null=True, 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='Дата обновления')),
], ],
@@ -38,7 +31,7 @@ class Migration(migrations.Migration):
'verbose_name': 'Магазин', 'verbose_name': 'Магазин',
'verbose_name_plural': 'Магазины', 'verbose_name_plural': 'Магазины',
'ordering': ['name'], 'ordering': ['name'],
'indexes': [models.Index(fields=['is_active'], name='shops_shop_is_acti_bbb154_idx'), models.Index(fields=['is_pickup_point'], name='shops_shop_is_pick_a04981_idx'), models.Index(fields=['district'], name='shops_shop_distric_04626c_idx')], 'indexes': [models.Index(fields=['is_active'], name='shops_shop_is_acti_bbb154_idx'), models.Index(fields=['is_pickup_point'], name='shops_shop_is_pick_a04981_idx')],
}, },
), ),
] ]

View File

@@ -1,61 +0,0 @@
# Generated by Django 5.0.10 on 2025-11-11 20:55
import phonenumber_field.modelfields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('shops', '0001_initial'),
]
operations = [
migrations.RemoveIndex(
model_name='shop',
name='shops_shop_distric_04626c_idx',
),
migrations.RemoveField(
model_name='shop',
name='closing_time',
),
migrations.RemoveField(
model_name='shop',
name='delivery_instructions',
),
migrations.RemoveField(
model_name='shop',
name='district',
),
migrations.RemoveField(
model_name='shop',
name='latitude',
),
migrations.RemoveField(
model_name='shop',
name='longitude',
),
migrations.RemoveField(
model_name='shop',
name='opening_time',
),
migrations.RemoveField(
model_name='shop',
name='working_days',
),
migrations.AlterField(
model_name='shop',
name='building_number',
field=models.CharField(blank=True, max_length=20, null=True, verbose_name='Номер здания'),
),
migrations.AlterField(
model_name='shop',
name='phone',
field=phonenumber_field.modelfields.PhoneNumberField(blank=True, help_text='Контактный телефон магазина', max_length=128, null=True, region=None, verbose_name='Телефон'),
),
migrations.AlterField(
model_name='shop',
name='street',
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Улица'),
),
]

View File

@@ -1,4 +1,4 @@
# Generated by Django 5.0.10 on 2025-11-09 22:18 # Generated by Django 5.0.10 on 2025-11-13 13:12
import django.core.validators import django.core.validators
import django.db.models.deletion import django.db.models.deletion