Рефакторинг: отделение 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,9 +1,8 @@
# -*- coding: utf-8 -*-
from django import forms
from django.forms import inlineformset_factory
from .models import Order, OrderItem, Transaction, Address, OrderStatus, Recipient
from .models import Order, OrderItem, Transaction, Address, OrderStatus, Recipient, Delivery
from customers.models import Customer
from inventory.models import Warehouse
from products.models import Product, ProductKit
from decimal import Decimal
@@ -123,17 +122,54 @@ class OrderForm(forms.ModelForm):
label='Уточнить адрес у получателя'
)
# Поля для доставки
delivery_type = forms.ChoiceField(
choices=Delivery.DELIVERY_TYPE_CHOICES,
required=True,
widget=forms.RadioSelect(attrs={'class': 'form-check-input'}),
label='Способ доставки',
initial=Delivery.DELIVERY_TYPE_COURIER
)
delivery_date = forms.DateField(
required=True,
widget=forms.DateInput(attrs={'class': 'form-control', 'type': 'date'}),
label='Дата доставки'
)
time_from = forms.TimeField(
required=True,
widget=forms.TimeInput(attrs={'class': 'form-control', 'type': 'time'}),
label='Время доставки от'
)
time_to = forms.TimeField(
required=True,
widget=forms.TimeInput(attrs={'class': 'form-control', 'type': 'time'}),
label='Время доставки до'
)
pickup_warehouse = forms.ModelChoiceField(
queryset=None, # Будет установлен в __init__
required=False,
widget=forms.Select(attrs={'class': 'form-select'}),
label='Склад самовывоза',
empty_label='Выберите склад'
)
delivery_cost = forms.DecimalField(
required=False,
max_digits=10,
decimal_places=2,
widget=forms.NumberInput(attrs={'class': 'form-control', 'step': '0.01', 'min': '0'}),
label='Стоимость доставки',
initial=0
)
class Meta:
model = Order
fields = [
'customer',
'is_delivery',
'delivery_address',
'pickup_warehouse',
'delivery_date',
'delivery_time_start',
'delivery_time_end',
'delivery_cost',
'customer_is_recipient',
'recipient',
'status',
@@ -141,9 +177,6 @@ class OrderForm(forms.ModelForm):
'special_instructions',
]
widgets = {
'delivery_date': forms.DateInput(attrs={'type': 'date'}, format='%Y-%m-%d'),
'delivery_time_start': forms.TimeInput(attrs={'type': 'time'}, format='%H:%M'),
'delivery_time_end': forms.TimeInput(attrs={'type': 'time'}, format='%H:%M'),
'special_instructions': forms.Textarea(attrs={'rows': 3}),
}
@@ -199,36 +232,12 @@ class OrderForm(forms.ModelForm):
'data-placeholder': 'Начните вводить имя, телефон или email'
})
self.fields['delivery_address'].widget.attrs.update({
'class': 'form-select select2',
'data-placeholder': 'Выберите адрес доставки'
})
# Адрес доставки не обязателен при редактировании (создаётся из отдельных полей)
self.fields['delivery_address'].required = False
self.fields['pickup_warehouse'].widget.attrs.update({
'class': 'form-select select2',
'data-placeholder': 'Выберите склад для самовывоза'
})
self.fields['pickup_warehouse'].required = False
# Опциональные поля даты/времени
self.fields['delivery_date'].required = False
self.fields['delivery_time_start'].required = False
self.fields['delivery_time_end'].required = False
# Подсказки
self.fields['is_delivery'].label = 'С доставкой'
self.fields['customer_is_recipient'].label = 'Покупатель = получатель'
# Поле получателя опционально
self.fields['recipient'].required = False
# Поле ручной стоимости доставки опционально
self.fields['delivery_cost'].required = False
self.fields['delivery_cost'].label = 'Ручная стоимость доставки'
self.fields['delivery_cost'].help_text = 'Оставьте пустым для автоматического расчета'
# Инициализируем queryset для recipient_from_history
if self.instance.pk and self.instance.customer:
# При редактировании заказа загружаем историю получателей этого клиента
@@ -240,62 +249,94 @@ class OrderForm(forms.ModelForm):
orders__in=customer_orders
).distinct().order_by('-created_at')
# Инициализируем queryset для address_from_history
# Это будет переопределено в представлении после выбора клиента
if self.instance.pk and self.instance.customer:
# При редактировании заказа загружаем историю адресов этого клиента
customer_orders = Order.objects.filter(
customer=self.instance.customer,
delivery_address__isnull=False
).order_by('-created_at')
self.fields['address_from_history'].queryset = Address.objects.filter(
orders__in=customer_orders
).distinct().order_by('-created_at')
# Инициализируем поля получателя из существующего recipient
if self.instance.pk and self.instance.recipient:
recipient = self.instance.recipient
self.fields['recipient_name'].initial = recipient.name or ''
self.fields['recipient_phone'].initial = recipient.phone or ''
# Инициализируем поля адреса из существующего delivery_address
if self.instance.pk and self.instance.delivery_address:
address = self.instance.delivery_address
self.fields['address_street'].initial = address.street or ''
self.fields['address_building_number'].initial = address.building_number or ''
self.fields['address_apartment_number'].initial = address.apartment_number or ''
self.fields['address_entrance'].initial = address.entrance or ''
self.fields['address_floor'].initial = address.floor or ''
self.fields['address_intercom_code'].initial = address.intercom_code or ''
self.fields['address_delivery_instructions'].initial = address.delivery_instructions or ''
self.fields['address_confirm_with_recipient'].initial = address.confirm_address_with_recipient
# Инициализируем queryset для pickup_warehouse
from inventory.models import Warehouse
self.fields['pickup_warehouse'].queryset = Warehouse.objects.filter(is_active=True).order_by('name')
# Инициализируем поля доставки из существующей Delivery
if self.instance.pk and hasattr(self.instance, 'delivery'):
delivery = self.instance.delivery
self.fields['delivery_type'].initial = delivery.delivery_type
self.fields['delivery_date'].initial = delivery.delivery_date
self.fields['time_from'].initial = delivery.time_from
self.fields['time_to'].initial = delivery.time_to
self.fields['pickup_warehouse'].initial = delivery.pickup_warehouse
self.fields['delivery_cost'].initial = delivery.cost
def clean(self):
"""Валидация формы заказа, включая обязательные поля доставки"""
cleaned_data = super().clean()
# Проверяем, является ли заказ черновиком
status = cleaned_data.get('status')
is_draft = status and hasattr(status, 'code') and status.code == 'draft'
# Для черновиков Delivery не обязательна
if is_draft:
return cleaned_data
# Для не-черновиков Delivery обязательна
delivery_type = cleaned_data.get('delivery_type')
delivery_date = cleaned_data.get('delivery_date')
time_from = cleaned_data.get('time_from')
time_to = cleaned_data.get('time_to')
pickup_warehouse = cleaned_data.get('pickup_warehouse')
# Проверяем обязательные поля доставки
if not delivery_type:
raise forms.ValidationError({'delivery_type': 'Необходимо выбрать способ доставки'})
if not delivery_date:
raise forms.ValidationError({'delivery_date': 'Необходимо указать дату доставки'})
if not time_from:
raise forms.ValidationError({'time_from': 'Необходимо указать время начала доставки'})
if not time_to:
raise forms.ValidationError({'time_to': 'Необходимо указать время окончания доставки'})
# Проверяем, что время "до" позже времени "от"
if time_from and time_to and time_from >= time_to:
raise forms.ValidationError({
'time_to': 'Время окончания доставки должно быть позже времени начала'
})
# Проверяем специфичные требования для каждого типа доставки
if delivery_type == Delivery.DELIVERY_TYPE_COURIER:
# Для курьерской доставки нужен адрес
address_mode = cleaned_data.get('address_mode')
address_from_history = cleaned_data.get('address_from_history')
address_street = cleaned_data.get('address_street', '').strip()
has_address = (
(address_mode == 'history' and address_from_history) or
(address_mode == 'new' and address_street)
)
if not has_address:
raise forms.ValidationError({
'address_mode': 'Для курьерской доставки необходимо указать адрес'
})
elif delivery_type == Delivery.DELIVERY_TYPE_PICKUP:
# Для самовывоза нужен склад
if not pickup_warehouse:
raise forms.ValidationError({
'pickup_warehouse': 'Для самовывоза необходимо выбрать склад'
})
return cleaned_data
def save(self, commit=True):
"""
Сохраняет форму с учетом автоматического/ручного расчета стоимости доставки.
Логика:
- Если delivery_cost заполнено → используется ручное значение (is_custom_delivery_cost = True)
- Если delivery_cost пустое → автоматический расчет (is_custom_delivery_cost = False)
ВАЖНО: reset_delivery_cost() вызывается только при commit=True,
т.к. требует наличия сохраненных items в БД.
"""
"""Сохраняет форму заказа."""
instance = super().save(commit=False)
# Получаем значение ручной стоимости доставки
delivery_cost = self.cleaned_data.get('delivery_cost')
if delivery_cost is not None and delivery_cost > 0:
# Ручное значение указано
instance.set_delivery_cost(delivery_cost, is_custom=True)
else:
# Пустое поле или 0 → помечаем что нужен автоматический расчет
# НО не вызываем reset_delivery_cost() если commit=False!
instance.is_custom_delivery_cost = False
if commit:
# Автоматический расчет только при commit=True
instance.reset_delivery_cost()
if commit:
instance.save()