feat(orders): add recipient management and enhance order forms
- Introduced Recipient model to manage order recipients separately from customers. - Updated Order model to link to Recipient, replacing recipient_name and recipient_phone fields. - Enhanced OrderForm to include recipient selection modes: customer, history, and new. - Added AJAX endpoint to fetch recipient history for customers. - Updated admin interface to manage recipients and display recipient information in order details. - Refactored address handling to accommodate new recipient logic. - Improved demo order creation to include random recipients.
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django.contrib import admin
|
||||
from django.utils.html import format_html
|
||||
from .models import Order, OrderItem, Transaction, PaymentMethod, Address, OrderStatus
|
||||
from .models import Order, OrderItem, Transaction, PaymentMethod, Address, OrderStatus, Recipient
|
||||
|
||||
|
||||
class TransactionInline(admin.TabularInline):
|
||||
@@ -61,7 +61,7 @@ class OrderAdmin(admin.ModelAdmin):
|
||||
'customer__name',
|
||||
'customer__phone',
|
||||
'customer__email',
|
||||
'delivery_address__recipient_name',
|
||||
'recipient__name',
|
||||
'delivery_address__street',
|
||||
]
|
||||
|
||||
@@ -240,8 +240,6 @@ class AddressAdmin(admin.ModelAdmin):
|
||||
Админ-панель для управления адресами доставки заказов.
|
||||
"""
|
||||
list_display = [
|
||||
'recipient_name',
|
||||
'recipient_phone',
|
||||
'full_address',
|
||||
'entrance',
|
||||
'floor',
|
||||
@@ -255,7 +253,6 @@ class AddressAdmin(admin.ModelAdmin):
|
||||
]
|
||||
|
||||
search_fields = [
|
||||
'recipient_name',
|
||||
'street',
|
||||
'building_number',
|
||||
]
|
||||
@@ -263,9 +260,6 @@ class AddressAdmin(admin.ModelAdmin):
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
|
||||
fieldsets = (
|
||||
('Информация о получателе', {
|
||||
'fields': ('recipient_name', 'recipient_phone')
|
||||
}),
|
||||
('Адрес доставки', {
|
||||
'fields': ('street', 'building_number', 'apartment_number', 'entrance', 'floor')
|
||||
}),
|
||||
@@ -284,6 +278,39 @@ class AddressAdmin(admin.ModelAdmin):
|
||||
)
|
||||
|
||||
|
||||
@admin.register(Recipient)
|
||||
class RecipientAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
Админ-панель для управления получателями заказов.
|
||||
"""
|
||||
list_display = [
|
||||
'name',
|
||||
'phone',
|
||||
'created_at',
|
||||
]
|
||||
|
||||
list_filter = [
|
||||
'created_at',
|
||||
]
|
||||
|
||||
search_fields = [
|
||||
'name',
|
||||
'phone',
|
||||
]
|
||||
|
||||
readonly_fields = ['created_at', 'updated_at']
|
||||
|
||||
fieldsets = (
|
||||
('Информация о получателе', {
|
||||
'fields': ('name', 'phone')
|
||||
}),
|
||||
('Даты', {
|
||||
'fields': ('created_at', 'updated_at'),
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
)
|
||||
|
||||
|
||||
@admin.register(OrderStatus)
|
||||
class OrderStatusAdmin(admin.ModelAdmin):
|
||||
"""
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from django import forms
|
||||
from django.forms import inlineformset_factory
|
||||
from .models import Order, OrderItem, Transaction, Address, OrderStatus
|
||||
from .models import Order, OrderItem, Transaction, Address, OrderStatus, Recipient
|
||||
from customers.models import Customer
|
||||
from inventory.models import Warehouse
|
||||
from products.models import Product, ProductKit
|
||||
@@ -11,7 +11,43 @@ from decimal import Decimal
|
||||
class OrderForm(forms.ModelForm):
|
||||
"""Форма для создания и редактирования заказа"""
|
||||
|
||||
# Поля для ввода адреса
|
||||
# Поля для работы с получателем
|
||||
recipient_mode = forms.ChoiceField(
|
||||
choices=[
|
||||
('customer', 'Покупатель является получателем'),
|
||||
('history', 'Выбрать из истории'),
|
||||
('new', 'Другой получатель'),
|
||||
],
|
||||
initial='customer',
|
||||
widget=forms.RadioSelect(attrs={'class': 'form-check-input'}),
|
||||
required=False,
|
||||
label='Получатель'
|
||||
)
|
||||
|
||||
# Выбор получателя из истории
|
||||
recipient_from_history = forms.ModelChoiceField(
|
||||
queryset=Recipient.objects.none(),
|
||||
required=False,
|
||||
widget=forms.Select(attrs={'class': 'form-select'}),
|
||||
label='Получатель из истории'
|
||||
)
|
||||
|
||||
# Поля для нового получателя
|
||||
recipient_name = forms.CharField(
|
||||
max_length=200,
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Имя получателя'}),
|
||||
label='Имя получателя'
|
||||
)
|
||||
|
||||
recipient_phone = forms.CharField(
|
||||
max_length=20,
|
||||
required=False,
|
||||
widget=forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Телефон получателя'}),
|
||||
label='Телефон получателя'
|
||||
)
|
||||
|
||||
# Поля для работы с адресом
|
||||
address_mode = forms.ChoiceField(
|
||||
choices=[
|
||||
('history', 'Выбрать из истории'),
|
||||
@@ -99,8 +135,7 @@ class OrderForm(forms.ModelForm):
|
||||
'delivery_time_end',
|
||||
'delivery_cost',
|
||||
'customer_is_recipient',
|
||||
'recipient_name',
|
||||
'recipient_phone',
|
||||
'recipient',
|
||||
'status',
|
||||
'is_anonymous',
|
||||
'special_instructions',
|
||||
@@ -186,15 +221,25 @@ class OrderForm(forms.ModelForm):
|
||||
self.fields['is_delivery'].label = 'С доставкой'
|
||||
self.fields['customer_is_recipient'].label = 'Покупатель = получатель'
|
||||
|
||||
# Поля получателя опциональны
|
||||
self.fields['recipient_name'].required = False
|
||||
self.fields['recipient_phone'].required = False
|
||||
# Поле получателя опционально
|
||||
self.fields['recipient'].required = False
|
||||
|
||||
# Поле ручной стоимости доставки опционально
|
||||
self.fields['delivery_cost'].required = False
|
||||
self.fields['delivery_cost'].label = 'Ручная стоимость доставки'
|
||||
self.fields['delivery_cost'].help_text = 'Оставьте пустым для автоматического расчета'
|
||||
|
||||
# Инициализируем queryset для recipient_from_history
|
||||
if self.instance.pk and self.instance.customer:
|
||||
# При редактировании заказа загружаем историю получателей этого клиента
|
||||
customer_orders = Order.objects.filter(
|
||||
customer=self.instance.customer,
|
||||
recipient__isnull=False
|
||||
).order_by('-created_at')
|
||||
self.fields['recipient_from_history'].queryset = Recipient.objects.filter(
|
||||
orders__in=customer_orders
|
||||
).distinct().order_by('-created_at')
|
||||
|
||||
# Инициализируем queryset для address_from_history
|
||||
# Это будет переопределено в представлении после выбора клиента
|
||||
if self.instance.pk and self.instance.customer:
|
||||
@@ -204,9 +249,15 @@ class OrderForm(forms.ModelForm):
|
||||
delivery_address__isnull=False
|
||||
).order_by('-created_at')
|
||||
self.fields['address_from_history'].queryset = Address.objects.filter(
|
||||
order__in=customer_orders
|
||||
orders__in=customer_orders
|
||||
).distinct().order_by('-created_at')
|
||||
|
||||
# Инициализируем поля получателя из существующего recipient
|
||||
if self.instance.pk and self.instance.recipient:
|
||||
recipient = self.instance.recipient
|
||||
self.fields['recipient_name'].initial = recipient.name or ''
|
||||
self.fields['recipient_phone'].initial = recipient.phone or ''
|
||||
|
||||
# Инициализируем поля адреса из существующего delivery_address
|
||||
if self.instance.pk and self.instance.delivery_address:
|
||||
address = self.instance.delivery_address
|
||||
|
||||
@@ -10,7 +10,7 @@ from datetime import datetime, timedelta
|
||||
import random
|
||||
from decimal import Decimal
|
||||
|
||||
from orders.models import Order, OrderItem, Address
|
||||
from orders.models import Order, OrderItem, Address, Recipient
|
||||
from customers.models import Customer
|
||||
from inventory.models import Warehouse
|
||||
from products.models import Product
|
||||
@@ -133,8 +133,14 @@ class Command(BaseCommand):
|
||||
# Дополнительная информация
|
||||
if random.random() > 0.7: # 30% - подарок другому человеку
|
||||
order.customer_is_recipient = False
|
||||
order.recipient_name = f"Получатель {i+1}"
|
||||
order.recipient_phone = f"+7{random.randint(9000000000, 9999999999)}"
|
||||
# Создаем получателя
|
||||
recipient_name = f"Получатель {i+1}"
|
||||
recipient_phone = f"+7{random.randint(9000000000, 9999999999)}"
|
||||
recipient, created = Recipient.objects.get_or_create(
|
||||
name=recipient_name,
|
||||
phone=recipient_phone
|
||||
)
|
||||
order.recipient = recipient
|
||||
|
||||
if random.random() > 0.8: # 20% анонимных
|
||||
order.is_anonymous = True
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
# Generated by Django 5.0.10 on 2025-12-22 19:32
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('orders', '0009_add_original_product_to_kit_item_snapshot'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='address',
|
||||
name='recipient_name',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='address',
|
||||
name='recipient_phone',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='historicalorder',
|
||||
name='recipient_name',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='historicalorder',
|
||||
name='recipient_phone',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='order',
|
||||
name='recipient_name',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='order',
|
||||
name='recipient_phone',
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='order',
|
||||
name='delivery_address',
|
||||
field=models.ForeignKey(blank=True, help_text='Обязательно для курьерской доставки', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='orders', to='orders.address', verbose_name='Адрес доставки'),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name='Recipient',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('name', models.CharField(help_text='ФИО или название организации получателя', max_length=200, verbose_name='Имя получателя')),
|
||||
('phone', models.CharField(help_text='Контактный телефон для связи с получателем', max_length=20, 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=['phone'], name='orders_reci_phone_735356_idx'), models.Index(fields=['name'], name='orders_reci_name_e52d5b_idx'), models.Index(fields=['created_at'], name='orders_reci_created_34a391_idx')],
|
||||
},
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='historicalorder',
|
||||
name='recipient',
|
||||
field=models.ForeignKey(blank=True, db_constraint=False, help_text='Заполняется, если покупатель не является получателем', null=True, on_delete=django.db.models.deletion.DO_NOTHING, related_name='+', to='orders.recipient', verbose_name='Получатель'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='order',
|
||||
name='recipient',
|
||||
field=models.ForeignKey(blank=True, help_text='Заполняется, если покупатель не является получателем', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='orders', to='orders.recipient', verbose_name='Получатель'),
|
||||
),
|
||||
]
|
||||
77
myproject/orders/migrations/0011_migrate_recipient_data.py
Normal file
77
myproject/orders/migrations/0011_migrate_recipient_data.py
Normal file
@@ -0,0 +1,77 @@
|
||||
# Generated by Django 5.0.10 on 2025-12-22 19:32
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
def migrate_recipient_data_forward(apps, schema_editor):
|
||||
"""
|
||||
Перенос данных получателей из старых полей Order в новую модель Recipient.
|
||||
Так как поля recipient_name и recipient_phone уже удалены,
|
||||
мы используем HistoricalOrder для восстановления данных.
|
||||
"""
|
||||
# Получаем модели
|
||||
HistoricalOrder = apps.get_model('orders', 'HistoricalOrder')
|
||||
Recipient = apps.get_model('orders', 'Recipient')
|
||||
Order = apps.get_model('orders', 'Order')
|
||||
|
||||
# Словарь для кэширования recipient'ов
|
||||
recipients_cache = {}
|
||||
|
||||
# Обрабатываем каждый заказ
|
||||
for order in Order.objects.all():
|
||||
# Находим последнюю историческую запись для этого заказа
|
||||
hist = HistoricalOrder.objects.filter(
|
||||
order_number=order.order_number
|
||||
).order_by('-history_date').first()
|
||||
|
||||
if not hist:
|
||||
continue
|
||||
|
||||
# Проверяем, есть ли данные получателя
|
||||
recipient_name = getattr(hist, 'recipient_name', None)
|
||||
recipient_phone = getattr(hist, 'recipient_phone', None)
|
||||
|
||||
# Если получатель не указан или customer_is_recipient=True, пропускаем
|
||||
if not recipient_name or not recipient_phone or order.customer_is_recipient:
|
||||
continue
|
||||
|
||||
# Создаем ключ для кэша
|
||||
cache_key = f"{recipient_name}|{recipient_phone}"
|
||||
|
||||
# Проверяем, есть ли уже такой получатель в кэше
|
||||
if cache_key in recipients_cache:
|
||||
recipient = recipients_cache[cache_key]
|
||||
else:
|
||||
# Создаем нового получателя
|
||||
recipient, created = Recipient.objects.get_or_create(
|
||||
name=recipient_name,
|
||||
phone=recipient_phone
|
||||
)
|
||||
recipients_cache[cache_key] = recipient
|
||||
|
||||
# Привязываем получателя к заказу
|
||||
order.recipient = recipient
|
||||
order.save(update_fields=['recipient'])
|
||||
|
||||
|
||||
def migrate_recipient_data_backward(apps, schema_editor):
|
||||
"""
|
||||
Обратная миграция - просто очищаем recipient поле в Order.
|
||||
Данные вернутся из HistoricalOrder при повторном apply.
|
||||
"""
|
||||
Order = apps.get_model('orders', 'Order')
|
||||
Order.objects.all().update(recipient=None)
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('orders', '0010_remove_address_recipient_name_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunPython(
|
||||
migrate_recipient_data_forward,
|
||||
migrate_recipient_data_backward
|
||||
),
|
||||
]
|
||||
@@ -4,6 +4,7 @@
|
||||
Структура:
|
||||
- OrderStatus: Статусы заказов
|
||||
- Address: Адреса доставки
|
||||
- Recipient: Получатели заказов
|
||||
- Order: Главная модель заказа
|
||||
- OrderItem: Позиции в заказе
|
||||
- PaymentMethod: Способы оплаты (справочник)
|
||||
@@ -17,6 +18,7 @@ from .payment_method import PaymentMethod
|
||||
|
||||
# 2. Модели с зависимостями от справочников
|
||||
from .address import Address
|
||||
from .recipient import Recipient
|
||||
|
||||
# 3. Главная модель Order (зависит от Status, Address)
|
||||
from .order import Order
|
||||
@@ -29,6 +31,7 @@ from .transaction import Transaction
|
||||
__all__ = [
|
||||
'OrderStatus',
|
||||
'Address',
|
||||
'Recipient',
|
||||
'Order',
|
||||
'OrderItem',
|
||||
'PaymentMethod',
|
||||
|
||||
@@ -3,26 +3,9 @@ from django.db import models
|
||||
|
||||
class Address(models.Model):
|
||||
"""
|
||||
Модель адреса доставки для заказа цветочного магазина в Минске.
|
||||
Адрес принадлежит конкретному заказу доставки.
|
||||
Модель адреса доставки.
|
||||
Адрес может использоваться в разных заказах и для разных получателей.
|
||||
"""
|
||||
# Информация о получателе
|
||||
recipient_name = models.CharField(
|
||||
max_length=200,
|
||||
blank=True,
|
||||
null=True,
|
||||
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,
|
||||
blank=True,
|
||||
@@ -103,12 +86,7 @@ class Address(models.Model):
|
||||
if self.apartment_number:
|
||||
address_parts.append(f"кв/офис {self.apartment_number}")
|
||||
|
||||
address_line = ", ".join(address_parts) if address_parts else "Адрес не указан"
|
||||
|
||||
# Формируем строку с именем получателя
|
||||
if self.recipient_name:
|
||||
return f"{self.recipient_name} - {address_line}"
|
||||
return address_line
|
||||
return ", ".join(address_parts) if address_parts else "Адрес не указан"
|
||||
|
||||
@property
|
||||
def full_address(self):
|
||||
|
||||
@@ -6,6 +6,7 @@ from inventory.models import Warehouse
|
||||
from simple_history.models import HistoricalRecords
|
||||
from .status import OrderStatus
|
||||
from .address import Address
|
||||
from .recipient import Recipient
|
||||
|
||||
|
||||
class Order(models.Model):
|
||||
@@ -38,12 +39,12 @@ class Order(models.Model):
|
||||
)
|
||||
|
||||
# Адрес доставки (для курьерской доставки)
|
||||
delivery_address = models.OneToOneField(
|
||||
delivery_address = models.ForeignKey(
|
||||
Address,
|
||||
on_delete=models.CASCADE,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='order',
|
||||
related_name='orders',
|
||||
verbose_name="Адрес доставки",
|
||||
help_text="Обязательно для курьерской доставки"
|
||||
)
|
||||
@@ -160,30 +161,24 @@ class Order(models.Model):
|
||||
help_text="Обновляется автоматически при добавлении платежей"
|
||||
)
|
||||
|
||||
# Дополнительная информация
|
||||
# Информация о получателе
|
||||
customer_is_recipient = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name="Покупатель является получателем",
|
||||
help_text="Если отмечено, данные получателя не требуются отдельно"
|
||||
)
|
||||
|
||||
# Данные получателя (если покупатель != получатель)
|
||||
recipient_name = models.CharField(
|
||||
max_length=200,
|
||||
blank=True,
|
||||
# Получатель (если покупатель != получатель)
|
||||
recipient = models.ForeignKey(
|
||||
Recipient,
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
verbose_name="Имя получателя",
|
||||
blank=True,
|
||||
related_name='orders',
|
||||
verbose_name="Получатель",
|
||||
help_text="Заполняется, если покупатель не является получателем"
|
||||
)
|
||||
|
||||
recipient_phone = models.CharField(
|
||||
max_length=20,
|
||||
blank=True,
|
||||
null=True,
|
||||
verbose_name="Телефон получателя",
|
||||
help_text="Контактный телефон получателя"
|
||||
)
|
||||
|
||||
is_anonymous = models.BooleanField(
|
||||
default=False,
|
||||
verbose_name="Анонимная доставка",
|
||||
|
||||
41
myproject/orders/models/recipient.py
Normal file
41
myproject/orders/models/recipient.py
Normal file
@@ -0,0 +1,41 @@
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Recipient(models.Model):
|
||||
"""
|
||||
Модель получателя заказа.
|
||||
Один получатель может получать доставки по разным адресам.
|
||||
"""
|
||||
name = models.CharField(
|
||||
max_length=200,
|
||||
verbose_name="Имя получателя",
|
||||
help_text="ФИО или название организации получателя"
|
||||
)
|
||||
|
||||
phone = models.CharField(
|
||||
max_length=20,
|
||||
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=['phone']),
|
||||
models.Index(fields=['name']),
|
||||
models.Index(fields=['created_at']),
|
||||
]
|
||||
ordering = ['-created_at']
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.name} ({self.phone})"
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
"""Форматированное имя для отображения"""
|
||||
return f"{self.name} - {self.phone}"
|
||||
@@ -1,12 +1,12 @@
|
||||
"""
|
||||
Сервис для работы с адресами заказов.
|
||||
Содержит логику создания, обновления и управления адресами доставки.
|
||||
Сервис для работы с адресами и получателями заказов.
|
||||
Содержит логику создания, обновления и управления адресами доставки и получателями.
|
||||
"""
|
||||
from ..models import Order, Address
|
||||
from ..models import Order, Address, Recipient
|
||||
|
||||
|
||||
class AddressService:
|
||||
"""Сервис для управления адресами доставки в заказах"""
|
||||
"""Сервис для управления адресами доставки и получателями в заказах"""
|
||||
|
||||
@staticmethod
|
||||
def create_address_from_form_data(form_data):
|
||||
@@ -27,8 +27,6 @@ class AddressService:
|
||||
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', ''),
|
||||
@@ -40,6 +38,76 @@ class AddressService:
|
||||
)
|
||||
return address
|
||||
|
||||
@staticmethod
|
||||
def create_recipient_from_form_data(form_data):
|
||||
"""
|
||||
Создает объект Recipient из данных формы.
|
||||
|
||||
Args:
|
||||
form_data (dict): Словарь с данными из формы
|
||||
- recipient_name
|
||||
- recipient_phone
|
||||
|
||||
Returns:
|
||||
Recipient: Новый объект получателя (не сохраненный в БД)
|
||||
"""
|
||||
recipient = Recipient(
|
||||
name=form_data.get('recipient_name', ''),
|
||||
phone=form_data.get('recipient_phone', ''),
|
||||
)
|
||||
return recipient
|
||||
|
||||
@staticmethod
|
||||
def process_recipient_from_form(order, form_data):
|
||||
"""
|
||||
Обрабатывает получателя из данных формы заказа.
|
||||
Создает нового Recipient или использует существующего в зависимости от режима.
|
||||
|
||||
Args:
|
||||
order (Order): Объект заказа
|
||||
form_data (dict): Все данные из формы
|
||||
|
||||
Returns:
|
||||
Recipient or None: Получатель для привязки к заказу или None
|
||||
"""
|
||||
recipient_mode = form_data.get('recipient_mode')
|
||||
|
||||
# Если режим "покупатель = получатель" - возвращаем None
|
||||
if recipient_mode == 'customer':
|
||||
return None
|
||||
|
||||
# Если режим "выбрать из истории" - возвращаем выбранного получателя
|
||||
if recipient_mode == 'history':
|
||||
recipient_id = form_data.get('recipient_from_history')
|
||||
if recipient_id:
|
||||
try:
|
||||
return Recipient.objects.get(pk=recipient_id)
|
||||
except Recipient.DoesNotExist:
|
||||
return None
|
||||
|
||||
# Если режим "новый получатель"
|
||||
if recipient_mode == 'new':
|
||||
name = form_data.get('recipient_name', '').strip()
|
||||
phone = form_data.get('recipient_phone', '').strip()
|
||||
|
||||
if not name or not phone:
|
||||
return None
|
||||
|
||||
# Проверяем, есть ли уже такой получатель в БД
|
||||
existing_recipient = Recipient.objects.filter(
|
||||
name=name,
|
||||
phone=phone
|
||||
).first()
|
||||
|
||||
if existing_recipient:
|
||||
return existing_recipient
|
||||
|
||||
# Создаем нового получателя
|
||||
recipient = AddressService.create_recipient_from_form_data(form_data)
|
||||
return recipient
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def process_address_from_form(order, form_data):
|
||||
"""
|
||||
@@ -90,7 +158,17 @@ class AddressService:
|
||||
# Если все поля адреса пустые, возвращаем None
|
||||
return None
|
||||
|
||||
# Создаем новый адрес (даже если не все обязательные поля заполнены)
|
||||
# Проверяем, есть ли уже такой адрес в БД (по основным полям)
|
||||
existing_address = Address.objects.filter(
|
||||
street=street,
|
||||
building_number=building_number,
|
||||
apartment_number=form_data.get('address_apartment_number', '').strip()
|
||||
).first()
|
||||
|
||||
if existing_address:
|
||||
return existing_address
|
||||
|
||||
# Создаем новый адрес
|
||||
address = AddressService.create_address_from_form_data(form_data)
|
||||
return address
|
||||
|
||||
@@ -113,7 +191,7 @@ class AddressService:
|
||||
).order_by('-created_at')
|
||||
|
||||
addresses = Address.objects.filter(
|
||||
order__in=customer_orders
|
||||
orders__in=customer_orders
|
||||
).distinct().order_by('-created_at')
|
||||
|
||||
return addresses
|
||||
|
||||
@@ -131,7 +131,7 @@
|
||||
</div>
|
||||
|
||||
<!-- Получатель -->
|
||||
{% if not order.customer_is_recipient %}
|
||||
{% if not order.customer_is_recipient and order.recipient %}
|
||||
<div class="card mb-3">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">Получатель</h5>
|
||||
@@ -139,11 +139,11 @@
|
||||
<div class="card-body">
|
||||
<div class="row mb-2">
|
||||
<div class="col-md-4"><strong>Имя получателя:</strong></div>
|
||||
<div class="col-md-8">{{ order.recipient_name|default:"Не указано" }}</div>
|
||||
<div class="col-md-8">{{ order.recipient.name|default:"Не указано" }}</div>
|
||||
</div>
|
||||
<div class="row mb-2">
|
||||
<div class="col-md-4"><strong>Телефон получателя:</strong></div>
|
||||
<div class="col-md-8">{{ order.recipient_phone|default:"Не указан" }}</div>
|
||||
<div class="col-md-8">{{ order.recipient.phone|default:"Не указан" }}</div>
|
||||
</div>
|
||||
{% if order.is_anonymous %}
|
||||
<div class="row mb-2">
|
||||
|
||||
@@ -504,24 +504,34 @@
|
||||
<div class="border-top pt-3 mt-3">
|
||||
<h6 class="mb-3">Получатель</h6>
|
||||
|
||||
<!-- Крупный переключатель "Покупатель = получатель" -->
|
||||
<!-- Режимы выбора получателя -->
|
||||
<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 }}"
|
||||
{% if form.customer_is_recipient.value %}checked{% endif %}
|
||||
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>
|
||||
Покупатель является получателем
|
||||
{% for choice in form.recipient_mode %}
|
||||
<div class="form-check">
|
||||
{{ choice.tag }}
|
||||
<label class="form-check-label" for="{{ choice.id_for_label }}">
|
||||
{{ choice.choice_label }}
|
||||
</label>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if form.recipient_mode.errors %}
|
||||
<div class="text-danger">{{ form.recipient_mode.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Поля получателя (показываются когда покупатель != получатель) -->
|
||||
<div class="row" id="recipient-fields" style="display: none;">
|
||||
<!-- Выбор получателя из истории -->
|
||||
<div class="mb-3" id="recipient-history-field" style="display: none;">
|
||||
<label for="{{ form.recipient_from_history.id_for_label }}" class="form-label">
|
||||
{{ form.recipient_from_history.label }}
|
||||
</label>
|
||||
{{ form.recipient_from_history }}
|
||||
{% if form.recipient_from_history.errors %}
|
||||
<div class="text-danger">{{ form.recipient_from_history.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<!-- Поля нового получателя -->
|
||||
<div class="row" id="recipient-new-fields" style="display: none;">
|
||||
<div class="col-md-6">
|
||||
<div class="mb-3">
|
||||
<label for="{{ form.recipient_name.id_for_label }}" class="form-label">
|
||||
@@ -1461,18 +1471,32 @@ document.addEventListener('DOMContentLoaded', function() {
|
||||
syncUIFromCheckbox();
|
||||
|
||||
// Показ/скрытие полей получателя
|
||||
const customerIsRecipientCheckbox = document.getElementById('{{ form.customer_is_recipient.id_for_label }}');
|
||||
const recipientFields = document.getElementById('recipient-fields');
|
||||
const recipientModeRadios = document.querySelectorAll('input[name="recipient_mode"]');
|
||||
const recipientHistoryField = document.getElementById('recipient-history-field');
|
||||
const recipientNewFields = document.getElementById('recipient-new-fields');
|
||||
|
||||
function toggleRecipientFields() {
|
||||
if (customerIsRecipientCheckbox.checked) {
|
||||
recipientFields.style.display = 'none';
|
||||
} else {
|
||||
recipientFields.style.display = '';
|
||||
const selectedMode = document.querySelector('input[name="recipient_mode"]:checked');
|
||||
if (!selectedMode) return;
|
||||
|
||||
const mode = selectedMode.value;
|
||||
|
||||
// Скрываем все поля
|
||||
recipientHistoryField.style.display = 'none';
|
||||
recipientNewFields.style.display = 'none';
|
||||
|
||||
// Показываем нужные поля
|
||||
if (mode === 'history') {
|
||||
recipientHistoryField.style.display = 'block';
|
||||
} else if (mode === 'new') {
|
||||
recipientNewFields.style.display = 'block';
|
||||
}
|
||||
// Для 'customer' ничего не показываем
|
||||
}
|
||||
|
||||
customerIsRecipientCheckbox.addEventListener('change', toggleRecipientFields);
|
||||
recipientModeRadios.forEach(radio => {
|
||||
radio.addEventListener('change', toggleRecipientFields);
|
||||
});
|
||||
toggleRecipientFields();
|
||||
|
||||
// === РАСЧЁТ ИТОГОВОЙ СУММЫ ТОВАРОВ ===
|
||||
|
||||
@@ -18,6 +18,7 @@ urlpatterns = [
|
||||
|
||||
# AJAX endpoints
|
||||
path('api/customer-address-history/', views.get_customer_address_history, name='api-customer-address-history'),
|
||||
path('api/customer-recipient-history/', views.get_customer_recipient_history, name='api-customer-recipient-history'),
|
||||
|
||||
# Wallet payment
|
||||
path('<int:order_number>/apply-wallet/', views.apply_wallet_payment, name='apply-wallet'),
|
||||
|
||||
@@ -97,6 +97,17 @@ def order_create(request):
|
||||
# Сохраняем форму БЕЗ commit, чтобы не вызывать reset_delivery_cost() до сохранения items
|
||||
order = form.save(commit=False)
|
||||
|
||||
# Обрабатываем получателя
|
||||
recipient = AddressService.process_recipient_from_form(order, form.cleaned_data)
|
||||
if recipient:
|
||||
# Если получатель не существует в БД, сохраняем его
|
||||
if not recipient.pk:
|
||||
recipient.save()
|
||||
order.recipient = recipient
|
||||
else:
|
||||
# Если покупатель является получателем
|
||||
order.recipient = None
|
||||
|
||||
# Обрабатываем адрес доставки
|
||||
if order.is_delivery:
|
||||
address = AddressService.process_address_from_form(order, form.cleaned_data)
|
||||
@@ -211,6 +222,17 @@ def order_update(request, order_number):
|
||||
with transaction.atomic():
|
||||
order = form.save(commit=False)
|
||||
|
||||
# Обрабатываем получателя
|
||||
recipient = AddressService.process_recipient_from_form(order, form.cleaned_data)
|
||||
if recipient:
|
||||
# Если получатель не существует в БД, сохраняем его
|
||||
if not recipient.pk:
|
||||
recipient.save()
|
||||
order.recipient = recipient
|
||||
else:
|
||||
# Если покупатель является получателем
|
||||
order.recipient = None
|
||||
|
||||
# Обрабатываем адрес доставки
|
||||
if order.is_delivery:
|
||||
address = AddressService.process_address_from_form(order, form.cleaned_data)
|
||||
@@ -220,21 +242,11 @@ def order_update(request, order_number):
|
||||
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()
|
||||
@@ -460,8 +472,6 @@ def get_customer_address_history(request):
|
||||
'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
|
||||
]
|
||||
@@ -479,6 +489,71 @@ def get_customer_address_history(request):
|
||||
}, status=500)
|
||||
|
||||
|
||||
@require_http_methods(["GET"])
|
||||
@login_required
|
||||
def get_customer_recipient_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
|
||||
from .models import Recipient
|
||||
|
||||
try:
|
||||
customer = Customer.objects.get(pk=customer_id)
|
||||
except Customer.DoesNotExist:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': 'Клиент не найден'
|
||||
}, status=404)
|
||||
|
||||
# Получаем получателей из истории заказов
|
||||
customer_orders = Order.objects.filter(
|
||||
customer=customer,
|
||||
recipient__isnull=False
|
||||
).order_by('-created_at')
|
||||
|
||||
recipients = Recipient.objects.filter(
|
||||
orders__in=customer_orders
|
||||
).distinct().order_by('-created_at')
|
||||
|
||||
# Форматируем для отправки клиенту
|
||||
recipients_data = [
|
||||
{
|
||||
'id': recipient.id,
|
||||
'name': recipient.name,
|
||||
'phone': recipient.phone,
|
||||
'display': f"{recipient.name} ({recipient.phone})",
|
||||
}
|
||||
for recipient in recipients
|
||||
]
|
||||
|
||||
return JsonResponse({
|
||||
'success': True,
|
||||
'recipients': recipients_data,
|
||||
'count': len(recipients_data)
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
return JsonResponse({
|
||||
'success': False,
|
||||
'error': f'Ошибка сервера: {str(e)}'
|
||||
}, status=500)
|
||||
|
||||
|
||||
# === УПРАВЛЕНИЕ СТАТУСАМИ ЗАКАЗОВ ===
|
||||
|
||||
@login_required
|
||||
|
||||
@@ -17,6 +17,7 @@ from django.apps import apps
|
||||
from django.conf import settings
|
||||
from django.core.files.storage import default_storage
|
||||
from django.utils import timezone
|
||||
from tenants.models import RESERVED_SCHEMA_NAMES
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -258,6 +259,19 @@ def cleanup_temp_media_for_schema(schema_name, ttl_hours=None):
|
||||
from django.conf import settings
|
||||
|
||||
try:
|
||||
# Пропускаем зарезервированные схемы (public, admin и т.д.)
|
||||
if schema_name in RESERVED_SCHEMA_NAMES:
|
||||
ttl = int(ttl_hours or getattr(settings, 'TEMP_MEDIA_TTL_HOURS', 24))
|
||||
logger.info(f"[Cleanup:{schema_name}] Skipping reserved schema")
|
||||
return {
|
||||
'status': 'skipped',
|
||||
'schema_name': schema_name,
|
||||
'reason': 'reserved_schema',
|
||||
'deleted': 0,
|
||||
'scanned': 0,
|
||||
'ttl_hours': ttl
|
||||
}
|
||||
|
||||
# Активируем схему тенанта
|
||||
connection.set_schema(schema_name)
|
||||
|
||||
@@ -272,7 +286,15 @@ def cleanup_temp_media_for_schema(schema_name, ttl_hours=None):
|
||||
for rel_dir in temp_dirs:
|
||||
try:
|
||||
# Получаем полный путь с учётом tenant_id
|
||||
try:
|
||||
full_dir = default_storage.path(rel_dir)
|
||||
except RuntimeError as storage_error:
|
||||
# Если не удается определить tenant_id (например, для public схемы)
|
||||
if 'Cannot determine tenant ID' in str(storage_error):
|
||||
logger.warning(f"[Cleanup:{schema_name}] Skipping {rel_dir}: {storage_error}")
|
||||
continue
|
||||
raise # Перебрасываем другие ошибки
|
||||
|
||||
if not os.path.isdir(full_dir):
|
||||
continue
|
||||
|
||||
@@ -331,7 +353,9 @@ def cleanup_temp_media_all(ttl_hours=None):
|
||||
connection.set_schema('public')
|
||||
from tenants.models import Client
|
||||
|
||||
schemas = list(Client.objects.values_list('schema_name', flat=True))
|
||||
# Фильтруем зарезервированные схемы, чтобы не запускать задачи для них
|
||||
schemas = [s for s in Client.objects.values_list('schema_name', flat=True)
|
||||
if s not in RESERVED_SCHEMA_NAMES]
|
||||
ttl = ttl_hours or getattr(settings, 'TEMP_MEDIA_TTL_HOURS', 24)
|
||||
|
||||
logger.info(f"[CleanupAll] Scheduling cleanup for {len(schemas)} tenants (TTL: {ttl}h)")
|
||||
|
||||
Reference in New Issue
Block a user