Реализована система управления стоимостью доставки и исправлен баг выбора клиента

Изменения:

1. **Стоимость доставки (автоматическая/ручная)**:
   - Добавлено поле `is_custom_delivery_cost` в модель Order для отслеживания ручной стоимости
   - Создан сервис DeliveryCostCalculator для автоматического расчета стоимости доставки
   - Реализована логика: бесплатная доставка от 100 руб., иначе 15 руб.
   - В форме поле "Ручная стоимость доставки" - если заполнено, используется ручная стоимость, если пустое - автоматический расчет
   - Добавлены методы Order.get_delivery_cost(), set_delivery_cost(), reset_delivery_cost(), recalculate_delivery_cost()

2. **Исправление бага выбора клиента в Select2**:
   - Удален избыточный обработчик события select2:opening, который блокировал клики
   - Добавлены проверки на наличие e.params.data в обработчиках select2:selecting и select2:select
   - Теперь выбор клиента работает корректно, создается черновик заказа и происходит редирект

3. **Опциональные поля адреса**:
   - Поля recipient_name, street, building_number в модели Address сделаны необязательными (blank=True, null=True)
   - Обновлены методы __str__ и full_address для безопасной работы с None значениями

4. **UI улучшения**:
   - Удалена звездочка обязательности с полей адреса
   - Добавлена подсказка под полем ручной стоимости доставки о правилах автоматического расчета

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-11 15:48:50 +03:00
parent 6c207e9451
commit 885ac839e2
5 changed files with 351 additions and 60 deletions

View File

@@ -213,7 +213,7 @@
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.address_street.id_for_label }}" class="form-label">
{{ form.address_street.label }} <span class="text-danger">*</span>
{{ form.address_street.label }}
</label>
{{ form.address_street }}
{% if form.address_street.errors %}
@@ -224,7 +224,7 @@
<div class="col-md-3">
<div class="mb-3">
<label for="{{ form.address_building_number.id_for_label }}" class="form-label">
{{ form.address_building_number.label }} <span class="text-danger">*</span>
{{ form.address_building_number.label }}
</label>
{{ form.address_building_number }}
{% if form.address_building_number.errors %}
@@ -297,11 +297,19 @@
<div class="row">
<div class="col-12">
<div class="mb-3 form-check">
{{ form.address_confirm_with_recipient }}
<label class="form-check-label" for="{{ form.address_confirm_with_recipient.id_for_label }}">
{{ form.address_confirm_with_recipient.label }}
</label>
<div class="mb-3">
<!-- Крупный переключатель (switch) -->
<div class="form-check form-switch" style="padding-left: 3.5em;">
<input class="form-check-input" type="checkbox" role="switch"
id="{{ form.address_confirm_with_recipient.id_for_label }}"
name="{{ form.address_confirm_with_recipient.name }}"
style="width: 3em; height: 1.5em; cursor: pointer;">
<label class="form-check-label" for="{{ form.address_confirm_with_recipient.id_for_label }}"
style="font-size: 1.1em; font-weight: 500; cursor: pointer; padding-left: 0.5em;">
<i class="bi bi-telephone-fill text-primary"></i>
{{ form.address_confirm_with_recipient.label }}
</label>
</div>
</div>
</div>
</div>
@@ -310,12 +318,41 @@
<div class="border-top pt-3 mt-3">
<h6 class="mb-3">Получатель</h6>
<!-- Чекбокс "Покупатель = получатель" -->
<div class="mb-3 form-check">
{{ form.customer_is_recipient }}
<label class="form-check-label" for="{{ form.customer_is_recipient.id_for_label }}">
Покупатель является получателем
</label>
<!-- Поля получателя из модели Address -->
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="id_address_recipient_name" class="form-label">
Имя получателя
</label>
<input type="text" name="address_recipient_name" id="id_address_recipient_name"
class="form-control" placeholder="Имя получателя">
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="id_address_recipient_phone" class="form-label">
Телефон получателя
</label>
<input type="text" name="address_recipient_phone" id="id_address_recipient_phone"
class="form-control" placeholder="Телефон получателя">
</div>
</div>
</div>
<!-- Крупный переключатель "Покупатель = получатель" -->
<div class="mb-3">
<div class="form-check form-switch" style="padding-left: 3.5em;">
<input class="form-check-input" type="checkbox" role="switch"
id="{{ form.customer_is_recipient.id_for_label }}"
name="{{ form.customer_is_recipient.name }}"
style="width: 3em; height: 1.5em; cursor: pointer;">
<label class="form-check-label" for="{{ form.customer_is_recipient.id_for_label }}"
style="font-size: 1.1em; font-weight: 500; cursor: pointer; padding-left: 0.5em;">
<i class="bi bi-person-check-fill text-primary"></i>
Покупатель является получателем
</label>
</div>
</div>
<!-- Поля получателя (показываются когда покупатель != получатель) -->
@@ -323,7 +360,7 @@
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.recipient_name.id_for_label }}" class="form-label">
Имя получателя <span class="text-danger">*</span>
Имя получателя
</label>
{{ form.recipient_name }}
{% if form.recipient_name.errors %}
@@ -334,7 +371,7 @@
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.recipient_phone.id_for_label }}" class="form-label">
Телефон получателя <span class="text-danger">*</span>
Телефон получателя
</label>
{{ form.recipient_phone }}
{% if form.recipient_phone.errors %}
@@ -349,11 +386,17 @@
<div class="row mt-3">
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.delivery_cost.id_for_label }}" class="form-label">Стоимость доставки</label>
<label for="{{ form.delivery_cost.id_for_label }}" class="form-label">
{{ form.delivery_cost.label }}
</label>
{{ form.delivery_cost }}
{% if form.delivery_cost.errors %}
<div class="text-danger">{{ form.delivery_cost.errors }}</div>
{% endif %}
<small class="d-block text-muted mt-1">
<i class="bi bi-info-circle"></i>
Оставьте пустым для автоматического расчета (бесплатно от 100 руб., иначе 15 руб.)
</small>
</div>
</div>
</div>
@@ -666,11 +709,19 @@ function initCustomerSelect2() {
// Обработчик для перехвата ПЕРЕД выбором (используется для фальшивых опций)
$customerSelect.on('select2:selecting', function(e) {
console.log('9. Событие select2:selecting, e.params:', e.params);
// Проверяем наличие e.params и e.params.data
if (!e.params || !e.params.data) {
console.log('9a. Нет данных в e.params на selecting, пропускаем');
return;
}
const data = e.params.data;
console.log('9a. Попытка выбрать элемент (перед выбором):', data);
console.log('9b. Попытка выбрать элемент (перед выбором):', data);
if (data.is_create_option) {
console.log('9b. Это опция создания клиента - предотвращаем выбор и открываем модаль');
console.log('9c. Это опция создания клиента - предотвращаем выбор и открываем модаль');
// Предотвращаем выбор этой опции
e.preventDefault();
// Очищаем значение
@@ -679,48 +730,21 @@ function initCustomerSelect2() {
window.openCreateCustomerModal(data.search_text);
return false;
}
});
// Обработчик прямого клика на результаты (для "create option")
$customerSelect.on('select2:opening', function(e) {
console.log('9_open. Dropdown открывается, добавляем обработчик клика');
// Добавляем обработчик на клик по результатам в следующем event tick
setTimeout(function() {
const resultsContainer = document.querySelector('.select2-results');
console.log('9_search. resultsContainer найден:', !!resultsContainer);
if (resultsContainer) {
resultsContainer.addEventListener('click', function handleCreateOptionClick(event) {
console.log('9_click. Клик по результатам:', event.target);
const target = event.target.closest('.select2-results__option');
console.log('9_option. Ближайший option:', target);
if (!target) return;
// Проверяем текст опции - если это "Создать клиента", открываем модаль
const optionText = target.textContent.trim();
console.log('9_text. Текст опции:', optionText);
if (optionText.startsWith('Создать клиента:')) {
console.log('9c. Клик на create option напрямую');
// Извлекаем поисковый текст (удаляем "Создать клиента: ")
const searchText = optionText.replace(/^Создать\s+клиента:\s*"([^"]*)"\s*$/, '$1') || optionText;
console.log('9d. Открываем модаль с текстом:', searchText);
window.openCreateCustomerModal(searchText);
// Закрываем dropdown
$customerSelect.select2('close');
// Удаляем обработчик
resultsContainer.removeEventListener('click', handleCreateOptionClick);
}
});
}
}, 0);
console.log('9d. Обычный клиент, разрешаем выбор');
});
$customerSelect.on('select2:select', function(e) {
console.log('10. Событие select2:select, e.params:', e.params);
// Проверяем наличие e.params и e.params.data
if (!e.params || !e.params.data) {
console.log('10a. Нет данных в e.params, пропускаем обработку');
return;
}
const data = e.params.data;
console.log('10. Выбран элемент:', data);
console.log('10b. Выбран элемент:', data);
if (data.is_create_option) {
console.log('11. Открываем модальное окно для создания клиента');
@@ -731,7 +755,7 @@ function initCustomerSelect2() {
window.openCreateCustomerModal(data.search_text);
} else {
// Триггерим нативное change событие для других обработчиков (например, draft-creator.js)
console.log('12. Триггерим нативное change событие');
console.log('12. Триггерим нативное change событие для customer ID:', data.id);
const changeEvent = new Event('change', { bubbles: true });
this.dispatchEvent(changeEvent);
}