Рефакторинг: отделение 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:
@@ -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()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user