Рефакторинг: отделение Delivery от Order, обязательные поля доставки, исправление доменов

- Отделена модель Delivery от Order (OneToOne связь)
- Добавлены обязательные поля delivery_date, time_from, time_to в Delivery
- Delivery обязательна при создании заказа (кроме черновиков)
- Добавлены методы calculate_total() и reset_delivery_cost() в Order
- Добавлена валидация полей доставки в OrderForm
- Исправлено создание доменов - убран порт из домена в БД
- Исправлен редирект после установки пароля (правильный формат URL)
- Исправлена ошибка NoReverseMatch в navbar для public схемы
- Удалены все старые миграции (база создается с нуля)
- Обновлены views для работы с новой моделью Delivery
This commit is contained in:
2025-12-23 23:52:59 +03:00
parent d29c736252
commit 94fe363cb1
61 changed files with 1342 additions and 2189 deletions

View File

@@ -1,7 +1,8 @@
# Generated by Django 5.0.10 on 2025-11-15 11:57
# Generated by Django 5.0.10 on 2025-12-23 20:38
import django.db.models.deletion
import simple_history.models
from decimal import Decimal
from django.conf import settings
from django.db import migrations, models
@@ -17,28 +18,54 @@ class Migration(migrations.Migration):
]
operations = [
migrations.CreateModel(
name='KitItemSnapshot',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('product_name', models.CharField(blank=True, max_length=200, verbose_name='Название товара')),
('product_sku', models.CharField(blank=True, max_length=100, verbose_name='Артикул товара')),
('product_price', models.DecimalField(decimal_places=2, default=Decimal('0'), max_digits=10, verbose_name='Цена товара')),
('variant_group_name', models.CharField(blank=True, max_length=200, verbose_name='Группа вариантов')),
('quantity', models.DecimalField(decimal_places=3, max_digits=10, verbose_name='Количество')),
],
options={
'verbose_name': 'Снимок компонента',
'verbose_name_plural': 'Снимки компонентов',
},
),
migrations.CreateModel(
name='KitSnapshot',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(max_length=200, verbose_name='Название')),
('sku', models.CharField(blank=True, max_length=100, verbose_name='Артикул')),
('description', models.TextField(blank=True, verbose_name='Описание')),
('base_price', models.DecimalField(decimal_places=2, default=Decimal('0'), max_digits=10, verbose_name='Базовая цена')),
('price', models.DecimalField(decimal_places=2, default=Decimal('0'), max_digits=10, verbose_name='Итоговая цена')),
('sale_price', models.DecimalField(blank=True, decimal_places=2, max_digits=10, null=True, verbose_name='Цена со скидкой')),
('price_adjustment_type', models.CharField(default='none', max_length=20, verbose_name='Тип корректировки')),
('price_adjustment_value', models.DecimalField(decimal_places=2, default=Decimal('0'), max_digits=10, verbose_name='Значение корректировки')),
('is_temporary', models.BooleanField(default=False, verbose_name='Временный комплект')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
],
options={
'verbose_name': 'Снимок комплекта',
'verbose_name_plural': 'Снимки комплектов',
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='Order',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('order_number', models.PositiveIntegerField(editable=False, help_text='Уникальный номер заказа', unique=True, verbose_name='Номер заказа')),
('is_delivery', models.BooleanField(default=True, help_text='True - доставка курьером, False - самовывоз', verbose_name='С доставкой')),
('delivery_date', models.DateField(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_cost', models.DecimalField(decimal_places=2, default=0, help_text='0 для самовывоза', max_digits=10, 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='Последнее автосохранение')),
('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='Оплачен')),
('total_amount', models.DecimalField(decimal_places=2, default=0, help_text='Общая сумма заказа включая доставку', max_digits=10, verbose_name='Итоговая сумма заказа')),
('discount_amount', models.DecimalField(decimal_places=2, default=0, help_text='Применяется вручную или через систему скидок', max_digits=10, verbose_name='Сумма скидки')),
('total_amount', models.DecimalField(decimal_places=2, default=0, help_text='Общая сумма заказа', max_digits=10, verbose_name='Итоговая сумма заказа')),
('amount_paid', models.DecimalField(decimal_places=2, default=0, help_text='Сумма, внесенная клиентом', max_digits=10, verbose_name='Оплачено')),
('payment_status', models.CharField(choices=[('unpaid', 'Не оплачен'), ('partial', 'Частично оплачен'), ('paid', 'Оплачен полностью')], default='unpaid', help_text='Обновляется автоматически при добавлении платежей', max_length=20, verbose_name='Статус оплаты')),
('customer_is_recipient', models.BooleanField(default=True, help_text='Если отмечено, данные получателя не требуются отдельно', verbose_name='Покупатель является получателем')),
('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='Телефон получателя')),
('is_anonymous', models.BooleanField(default=False, help_text='Не сообщать получателю имя отправителя', verbose_name='Анонимная доставка')),
('special_instructions', models.TextField(blank=True, help_text='Комментарии и пожелания к заказу', null=True, verbose_name='Особые пожелания')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
@@ -54,9 +81,12 @@ class Migration(migrations.Migration):
name='OrderItem',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('item_name_snapshot', models.CharField(default='', max_length=200, verbose_name='Название на момент заказа')),
('item_sku_snapshot', models.CharField(blank=True, max_length=100, verbose_name='Артикул на момент заказа')),
('quantity', models.PositiveIntegerField(default=1, verbose_name='Количество')),
('price', models.DecimalField(decimal_places=2, help_text='Цена на момент создания заказа (фиксируется)', max_digits=10, verbose_name='Цена за единицу')),
('is_custom_price', models.BooleanField(default=False, help_text='True если цена была изменена вручную при создании заказа', verbose_name='Цена изменена вручную')),
('is_from_showcase', models.BooleanField(default=False, help_text='True если товар продан с витрины', verbose_name='С витрины')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата добавления')),
],
options={
@@ -87,26 +117,59 @@ class Migration(migrations.Migration):
},
),
migrations.CreateModel(
name='Payment',
name='PaymentMethod',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('amount', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Сумма платежа')),
('payment_method', models.CharField(choices=[('cash_to_courier', 'Наличные курьеру'), ('card_to_courier', 'Карта курьеру'), ('online', 'Онлайн оплата'), ('bank_transfer', 'Банковский перевод')], max_length=20, verbose_name='Способ оплаты')),
('payment_date', models.DateTimeField(auto_now_add=True, verbose_name='Дата и время платежа')),
('notes', models.TextField(blank=True, help_text='Дополнительная информация о платеже', null=True, verbose_name='Примечания')),
('code', models.SlugField(help_text="Уникальный идентификатор (например: 'cash_to_courier', 'card_to_courier')", unique=True, verbose_name='Код способа оплаты')),
('name', models.CharField(max_length=100, verbose_name='Название способа оплаты')),
('description', models.TextField(blank=True, help_text='Дополнительная информация о способе оплаты', verbose_name='Описание')),
('is_active', models.BooleanField(default=True, help_text='Отключенные способы оплаты не отображаются при создании заказа', verbose_name='Активен')),
('order', models.PositiveIntegerField(default=0, verbose_name='Порядок отображения')),
('is_system', models.BooleanField(default=False, help_text='Системные способы оплаты нельзя удалить через интерфейс', verbose_name='Системный')),
('created_at', models.DateTimeField(auto_now_add=True)),
('updated_at', models.DateTimeField(auto_now=True)),
],
options={
'verbose_name': 'Платеж',
'verbose_name_plural': 'Платежи',
'ordering': ['-payment_date'],
'verbose_name': 'Способ оплаты',
'verbose_name_plural': 'Способы оплаты',
'ordering': ['order', 'name'],
},
),
migrations.CreateModel(
name='Recipient',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='ФИО или название организации получателя', max_length=200, verbose_name='Имя получателя')),
('phone', models.CharField(help_text='Контактный телефон для связи с получателем', max_length=20, 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'],
},
),
migrations.CreateModel(
name='Transaction',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('transaction_type', models.CharField(choices=[('payment', 'Платёж'), ('refund', 'Возврат')], default='payment', max_length=20, verbose_name='Тип транзакции')),
('amount', models.DecimalField(decimal_places=2, help_text="Всегда положительная. Для возврата используется transaction_type='refund'", max_digits=10, verbose_name='Сумма')),
('transaction_date', models.DateTimeField(auto_now_add=True, verbose_name='Дата и время транзакции')),
('notes', models.TextField(blank=True, null=True, verbose_name='Примечания')),
('reason', models.CharField(blank=True, help_text='Причина возврата или особенности платежа', max_length=255, null=True, verbose_name='Причина')),
],
options={
'verbose_name': 'Транзакция',
'verbose_name_plural': 'Транзакции',
'ordering': ['-transaction_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='Номер квартиры/офиса')),
@@ -125,28 +188,38 @@ class Migration(migrations.Migration):
'indexes': [models.Index(fields=['created_at'], name='orders_addr_created_98ad97_idx')],
},
),
migrations.CreateModel(
name='Delivery',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('delivery_type', models.CharField(choices=[('courier', 'Доставка курьером'), ('pickup', 'Самовывоз')], db_index=True, default='courier', max_length=20, verbose_name='Способ доставки')),
('delivery_date', models.DateField(help_text='Дата, когда должна быть выполнена доставка', verbose_name='Дата доставки')),
('time_from', models.TimeField(help_text='Начальное время временного интервала доставки', verbose_name='Время доставки от')),
('time_to', models.TimeField(help_text='Конечное время временного интервала доставки', verbose_name='Время доставки до')),
('cost', models.DecimalField(decimal_places=2, default=0, help_text='Стоимость доставки в рублях. 0 для бесплатной доставки/самовывоза', max_digits=10, verbose_name='Стоимость доставки')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Дата обновления')),
('address', models.ForeignKey(blank=True, help_text='Адрес для курьерской доставки. На один адрес может быть много доставок', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='deliveries', to='orders.address', verbose_name='Адрес доставки')),
('pickup_warehouse', models.ForeignKey(blank=True, help_text='Склад для самовывоза заказа', null=True, on_delete=django.db.models.deletion.PROTECT, related_name='deliveries', to='inventory.warehouse', verbose_name='Склад самовывоза')),
],
options={
'verbose_name': 'Доставка',
'verbose_name_plural': 'Доставки',
'ordering': ['-created_at'],
},
),
migrations.CreateModel(
name='HistoricalOrder',
fields=[
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('order_number', models.PositiveIntegerField(db_index=True, editable=False, help_text='Уникальный номер заказа', verbose_name='Номер заказа')),
('is_delivery', models.BooleanField(default=True, help_text='True - доставка курьером, False - самовывоз', verbose_name='С доставкой')),
('delivery_date', models.DateField(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_cost', models.DecimalField(decimal_places=2, default=0, help_text='0 для самовывоза', max_digits=10, 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='Последнее автосохранение')),
('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='Оплачен')),
('total_amount', models.DecimalField(decimal_places=2, default=0, help_text='Общая сумма заказа включая доставку', max_digits=10, verbose_name='Итоговая сумма заказа')),
('discount_amount', models.DecimalField(decimal_places=2, default=0, help_text='Применяется вручную или через систему скидок', max_digits=10, verbose_name='Сумма скидки')),
('total_amount', models.DecimalField(decimal_places=2, default=0, help_text='Общая сумма заказа', max_digits=10, verbose_name='Итоговая сумма заказа')),
('amount_paid', models.DecimalField(decimal_places=2, default=0, help_text='Сумма, внесенная клиентом', max_digits=10, verbose_name='Оплачено')),
('payment_status', models.CharField(choices=[('unpaid', 'Не оплачен'), ('partial', 'Частично оплачен'), ('paid', 'Оплачен полностью')], default='unpaid', help_text='Обновляется автоматически при добавлении платежей', max_length=20, verbose_name='Статус оплаты')),
('customer_is_recipient', models.BooleanField(default=True, help_text='Если отмечено, данные получателя не требуются отдельно', verbose_name='Покупатель является получателем')),
('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='Телефон получателя')),
('is_anonymous', models.BooleanField(default=False, help_text='Не сообщать получателю имя отправителя', verbose_name='Анонимная доставка')),
('special_instructions', models.TextField(blank=True, help_text='Комментарии и пожелания к заказу', null=True, verbose_name='Особые пожелания')),
('created_at', models.DateTimeField(blank=True, editable=False, verbose_name='Дата создания')),
@@ -156,10 +229,8 @@ class Migration(migrations.Migration):
('history_change_reason', models.CharField(max_length=100, null=True)),
('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='Клиент')),
('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)),
('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_warehouse', models.ForeignKey(blank=True, db_constraint=False, help_text='Обязательно для самовывоза', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='inventory.warehouse', verbose_name='Склад для самовывоза')),
],
options={
'verbose_name': 'historical Заказ',
@@ -173,9 +244,12 @@ class Migration(migrations.Migration):
name='HistoricalOrderItem',
fields=[
('id', models.BigIntegerField(auto_created=True, blank=True, db_index=True, verbose_name='ID')),
('item_name_snapshot', models.CharField(default='', max_length=200, verbose_name='Название на момент заказа')),
('item_sku_snapshot', models.CharField(blank=True, max_length=100, verbose_name='Артикул на момент заказа')),
('quantity', models.PositiveIntegerField(default=1, verbose_name='Количество')),
('price', models.DecimalField(decimal_places=2, help_text='Цена на момент создания заказа (фиксируется)', max_digits=10, verbose_name='Цена за единицу')),
('is_custom_price', models.BooleanField(default=False, help_text='True если цена была изменена вручную при создании заказа', verbose_name='Цена изменена вручную')),
('is_from_showcase', models.BooleanField(default=False, help_text='True если товар продан с витрины', verbose_name='С витрины')),
('created_at', models.DateTimeField(blank=True, editable=False, verbose_name='Дата добавления')),
('history_id', models.AutoField(primary_key=True, serialize=False)),
('history_date', models.DateTimeField(db_index=True)),