From 98470c83af1594ccc9df88693d520324418e8f36 Mon Sep 17 00:00:00 2001 From: Andrey Smakotin Date: Wed, 24 Dec 2025 22:51:14 +0300 Subject: [PATCH] =?UTF-8?q?=D0=A0=D0=B5=D1=84=D0=B0=D0=BA=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=B8=D0=BD=D0=B3=20=D0=B1=D0=BB=D0=BE=D0=BA=D0=B0=20?= =?UTF-8?q?=D0=B4=D0=BE=D1=81=D1=82=D0=B0=D0=B2=D0=BA=D0=B8:=20=D0=BE?= =?UTF-8?q?=D0=B1=D1=8A=D0=B5=D0=B4=D0=B8=D0=BD=D0=B5=D0=BD=D0=B8=D0=B5=20?= =?UTF-8?q?=D1=81=20=D0=B4=D0=B0=D1=82=D0=BE=D0=B9/=D0=B2=D1=80=D0=B5?= =?UTF-8?q?=D0=BC=D0=B5=D0=BD=D0=B5=D0=BC,=20=D1=83=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D1=89=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=81=D1=82=D1=80=D1=83=D0=BA?= =?UTF-8?q?=D1=82=D1=83=D1=80=D1=8B=20=D0=B0=D0=B4=D1=80=D0=B5=D1=81=D0=B0?= =?UTF-8?q?,=20=D0=B2=D1=8B=D0=BD=D0=BE=D1=81=20=D0=BF=D0=BE=D0=BB=D1=83?= =?UTF-8?q?=D1=87=D0=B0=D1=82=D0=B5=D0=BB=D1=8F,=20=D0=B0=D0=B2=D1=82?= =?UTF-8?q?=D0=BE-=D0=B2=D1=8B=D0=B1=D0=BE=D1=80=20=D1=81=D0=BA=D0=BB?= =?UTF-8?q?=D0=B0=D0=B4=D0=B0=20=D0=BF=D0=BE=20=D1=83=D0=BC=D0=BE=D0=BB?= =?UTF-8?q?=D1=87=D0=B0=D0=BD=D0=B8=D1=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- myproject/orders/forms.py | 62 +- .../orders/templates/orders/order_form.html | 754 +++++++++++------- 2 files changed, 522 insertions(+), 294 deletions(-) diff --git a/myproject/orders/forms.py b/myproject/orders/forms.py index 2e73257..df2c7c6 100644 --- a/myproject/orders/forms.py +++ b/myproject/orders/forms.py @@ -282,8 +282,23 @@ class OrderForm(forms.ModelForm): self.fields['recipient_from_history'].queryset = Recipient.objects.none() # Инициализируем queryset для pickup_warehouse + # Фильтруем только активные склады, доступные для самовывоза + # Сортируем: сначала склад по умолчанию, потом по названию from inventory.models import Warehouse - self.fields['pickup_warehouse'].queryset = Warehouse.objects.filter(is_active=True).order_by('name') + self.fields['pickup_warehouse'].queryset = Warehouse.objects.filter( + is_active=True, + is_pickup_point=True + ).order_by('-is_default', 'name') + + # Если это новый заказ и еще не выбран склад, выбираем склад по умолчанию + if not self.instance.pk: + default_warehouse = Warehouse.objects.filter( + is_active=True, + is_pickup_point=True, + is_default=True + ).first() + if default_warehouse: + self.fields['pickup_warehouse'].initial = default_warehouse # Инициализируем поля доставки из существующей Delivery if self.instance.pk and hasattr(self.instance, 'delivery'): @@ -294,6 +309,51 @@ class OrderForm(forms.ModelForm): self.fields['time_to'].initial = delivery.time_to self.fields['pickup_warehouse'].initial = delivery.pickup_warehouse self.fields['delivery_cost'].initial = delivery.cost + + # Если выбран самовывоз, но склад не указан - выбираем склад по умолчанию + if delivery.delivery_type == Delivery.DELIVERY_TYPE_PICKUP and not delivery.pickup_warehouse: + default_warehouse = Warehouse.objects.filter( + is_active=True, + is_pickup_point=True, + is_default=True + ).first() + if default_warehouse: + self.fields['pickup_warehouse'].initial = default_warehouse + + # Инициализируем поля адреса, если есть адрес доставки + if delivery.address: + # Проверяем, есть ли этот адрес в истории клиента + if self.instance.customer: + customer_addresses = Address.objects.filter( + deliveries__order__customer=self.instance.customer + ).distinct() + if delivery.address in customer_addresses: + # Адрес есть в истории - используем режим "история" + self.fields['address_mode'].initial = 'history' + self.fields['address_from_history'].queryset = customer_addresses + self.fields['address_from_history'].initial = delivery.address.pk + 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 + 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): """Валидация формы заказа, включая обязательные поля доставки""" diff --git a/myproject/orders/templates/orders/order_form.html b/myproject/orders/templates/orders/order_form.html index 8af2c28..0208a3b 100644 --- a/myproject/orders/templates/orders/order_form.html +++ b/myproject/orders/templates/orders/order_form.html @@ -195,50 +195,6 @@ - -
-
-
Дата и время доставки
-
-
-
- -
-
- - {{ form.delivery_date }} - {% if form.delivery_date.errors %} -
{{ form.delivery_date.errors }}
- {% endif %} -
-
- - {{ form.time_from }} - {% if form.time_from.errors %} -
{{ form.time_from.errors }}
- {% endif %} -
-
- - {{ form.time_to }} - {% if form.time_to.errors %} -
{{ form.time_to.errors }}
- {% endif %} -
-
- - -
-
-
-
-
@@ -398,147 +354,262 @@
-
Доставка
+
Доставка
- -
- -
- - -
- -
- - - - - -
-
- - -
- -
-
-
- - {{ form.address_street }} - {% if form.address_street.errors %} -
{{ form.address_street.errors }}
- {% endif %} -
-
-
-
- - {{ form.address_building_number }} - {% if form.address_building_number.errors %} -
{{ form.address_building_number.errors }}
- {% endif %} -
-
-
-
- - {{ form.address_apartment_number }} - {% if form.address_apartment_number.errors %} -
{{ form.address_apartment_number.errors }}
- {% endif %} -
-
-
- -
-
-
- - {{ form.address_entrance }} - {% if form.address_entrance.errors %} -
{{ form.address_entrance.errors }}
- {% endif %} -
-
-
-
- - {{ form.address_floor }} - {% if form.address_floor.errors %} -
{{ form.address_floor.errors }}
- {% endif %} -
-
-
-
- - {{ form.address_intercom_code }} - {% if form.address_intercom_code.errors %} -
{{ form.address_intercom_code.errors }}
- {% endif %} -
-
-
- -
-
-
- - {{ form.address_delivery_instructions }} - {% if form.address_delivery_instructions.errors %} -
{{ form.address_delivery_instructions.errors }}
- {% endif %} -
-
-
- -
-
-
- -
- -
+
- -
-
Получатель
+ +
+
+
Дополнительно
+
+
+
+ {{ form.is_anonymous }} + +
+
+ + {{ form.special_instructions }} +
+
+
+ +
+
+
Получатель
+
+
@@ -547,7 +618,7 @@ {{ form.other_recipient.label }} {% if form.other_recipient.help_text %} - {{ form.other_recipient.help_text }} + {{ form.other_recipient.help_text }} {% endif %}
{% if form.other_recipient.errors %} @@ -611,61 +682,6 @@
- - -
-
-
- - {{ form.delivery_cost }} - {% if form.delivery_cost.errors %} -
{{ form.delivery_cost.errors }}
- {% endif %} - - - Оставьте пустым для автоматического расчета стоимости доставки - -
-
-
-
- - - -
-
- - -
-
-
Дополнительно
-
-
-
- {{ form.is_anonymous }} - -
-
- - {{ form.special_instructions }} -
-
@@ -954,62 +970,94 @@ if (typeof $ !== 'undefined') { }); } -// === УПРАВЛЕНИЕ ТИПОМ ДОСТАВКИ (КНОПКИ) === -// ВАЖНО: Этот код должен быть ВНЕ jQuery document.ready, -// чтобы выполниться после полной загрузки DOM +// === УПРАВЛЕНИЕ ТИПОМ ДОСТАВКИ === document.addEventListener('DOMContentLoaded', function() { - const deliveryTypeRadios = document.querySelectorAll('input[name="delivery-type"]'); - const isDeliveryCheckbox = document.getElementById('{{ form.is_delivery.id_for_label }}'); - const deliveryModeFields = document.getElementById('delivery-mode-fields'); + const deliveryTypeRadios = document.querySelectorAll('input[name="{{ form.delivery_type.name }}"]'); + const deliveryAddressFields = document.getElementById('delivery-address-fields'); const pickupFields = document.getElementById('pickup-fields'); + const addressModeRadios = document.querySelectorAll('input[name="{{ form.address_mode.name }}"]'); + const addressHistoryField = document.getElementById('address-history-field'); + const addressNewFields = document.getElementById('address-new-fields'); - function syncDeliveryTypeFromRadio() { - const selectedRadio = document.querySelector('input[name="delivery-type"]:checked'); - if (!selectedRadio) return; // Защита от null + // Функция переключения между доставкой и самовывозом + function toggleDeliveryType() { + const selectedRadio = document.querySelector('input[name="{{ form.delivery_type.name }}"]:checked'); + if (!selectedRadio) return; - const selectedType = selectedRadio.value; + const deliveryType = selectedRadio.value; - if (selectedType === 'delivery') { - isDeliveryCheckbox.checked = true; - deliveryModeFields.style.display = 'block'; - pickupFields.style.display = 'none'; - } else { - isDeliveryCheckbox.checked = false; - deliveryModeFields.style.display = 'none'; - pickupFields.style.display = 'block'; - } - } - - function syncUIFromCheckbox() { - if (!isDeliveryCheckbox) return; // Защита от null - - if (isDeliveryCheckbox.checked) { - const deliveryRadio = document.getElementById('delivery-type-delivery'); - if (deliveryRadio) { - deliveryRadio.checked = true; - } - if (deliveryModeFields) deliveryModeFields.style.display = 'block'; + if (deliveryType === 'courier') { + // Показываем поля адреса, скрываем склад + if (deliveryAddressFields) deliveryAddressFields.style.display = 'block'; if (pickupFields) pickupFields.style.display = 'none'; - } else { - const pickupRadio = document.getElementById('delivery-type-pickup'); - if (pickupRadio) { - pickupRadio.checked = true; - } - if (deliveryModeFields) deliveryModeFields.style.display = 'none'; + } else if (deliveryType === 'pickup') { + // Скрываем поля адреса, показываем склад + if (deliveryAddressFields) deliveryAddressFields.style.display = 'none'; if (pickupFields) pickupFields.style.display = 'block'; + + // Автоматически выбираем склад по умолчанию, если он еще не выбран + const pickupWarehouseSelect = document.getElementById('{{ form.pickup_warehouse.id_for_label }}'); + if (pickupWarehouseSelect && !pickupWarehouseSelect.value) { + // Ищем первый option со значением (склад по умолчанию должен быть первым в списке) + const options = pickupWarehouseSelect.querySelectorAll('option[value]:not([value=""])'); + if (options.length > 0) { + // Первый option - это склад по умолчанию (благодаря сортировке -is_default) + pickupWarehouseSelect.value = options[0].value; + // Триггерим событие change для обновления UI + pickupWarehouseSelect.dispatchEvent(new Event('change', { bubbles: true })); + } + } } } - // Обработчики для кнопок + // Функция переключения режима адреса + function toggleAddressMode() { + const selectedMode = document.querySelector('input[name="{{ form.address_mode.name }}"]:checked'); + if (!selectedMode) return; + + const mode = selectedMode.value; + + // Скрываем все поля + if (addressHistoryField) addressHistoryField.style.display = 'none'; + if (addressNewFields) addressNewFields.style.display = 'none'; + + // Показываем нужные поля + if (mode === 'history') { + if (addressHistoryField) addressHistoryField.style.display = 'block'; + } else if (mode === 'new') { + if (addressNewFields) addressNewFields.style.display = 'block'; + } + } + + // Обработчики для типа доставки if (deliveryTypeRadios && deliveryTypeRadios.length > 0) { deliveryTypeRadios.forEach(radio => { - radio.addEventListener('change', syncDeliveryTypeFromRadio); + radio.addEventListener('change', toggleDeliveryType); + }); + } + + // Обработчики для режима адреса + if (addressModeRadios && addressModeRadios.length > 0) { + addressModeRadios.forEach(radio => { + radio.addEventListener('change', toggleAddressMode); }); } // Инициализация при загрузке - if (isDeliveryCheckbox) { - syncUIFromCheckbox(); + toggleDeliveryType(); + toggleAddressMode(); + + // При загрузке страницы, если выбран самовывоз, но склад не выбран - выбираем по умолчанию + const selectedDeliveryType = document.querySelector('input[name="{{ form.delivery_type.name }}"]:checked'); + if (selectedDeliveryType && selectedDeliveryType.value === 'pickup') { + const pickupWarehouseSelect = document.getElementById('{{ form.pickup_warehouse.id_for_label }}'); + if (pickupWarehouseSelect && !pickupWarehouseSelect.value) { + // Ищем первый option со значением (склад по умолчанию должен быть первым) + const options = pickupWarehouseSelect.querySelectorAll('option[value]:not([value=""])'); + if (options.length > 0) { + pickupWarehouseSelect.value = options[0].value; + } + } } // Показ/скрытие полей получателя @@ -1058,7 +1106,6 @@ document.addEventListener('DOMContentLoaded', function() { recipientSourceRadios.forEach(radio => { radio.addEventListener('change', toggleRecipientSourceFields); }); - toggleRecipientFields(); // === РАСЧЁТ ИТОГОВОЙ СУММЫ ТОВАРОВ === // Вычисление суммы по всем видимым позициям @@ -1154,6 +1201,56 @@ document.addEventListener('DOMContentLoaded', function() { const totalFormsInput = document.querySelector('#id_items-TOTAL_FORMS'); const emptyFormTemplate = document.getElementById('empty-form-template'); + /** + * Безопасная инициализация Select2 для элемента формы заказа + * Проверяет наличие всех зависимостей и повторяет попытки при необходимости + * @param {Element} selectElement - DOM элемент select для инициализации + * @param {number} retryCount - Количество оставшихся попыток (по умолчанию 10) + * @param {number} delay - Задержка между попытками в миллисекундах (по умолчанию 100) + */ + window.safeInitOrderItemSelect2 = function(selectElement, retryCount = 10, delay = 100) { + if (!selectElement) { + console.warn('[safeInitOrderItemSelect2] Элемент не найден'); + return false; + } + + // Проверяем наличие всех зависимостей + if (typeof $ === 'undefined') { + console.log('[safeInitOrderItemSelect2] jQuery еще не загружен, ожидание...'); + if (retryCount > 0) { + setTimeout(() => window.safeInitOrderItemSelect2(selectElement, retryCount - 1, delay), delay); + } + return false; + } + + if (typeof window.initOrderItemSelect2 !== 'function') { + console.log('[safeInitOrderItemSelect2] initOrderItemSelect2 еще не загружена, ожидание...'); + if (retryCount > 0) { + setTimeout(() => window.safeInitOrderItemSelect2(selectElement, retryCount - 1, delay), delay); + } + return false; + } + + // Проверяем, не инициализирован ли уже Select2 + if ($(selectElement).data('select2')) { + console.log('[safeInitOrderItemSelect2] Select2 уже инициализирован для этого элемента'); + return true; + } + + // Все зависимости готовы, инициализируем + try { + window.initOrderItemSelect2(selectElement); + console.log('[safeInitOrderItemSelect2] Select2 успешно инициализирован'); + return true; + } catch (error) { + console.error('[safeInitOrderItemSelect2] Ошибка при инициализации Select2:', error); + if (retryCount > 0) { + setTimeout(() => window.safeInitOrderItemSelect2(selectElement, retryCount - 1, delay), delay); + } + return false; + } + }; + // Функция для добавления новой формы function addNewForm() { const formCount = parseInt(totalFormsInput.value); @@ -1169,19 +1266,24 @@ document.addEventListener('DOMContentLoaded', function() { totalFormsInput.value = formCount + 1; - // Инициализируем Select2 для новой формы + // Инициализируем Select2 для новой формы с безопасной проверкой const select2Element = newForm.querySelector('.select2-order-item'); - select2Element.dataset.formIndex = formCount; - if (typeof window.initOrderItemSelect2 === 'function') { - window.initOrderItemSelect2(select2Element); + if (select2Element) { + select2Element.dataset.formIndex = formCount; + // Используем безопасную инициализацию с повторными попытками + window.safeInitOrderItemSelect2(select2Element); + } else { + console.error('[addNewForm] Элемент .select2-order-item не найден в новой форме'); } initPriceTracking(newForm); const removeBtn = newForm.querySelector('.remove-item-btn'); - removeBtn.addEventListener('click', function() { - removeForm(newForm); - }); + if (removeBtn) { + removeBtn.addEventListener('click', function() { + removeForm(newForm); + }); + } updateTotalDisplay(); @@ -1263,10 +1365,35 @@ document.addEventListener('DOMContentLoaded', function() { }); // Если нет ни одной формы, добавляем одну автоматически - if (container.querySelectorAll('.order-item-form').length === 0) { - addNewForm(); + // Убеждаемся, что это происходит после загрузки всех скриптов + function ensureInitialForm() { + const existingForms = container.querySelectorAll('.order-item-form:not(.deleted)'); + if (existingForms.length === 0) { + console.log('[Order Items] Нет форм, добавляем первую автоматически...'); + const newForm = addNewForm(); + + // Переинициализируем Select2 для добавленной формы после небольшой задержки + // чтобы убедиться, что все скрипты загружены + setTimeout(() => { + const select2Element = newForm.querySelector('.select2-order-item'); + if (select2Element && (!window.$ || !$(select2Element).data('select2'))) { + console.log('[Order Items] Переинициализация Select2 для автоматически добавленной формы...'); + if (window.safeInitOrderItemSelect2) { + window.safeInitOrderItemSelect2(select2Element); + } else if (window.initOrderItemSelect2 && typeof window.initOrderItemSelect2 === 'function') { + window.initOrderItemSelect2(select2Element); + } + } + }, 200); + } } + // Пытаемся добавить форму сразу, но также проверяем после загрузки всех скриптов + ensureInitialForm(); + + // Дополнительная проверка после небольшой задержки на случай, если скрипты загружаются асинхронно + setTimeout(ensureInitialForm, 500); + // Инициализируем итоговую сумму updateTotalDisplay(); @@ -1643,12 +1770,6 @@ document.addEventListener('DOMContentLoaded', function() { return; } - if (typeof window.initOrderItemSelect2 !== 'function') { - console.log('[Order Items] Ожидание инициализации initOrderItemSelect2...'); - setTimeout(initExistingOrderItems, 100); - return; - } - if (typeof window.initOrderItemSelect2 !== 'function') { console.log('[Order Items] Ожидание загрузки initOrderItemSelect2 из select2-product-search.js...'); setTimeout(initExistingOrderItems, 100); @@ -1660,12 +1781,31 @@ document.addEventListener('DOMContentLoaded', function() { const items = document.querySelectorAll('.select2-order-item'); console.log('[Order Items] Найдено элементов для инициализации:', items.length); - items.forEach((selectElement, index) => { - console.log(`[Order Items] 📋 Инициализация элемента ${index + 1}/${items.length}`); + // Функция для инициализации одного элемента + function initOrderItemElement(selectElement, index) { + console.log(`[Order Items] 📋 Инициализация элемента ${index + 1}`); - // Сначала инициализируем Select2 - window.initOrderItemSelect2(selectElement); - console.log(`[Order Items] ✅ Select2 инициализирован для элемента ${index}`); + // Используем безопасную инициализацию + const initialized = window.safeInitOrderItemSelect2 ? + window.safeInitOrderItemSelect2(selectElement) : + (window.initOrderItemSelect2 ? (window.initOrderItemSelect2(selectElement), true) : false); + + if (initialized) { + console.log(`[Order Items] ✅ Select2 инициализирован для элемента ${index}`); + } else { + console.warn(`[Order Items] ⚠️ Не удалось инициализировать Select2 для элемента ${index}, будет повторная попытка`); + // Повторная попытка через небольшую задержку + setTimeout(() => { + if (window.initOrderItemSelect2 && typeof window.initOrderItemSelect2 === 'function') { + window.initOrderItemSelect2(selectElement); + console.log(`[Order Items] ✅ Select2 инициализирован для элемента ${index} (повторная попытка)`); + } + }, 300); + } + } + + items.forEach((selectElement, index) => { + initOrderItemElement(selectElement, index); // Затем проверяем, есть ли предзаполненные данные в скрытых полях const form = selectElement.closest('.order-item-form'); @@ -1768,6 +1908,34 @@ document.addEventListener('DOMContentLoaded', function() { }); console.log('[Order Items] Инициализация всех существующих элементов завершена'); + + // Переинициализация для форм, которые могут быть добавлены позже + // Используем MutationObserver для отслеживания новых форм + if (typeof MutationObserver !== 'undefined') { + const observer = new MutationObserver(function(mutations) { + mutations.forEach(function(mutation) { + mutation.addedNodes.forEach(function(node) { + if (node.nodeType === 1 && node.classList && node.classList.contains('order-item-form')) { + const select2Element = node.querySelector('.select2-order-item'); + if (select2Element && !$(select2Element).data('select2')) { + console.log('[Order Items] Обнаружена новая форма, инициализация Select2...'); + setTimeout(() => { + if (window.initOrderItemSelect2 && typeof window.initOrderItemSelect2 === 'function') { + window.initOrderItemSelect2(select2Element); + } + }, 100); + } + } + }); + }); + }); + + const container = document.getElementById('order-items-container'); + if (container) { + observer.observe(container, { childList: true, subtree: true }); + console.log('[Order Items] MutationObserver установлен для отслеживания новых форм'); + } + } })();