Реализована система управления стоимостью доставки и исправлен баг выбора клиента
Изменения: 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:
@@ -164,6 +164,11 @@ class OrderForm(forms.ModelForm):
|
|||||||
self.fields['recipient_name'].required = False
|
self.fields['recipient_name'].required = False
|
||||||
self.fields['recipient_phone'].required = False
|
self.fields['recipient_phone'].required = False
|
||||||
|
|
||||||
|
# Поле ручной стоимости доставки опционально
|
||||||
|
self.fields['delivery_cost'].required = False
|
||||||
|
self.fields['delivery_cost'].label = 'Ручная стоимость доставки'
|
||||||
|
self.fields['delivery_cost'].help_text = 'Оставьте пустым для автоматического расчета'
|
||||||
|
|
||||||
# Инициализируем queryset для address_from_history
|
# Инициализируем queryset для address_from_history
|
||||||
# Это будет переопределено в представлении после выбора клиента
|
# Это будет переопределено в представлении после выбора клиента
|
||||||
if self.instance.pk and self.instance.customer:
|
if self.instance.pk and self.instance.customer:
|
||||||
@@ -176,6 +181,30 @@ class OrderForm(forms.ModelForm):
|
|||||||
order__in=customer_orders
|
order__in=customer_orders
|
||||||
).distinct().order_by('-created_at')
|
).distinct().order_by('-created_at')
|
||||||
|
|
||||||
|
def save(self, commit=True):
|
||||||
|
"""
|
||||||
|
Сохраняет форму с учетом автоматического/ручного расчета стоимости доставки.
|
||||||
|
Логика:
|
||||||
|
- Если delivery_cost заполнено → используется ручное значение (is_custom_delivery_cost = True)
|
||||||
|
- Если delivery_cost пустое → автоматический расчет (is_custom_delivery_cost = False)
|
||||||
|
"""
|
||||||
|
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 → автоматический расчет
|
||||||
|
instance.reset_delivery_cost()
|
||||||
|
|
||||||
|
if commit:
|
||||||
|
instance.save()
|
||||||
|
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
class OrderItemForm(forms.ModelForm):
|
class OrderItemForm(forms.ModelForm):
|
||||||
"""Форма для позиции заказа"""
|
"""Форма для позиции заказа"""
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
# Generated by Django 5.0.10 on 2025-11-11 09:52
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('customers', '0002_remove_address_model'),
|
||||||
|
('orders', '0004_remove_address_orders_addr_distric_fd94e9_idx_and_more'),
|
||||||
|
('shops', '0001_initial'),
|
||||||
|
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='historicalorder',
|
||||||
|
name='is_custom_delivery_cost',
|
||||||
|
field=models.BooleanField(default=False, help_text='True если стоимость доставки была изменена вручную', verbose_name='Стоимость доставки установлена вручную'),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='order',
|
||||||
|
name='is_custom_delivery_cost',
|
||||||
|
field=models.BooleanField(default=False, help_text='True если стоимость доставки была изменена вручную', verbose_name='Стоимость доставки установлена вручную'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='address',
|
||||||
|
name='building_number',
|
||||||
|
field=models.CharField(blank=True, max_length=20, null=True, verbose_name='Номер здания'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='address',
|
||||||
|
name='recipient_name',
|
||||||
|
field=models.CharField(blank=True, help_text='Имя человека, которому будет доставлен заказ', max_length=200, null=True, verbose_name='Имя получателя'),
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='address',
|
||||||
|
name='street',
|
||||||
|
field=models.CharField(blank=True, max_length=255, null=True, verbose_name='Улица'),
|
||||||
|
),
|
||||||
|
migrations.AddIndex(
|
||||||
|
model_name='order',
|
||||||
|
index=models.Index(fields=['is_custom_delivery_cost'], name='orders_orde_is_cust_108e98_idx'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -15,6 +15,8 @@ class Address(models.Model):
|
|||||||
# Информация о получателе
|
# Информация о получателе
|
||||||
recipient_name = models.CharField(
|
recipient_name = models.CharField(
|
||||||
max_length=200,
|
max_length=200,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
verbose_name="Имя получателя",
|
verbose_name="Имя получателя",
|
||||||
help_text="Имя человека, которому будет доставлен заказ"
|
help_text="Имя человека, которому будет доставлен заказ"
|
||||||
)
|
)
|
||||||
@@ -29,11 +31,15 @@ class Address(models.Model):
|
|||||||
|
|
||||||
street = models.CharField(
|
street = models.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
verbose_name="Улица"
|
verbose_name="Улица"
|
||||||
)
|
)
|
||||||
|
|
||||||
building_number = models.CharField(
|
building_number = models.CharField(
|
||||||
max_length=20,
|
max_length=20,
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
verbose_name="Номер здания"
|
verbose_name="Номер здания"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -94,17 +100,43 @@ class Address(models.Model):
|
|||||||
ordering = ['-created_at']
|
ordering = ['-created_at']
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
address_line = f"{self.street}, {self.building_number}"
|
# Собираем компоненты адреса
|
||||||
|
address_parts = []
|
||||||
|
if self.street:
|
||||||
|
address_parts.append(self.street)
|
||||||
|
if self.building_number:
|
||||||
|
address_parts.append(self.building_number)
|
||||||
if self.apartment_number:
|
if self.apartment_number:
|
||||||
address_line += f", кв/офис {self.apartment_number}"
|
address_parts.append(f"кв/офис {self.apartment_number}")
|
||||||
return f"{self.recipient_name} - {address_line}"
|
|
||||||
|
address_line = ", ".join(address_parts) if address_parts else "Адрес не указан"
|
||||||
|
|
||||||
|
# Формируем строку с именем получателя
|
||||||
|
if self.recipient_name:
|
||||||
|
return f"{self.recipient_name} - {address_line}"
|
||||||
|
return address_line
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def full_address(self):
|
def full_address(self):
|
||||||
"""Полный адрес для доставки"""
|
"""Полный адрес для доставки"""
|
||||||
address = f"{self.street}, {self.building_number}"
|
# Собираем основные компоненты адреса
|
||||||
|
address_parts = []
|
||||||
|
if self.street:
|
||||||
|
address_parts.append(self.street)
|
||||||
|
if self.building_number:
|
||||||
|
address_parts.append(self.building_number)
|
||||||
|
|
||||||
|
# Если нет основных данных, возвращаем сообщение
|
||||||
|
if not address_parts:
|
||||||
|
return "Адрес не указан"
|
||||||
|
|
||||||
|
address = ", ".join(address_parts)
|
||||||
|
|
||||||
|
# Добавляем квартиру/офис
|
||||||
if self.apartment_number:
|
if self.apartment_number:
|
||||||
address += f", кв/офис {self.apartment_number}"
|
address += f", кв/офис {self.apartment_number}"
|
||||||
|
|
||||||
|
# Собираем дополнительные детали
|
||||||
details = []
|
details = []
|
||||||
if self.entrance:
|
if self.entrance:
|
||||||
details.append(f"подъезд {self.entrance}")
|
details.append(f"подъезд {self.entrance}")
|
||||||
@@ -112,6 +144,7 @@ class Address(models.Model):
|
|||||||
details.append(f"этаж {self.floor}")
|
details.append(f"этаж {self.floor}")
|
||||||
if details:
|
if details:
|
||||||
address += f" ({', '.join(details)})"
|
address += f" ({', '.join(details)})"
|
||||||
|
|
||||||
return address
|
return address
|
||||||
|
|
||||||
|
|
||||||
@@ -193,6 +226,12 @@ class Order(models.Model):
|
|||||||
help_text="0 для самовывоза"
|
help_text="0 для самовывоза"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
is_custom_delivery_cost = models.BooleanField(
|
||||||
|
default=False,
|
||||||
|
verbose_name="Стоимость доставки установлена вручную",
|
||||||
|
help_text="True если стоимость доставки была изменена вручную"
|
||||||
|
)
|
||||||
|
|
||||||
# Статус заказа
|
# Статус заказа
|
||||||
STATUS_CHOICES = [
|
STATUS_CHOICES = [
|
||||||
('draft', 'Черновик'),
|
('draft', 'Черновик'),
|
||||||
@@ -351,6 +390,7 @@ class Order(models.Model):
|
|||||||
models.Index(fields=['payment_status']),
|
models.Index(fields=['payment_status']),
|
||||||
models.Index(fields=['created_at']),
|
models.Index(fields=['created_at']),
|
||||||
models.Index(fields=['order_number']),
|
models.Index(fields=['order_number']),
|
||||||
|
models.Index(fields=['is_custom_delivery_cost']),
|
||||||
]
|
]
|
||||||
ordering = ['-created_at']
|
ordering = ['-created_at']
|
||||||
|
|
||||||
@@ -387,9 +427,56 @@ class Order(models.Model):
|
|||||||
'delivery_time_end': 'Время окончания должно быть позже времени начала'
|
'delivery_time_end': 'Время окончания должно быть позже времени начала'
|
||||||
})
|
})
|
||||||
|
|
||||||
|
def get_delivery_cost(self):
|
||||||
|
"""
|
||||||
|
Возвращает стоимость доставки:
|
||||||
|
- Если установлена вручную - использует ручное значение
|
||||||
|
- Если автоматическая - вычисляет на основе правил
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Decimal: Стоимость доставки
|
||||||
|
"""
|
||||||
|
if self.is_custom_delivery_cost:
|
||||||
|
return self.delivery_cost
|
||||||
|
else:
|
||||||
|
from orders.services.delivery_cost_calculator import DeliveryCostCalculator
|
||||||
|
return DeliveryCostCalculator.calculate(self)
|
||||||
|
|
||||||
|
def set_delivery_cost(self, cost, is_custom=True):
|
||||||
|
"""
|
||||||
|
Устанавливает стоимость доставки.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cost: Новая стоимость доставки (Decimal)
|
||||||
|
is_custom: True если устанавливается вручную, False если автоматически
|
||||||
|
"""
|
||||||
|
self.delivery_cost = cost
|
||||||
|
self.is_custom_delivery_cost = is_custom
|
||||||
|
|
||||||
|
def reset_delivery_cost(self):
|
||||||
|
"""
|
||||||
|
Сбрасывает стоимость доставки на автоматический расчет.
|
||||||
|
"""
|
||||||
|
from orders.services.delivery_cost_calculator import DeliveryCostCalculator
|
||||||
|
self.delivery_cost = DeliveryCostCalculator.calculate(self)
|
||||||
|
self.is_custom_delivery_cost = False
|
||||||
|
|
||||||
|
def recalculate_delivery_cost(self):
|
||||||
|
"""
|
||||||
|
Пересчитывает стоимость доставки, если она не установлена вручную.
|
||||||
|
Используется при изменении параметров заказа (товаров, адреса и т.д.)
|
||||||
|
"""
|
||||||
|
if not self.is_custom_delivery_cost:
|
||||||
|
from orders.services.delivery_cost_calculator import DeliveryCostCalculator
|
||||||
|
self.delivery_cost = DeliveryCostCalculator.calculate(self)
|
||||||
|
|
||||||
def calculate_total(self):
|
def calculate_total(self):
|
||||||
"""Рассчитывает итоговую сумму заказа"""
|
"""Рассчитывает итоговую сумму заказа"""
|
||||||
items_total = sum(item.get_total_price() for item in self.items.all())
|
items_total = sum(item.get_total_price() for item in self.items.all())
|
||||||
|
|
||||||
|
# Пересчитываем стоимость доставки если она автоматическая
|
||||||
|
self.recalculate_delivery_cost()
|
||||||
|
|
||||||
subtotal = items_total + self.delivery_cost
|
subtotal = items_total + self.delivery_cost
|
||||||
self.total_amount = subtotal - self.discount_amount
|
self.total_amount = subtotal - self.discount_amount
|
||||||
return self.total_amount
|
return self.total_amount
|
||||||
@@ -416,6 +503,16 @@ class Order(models.Model):
|
|||||||
"""Остаток к оплате"""
|
"""Остаток к оплате"""
|
||||||
return max(self.total_amount - self.amount_paid, 0)
|
return max(self.total_amount - self.amount_paid, 0)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def delivery_cost_display(self):
|
||||||
|
"""
|
||||||
|
Возвращает строку для отображения стоимости доставки с пометкой.
|
||||||
|
Полезно в админке и шаблонах.
|
||||||
|
"""
|
||||||
|
cost = self.get_delivery_cost()
|
||||||
|
suffix = " (ручная)" if self.is_custom_delivery_cost else " (авто)"
|
||||||
|
return f"{cost} руб.{suffix}"
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def delivery_info(self):
|
def delivery_info(self):
|
||||||
"""Информация о доставке для отображения"""
|
"""Информация о доставке для отображения"""
|
||||||
|
|||||||
95
myproject/orders/services/delivery_cost_calculator.py
Normal file
95
myproject/orders/services/delivery_cost_calculator.py
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
Сервис для расчета стоимости доставки.
|
||||||
|
Содержит расширяемую логику вычисления на основе различных условий.
|
||||||
|
"""
|
||||||
|
from decimal import Decimal
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from orders.models import Order
|
||||||
|
|
||||||
|
|
||||||
|
class DeliveryCostCalculator:
|
||||||
|
"""
|
||||||
|
Калькулятор стоимости доставки.
|
||||||
|
Применяет различные правила для автоматического расчета.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Константы для правил расчета
|
||||||
|
FREE_DELIVERY_THRESHOLD = Decimal('100.00') # Бесплатная доставка от суммы
|
||||||
|
BASE_DELIVERY_COST = Decimal('15.00') # Базовая стоимость доставки
|
||||||
|
MIN_DELIVERY_COST = Decimal('0.00') # Минимальная стоимость
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def calculate(cls, order: 'Order') -> Decimal:
|
||||||
|
"""
|
||||||
|
Рассчитывает стоимость доставки на основе условий заказа.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
order: Заказ для расчета
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Decimal: Рассчитанная стоимость доставки
|
||||||
|
"""
|
||||||
|
# Самовывоз - доставка бесплатная
|
||||||
|
if not order.is_delivery:
|
||||||
|
return cls.MIN_DELIVERY_COST
|
||||||
|
|
||||||
|
# Рассчитываем сумму товаров
|
||||||
|
items_total = sum(
|
||||||
|
item.get_total_price()
|
||||||
|
for item in order.items.all()
|
||||||
|
)
|
||||||
|
|
||||||
|
# Применяем правила расчета
|
||||||
|
cost = cls._apply_calculation_rules(order, items_total)
|
||||||
|
|
||||||
|
return cost
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _apply_calculation_rules(cls, order: 'Order', items_total: Decimal) -> Decimal:
|
||||||
|
"""
|
||||||
|
Применяет правила расчета стоимости доставки.
|
||||||
|
Этот метод легко расширить для добавления новых правил.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
order: Заказ
|
||||||
|
items_total: Сумма товаров в заказе
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Decimal: Стоимость доставки
|
||||||
|
"""
|
||||||
|
# Правило 1: Бесплатная доставка при заказе от определенной суммы
|
||||||
|
if items_total >= cls.FREE_DELIVERY_THRESHOLD:
|
||||||
|
return cls.MIN_DELIVERY_COST
|
||||||
|
|
||||||
|
# Правило 2: Базовая стоимость доставки
|
||||||
|
cost = cls.BASE_DELIVERY_COST
|
||||||
|
|
||||||
|
# Правило 3: Можно добавить расчет по адресу
|
||||||
|
# if order.delivery_address:
|
||||||
|
# cost += cls._calculate_distance_cost(order.delivery_address)
|
||||||
|
|
||||||
|
# Правило 4: Можно добавить надбавку за срочность
|
||||||
|
# if cls._is_urgent_delivery(order):
|
||||||
|
# cost *= Decimal('1.5')
|
||||||
|
|
||||||
|
return cost
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _calculate_distance_cost(cls, address) -> Decimal:
|
||||||
|
"""
|
||||||
|
Рассчитывает надбавку за расстояние.
|
||||||
|
Placeholder для будущей реализации с геокодингом.
|
||||||
|
"""
|
||||||
|
# TODO: Интеграция с картами для расчета расстояния
|
||||||
|
return Decimal('0.00')
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _is_urgent_delivery(cls, order: 'Order') -> bool:
|
||||||
|
"""
|
||||||
|
Проверяет, является ли доставка срочной.
|
||||||
|
"""
|
||||||
|
# TODO: Логика определения срочности
|
||||||
|
return False
|
||||||
@@ -213,7 +213,7 @@
|
|||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="{{ form.address_street.id_for_label }}" class="form-label">
|
<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>
|
</label>
|
||||||
{{ form.address_street }}
|
{{ form.address_street }}
|
||||||
{% if form.address_street.errors %}
|
{% if form.address_street.errors %}
|
||||||
@@ -224,7 +224,7 @@
|
|||||||
<div class="col-md-3">
|
<div class="col-md-3">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="{{ form.address_building_number.id_for_label }}" class="form-label">
|
<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>
|
</label>
|
||||||
{{ form.address_building_number }}
|
{{ form.address_building_number }}
|
||||||
{% if form.address_building_number.errors %}
|
{% if form.address_building_number.errors %}
|
||||||
@@ -297,11 +297,19 @@
|
|||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12">
|
<div class="col-12">
|
||||||
<div class="mb-3 form-check">
|
<div class="mb-3">
|
||||||
{{ form.address_confirm_with_recipient }}
|
<!-- Крупный переключатель (switch) -->
|
||||||
<label class="form-check-label" for="{{ form.address_confirm_with_recipient.id_for_label }}">
|
<div class="form-check form-switch" style="padding-left: 3.5em;">
|
||||||
{{ form.address_confirm_with_recipient.label }}
|
<input class="form-check-input" type="checkbox" role="switch"
|
||||||
</label>
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -310,12 +318,41 @@
|
|||||||
<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>
|
||||||
|
|
||||||
<!-- Чекбокс "Покупатель = получатель" -->
|
<!-- Поля получателя из модели Address -->
|
||||||
<div class="mb-3 form-check">
|
<div class="row">
|
||||||
{{ form.customer_is_recipient }}
|
<div class="col-md-6">
|
||||||
<label class="form-check-label" for="{{ form.customer_is_recipient.id_for_label }}">
|
<div class="mb-3">
|
||||||
Покупатель является получателем
|
<label for="id_address_recipient_name" class="form-label">
|
||||||
</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>
|
</div>
|
||||||
|
|
||||||
<!-- Поля получателя (показываются когда покупатель != получатель) -->
|
<!-- Поля получателя (показываются когда покупатель != получатель) -->
|
||||||
@@ -323,7 +360,7 @@
|
|||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="{{ form.recipient_name.id_for_label }}" class="form-label">
|
<label for="{{ form.recipient_name.id_for_label }}" class="form-label">
|
||||||
Имя получателя <span class="text-danger">*</span>
|
Имя получателя
|
||||||
</label>
|
</label>
|
||||||
{{ form.recipient_name }}
|
{{ form.recipient_name }}
|
||||||
{% if form.recipient_name.errors %}
|
{% if form.recipient_name.errors %}
|
||||||
@@ -334,7 +371,7 @@
|
|||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="{{ form.recipient_phone.id_for_label }}" class="form-label">
|
<label for="{{ form.recipient_phone.id_for_label }}" class="form-label">
|
||||||
Телефон получателя <span class="text-danger">*</span>
|
Телефон получателя
|
||||||
</label>
|
</label>
|
||||||
{{ form.recipient_phone }}
|
{{ form.recipient_phone }}
|
||||||
{% if form.recipient_phone.errors %}
|
{% if form.recipient_phone.errors %}
|
||||||
@@ -349,11 +386,17 @@
|
|||||||
<div class="row mt-3">
|
<div class="row mt-3">
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="mb-3">
|
<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 }}
|
{{ form.delivery_cost }}
|
||||||
{% if form.delivery_cost.errors %}
|
{% if form.delivery_cost.errors %}
|
||||||
<div class="text-danger">{{ form.delivery_cost.errors }}</div>
|
<div class="text-danger">{{ form.delivery_cost.errors }}</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<small class="d-block text-muted mt-1">
|
||||||
|
<i class="bi bi-info-circle"></i>
|
||||||
|
Оставьте пустым для автоматического расчета (бесплатно от 100 руб., иначе 15 руб.)
|
||||||
|
</small>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -666,11 +709,19 @@ function initCustomerSelect2() {
|
|||||||
|
|
||||||
// Обработчик для перехвата ПЕРЕД выбором (используется для фальшивых опций)
|
// Обработчик для перехвата ПЕРЕД выбором (используется для фальшивых опций)
|
||||||
$customerSelect.on('select2:selecting', function(e) {
|
$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;
|
const data = e.params.data;
|
||||||
console.log('9a. Попытка выбрать элемент (перед выбором):', data);
|
console.log('9b. Попытка выбрать элемент (перед выбором):', data);
|
||||||
|
|
||||||
if (data.is_create_option) {
|
if (data.is_create_option) {
|
||||||
console.log('9b. Это опция создания клиента - предотвращаем выбор и открываем модаль');
|
console.log('9c. Это опция создания клиента - предотвращаем выбор и открываем модаль');
|
||||||
// Предотвращаем выбор этой опции
|
// Предотвращаем выбор этой опции
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
// Очищаем значение
|
// Очищаем значение
|
||||||
@@ -679,48 +730,21 @@ function initCustomerSelect2() {
|
|||||||
window.openCreateCustomerModal(data.search_text);
|
window.openCreateCustomerModal(data.search_text);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
// Обработчик прямого клика на результаты (для "create option")
|
console.log('9d. Обычный клиент, разрешаем выбор');
|
||||||
$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);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
$customerSelect.on('select2:select', function(e) {
|
$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;
|
const data = e.params.data;
|
||||||
console.log('10. Выбран элемент:', data);
|
console.log('10b. Выбран элемент:', data);
|
||||||
|
|
||||||
if (data.is_create_option) {
|
if (data.is_create_option) {
|
||||||
console.log('11. Открываем модальное окно для создания клиента');
|
console.log('11. Открываем модальное окно для создания клиента');
|
||||||
@@ -731,7 +755,7 @@ function initCustomerSelect2() {
|
|||||||
window.openCreateCustomerModal(data.search_text);
|
window.openCreateCustomerModal(data.search_text);
|
||||||
} else {
|
} else {
|
||||||
// Триггерим нативное change событие для других обработчиков (например, draft-creator.js)
|
// Триггерим нативное change событие для других обработчиков (например, draft-creator.js)
|
||||||
console.log('12. Триггерим нативное change событие');
|
console.log('12. Триггерим нативное change событие для customer ID:', data.id);
|
||||||
const changeEvent = new Event('change', { bubbles: true });
|
const changeEvent = new Event('change', { bubbles: true });
|
||||||
this.dispatchEvent(changeEvent);
|
this.dispatchEvent(changeEvent);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user