Упрощение системы получателей доставки

- Удалено избыточное поле customer_is_recipient из модели Order
- Добавлено свойство @property is_customer_recipient для обратной совместимости
- Заменены радиокнопки recipient_mode на чекбокс 'Другой получатель' в форме
- Добавлено поле recipient_source для выбора между историей и новым получателем
- Обновлен AddressService.process_recipient_from_form() для работы с чекбоксом
- Обновлены шаблоны: order_form.html (чекбокс вместо радиокнопок) и order_detail.html
- Удалено customer_is_recipient из admin и demo команды
- Создана миграция для удаления поля customer_is_recipient

Логика упрощена: recipient is None = получатель = покупатель, иначе - отдельный получатель
This commit is contained in:
2025-12-24 17:54:57 +03:00
parent 9f4f03e340
commit d62caa924b
9 changed files with 168 additions and 82 deletions

View File

@@ -86,9 +86,9 @@ class OrderAdmin(admin.ModelAdmin):
}), }),
('Получатель', { ('Получатель', {
'fields': ( 'fields': (
'customer_is_recipient',
'recipient', 'recipient',
) ),
'description': 'Если получатель не указан, получателем является покупатель'
}), }),
('Оплата', { ('Оплата', {
'fields': ( 'fields': (

View File

@@ -11,16 +11,24 @@ class OrderForm(forms.ModelForm):
"""Форма для создания и редактирования заказа""" """Форма для создания и редактирования заказа"""
# Поля для работы с получателем # Поля для работы с получателем
recipient_mode = forms.ChoiceField( other_recipient = forms.BooleanField(
required=False,
initial=False,
widget=forms.CheckboxInput(attrs={'class': 'form-check-input'}),
label='Другой получатель',
help_text='Если отмечено, получатель отличается от покупателя'
)
# Режим выбора другого получателя (история или новый)
recipient_source = forms.ChoiceField(
choices=[ choices=[
('customer', 'Покупатель является получателем'),
('history', 'Выбрать из истории'), ('history', 'Выбрать из истории'),
('new', 'Другой получатель'), ('new', 'Новый получатель'),
], ],
initial='customer', initial='new',
widget=forms.RadioSelect(attrs={'class': 'form-check-input'}), widget=forms.RadioSelect(attrs={'class': 'form-check-input'}),
required=False, required=False,
label='Получатель' label='Способ указания получателя'
) )
# Выбор получателя из истории # Выбор получателя из истории
@@ -170,7 +178,6 @@ class OrderForm(forms.ModelForm):
model = Order model = Order
fields = [ fields = [
'customer', 'customer',
'customer_is_recipient',
'recipient', 'recipient',
'status', 'status',
'is_anonymous', 'is_anonymous',
@@ -232,28 +239,47 @@ class OrderForm(forms.ModelForm):
'data-placeholder': 'Начните вводить имя, телефон или email' 'data-placeholder': 'Начните вводить имя, телефон или email'
}) })
# Подсказки
self.fields['customer_is_recipient'].label = 'Покупатель = получатель'
# Поле получателя опционально # Поле получателя опционально
self.fields['recipient'].required = False self.fields['recipient'].required = False
# Инициализируем чекбокс other_recipient на основе наличия recipient
if self.instance.pk:
# При редактировании заказа: если есть recipient, чекбокс включен
if self.instance.recipient:
self.fields['other_recipient'].initial = True
# Определяем источник получателя
# Проверяем, есть ли этот recipient в истории клиента
if self.instance.customer:
customer_orders = Order.objects.filter(
customer=self.instance.customer,
recipient__isnull=False
).exclude(pk=self.instance.pk).order_by('-created_at')
history_recipients = Recipient.objects.filter(
orders__in=customer_orders
).distinct()
if self.instance.recipient in history_recipients:
self.fields['recipient_source'].initial = 'history'
self.fields['recipient_from_history'].initial = self.instance.recipient.pk
else:
self.fields['recipient_source'].initial = 'new'
self.fields['recipient_name'].initial = self.instance.recipient.name or ''
self.fields['recipient_phone'].initial = self.instance.recipient.phone or ''
else:
self.fields['other_recipient'].initial = False
# Инициализируем queryset для recipient_from_history # Инициализируем queryset для recipient_from_history
if self.instance.pk and self.instance.customer: if self.instance.pk and self.instance.customer:
# При редактировании заказа загружаем историю получателей этого клиента # При редактировании заказа загружаем историю получателей этого клиента
customer_orders = Order.objects.filter( customer_orders = Order.objects.filter(
customer=self.instance.customer, customer=self.instance.customer,
recipient__isnull=False recipient__isnull=False
).order_by('-created_at') ).exclude(pk=self.instance.pk).order_by('-created_at')
self.fields['recipient_from_history'].queryset = Recipient.objects.filter( self.fields['recipient_from_history'].queryset = Recipient.objects.filter(
orders__in=customer_orders orders__in=customer_orders
).distinct().order_by('-created_at') ).distinct().order_by('-created_at')
elif not self.instance.pk:
# Инициализируем поля получателя из существующего recipient # При создании нового заказа queryset пустой (будет заполнен через JS при выборе клиента)
if self.instance.pk and self.instance.recipient: self.fields['recipient_from_history'].queryset = Recipient.objects.none()
recipient = self.instance.recipient
self.fields['recipient_name'].initial = recipient.name or ''
self.fields['recipient_phone'].initial = recipient.phone or ''
# Инициализируем queryset для pickup_warehouse # Инициализируем queryset для pickup_warehouse
from inventory.models import Warehouse from inventory.models import Warehouse

View File

@@ -132,7 +132,6 @@ class Command(BaseCommand):
# Дополнительная информация # Дополнительная информация
if random.random() > 0.7: # 30% - подарок другому человеку if random.random() > 0.7: # 30% - подарок другому человеку
order.customer_is_recipient = False
# Создаем получателя # Создаем получателя
recipient_name = f"Получатель {i+1}" recipient_name = f"Получатель {i+1}"
recipient_phone = f"+7{random.randint(9000000000, 9999999999)}" recipient_phone = f"+7{random.randint(9000000000, 9999999999)}"

View File

@@ -0,0 +1,21 @@
# Generated by Django 5.0.10 on 2025-12-24 10:51
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('orders', '0002_initial'),
]
operations = [
migrations.RemoveField(
model_name='historicalorder',
name='customer_is_recipient',
),
migrations.RemoveField(
model_name='order',
name='customer_is_recipient',
),
]

View File

@@ -94,13 +94,7 @@ class Order(models.Model):
) )
# Информация о получателе # Информация о получателе
customer_is_recipient = models.BooleanField( # Получатель (если None - получатель = покупатель, иначе - отдельный получатель)
default=True,
verbose_name="Покупатель является получателем",
help_text="Если отмечено, данные получателя не требуются отдельно"
)
# Получатель (если покупатель != получатель)
recipient = models.ForeignKey( recipient = models.ForeignKey(
Recipient, Recipient,
on_delete=models.SET_NULL, on_delete=models.SET_NULL,
@@ -324,3 +318,8 @@ class Order(models.Model):
if hasattr(self, 'delivery') and self.delivery: if hasattr(self, 'delivery') and self.delivery:
return self.delivery.pickup_warehouse return self.delivery.pickup_warehouse
return None return None
@property
def is_customer_recipient(self):
"""Является ли покупатель получателем (обратная совместимость)"""
return self.recipient is None

View File

@@ -38,4 +38,4 @@ class Recipient(models.Model):
@property @property
def display_name(self): def display_name(self):
"""Форматированное имя для отображения""" """Форматированное имя для отображения"""
return f"{self.name} - {self.phone}" return f"Получатель{self.name} - {self.phone}"

View File

@@ -70,14 +70,16 @@ class AddressService:
Returns: Returns:
Recipient or None: Получатель для привязки к заказу или None Recipient or None: Получатель для привязки к заказу или None
""" """
recipient_mode = form_data.get('recipient_mode') # Если чекбокс "Другой получатель" не включен - возвращаем None (получатель = покупатель)
other_recipient = form_data.get('other_recipient', False)
# Если режим "покупатель = получатель" - возвращаем None if not other_recipient:
if recipient_mode == 'customer':
return None return None
# Определяем источник получателя
recipient_source = form_data.get('recipient_source', 'new')
# Если режим "выбрать из истории" - возвращаем выбранного получателя # Если режим "выбрать из истории" - возвращаем выбранного получателя
if recipient_mode == 'history': if recipient_source == 'history':
recipient_id = form_data.get('recipient_from_history') recipient_id = form_data.get('recipient_from_history')
if recipient_id: if recipient_id:
try: try:
@@ -86,7 +88,7 @@ class AddressService:
return None return None
# Если режим "новый получатель" # Если режим "новый получатель"
if recipient_mode == 'new': if recipient_source == 'new':
name = form_data.get('recipient_name', '').strip() name = form_data.get('recipient_name', '').strip()
phone = form_data.get('recipient_phone', '').strip() phone = form_data.get('recipient_phone', '').strip()

View File

@@ -131,7 +131,7 @@
</div> </div>
<!-- Получатель --> <!-- Получатель -->
{% if not order.customer_is_recipient and order.recipient %} {% if order.recipient %}
<div class="card mb-3"> <div class="card mb-3">
<div class="card-header"> <div class="card-header">
<h5 class="mb-0">Получатель</h5> <h5 class="mb-0">Получатель</h5>

View File

@@ -532,54 +532,74 @@
<div class="border-top pt-3 mt-3"> <div class="border-top pt-3 mt-3">
<h6 class="mb-3">Получатель</h6> <h6 class="mb-3">Получатель</h6>
<!-- Режимы выбора получателя --> <!-- Чекбокс "Другой получатель" -->
<div class="mb-3"> <div class="mb-3">
{% for choice in form.recipient_mode %}
<div class="form-check"> <div class="form-check">
{{ choice.tag }} {{ form.other_recipient }}
<label class="form-check-label" for="{{ choice.id_for_label }}"> <label class="form-check-label" for="{{ form.other_recipient.id_for_label }}">
{{ choice.choice_label }} {{ form.other_recipient.label }}
</label> </label>
{% if form.other_recipient.help_text %}
<small class="form-text text-muted">{{ form.other_recipient.help_text }}</small>
{% endif %}
</div> </div>
{% endfor %} {% if form.other_recipient.errors %}
{% if form.recipient_mode.errors %} <div class="text-danger">{{ form.other_recipient.errors }}</div>
<div class="text-danger">{{ form.recipient_mode.errors }}</div>
{% endif %} {% endif %}
</div> </div>
<!-- Выбор получателя из истории --> <!-- Блок для другого получателя (показывается при включенном чекбоксе) -->
<div class="mb-3" id="recipient-history-field" style="display: none;"> <div id="other-recipient-block" style="display: none;">
<label for="{{ form.recipient_from_history.id_for_label }}" class="form-label"> <!-- Режим выбора получателя (история или новый) -->
{{ form.recipient_from_history.label }} <div class="mb-3">
</label> <label class="form-label d-block mb-2">{{ form.recipient_source.label }}</label>
{{ form.recipient_from_history }} {% for choice in form.recipient_source %}
{% if form.recipient_from_history.errors %} <div class="form-check">
<div class="text-danger">{{ form.recipient_from_history.errors }}</div> {{ choice.tag }}
{% endif %} <label class="form-check-label" for="{{ choice.id_for_label }}">
</div> {{ choice.choice_label }}
<!-- Поля нового получателя -->
<div class="row" id="recipient-new-fields" style="display: none;">
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.recipient_name.id_for_label }}" class="form-label">
Имя получателя
</label> </label>
{{ form.recipient_name }}
{% if form.recipient_name.errors %}
<div class="text-danger">{{ form.recipient_name.errors }}</div>
{% endif %}
</div> </div>
{% endfor %}
{% if form.recipient_source.errors %}
<div class="text-danger">{{ form.recipient_source.errors }}</div>
{% endif %}
</div> </div>
<div class="col-md-6">
<div class="mb-3"> <!-- Выбор получателя из истории -->
<label for="{{ form.recipient_phone.id_for_label }}" class="form-label"> <div class="mb-3" id="recipient-history-field" style="display: none;">
Телефон получателя <label for="{{ form.recipient_from_history.id_for_label }}" class="form-label">
</label> {{ form.recipient_from_history.label }}
{{ form.recipient_phone }} </label>
{% if form.recipient_phone.errors %} {{ form.recipient_from_history }}
<div class="text-danger">{{ form.recipient_phone.errors }}</div> {% if form.recipient_from_history.errors %}
{% endif %} <div class="text-danger">{{ form.recipient_from_history.errors }}</div>
{% endif %}
</div>
<!-- Поля нового получателя -->
<div class="row" id="recipient-new-fields" style="display: none;">
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.recipient_name.id_for_label }}" class="form-label">
Имя получателя
</label>
{{ form.recipient_name }}
{% if form.recipient_name.errors %}
<div class="text-danger">{{ form.recipient_name.errors }}</div>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.recipient_phone.id_for_label }}" class="form-label">
Телефон получателя
</label>
{{ form.recipient_phone }}
{% if form.recipient_phone.errors %}
<div class="text-danger">{{ form.recipient_phone.errors }}</div>
{% endif %}
</div>
</div> </div>
</div> </div>
</div> </div>
@@ -971,31 +991,50 @@ document.addEventListener('DOMContentLoaded', function() {
syncUIFromCheckbox(); syncUIFromCheckbox();
// Показ/скрытие полей получателя // Показ/скрытие полей получателя
const recipientModeRadios = document.querySelectorAll('input[name="recipient_mode"]'); const otherRecipientCheckbox = document.getElementById('{{ form.other_recipient.id_for_label }}');
const otherRecipientBlock = document.getElementById('other-recipient-block');
const recipientSourceRadios = document.querySelectorAll('input[name="recipient_source"]');
const recipientHistoryField = document.getElementById('recipient-history-field'); const recipientHistoryField = document.getElementById('recipient-history-field');
const recipientNewFields = document.getElementById('recipient-new-fields'); const recipientNewFields = document.getElementById('recipient-new-fields');
function toggleRecipientFields() { function toggleOtherRecipientBlock() {
const selectedMode = document.querySelector('input[name="recipient_mode"]:checked'); if (otherRecipientCheckbox.checked) {
if (!selectedMode) return; otherRecipientBlock.style.display = 'block';
toggleRecipientSourceFields();
} else {
otherRecipientBlock.style.display = 'none';
recipientHistoryField.style.display = 'none';
recipientNewFields.style.display = 'none';
}
}
const mode = selectedMode.value; function toggleRecipientSourceFields() {
const selectedSource = document.querySelector('input[name="recipient_source"]:checked');
if (!selectedSource) return;
const source = selectedSource.value;
// Скрываем все поля // Скрываем все поля
recipientHistoryField.style.display = 'none'; recipientHistoryField.style.display = 'none';
recipientNewFields.style.display = 'none'; recipientNewFields.style.display = 'none';
// Показываем нужные поля // Показываем нужные поля
if (mode === 'history') { if (source === 'history') {
recipientHistoryField.style.display = 'block'; recipientHistoryField.style.display = 'block';
} else if (mode === 'new') { } else if (source === 'new') {
recipientNewFields.style.display = 'block'; recipientNewFields.style.display = 'block';
} }
// Для 'customer' ничего не показываем
} }
recipientModeRadios.forEach(radio => { // Обработчики событий
radio.addEventListener('change', toggleRecipientFields); if (otherRecipientCheckbox) {
otherRecipientCheckbox.addEventListener('change', toggleOtherRecipientBlock);
// Инициализация при загрузке
toggleOtherRecipientBlock();
}
recipientSourceRadios.forEach(radio => {
radio.addEventListener('change', toggleRecipientSourceFields);
}); });
toggleRecipientFields(); toggleRecipientFields();