Исправление проблем с сохранением адреса, получателя и даты доставки

- Исправлено: адрес теперь сохраняется для черновиков заказов
- Исправлено: получатель корректно предзаполняется при редактировании заказа
- Исправлено: адрес при редактировании отображается в режиме 'новый' для возможности редактирования
- Исправлено: дата доставки корректно предзаполняется при редактировании заказа
- Исправлено: при редактировании получателя обновляется существующий объект вместо создания нового
- Улучшена логика обработки Delivery для черновиков (создание с опциональными полями)
- Улучшена логика обновления получателя через загрузку заказа из БД с select_related
This commit is contained in:
2025-12-25 00:30:27 +03:00
parent 98470c83af
commit 298d797286
5 changed files with 192 additions and 91 deletions

View File

@@ -303,6 +303,8 @@ class OrderForm(forms.ModelForm):
# Инициализируем поля доставки из существующей Delivery # Инициализируем поля доставки из существующей Delivery
if self.instance.pk and hasattr(self.instance, 'delivery'): if self.instance.pk and hasattr(self.instance, 'delivery'):
delivery = self.instance.delivery delivery = self.instance.delivery
# Устанавливаем значения через fields.initial для правильной работы виджетов
self.fields['delivery_type'].initial = delivery.delivery_type self.fields['delivery_type'].initial = delivery.delivery_type
self.fields['delivery_date'].initial = delivery.delivery_date self.fields['delivery_date'].initial = delivery.delivery_date
self.fields['time_from'].initial = delivery.time_from self.fields['time_from'].initial = delivery.time_from
@@ -310,6 +312,19 @@ class OrderForm(forms.ModelForm):
self.fields['pickup_warehouse'].initial = delivery.pickup_warehouse self.fields['pickup_warehouse'].initial = delivery.pickup_warehouse
self.fields['delivery_cost'].initial = delivery.cost self.fields['delivery_cost'].initial = delivery.cost
# Также устанавливаем через self.initial для совместимости
self.initial['delivery_type'] = delivery.delivery_type
self.initial['delivery_date'] = delivery.delivery_date
self.initial['time_from'] = delivery.time_from
self.initial['time_to'] = delivery.time_to
self.initial['pickup_warehouse'] = delivery.pickup_warehouse
self.initial['delivery_cost'] = delivery.cost
# Для DateInput с type='date' нужно установить значение в формате YYYY-MM-DD
if delivery.delivery_date:
# Устанавливаем значение напрямую в виджете
self.fields['delivery_date'].widget.attrs['value'] = delivery.delivery_date.strftime('%Y-%m-%d')
# Если выбран самовывоз, но склад не указан - выбираем склад по умолчанию # Если выбран самовывоз, но склад не указан - выбираем склад по умолчанию
if delivery.delivery_type == Delivery.DELIVERY_TYPE_PICKUP and not delivery.pickup_warehouse: if delivery.delivery_type == Delivery.DELIVERY_TYPE_PICKUP and not delivery.pickup_warehouse:
default_warehouse = Warehouse.objects.filter( default_warehouse = Warehouse.objects.filter(
@@ -322,38 +337,26 @@ class OrderForm(forms.ModelForm):
# Инициализируем поля адреса, если есть адрес доставки # Инициализируем поля адреса, если есть адрес доставки
if delivery.address: if delivery.address:
# Проверяем, есть ли этот адрес в истории клиента # При редактировании всегда используем режим "новый", чтобы можно было редактировать адрес
# Инициализируем queryset для истории адресов (для выбора из истории, если нужно)
if self.instance.customer: if self.instance.customer:
customer_addresses = Address.objects.filter( customer_addresses = Address.objects.filter(
deliveries__order__customer=self.instance.customer deliveries__order__customer=self.instance.customer
).exclude(
deliveries__order=self.instance # Исключаем адрес текущего заказа
).distinct() ).distinct()
if delivery.address in customer_addresses: self.fields['address_from_history'].queryset = customer_addresses
# Адрес есть в истории - используем режим "история"
self.fields['address_mode'].initial = 'history' # Всегда используем режим "новый" для редактирования, чтобы можно было редактировать
self.fields['address_from_history'].queryset = customer_addresses self.initial['address_mode'] = 'new'
self.fields['address_from_history'].initial = delivery.address.pk self.initial['address_street'] = delivery.address.street
else: self.initial['address_building_number'] = delivery.address.building_number
# Адреса нет в истории - используем режим "новый" и заполняем поля self.initial['address_apartment_number'] = delivery.address.apartment_number
self.fields['address_mode'].initial = 'new' self.initial['address_entrance'] = delivery.address.entrance
self.fields['address_street'].initial = delivery.address.street self.initial['address_floor'] = delivery.address.floor
self.fields['address_building_number'].initial = delivery.address.building_number self.initial['address_intercom_code'] = delivery.address.intercom_code
self.fields['address_apartment_number'].initial = delivery.address.apartment_number self.initial['address_delivery_instructions'] = delivery.address.delivery_instructions
self.fields['address_entrance'].initial = delivery.address.entrance self.initial['address_confirm_with_recipient'] = delivery.address.confirm_address_with_recipient
self.fields['address_floor'].initial = delivery.address.floor
self.fields['address_intercom_code'].initial = delivery.address.intercom_code
self.fields['address_delivery_instructions'].initial = delivery.address.delivery_instructions
self.fields['address_confirm_with_recipient'].initial = delivery.address.confirm_address_with_recipient
else:
# Нет клиента - просто заполняем поля
self.fields['address_mode'].initial = 'new'
self.fields['address_street'].initial = delivery.address.street
self.fields['address_building_number'].initial = delivery.address.building_number
self.fields['address_apartment_number'].initial = delivery.address.apartment_number
self.fields['address_entrance'].initial = delivery.address.entrance
self.fields['address_floor'].initial = delivery.address.floor
self.fields['address_intercom_code'].initial = delivery.address.intercom_code
self.fields['address_delivery_instructions'].initial = delivery.address.delivery_instructions
self.fields['address_confirm_with_recipient'].initial = delivery.address.confirm_address_with_recipient
def clean(self): def clean(self):
"""Валидация формы заказа, включая обязательные поля доставки""" """Валидация формы заказа, включая обязательные поля доставки"""

View File

@@ -121,6 +121,16 @@ class Delivery(models.Model):
"""Валидация модели""" """Валидация модели"""
super().clean() super().clean()
# Для черновиков пропускаем строгую валидацию
if self.order and self.order.status and hasattr(self.order.status, 'code') and self.order.status.code == 'draft':
# Для черновиков только проверяем время, если оно указано
if self.time_from and self.time_to and self.time_from >= self.time_to:
raise ValidationError({
'time_to': 'Время окончания доставки должно быть позже времени начала'
})
return
# Для не-черновиков полная валидация
# Проверка: для курьерской доставки должен быть адрес # Проверка: для курьерской доставки должен быть адрес
if self.delivery_type == self.DELIVERY_TYPE_COURIER: if self.delivery_type == self.DELIVERY_TYPE_COURIER:
if not self.address: if not self.address:

View File

@@ -95,7 +95,22 @@ class AddressService:
if not name or not phone: if not name or not phone:
return None return None
# Проверяем, есть ли уже такой получатель в БД # Если у заказа уже есть получатель - обновляем его вместо создания нового
# Загружаем заказ из БД, чтобы получить актуального получателя
if order.pk:
try:
# Order уже импортирован в начале файла
db_order = Order.objects.select_related('recipient').get(pk=order.pk)
if db_order.recipient:
# Обновляем существующего получателя
db_order.recipient.name = name
db_order.recipient.phone = phone
# Сохранять будем в views.py, здесь просто возвращаем объект
return db_order.recipient
except Order.DoesNotExist:
pass
# Проверяем, есть ли уже такой получатель в БД (по имени и телефону)
existing_recipient = Recipient.objects.filter( existing_recipient = Recipient.objects.filter(
name=name, name=name,
phone=phone phone=phone

View File

@@ -585,25 +585,6 @@
</div> </div>
</div> </div>
<!-- Дополнительно -->
<div class="card mb-3">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-three-dots"></i> Дополнительно</h5>
</div>
<div class="card-body">
<div class="mb-3 form-check">
{{ form.is_anonymous }}
<label class="form-check-label" for="{{ form.is_anonymous.id_for_label }}">
Анонимная доставка
</label>
</div>
<div class="mb-3">
<label for="{{ form.special_instructions.id_for_label }}" class="form-label">Особые пожелания</label>
{{ form.special_instructions }}
</div>
</div>
</div>
<!-- Получатель --> <!-- Получатель -->
<div class="card mb-3"> <div class="card mb-3">
<div class="card-header"> <div class="card-header">
@@ -613,7 +594,10 @@
<!-- Чекбокс "Другой получатель" --> <!-- Чекбокс "Другой получатель" -->
<div class="mb-3"> <div class="mb-3">
<div class="form-check"> <div class="form-check">
{{ form.other_recipient }} <input type="checkbox" class="form-check-input"
id="{{ form.other_recipient.id_for_label }}"
name="{{ form.other_recipient.html_name }}"
{% if form.other_recipient.value or form.other_recipient.field.initial %}checked{% endif %}>
<label class="form-check-label" for="{{ form.other_recipient.id_for_label }}"> <label class="form-check-label" for="{{ form.other_recipient.id_for_label }}">
{{ form.other_recipient.label }} {{ form.other_recipient.label }}
</label> </label>
@@ -683,6 +667,25 @@
</div> </div>
</div> </div>
</div> </div>
<!-- Дополнительно -->
<div class="card mb-3">
<div class="card-header">
<h5 class="mb-0"><i class="bi bi-three-dots"></i> Дополнительно</h5>
</div>
<div class="card-body">
<div class="mb-3 form-check">
{{ form.is_anonymous }}
<label class="form-check-label" for="{{ form.is_anonymous.id_for_label }}">
Анонимная доставка
</label>
</div>
<div class="mb-3">
<label for="{{ form.special_instructions.id_for_label }}" class="form-label">Особые пожелания</label>
{{ form.special_instructions }}
</div>
</div>
</div>
</form> </form>
</div> </div>
<!-- Конец левой колонки --> <!-- Конец левой колонки -->
@@ -935,6 +938,22 @@ document.addEventListener('DOMContentLoaded', function() {
<!-- Delivery Date/Time Widget --> <!-- Delivery Date/Time Widget -->
<script src="{% static 'orders/js/delivery_datetime.js' %}"></script> <script src="{% static 'orders/js/delivery_datetime.js' %}"></script>
<script>
// Убеждаемся, что дата доставки правильно отображается при редактировании
document.addEventListener('DOMContentLoaded', function() {
const deliveryDateField = document.getElementById('{{ form.delivery_date.id_for_label }}');
{% if form.delivery_date.initial %}
if (deliveryDateField && !deliveryDateField.value) {
// Если поле пустое, но есть initial значение, устанавливаем его
const initialDate = '{{ form.delivery_date.initial|date:"Y-m-d" }}';
if (initialDate && initialDate !== 'None') {
deliveryDateField.value = initialDate;
console.log('[Delivery Date] Установлена дата из initial:', initialDate);
}
}
{% endif %}
});
</script>
<!-- Unified Transaction Form --> <!-- Unified Transaction Form -->
<script src="{% static 'orders/js/unified_transaction_form.js' %}"></script> <script src="{% static 'orders/js/unified_transaction_form.js' %}"></script>
@@ -1099,8 +1118,17 @@ document.addEventListener('DOMContentLoaded', function() {
// Обработчики событий // Обработчики событий
if (otherRecipientCheckbox) { if (otherRecipientCheckbox) {
otherRecipientCheckbox.addEventListener('change', toggleOtherRecipientBlock); otherRecipientCheckbox.addEventListener('change', toggleOtherRecipientBlock);
// Инициализация при загрузке // Инициализация при загрузке - проверяем checked состояние
toggleOtherRecipientBlock(); // Используем DOMContentLoaded чтобы убедиться, что DOM полностью загружен
// Но также проверяем сразу, если DOM уже загружен
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
toggleOtherRecipientBlock();
});
} else {
// DOM уже загружен
toggleOtherRecipientBlock();
}
} }
recipientSourceRadios.forEach(radio => { recipientSourceRadios.forEach(radio => {

View File

@@ -121,30 +121,53 @@ def order_create(request):
# Проверяем, является ли заказ черновиком # Проверяем, является ли заказ черновиком
is_draft = order.status and order.status.code == 'draft' is_draft = order.status and order.status.code == 'draft'
# Создаем Delivery (обязательно, кроме черновиков) # Получаем данные из формы (уже провалидированы)
if not is_draft: delivery_type = form.cleaned_data.get('delivery_type')
# Получаем данные из формы (уже провалидированы) delivery_date = form.cleaned_data.get('delivery_date')
delivery_type = form.cleaned_data.get('delivery_type') time_from = form.cleaned_data.get('time_from')
delivery_date = form.cleaned_data.get('delivery_date') time_to = form.cleaned_data.get('time_to')
time_from = form.cleaned_data.get('time_from') delivery_cost = form.cleaned_data.get('delivery_cost', Decimal('0'))
time_to = form.cleaned_data.get('time_to') pickup_warehouse = form.cleaned_data.get('pickup_warehouse')
delivery_cost = form.cleaned_data.get('delivery_cost', Decimal('0'))
pickup_warehouse = form.cleaned_data.get('pickup_warehouse')
# Проверяем наличие обязательных полей (время необязательно) # Обрабатываем адрес для курьерской доставки (даже для черновиков, если указан)
address = None
if delivery_type == Delivery.DELIVERY_TYPE_COURIER:
# Для курьерской доставки обрабатываем адрес, если он указан
address = AddressService.process_address_from_form(order, form.cleaned_data)
if address and not address.pk:
address.save()
# Создаем или обновляем Delivery
# Для черновиков создаем Delivery без обязательных проверок, чтобы сохранить адрес
if is_draft:
# Для черновиков создаем Delivery, если есть хотя бы адрес или данные доставки
if address or delivery_type or pickup_warehouse:
# Для черновиков используем значения по умолчанию, если не указаны
from django.utils import timezone
draft_delivery_type = delivery_type or Delivery.DELIVERY_TYPE_COURIER
draft_delivery_date = delivery_date or timezone.now().date()
Delivery.objects.update_or_create(
order=order,
defaults={
'delivery_type': draft_delivery_type,
'delivery_date': draft_delivery_date,
'time_from': time_from,
'time_to': time_to,
'address': address,
'pickup_warehouse': pickup_warehouse,
'cost': delivery_cost if delivery_cost else Decimal('0')
}
)
else:
# Для не-черновиков проверяем обязательные поля
if not delivery_type or not delivery_date: if not delivery_type or not delivery_date:
raise ValidationError('Необходимо указать способ доставки и дату доставки') raise ValidationError('Необходимо указать способ доставки и дату доставки')
# Обрабатываем адрес для курьерской доставки
address = None
if delivery_type == Delivery.DELIVERY_TYPE_COURIER: if delivery_type == Delivery.DELIVERY_TYPE_COURIER:
# Для курьерской доставки нужен адрес # Для курьерской доставки нужен адрес
address = AddressService.process_address_from_form(order, form.cleaned_data)
if not address: if not address:
raise ValidationError('Для курьерской доставки необходимо указать адрес') raise ValidationError('Для курьерской доставки необходимо указать адрес')
if not address.pk:
address.save()
elif delivery_type == Delivery.DELIVERY_TYPE_PICKUP: elif delivery_type == Delivery.DELIVERY_TYPE_PICKUP:
# Для самовывоза нужен склад # Для самовывоза нужен склад
if not pickup_warehouse: if not pickup_warehouse:
@@ -259,9 +282,8 @@ def order_update(request, order_number):
# Обрабатываем получателя # Обрабатываем получателя
recipient = AddressService.process_recipient_from_form(order, form.cleaned_data) recipient = AddressService.process_recipient_from_form(order, form.cleaned_data)
if recipient: if recipient:
# Если получатель не существует в БД, сохраняем его # Сохраняем получателя: если новый - создаем, если существующий - обновляем
if not recipient.pk: recipient.save() # Django автоматически определит create или update
recipient.save()
order.recipient = recipient order.recipient = recipient
else: else:
# Если покупатель является получателем # Если покупатель является получателем
@@ -274,30 +296,56 @@ def order_update(request, order_number):
# Проверяем, является ли заказ черновиком # Проверяем, является ли заказ черновиком
is_draft = order.status and order.status.code == 'draft' is_draft = order.status and order.status.code == 'draft'
# Создаем или обновляем Delivery (обязательно, кроме черновиков) # Получаем данные из формы (уже провалидированы)
if not is_draft: delivery_type = form.cleaned_data.get('delivery_type')
# Получаем данные из формы (уже провалидированы) delivery_date = form.cleaned_data.get('delivery_date')
delivery_type = form.cleaned_data.get('delivery_type') time_from = form.cleaned_data.get('time_from')
delivery_date = form.cleaned_data.get('delivery_date') time_to = form.cleaned_data.get('time_to')
time_from = form.cleaned_data.get('time_from') delivery_cost = form.cleaned_data.get('delivery_cost', Decimal('0'))
time_to = form.cleaned_data.get('time_to') pickup_warehouse = form.cleaned_data.get('pickup_warehouse')
delivery_cost = form.cleaned_data.get('delivery_cost', Decimal('0'))
pickup_warehouse = form.cleaned_data.get('pickup_warehouse')
# Проверяем наличие обязательных полей (время необязательно) # Обрабатываем адрес для курьерской доставки (даже для черновиков, если указан)
address = None
if delivery_type == Delivery.DELIVERY_TYPE_COURIER:
# Для курьерской доставки обрабатываем адрес, если он указан
address = AddressService.process_address_from_form(order, form.cleaned_data)
if address and not address.pk:
address.save()
# Создаем или обновляем Delivery
# Для черновиков создаем Delivery без обязательных проверок, чтобы сохранить адрес
if is_draft:
# Для черновиков создаем Delivery, если есть хотя бы адрес или данные доставки
if address or delivery_type or pickup_warehouse:
# Для черновиков используем значения по умолчанию, если не указаны
from django.utils import timezone
draft_delivery_type = delivery_type or Delivery.DELIVERY_TYPE_COURIER
draft_delivery_date = delivery_date or timezone.now().date()
Delivery.objects.update_or_create(
order=order,
defaults={
'delivery_type': draft_delivery_type,
'delivery_date': draft_delivery_date,
'time_from': time_from,
'time_to': time_to,
'address': address,
'pickup_warehouse': pickup_warehouse,
'cost': delivery_cost if delivery_cost else Decimal('0')
}
)
elif hasattr(order, 'delivery'):
# Если заказ стал черновиком и нет данных доставки, удаляем Delivery
order.delivery.delete()
else:
# Для не-черновиков проверяем обязательные поля
if not delivery_type or not delivery_date: if not delivery_type or not delivery_date:
raise ValidationError('Необходимо указать способ доставки и дату доставки') raise ValidationError('Необходимо указать способ доставки и дату доставки')
# Обрабатываем адрес для курьерской доставки
address = None
if delivery_type == Delivery.DELIVERY_TYPE_COURIER: if delivery_type == Delivery.DELIVERY_TYPE_COURIER:
# Для курьерской доставки нужен адрес # Для курьерской доставки нужен адрес
address = AddressService.process_address_from_form(order, form.cleaned_data)
if not address: if not address:
raise ValidationError('Для курьерской доставки необходимо указать адрес') raise ValidationError('Для курьерской доставки необходимо указать адрес')
if not address.pk:
address.save()
elif delivery_type == Delivery.DELIVERY_TYPE_PICKUP: elif delivery_type == Delivery.DELIVERY_TYPE_PICKUP:
# Для самовывоза нужен склад # Для самовывоза нужен склад
if not pickup_warehouse: if not pickup_warehouse:
@@ -316,9 +364,6 @@ def order_update(request, order_number):
'cost': delivery_cost if delivery_cost else Decimal('0') 'cost': delivery_cost if delivery_cost else Decimal('0')
} }
) )
elif hasattr(order, 'delivery'):
# Если заказ стал черновиком, удаляем Delivery
order.delivery.delete()
# Пересчитываем итоговую стоимость # Пересчитываем итоговую стоимость
order.calculate_total() order.calculate_total()