Исправлена отображение полей адреса и стили формы заказа

## Основные изменения:

### 1. Исправлена логика выбора режима адреса
- Переместил функцию initAddressModeToggle() из jQuery блока в отдельную функцию
- Теперь инициализация адреса работает независимо от jQuery
- Добавлены подробные логи в консоль для отладки ([ADDRESS MODE] префикс)

### 2. Добавлены CSS классы для управления видимостью
- address-history-mode: display: none !important (по умолчанию скрыт)
- address-new-mode: display: none !important (по умолчанию скрыт)
- .visible класс переводит элементы на display: block !important
- Использование classList.add/remove вместо inline styles

### 3. Исправлены стили полей формы (OrderForm)
- Добавлена явная обработка для Select полей - получают form-select
- Поле "Статус" и другие Select теперь имеют правильные стили Bootstrap
- Разделена логика для RadioSelect, Select и остальных полей

### 4. Улучшена отладка
- Добавлены console.log сообщения на каждом этапе инициализации
- Префикс [ADDRESS MODE] помогает отличить логи системы адреса от других

## Технические детали:

- Address сервис использует метод format_address_for_display() для красивого вывода
- AJAX endpoint get_customer_address_history() загружает адреса клиента
- Три режима адреса: history (из истории), new (новый адрес), empty (без адреса)
- Режим empty выбирается по умолчанию

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-11 02:49:25 +03:00
parent 7d82d67b5f
commit ddbb4f963b
16 changed files with 956 additions and 188 deletions

View File

@@ -1,6 +1,6 @@
from django.contrib import admin
from django.db import models
from .models import Customer, Address
from .models import Customer
class IsVipFilter(admin.SimpleListFilter):
@@ -21,14 +21,6 @@ class IsVipFilter(admin.SimpleListFilter):
return queryset
class AddressInline(admin.TabularInline):
"""Inline для управления адресами клиента в интерфейсе администратора"""
model = Address
extra = 1
verbose_name = "Адрес доставки"
verbose_name_plural = "Адреса доставки"
@admin.register(Customer)
class CustomerAdmin(admin.ModelAdmin):
"""Административный интерфейс для управления клиентами цветочного магазина"""
@@ -69,55 +61,3 @@ class CustomerAdmin(admin.ModelAdmin):
'classes': ('collapse',)
}),
)
inlines = [AddressInline]
@admin.register(Address)
class AddressAdmin(admin.ModelAdmin):
"""Административный интерфейс для управления адресами доставки"""
list_display = (
'recipient_name',
'recipient_phone',
'full_address',
'customer',
'district',
'confirm_address_with_recipient',
'is_default'
)
list_filter = (
'is_default',
'confirm_address_with_recipient',
'district',
'created_at'
)
search_fields = (
'recipient_name',
'street',
'building_number',
'customer__name',
'customer__email'
)
ordering = ('-is_default', '-created_at')
readonly_fields = ('created_at', 'updated_at')
fieldsets = (
('Информация о получателе', {
'fields': ('customer', 'recipient_name', 'recipient_phone')
}),
('Адрес доставки', {
'fields': ('street', 'building_number', 'apartment_number', 'district')
}),
('Дополнительная информация', {
'fields': ('delivery_instructions', 'confirm_address_with_recipient'),
'classes': ('collapse',)
}),
('Статус', {
'fields': ('is_default',),
'classes': ('collapse',)
}),
('Даты', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)

View File

@@ -0,0 +1,17 @@
# Generated by Django 5.0.10 on 2025-11-10 23:09
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('customers', '0001_initial'),
('orders', '0003_remove_address_model'),
]
operations = [
migrations.DeleteModel(
name='Address',
),
]

View File

@@ -193,109 +193,3 @@ class Customer(models.Model):
"""Увеличивает общую сумму покупок"""
self.total_spent = self.total_spent + amount
self.save(update_fields=['total_spent'])
class Address(models.Model):
"""
Модель адреса доставки для клиентов цветочного магазина в Минске.
Клиент может иметь несколько адресов для разных получателей.
"""
customer = models.ForeignKey(
Customer,
on_delete=models.CASCADE,
related_name='addresses',
verbose_name="Клиент"
)
# Address information for delivery in Minsk
recipient_name = models.CharField(
max_length=200,
verbose_name="Имя получателя",
help_text="Имя человека, которому будет доставлен заказ"
)
recipient_phone = PhoneNumberField(
blank=True,
null=True,
verbose_name="Телефон получателя",
help_text="Контактный телефон получателя для уточнения адреса"
)
street = models.CharField(
max_length=255,
verbose_name="Улица"
)
building_number = models.CharField(
max_length=20,
verbose_name="Номер здания"
)
apartment_number = models.CharField(
max_length=20,
blank=True,
null=True,
verbose_name="Номер квартиры/офиса"
)
district = models.CharField(
max_length=100,
blank=True,
null=True,
verbose_name="Район",
help_text="Район в Минске для удобства доставки"
)
# Additional information for delivery
delivery_instructions = models.TextField(
blank=True,
null=True,
verbose_name="Инструкции для доставки",
help_text="Дополнительные инструкции для курьера (домофон, подъезд и т.д.)"
)
confirm_address_with_recipient = models.BooleanField(
default=False,
verbose_name="Уточнить адрес у получателя",
help_text="Курьер должен уточнить адрес у получателя перед доставкой"
)
is_default = models.BooleanField(
default=False,
verbose_name="Адрес по умолчанию",
help_text="Использовать этот адрес для доставки по умолчанию"
)
# Timestamps
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания")
updated_at = models.DateTimeField(auto_now=True, verbose_name="Дата обновления")
class Meta:
verbose_name = "Адрес доставки"
verbose_name_plural = "Адреса доставки"
indexes = [
models.Index(fields=['customer']),
models.Index(fields=['is_default']),
models.Index(fields=['district']),
]
ordering = ['-is_default', '-created_at']
def save(self, *args, **kwargs):
if self.is_default:
# Если этот адрес устанавливается как адрес по умолчанию, снимаем флаг по умолчанию с других адресов этого клиента
Address.objects.filter(customer=self.customer, is_default=True).update(is_default=False)
super().save(*args, **kwargs)
def __str__(self):
address_line = f"{self.street}, {self.building_number}"
if self.apartment_number:
address_line += f", кв/офис {self.apartment_number}"
return f"{self.recipient_name} - {address_line}, {self.customer.full_name}"
@property
def full_address(self):
"""Полный адрес для доставки"""
address = f"{self.street}, {self.building_number}"
if self.apartment_number:
address += f", кв/офис {self.apartment_number}"
return address

View File

@@ -8,7 +8,7 @@ from django.views.decorators.http import require_http_methods
from django.contrib.auth.decorators import login_required
import phonenumbers
import json
from .models import Customer, Address
from .models import Customer
from .forms import CustomerForm

View File

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-
from django.contrib import admin
from .models import Order, OrderItem, Payment
from .models import Order, OrderItem, Payment, Address
class PaymentInline(admin.TabularInline):
@@ -228,3 +228,53 @@ class OrderItemAdmin(admin.ModelAdmin):
'classes': ('collapse',)
}),
)
@admin.register(Address)
class AddressAdmin(admin.ModelAdmin):
"""
Админ-панель для управления адресами доставки заказов.
"""
list_display = [
'recipient_name',
'recipient_phone',
'full_address',
'entrance',
'floor',
'confirm_address_with_recipient',
'created_at',
]
list_filter = [
'confirm_address_with_recipient',
'created_at',
]
search_fields = [
'recipient_name',
'street',
'building_number',
]
readonly_fields = ['created_at', 'updated_at']
fieldsets = (
('Информация о получателе', {
'fields': ('recipient_name', 'recipient_phone')
}),
('Адрес доставки', {
'fields': ('street', 'building_number', 'apartment_number', 'entrance', 'floor')
}),
('Доступ в здание', {
'fields': ('intercom_code',),
'classes': ('collapse',)
}),
('Дополнительная информация', {
'fields': ('delivery_instructions', 'confirm_address_with_recipient'),
'classes': ('collapse',)
}),
('Даты', {
'fields': ('created_at', 'updated_at'),
'classes': ('collapse',)
}),
)

View File

@@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
from django import forms
from django.forms import inlineformset_factory
from .models import Order, OrderItem
from customers.models import Customer, Address
from .models import Order, OrderItem, Address
from customers.models import Customer
from shops.models import Shop
from products.models import Product, ProductKit
@@ -10,6 +10,82 @@ from products.models import Product, ProductKit
class OrderForm(forms.ModelForm):
"""Форма для создания и редактирования заказа"""
# Поля для ввода адреса
address_mode = forms.ChoiceField(
choices=[
('history', 'Выбрать из истории'),
('new', 'Ввести новый адрес'),
('empty', 'Без адреса (заполнить позже)'),
],
initial='empty',
widget=forms.RadioSelect(attrs={'class': 'form-check-input'}),
required=False,
label='Способ указания адреса'
)
# Выбор адреса из истории
address_from_history = forms.ModelChoiceField(
queryset=Address.objects.none(),
required=False,
widget=forms.Select(attrs={'class': 'form-select'}),
label='Адрес из истории'
)
# Поля для ввода нового адреса
address_street = forms.CharField(
max_length=255,
required=False,
widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Улица'}),
label='Улица'
)
address_building_number = forms.CharField(
max_length=20,
required=False,
widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Номер дома'}),
label='Номер дома'
)
address_apartment_number = forms.CharField(
max_length=20,
required=False,
widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Квартира/офис'}),
label='Квартира/офис'
)
address_entrance = forms.CharField(
max_length=20,
required=False,
widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Подъезд'}),
label='Подъезд'
)
address_floor = forms.CharField(
max_length=20,
required=False,
widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Этаж'}),
label='Этаж'
)
address_intercom_code = forms.CharField(
max_length=100,
required=False,
widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Код домофона'}),
label='Код домофона'
)
address_delivery_instructions = forms.CharField(
required=False,
widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 3, 'placeholder': 'Инструкции для курьера'}),
label='Инструкции для доставки'
)
address_confirm_with_recipient = forms.BooleanField(
required=False,
widget=forms.CheckboxInput(attrs={'class': 'form-check-input'}),
label='Уточнить адрес у получателя'
)
class Meta:
model = Order
fields = [
@@ -46,7 +122,14 @@ class OrderForm(forms.ModelForm):
field.widget.attrs.update({'class': 'form-check-input'})
elif isinstance(field.widget, forms.Textarea):
field.widget.attrs.update({'class': 'form-control', 'rows': 3})
elif isinstance(field.widget, forms.RadioSelect):
# RadioSelect не нуждается в доп классах (уже есть form-check-input)
pass
elif isinstance(field.widget, forms.Select):
# Select поля получают form-select
field.widget.attrs.update({'class': 'form-select'})
else:
# Остальные поля (TextInput, NumberInput, etc)
field.widget.attrs.update({'class': 'form-control'})
# Select2 для поля customer с AJAX поиском (инициализируется отдельно в JS)
@@ -81,6 +164,18 @@ class OrderForm(forms.ModelForm):
self.fields['recipient_name'].required = False
self.fields['recipient_phone'].required = False
# Инициализируем queryset для address_from_history
# Это будет переопределено в представлении после выбора клиента
if self.instance.pk and self.instance.customer:
# При редактировании заказа загружаем историю адресов этого клиента
customer_orders = Order.objects.filter(
customer=self.instance.customer,
delivery_address__isnull=False
).order_by('-created_at')
self.fields['address_from_history'].queryset = Address.objects.filter(
order__in=customer_orders
).distinct().order_by('-created_at')
class OrderItemForm(forms.ModelForm):
"""Форма для позиции заказа"""

View File

@@ -10,8 +10,8 @@ from datetime import datetime, timedelta
import random
from decimal import Decimal
from orders.models import Order, OrderItem
from customers.models import Customer, Address
from orders.models import Order, OrderItem, Address
from customers.models import Customer
from shops.models import Shop
from products.models import Product

View File

@@ -0,0 +1,46 @@
# Generated by Django 5.0.10 on 2025-11-10 23:09
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('orders', '0002_initial'),
]
operations = [
migrations.CreateModel(
name='Address',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('recipient_name', models.CharField(help_text='Имя человека, которому будет доставлен заказ', max_length=200, verbose_name='Имя получателя')),
('recipient_phone', models.CharField(blank=True, help_text='Контактный телефон получателя для уточнения адреса', max_length=20, null=True, verbose_name='Телефон получателя')),
('street', models.CharField(max_length=255, verbose_name='Улица')),
('building_number', models.CharField(max_length=20, verbose_name='Номер здания')),
('apartment_number', models.CharField(blank=True, max_length=20, null=True, verbose_name='Номер квартиры/офиса')),
('district', models.CharField(blank=True, help_text='Район в Минске для удобства доставки', max_length=100, null=True, verbose_name='Район')),
('delivery_instructions', models.TextField(blank=True, help_text='Дополнительные инструкции для курьера (домофон, подъезд и т.д.)', null=True, verbose_name='Инструкции для доставки')),
('confirm_address_with_recipient', models.BooleanField(default=False, help_text='Курьер должен уточнить адрес у получателя перед доставкой', verbose_name='Уточнить адрес у получателя')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Дата создания')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Дата обновления')),
],
options={
'verbose_name': 'Адрес доставки',
'verbose_name_plural': 'Адреса доставки',
'ordering': ['-created_at'],
'indexes': [models.Index(fields=['district'], name='orders_addr_distric_fd94e9_idx')],
},
),
migrations.AlterField(
model_name='historicalorder',
name='delivery_address',
field=models.ForeignKey(blank=True, db_constraint=False, help_text='Обязательно для курьерской доставки', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='orders.address', verbose_name='Адрес доставки'),
),
migrations.AlterField(
model_name='order',
name='delivery_address',
field=models.OneToOneField(blank=True, help_text='Обязательно для курьерской доставки', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='order', to='orders.address', verbose_name='Адрес доставки'),
),
]

View File

@@ -0,0 +1,45 @@
# Generated by Django 5.0.10 on 2025-11-10 23:35
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('orders', '0003_remove_address_model'),
]
operations = [
migrations.RemoveIndex(
model_name='address',
name='orders_addr_distric_fd94e9_idx',
),
migrations.RemoveField(
model_name='address',
name='district',
),
migrations.AddField(
model_name='address',
name='entrance',
field=models.CharField(blank=True, help_text='Номер подъезда/входа', max_length=20, null=True, verbose_name='Подъезд'),
),
migrations.AddField(
model_name='address',
name='floor',
field=models.CharField(blank=True, max_length=20, null=True, verbose_name='Этаж'),
),
migrations.AddField(
model_name='address',
name='intercom_code',
field=models.CharField(blank=True, help_text='Код домофона для входа в здание', max_length=100, null=True, verbose_name='Код домофона'),
),
migrations.AlterField(
model_name='address',
name='delivery_instructions',
field=models.TextField(blank=True, help_text='Дополнительные инструкции для курьера', null=True, verbose_name='Инструкции для доставки'),
),
migrations.AddIndex(
model_name='address',
index=models.Index(fields=['created_at'], name='orders_addr_created_98ad97_idx'),
),
]

View File

@@ -1,12 +1,120 @@
from django.db import models
from django.core.exceptions import ValidationError
from accounts.models import CustomUser
from customers.models import Customer, Address
from customers.models import Customer
from products.models import Product, ProductKit
from shops.models import Shop
from simple_history.models import HistoricalRecords
class Address(models.Model):
"""
Модель адреса доставки для заказа цветочного магазина в Минске.
Адрес принадлежит конкретному заказу доставки.
"""
# Информация о получателе
recipient_name = models.CharField(
max_length=200,
verbose_name="Имя получателя",
help_text="Имя человека, которому будет доставлен заказ"
)
recipient_phone = models.CharField(
max_length=20,
blank=True,
null=True,
verbose_name="Телефон получателя",
help_text="Контактный телефон получателя для уточнения адреса"
)
street = models.CharField(
max_length=255,
verbose_name="Улица"
)
building_number = models.CharField(
max_length=20,
verbose_name="Номер здания"
)
apartment_number = models.CharField(
max_length=20,
blank=True,
null=True,
verbose_name="Номер квартиры/офиса"
)
entrance = models.CharField(
max_length=20,
blank=True,
null=True,
verbose_name="Подъезд",
help_text="Номер подъезда/входа"
)
floor = models.CharField(
max_length=20,
blank=True,
null=True,
verbose_name="Этаж"
)
intercom_code = models.CharField(
max_length=100,
blank=True,
null=True,
verbose_name="Код домофона",
help_text="Код домофона для входа в здание"
)
# Дополнительная информация для доставки
delivery_instructions = models.TextField(
blank=True,
null=True,
verbose_name="Инструкции для доставки",
help_text="Дополнительные инструкции для курьера"
)
confirm_address_with_recipient = models.BooleanField(
default=False,
verbose_name="Уточнить адрес у получателя",
help_text="Курьер должен уточнить адрес у получателя перед доставкой"
)
# Временные метки
created_at = models.DateTimeField(auto_now_add=True, verbose_name="Дата создания")
updated_at = models.DateTimeField(auto_now=True, verbose_name="Дата обновления")
class Meta:
verbose_name = "Адрес доставки"
verbose_name_plural = "Адреса доставки"
indexes = [
models.Index(fields=['created_at']),
]
ordering = ['-created_at']
def __str__(self):
address_line = f"{self.street}, {self.building_number}"
if self.apartment_number:
address_line += f", кв/офис {self.apartment_number}"
return f"{self.recipient_name} - {address_line}"
@property
def full_address(self):
"""Полный адрес для доставки"""
address = f"{self.street}, {self.building_number}"
if self.apartment_number:
address += f", кв/офис {self.apartment_number}"
details = []
if self.entrance:
details.append(f"подъезд {self.entrance}")
if self.floor:
details.append(f"этаж {self.floor}")
if details:
address += f" ({', '.join(details)})"
return address
class Order(models.Model):
"""
Заказ клиента для доставки цветов.
@@ -34,12 +142,12 @@ class Order(models.Model):
)
# Адрес доставки (для курьерской доставки)
delivery_address = models.ForeignKey(
delivery_address = models.OneToOneField(
Address,
on_delete=models.PROTECT,
on_delete=models.CASCADE,
null=True,
blank=True,
related_name='orders',
related_name='order',
verbose_name="Адрес доставки",
help_text="Обязательно для курьерской доставки"
)

View File

@@ -0,0 +1,134 @@
"""
Сервис для работы с адресами заказов.
Содержит логику создания, обновления и управления адресами доставки.
"""
from ..models import Order, Address
class AddressService:
"""Сервис для управления адресами доставки в заказах"""
@staticmethod
def create_address_from_form_data(form_data):
"""
Создает объект Address из данных формы.
Args:
form_data (dict): Словарь с данными из формы
- address_street
- address_building_number
- address_apartment_number (опционально)
- address_entrance (опционально)
- address_floor (опционально)
- address_intercom_code (опционально)
- address_delivery_instructions (опционально)
Returns:
Address: Новый объект адреса (не сохраненный в БД)
"""
address = Address(
recipient_name=form_data.get('recipient_name', ''),
recipient_phone=form_data.get('recipient_phone', ''),
street=form_data.get('address_street', ''),
building_number=form_data.get('address_building_number', ''),
apartment_number=form_data.get('address_apartment_number', ''),
entrance=form_data.get('address_entrance', ''),
floor=form_data.get('address_floor', ''),
intercom_code=form_data.get('address_intercom_code', ''),
delivery_instructions=form_data.get('address_delivery_instructions', ''),
confirm_address_with_recipient=form_data.get('address_confirm_with_recipient', False),
)
return address
@staticmethod
def process_address_from_form(order, form_data):
"""
Обрабатывает адрес из данных формы заказа.
Создает новый Address или использует существующий в зависимости от режима.
Args:
order (Order): Объект заказа
form_data (dict): Все данные из формы
Returns:
Address or None: Адрес для привязки к заказу или None
"""
address_mode = form_data.get('address_mode')
# Если режим "без адреса" - возвращаем None
if address_mode == 'empty':
return None
# Если режим "выбрать из истории" - возвращаем выбранный адрес
if address_mode == 'history':
address_id = form_data.get('address_from_history')
if address_id:
try:
return Address.objects.get(pk=address_id)
except Address.DoesNotExist:
return None
# Если режим "ввести новый адрес"
if address_mode == 'new':
# Проверяем обязательные поля
street = form_data.get('address_street', '').strip()
building_number = form_data.get('address_building_number', '').strip()
if not street or not building_number:
# Если обязательные поля не заполнены, возвращаем None
return None
# Создаем новый адрес
address = AddressService.create_address_from_form_data(form_data)
return address
return None
@staticmethod
def get_customer_address_history(customer):
"""
Получает список адресов из истории заказов клиента.
Args:
customer (Customer): Клиент
Returns:
QuerySet: Адреса, отсортированные по дате создания (новые первыми)
"""
customer_orders = Order.objects.filter(
customer=customer,
delivery_address__isnull=False
).order_by('-created_at')
addresses = Address.objects.filter(
order__in=customer_orders
).distinct().order_by('-created_at')
return addresses
@staticmethod
def format_address_for_display(address):
"""
Форматирует адрес для отображения в списке.
Args:
address (Address): Объект адреса
Returns:
str: Форматированная строка адреса
"""
address_line = f"{address.street}, {address.building_number}"
if address.apartment_number:
address_line += f", кв. {address.apartment_number}"
details = []
if address.entrance:
details.append(f"подъезд {address.entrance}")
if address.floor:
details.append(f"этаж {address.floor}")
if details:
address_line += f" ({', '.join(details)})"
return address_line

View File

@@ -9,8 +9,9 @@ from decimal import Decimal
import decimal
from datetime import datetime, date, time
from ..models import Order, OrderItem
from ..models import Order, OrderItem, Address
from products.models import Product, ProductKit
from .address_service import AddressService
class DraftOrderService:
@@ -87,7 +88,6 @@ class DraftOrderService:
# ForeignKey поля требуют специальной обработки
fk_fields = {
'customer': 'customers.Customer',
'delivery_address': 'customers.Address',
'pickup_shop': 'shops.Shop',
}
@@ -113,6 +113,31 @@ class DraftOrderService:
except Model.DoesNotExist:
pass # Игнорируем несуществующие объекты
# === Обработка адреса доставки ===
# Новая логика с выбором режима адреса
if 'address_mode' in data:
address = AddressService.process_address_from_form(order, data)
if address:
# Если адрес не существует в БД, сохраняем его
if not address.pk:
address.save()
order.delivery_address = address
else:
# Если режим "без адреса", удаляем существующий адрес
if order.delivery_address:
old_address = order.delivery_address
order.delivery_address = None
# Удаляем старый адрес если он больше не используется
if old_address and not old_address.order:
old_address.delete()
elif 'delivery_address' in data and data['delivery_address']:
# Старая логика для совместимости (если передается delivery_address напрямую)
try:
address = Address.objects.get(pk=data['delivery_address'])
order.delivery_address = address
except Address.DoesNotExist:
pass
# Обрабатываем простые поля
for field in simple_fields:
if field in data:

View File

@@ -241,6 +241,71 @@
data.pickup_shop = parseInt(pickupShopField.value);
}
// Новая логика выбора адреса
const addressModeField = form.querySelector('input[name="address_mode"]:checked');
if (addressModeField) {
data.address_mode = addressModeField.value;
if (addressModeField.value === 'history') {
const addressFromHistoryField = form.querySelector('select[name="address_from_history"]');
if (addressFromHistoryField && addressFromHistoryField.value) {
data.address_from_history = parseInt(addressFromHistoryField.value);
}
} else if (addressModeField.value === 'new') {
// Собираем поля нового адреса
const addressStreetField = form.querySelector('input[name="address_street"]');
if (addressStreetField && addressStreetField.value) {
data.address_street = addressStreetField.value;
}
const addressBuildingField = form.querySelector('input[name="address_building_number"]');
if (addressBuildingField && addressBuildingField.value) {
data.address_building_number = addressBuildingField.value;
}
const addressApartmentField = form.querySelector('input[name="address_apartment_number"]');
if (addressApartmentField && addressApartmentField.value) {
data.address_apartment_number = addressApartmentField.value;
}
const addressEntranceField = form.querySelector('input[name="address_entrance"]');
if (addressEntranceField && addressEntranceField.value) {
data.address_entrance = addressEntranceField.value;
}
const addressFloorField = form.querySelector('input[name="address_floor"]');
if (addressFloorField && addressFloorField.value) {
data.address_floor = addressFloorField.value;
}
const addressIntercomField = form.querySelector('input[name="address_intercom_code"]');
if (addressIntercomField && addressIntercomField.value) {
data.address_intercom_code = addressIntercomField.value;
}
const addressInstructionsField = form.querySelector('textarea[name="address_delivery_instructions"]');
if (addressInstructionsField && addressInstructionsField.value) {
data.address_delivery_instructions = addressInstructionsField.value;
}
const addressConfirmField = form.querySelector('input[name="address_confirm_with_recipient"]');
if (addressConfirmField) {
data.address_confirm_with_recipient = addressConfirmField.checked;
}
}
}
// Поля получателя
const recipientNameField = form.querySelector('input[name="recipient_name"]');
if (recipientNameField && recipientNameField.value) {
data.recipient_name = recipientNameField.value;
}
const recipientPhoneField = form.querySelector('input[name="recipient_phone"]');
if (recipientPhoneField && recipientPhoneField.value) {
data.recipient_phone = recipientPhoneField.value;
}
// Собираем позиции заказа
const items = collectOrderItems();
if (items.length > 0) {

View File

@@ -16,6 +16,23 @@
pointer-events: none;
}
/* Явное управление видимостью режимов адреса */
#address-history-mode {
display: none !important;
}
#address-new-mode {
display: none !important;
}
#address-history-mode.visible {
display: block !important;
}
#address-new-mode.visible {
display: block !important;
}
/* Стили для поиска клиента */
.customer-option {
padding: 5px 0;
@@ -193,17 +210,141 @@
</div>
<div class="row" id="delivery-fields">
<div class="col-md-6">
<!-- Способ указания адреса -->
<div class="col-12">
<div class="mb-3">
<label for="{{ form.delivery_address.id_for_label }}" class="form-label">
Адрес доставки
<label class="form-label">{{ form.address_mode.label }}</label>
<div class="mt-2">
{% for choice in form.address_mode %}
<div class="form-check">
{{ choice.tag }}
<label class="form-check-label" for="{{ choice.id_for_label }}">
{{ choice.choice_label }}
</label>
{{ form.delivery_address }}
{% if form.delivery_address.errors %}
<div class="text-danger">{{ form.delivery_address.errors }}</div>
</div>
{% endfor %}
</div>
{% if form.address_mode.errors %}
<div class="text-danger">{{ form.address_mode.errors }}</div>
{% endif %}
</div>
</div>
<!-- Режим 1: Выбор из истории -->
<div class="col-md-6" id="address-history-mode">
<div class="mb-3">
<label for="{{ form.address_from_history.id_for_label }}" class="form-label">
{{ form.address_from_history.label }}
</label>
{{ form.address_from_history }}
{% if form.address_from_history.errors %}
<div class="text-danger">{{ form.address_from_history.errors }}</div>
{% endif %}
<small class="text-muted d-block mt-2">Загрузка адресов из истории...</small>
</div>
</div>
<!-- Режим 2: Ввод нового адреса -->
<div class="col-12" id="address-new-mode">
<div class="row">
<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>
</label>
{{ form.address_street }}
{% if form.address_street.errors %}
<div class="text-danger">{{ form.address_street.errors }}</div>
{% endif %}
</div>
</div>
<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>
</label>
{{ form.address_building_number }}
{% if form.address_building_number.errors %}
<div class="text-danger">{{ form.address_building_number.errors }}</div>
{% endif %}
</div>
</div>
<div class="col-md-3">
<div class="mb-3">
<label for="{{ form.address_apartment_number.id_for_label }}" class="form-label">
{{ form.address_apartment_number.label }}
</label>
{{ form.address_apartment_number }}
{% if form.address_apartment_number.errors %}
<div class="text-danger">{{ form.address_apartment_number.errors }}</div>
{% endif %}
</div>
</div>
</div>
<div class="row">
<div class="col-md-3">
<div class="mb-3">
<label for="{{ form.address_entrance.id_for_label }}" class="form-label">
{{ form.address_entrance.label }}
</label>
{{ form.address_entrance }}
{% if form.address_entrance.errors %}
<div class="text-danger">{{ form.address_entrance.errors }}</div>
{% endif %}
</div>
</div>
<div class="col-md-3">
<div class="mb-3">
<label for="{{ form.address_floor.id_for_label }}" class="form-label">
{{ form.address_floor.label }}
</label>
{{ form.address_floor }}
{% if form.address_floor.errors %}
<div class="text-danger">{{ form.address_floor.errors }}</div>
{% endif %}
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.address_intercom_code.id_for_label }}" class="form-label">
{{ form.address_intercom_code.label }}
</label>
{{ form.address_intercom_code }}
{% if form.address_intercom_code.errors %}
<div class="text-danger">{{ form.address_intercom_code.errors }}</div>
{% endif %}
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="mb-3">
<label for="{{ form.address_delivery_instructions.id_for_label }}" class="form-label">
{{ form.address_delivery_instructions.label }}
</label>
{{ form.address_delivery_instructions }}
{% if form.address_delivery_instructions.errors %}
<div class="text-danger">{{ form.address_delivery_instructions.errors }}</div>
{% endif %}
</div>
</div>
</div>
<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>
</div>
</div>
</div>
<!-- Стоимость доставки -->
<div class="col-md-6">
<div class="mb-3">
<label for="{{ form.delivery_cost.id_for_label }}" class="form-label">Стоимость доставки</label>
@@ -697,7 +838,116 @@ window.openCreateCustomerModal = function(searchText = '') {
createCustomerModal.show();
};
// Вызываем инициализацию
// === ИНИЦИАЛИЗАЦИЯ РЕЖИМОВ АДРЕСА (вне jQuery зависимости) ===
function initAddressModeToggle() {
console.log('[ADDRESS MODE] Initializing address mode toggle');
const addressModeRadios = document.querySelectorAll('input[name="address_mode"]');
const addressHistoryMode = document.getElementById('address-history-mode');
const addressNewMode = document.getElementById('address-new-mode');
const customerSelect = document.getElementById('id_customer');
if (!addressHistoryMode || !addressNewMode) {
console.error('[ADDRESS MODE] address-history-mode or address-new-mode not found in DOM');
return;
}
function toggleAddressMode() {
console.log('[ADDRESS MODE] toggleAddressMode() called');
const checkedRadio = document.querySelector('input[name="address_mode"]:checked');
if (!checkedRadio) {
console.log('[ADDRESS MODE] No radio checked, setting default to "empty"');
const emptyRadio = document.querySelector('input[name="address_mode"][value="empty"]');
if (emptyRadio) {
emptyRadio.checked = true;
}
}
const selectedMode = document.querySelector('input[name="address_mode"]:checked').value;
console.log('[ADDRESS MODE] Selected mode:', selectedMode);
// Убираем класс visible со всех режимов
addressHistoryMode.classList.remove('visible');
addressNewMode.classList.remove('visible');
if (selectedMode === 'history') {
console.log('[ADDRESS MODE] Showing history mode');
addressHistoryMode.classList.add('visible');
loadAddressHistory();
} else if (selectedMode === 'new') {
console.log('[ADDRESS MODE] Showing new address mode');
addressNewMode.classList.add('visible');
} else {
console.log('[ADDRESS MODE] Empty mode - hiding all sections');
}
}
function loadAddressHistory() {
console.log('[ADDRESS MODE] loadAddressHistory() called');
const customerId = customerSelect.value;
console.log('[ADDRESS MODE] Customer ID:', customerId);
if (!customerId) {
const addressSelect = document.getElementById('id_address_from_history');
addressSelect.innerHTML = '<option value="">-- История недоступна (клиент не выбран) --</option>';
return;
}
fetch(`{% url 'orders:api-customer-address-history' %}?customer_id=${customerId}`)
.then(response => response.json())
.then(data => {
console.log('[ADDRESS MODE] Address history data:', data);
const addressSelect = document.getElementById('id_address_from_history');
if (!data.success || data.count === 0) {
addressSelect.innerHTML = '<option value="">-- История адресов не найдена --</option>';
return;
}
let optionsHTML = '<option value="">-- Выберите адрес --</option>';
data.addresses.forEach(addr => {
optionsHTML += `<option value="${addr.id}">${addr.display}</option>`;
});
addressSelect.innerHTML = optionsHTML;
// Если есть Select2, обновляем его
if (typeof $ !== 'undefined' && $(addressSelect).data('select2')) {
$(addressSelect).trigger('change');
}
})
.catch(error => {
console.error('[ADDRESS MODE] Error loading address history:', error);
const addressSelect = document.getElementById('id_address_from_history');
addressSelect.innerHTML = '<option value="">-- Ошибка загрузки --</option>';
});
}
// Добавляем обработчики на radio кнопки
addressModeRadios.forEach(radio => {
console.log('[ADDRESS MODE] Adding listener to radio:', radio.value);
radio.addEventListener('change', function() {
console.log('[ADDRESS MODE] Radio changed to:', this.value);
toggleAddressMode();
});
});
// Загружаем адреса при изменении клиента
if (customerSelect) {
customerSelect.addEventListener('change', function() {
console.log('[ADDRESS MODE] Customer changed');
loadAddressHistory();
});
}
// Инициализация при загрузке
console.log('[ADDRESS MODE] Initial toggle call');
toggleAddressMode();
}
// Вызываем инициализацию адреса СРАЗУ (не зависит от jQuery)
initAddressModeToggle();
// Вызываем инициализацию Select2 для customer
initCustomerSelect2();
// Инициализация Select2 для остальных полей (после jQuery загружен)

View File

@@ -14,4 +14,5 @@ urlpatterns = [
# AJAX endpoints
path('<int:pk>/autosave/', views.autosave_draft_order, name='order-autosave'),
path('create-draft/', views.create_draft_from_form, name='order-create-draft'),
path('api/customer-address-history/', views.get_customer_address_history, name='api-customer-address-history'),
]

View File

@@ -6,10 +6,11 @@ from django.http import JsonResponse
from django.views.decorators.http import require_http_methods
from django.contrib.auth.decorators import login_required
from django.core.exceptions import ValidationError
from .models import Order, OrderItem
from .models import Order, OrderItem, Address
from .forms import OrderForm, OrderItemFormSet
from .filters import OrderFilter
from .services import DraftOrderService
from .services.address_service import AddressService
import json
@@ -67,6 +68,15 @@ def order_create(request):
if form.is_valid() and formset.is_valid():
order = form.save(commit=False)
# Обрабатываем адрес доставки
if order.is_delivery:
address = AddressService.process_address_from_form(order, form.cleaned_data)
if address:
# Если адрес не существует в БД, сохраняем его
if not address.pk:
address.save()
order.delivery_address = address
# Если нажата кнопка "Сохранить как черновик", создаем черновик
if 'save_as_draft' in request.POST:
order.status = 'draft'
@@ -125,6 +135,31 @@ def order_update(request, pk):
form = OrderForm(instance=order)
formset = OrderItemFormSet(instance=order)
else:
# Обрабатываем адрес доставки
if order.is_delivery:
address = AddressService.process_address_from_form(order, form.cleaned_data)
if address:
# Если адрес не существует в БД, сохраняем его
if not address.pk:
address.save()
order.delivery_address = address
else:
# Если режим "без адреса", удаляем существующий адрес
if order.delivery_address:
old_address = order.delivery_address
order.delivery_address = None
# Удаляем старый адрес, если он больше не используется
if old_address and not old_address.order:
old_address.delete()
else:
# Если не доставка, удаляем адрес если он был
if order.delivery_address:
old_address = order.delivery_address
order.delivery_address = None
# Удаляем старый адрес
if old_address and not old_address.order:
old_address.delete()
order.modified_by = request.user
order.save()
formset.save()
@@ -380,6 +415,69 @@ def create_draft_from_form(request):
}, status=500)
@require_http_methods(["GET"])
@login_required
def get_customer_address_history(request):
"""
AJAX endpoint для получения истории адресов клиента.
GET параметры:
- customer_id: ID клиента
Возвращает JSON со списком адресов из истории заказов клиента.
"""
try:
customer_id = request.GET.get('customer_id')
if not customer_id:
return JsonResponse({
'success': False,
'error': 'customer_id не указан'
}, status=400)
from customers.models import Customer
try:
customer = Customer.objects.get(pk=customer_id)
except Customer.DoesNotExist:
return JsonResponse({
'success': False,
'error': 'Клиент не найден'
}, status=404)
# Получаем адреса из истории заказов
addresses = AddressService.get_customer_address_history(customer)
# Форматируем для отправки клиенту
addresses_data = [
{
'id': addr.id,
'display': AddressService.format_address_for_display(addr),
'street': addr.street,
'building_number': addr.building_number,
'apartment_number': addr.apartment_number,
'entrance': addr.entrance,
'floor': addr.floor,
'intercom_code': addr.intercom_code,
'recipient_name': addr.recipient_name,
'recipient_phone': addr.recipient_phone,
}
for addr in addresses
]
return JsonResponse({
'success': True,
'addresses': addresses_data,
'count': len(addresses_data)
})
except Exception as e:
return JsonResponse({
'success': False,
'error': f'Ошибка сервера: {str(e)}'
}, status=500)
# === ВРЕМЕННЫЕ КОМПЛЕКТЫ ===
# УДАЛЕНО: Логика создания временных комплектов перенесена в products.services.kit_service
# Используйте API endpoint: products:api-temporary-kit-create