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 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
from django.utils.html import format_html
|
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):
|
class TransactionInline(admin.TabularInline):
|
||||||
@@ -61,7 +61,7 @@ class OrderAdmin(admin.ModelAdmin):
|
|||||||
'customer__name',
|
'customer__name',
|
||||||
'customer__phone',
|
'customer__phone',
|
||||||
'customer__email',
|
'customer__email',
|
||||||
'delivery_address__recipient_name',
|
'recipient__name',
|
||||||
'delivery_address__street',
|
'delivery_address__street',
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -240,8 +240,6 @@ class AddressAdmin(admin.ModelAdmin):
|
|||||||
Админ-панель для управления адресами доставки заказов.
|
Админ-панель для управления адресами доставки заказов.
|
||||||
"""
|
"""
|
||||||
list_display = [
|
list_display = [
|
||||||
'recipient_name',
|
|
||||||
'recipient_phone',
|
|
||||||
'full_address',
|
'full_address',
|
||||||
'entrance',
|
'entrance',
|
||||||
'floor',
|
'floor',
|
||||||
@@ -255,7 +253,6 @@ class AddressAdmin(admin.ModelAdmin):
|
|||||||
]
|
]
|
||||||
|
|
||||||
search_fields = [
|
search_fields = [
|
||||||
'recipient_name',
|
|
||||||
'street',
|
'street',
|
||||||
'building_number',
|
'building_number',
|
||||||
]
|
]
|
||||||
@@ -263,9 +260,6 @@ class AddressAdmin(admin.ModelAdmin):
|
|||||||
readonly_fields = ['created_at', 'updated_at']
|
readonly_fields = ['created_at', 'updated_at']
|
||||||
|
|
||||||
fieldsets = (
|
fieldsets = (
|
||||||
('Информация о получателе', {
|
|
||||||
'fields': ('recipient_name', 'recipient_phone')
|
|
||||||
}),
|
|
||||||
('Адрес доставки', {
|
('Адрес доставки', {
|
||||||
'fields': ('street', 'building_number', 'apartment_number', 'entrance', 'floor')
|
'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)
|
@admin.register(OrderStatus)
|
||||||
class OrderStatusAdmin(admin.ModelAdmin):
|
class OrderStatusAdmin(admin.ModelAdmin):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
from django import forms
|
from django import forms
|
||||||
from django.forms import inlineformset_factory
|
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 customers.models import Customer
|
||||||
from inventory.models import Warehouse
|
from inventory.models import Warehouse
|
||||||
from products.models import Product, ProductKit
|
from products.models import Product, ProductKit
|
||||||
@@ -11,7 +11,43 @@ from decimal import Decimal
|
|||||||
class OrderForm(forms.ModelForm):
|
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(
|
address_mode = forms.ChoiceField(
|
||||||
choices=[
|
choices=[
|
||||||
('history', 'Выбрать из истории'),
|
('history', 'Выбрать из истории'),
|
||||||
@@ -99,8 +135,7 @@ class OrderForm(forms.ModelForm):
|
|||||||
'delivery_time_end',
|
'delivery_time_end',
|
||||||
'delivery_cost',
|
'delivery_cost',
|
||||||
'customer_is_recipient',
|
'customer_is_recipient',
|
||||||
'recipient_name',
|
'recipient',
|
||||||
'recipient_phone',
|
|
||||||
'status',
|
'status',
|
||||||
'is_anonymous',
|
'is_anonymous',
|
||||||
'special_instructions',
|
'special_instructions',
|
||||||
@@ -186,15 +221,25 @@ class OrderForm(forms.ModelForm):
|
|||||||
self.fields['is_delivery'].label = 'С доставкой'
|
self.fields['is_delivery'].label = 'С доставкой'
|
||||||
self.fields['customer_is_recipient'].label = 'Покупатель = получатель'
|
self.fields['customer_is_recipient'].label = 'Покупатель = получатель'
|
||||||
|
|
||||||
# Поля получателя опциональны
|
# Поле получателя опционально
|
||||||
self.fields['recipient_name'].required = False
|
self.fields['recipient'].required = False
|
||||||
self.fields['recipient_phone'].required = False
|
|
||||||
|
|
||||||
# Поле ручной стоимости доставки опционально
|
# Поле ручной стоимости доставки опционально
|
||||||
self.fields['delivery_cost'].required = False
|
self.fields['delivery_cost'].required = False
|
||||||
self.fields['delivery_cost'].label = 'Ручная стоимость доставки'
|
self.fields['delivery_cost'].label = 'Ручная стоимость доставки'
|
||||||
self.fields['delivery_cost'].help_text = 'Оставьте пустым для автоматического расчета'
|
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
|
# Инициализируем queryset для address_from_history
|
||||||
# Это будет переопределено в представлении после выбора клиента
|
# Это будет переопределено в представлении после выбора клиента
|
||||||
if self.instance.pk and self.instance.customer:
|
if self.instance.pk and self.instance.customer:
|
||||||
@@ -204,9 +249,15 @@ class OrderForm(forms.ModelForm):
|
|||||||
delivery_address__isnull=False
|
delivery_address__isnull=False
|
||||||
).order_by('-created_at')
|
).order_by('-created_at')
|
||||||
self.fields['address_from_history'].queryset = Address.objects.filter(
|
self.fields['address_from_history'].queryset = Address.objects.filter(
|
||||||
order__in=customer_orders
|
orders__in=customer_orders
|
||||||
).distinct().order_by('-created_at')
|
).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
|
# Инициализируем поля адреса из существующего delivery_address
|
||||||
if self.instance.pk and self.instance.delivery_address:
|
if self.instance.pk and self.instance.delivery_address:
|
||||||
address = self.instance.delivery_address
|
address = self.instance.delivery_address
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ from datetime import datetime, timedelta
|
|||||||
import random
|
import random
|
||||||
from decimal import Decimal
|
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 customers.models import Customer
|
||||||
from inventory.models import Warehouse
|
from inventory.models import Warehouse
|
||||||
from products.models import Product
|
from products.models import Product
|
||||||
@@ -133,8 +133,14 @@ class Command(BaseCommand):
|
|||||||
# Дополнительная информация
|
# Дополнительная информация
|
||||||
if random.random() > 0.7: # 30% - подарок другому человеку
|
if random.random() > 0.7: # 30% - подарок другому человеку
|
||||||
order.customer_is_recipient = False
|
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% анонимных
|
if random.random() > 0.8: # 20% анонимных
|
||||||
order.is_anonymous = True
|
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: Статусы заказов
|
- OrderStatus: Статусы заказов
|
||||||
- Address: Адреса доставки
|
- Address: Адреса доставки
|
||||||
|
- Recipient: Получатели заказов
|
||||||
- Order: Главная модель заказа
|
- Order: Главная модель заказа
|
||||||
- OrderItem: Позиции в заказе
|
- OrderItem: Позиции в заказе
|
||||||
- PaymentMethod: Способы оплаты (справочник)
|
- PaymentMethod: Способы оплаты (справочник)
|
||||||
@@ -17,6 +18,7 @@ from .payment_method import PaymentMethod
|
|||||||
|
|
||||||
# 2. Модели с зависимостями от справочников
|
# 2. Модели с зависимостями от справочников
|
||||||
from .address import Address
|
from .address import Address
|
||||||
|
from .recipient import Recipient
|
||||||
|
|
||||||
# 3. Главная модель Order (зависит от Status, Address)
|
# 3. Главная модель Order (зависит от Status, Address)
|
||||||
from .order import Order
|
from .order import Order
|
||||||
@@ -29,6 +31,7 @@ from .transaction import Transaction
|
|||||||
__all__ = [
|
__all__ = [
|
||||||
'OrderStatus',
|
'OrderStatus',
|
||||||
'Address',
|
'Address',
|
||||||
|
'Recipient',
|
||||||
'Order',
|
'Order',
|
||||||
'OrderItem',
|
'OrderItem',
|
||||||
'PaymentMethod',
|
'PaymentMethod',
|
||||||
|
|||||||
@@ -3,26 +3,9 @@ from django.db import models
|
|||||||
|
|
||||||
class Address(models.Model):
|
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(
|
street = models.CharField(
|
||||||
max_length=255,
|
max_length=255,
|
||||||
blank=True,
|
blank=True,
|
||||||
@@ -103,12 +86,7 @@ class Address(models.Model):
|
|||||||
if self.apartment_number:
|
if self.apartment_number:
|
||||||
address_parts.append(f"кв/офис {self.apartment_number}")
|
address_parts.append(f"кв/офис {self.apartment_number}")
|
||||||
|
|
||||||
address_line = ", ".join(address_parts) if address_parts else "Адрес не указан"
|
return ", ".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):
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from inventory.models import Warehouse
|
|||||||
from simple_history.models import HistoricalRecords
|
from simple_history.models import HistoricalRecords
|
||||||
from .status import OrderStatus
|
from .status import OrderStatus
|
||||||
from .address import Address
|
from .address import Address
|
||||||
|
from .recipient import Recipient
|
||||||
|
|
||||||
|
|
||||||
class Order(models.Model):
|
class Order(models.Model):
|
||||||
@@ -38,12 +39,12 @@ class Order(models.Model):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Адрес доставки (для курьерской доставки)
|
# Адрес доставки (для курьерской доставки)
|
||||||
delivery_address = models.OneToOneField(
|
delivery_address = models.ForeignKey(
|
||||||
Address,
|
Address,
|
||||||
on_delete=models.CASCADE,
|
on_delete=models.SET_NULL,
|
||||||
null=True,
|
null=True,
|
||||||
blank=True,
|
blank=True,
|
||||||
related_name='order',
|
related_name='orders',
|
||||||
verbose_name="Адрес доставки",
|
verbose_name="Адрес доставки",
|
||||||
help_text="Обязательно для курьерской доставки"
|
help_text="Обязательно для курьерской доставки"
|
||||||
)
|
)
|
||||||
@@ -160,30 +161,24 @@ class Order(models.Model):
|
|||||||
help_text="Обновляется автоматически при добавлении платежей"
|
help_text="Обновляется автоматически при добавлении платежей"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Дополнительная информация
|
# Информация о получателе
|
||||||
customer_is_recipient = models.BooleanField(
|
customer_is_recipient = models.BooleanField(
|
||||||
default=True,
|
default=True,
|
||||||
verbose_name="Покупатель является получателем",
|
verbose_name="Покупатель является получателем",
|
||||||
help_text="Если отмечено, данные получателя не требуются отдельно"
|
help_text="Если отмечено, данные получателя не требуются отдельно"
|
||||||
)
|
)
|
||||||
|
|
||||||
# Данные получателя (если покупатель != получатель)
|
# Получатель (если покупатель != получатель)
|
||||||
recipient_name = models.CharField(
|
recipient = models.ForeignKey(
|
||||||
max_length=200,
|
Recipient,
|
||||||
blank=True,
|
on_delete=models.SET_NULL,
|
||||||
null=True,
|
null=True,
|
||||||
verbose_name="Имя получателя",
|
blank=True,
|
||||||
|
related_name='orders',
|
||||||
|
verbose_name="Получатель",
|
||||||
help_text="Заполняется, если покупатель не является получателем"
|
help_text="Заполняется, если покупатель не является получателем"
|
||||||
)
|
)
|
||||||
|
|
||||||
recipient_phone = models.CharField(
|
|
||||||
max_length=20,
|
|
||||||
blank=True,
|
|
||||||
null=True,
|
|
||||||
verbose_name="Телефон получателя",
|
|
||||||
help_text="Контактный телефон получателя"
|
|
||||||
)
|
|
||||||
|
|
||||||
is_anonymous = models.BooleanField(
|
is_anonymous = models.BooleanField(
|
||||||
default=False,
|
default=False,
|
||||||
verbose_name="Анонимная доставка",
|
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:
|
class AddressService:
|
||||||
"""Сервис для управления адресами доставки в заказах"""
|
"""Сервис для управления адресами доставки и получателями в заказах"""
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_address_from_form_data(form_data):
|
def create_address_from_form_data(form_data):
|
||||||
@@ -27,8 +27,6 @@ class AddressService:
|
|||||||
Address: Новый объект адреса (не сохраненный в БД)
|
Address: Новый объект адреса (не сохраненный в БД)
|
||||||
"""
|
"""
|
||||||
address = Address(
|
address = Address(
|
||||||
recipient_name=form_data.get('recipient_name', ''),
|
|
||||||
recipient_phone=form_data.get('recipient_phone', ''),
|
|
||||||
street=form_data.get('address_street', ''),
|
street=form_data.get('address_street', ''),
|
||||||
building_number=form_data.get('address_building_number', ''),
|
building_number=form_data.get('address_building_number', ''),
|
||||||
apartment_number=form_data.get('address_apartment_number', ''),
|
apartment_number=form_data.get('address_apartment_number', ''),
|
||||||
@@ -40,6 +38,76 @@ class AddressService:
|
|||||||
)
|
)
|
||||||
return address
|
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
|
@staticmethod
|
||||||
def process_address_from_form(order, form_data):
|
def process_address_from_form(order, form_data):
|
||||||
"""
|
"""
|
||||||
@@ -90,7 +158,17 @@ class AddressService:
|
|||||||
# Если все поля адреса пустые, возвращаем None
|
# Если все поля адреса пустые, возвращаем None
|
||||||
return 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)
|
address = AddressService.create_address_from_form_data(form_data)
|
||||||
return address
|
return address
|
||||||
|
|
||||||
@@ -113,7 +191,7 @@ class AddressService:
|
|||||||
).order_by('-created_at')
|
).order_by('-created_at')
|
||||||
|
|
||||||
addresses = Address.objects.filter(
|
addresses = Address.objects.filter(
|
||||||
order__in=customer_orders
|
orders__in=customer_orders
|
||||||
).distinct().order_by('-created_at')
|
).distinct().order_by('-created_at')
|
||||||
|
|
||||||
return addresses
|
return addresses
|
||||||
|
|||||||
@@ -131,7 +131,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Получатель -->
|
<!-- Получатель -->
|
||||||
{% if not order.customer_is_recipient %}
|
{% if not order.customer_is_recipient and order.recipient %}
|
||||||
<div class="card mb-3">
|
<div class="card mb-3">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h5 class="mb-0">Получатель</h5>
|
<h5 class="mb-0">Получатель</h5>
|
||||||
@@ -139,11 +139,11 @@
|
|||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row mb-2">
|
<div class="row mb-2">
|
||||||
<div class="col-md-4"><strong>Имя получателя:</strong></div>
|
<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>
|
||||||
<div class="row mb-2">
|
<div class="row mb-2">
|
||||||
<div class="col-md-4"><strong>Телефон получателя:</strong></div>
|
<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>
|
</div>
|
||||||
{% if order.is_anonymous %}
|
{% if order.is_anonymous %}
|
||||||
<div class="row mb-2">
|
<div class="row mb-2">
|
||||||
|
|||||||
@@ -504,24 +504,34 @@
|
|||||||
<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>
|
||||||
|
|
||||||
<!-- Крупный переключатель "Покупатель = получатель" -->
|
<!-- Режимы выбора получателя -->
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<div class="form-check form-switch" style="padding-left: 3.5em;">
|
{% for choice in form.recipient_mode %}
|
||||||
<input class="form-check-input" type="checkbox" role="switch"
|
<div class="form-check">
|
||||||
id="{{ form.customer_is_recipient.id_for_label }}"
|
{{ choice.tag }}
|
||||||
name="{{ form.customer_is_recipient.name }}"
|
<label class="form-check-label" for="{{ choice.id_for_label }}">
|
||||||
{% if form.customer_is_recipient.value %}checked{% endif %}
|
{{ choice.choice_label }}
|
||||||
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>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
{% if form.recipient_mode.errors %}
|
||||||
|
<div class="text-danger">{{ form.recipient_mode.errors }}</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</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="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">
|
||||||
@@ -1461,18 +1471,32 @@ document.addEventListener('DOMContentLoaded', function() {
|
|||||||
syncUIFromCheckbox();
|
syncUIFromCheckbox();
|
||||||
|
|
||||||
// Показ/скрытие полей получателя
|
// Показ/скрытие полей получателя
|
||||||
const customerIsRecipientCheckbox = document.getElementById('{{ form.customer_is_recipient.id_for_label }}');
|
const recipientModeRadios = document.querySelectorAll('input[name="recipient_mode"]');
|
||||||
const recipientFields = document.getElementById('recipient-fields');
|
const recipientHistoryField = document.getElementById('recipient-history-field');
|
||||||
|
const recipientNewFields = document.getElementById('recipient-new-fields');
|
||||||
|
|
||||||
function toggleRecipientFields() {
|
function toggleRecipientFields() {
|
||||||
if (customerIsRecipientCheckbox.checked) {
|
const selectedMode = document.querySelector('input[name="recipient_mode"]:checked');
|
||||||
recipientFields.style.display = 'none';
|
if (!selectedMode) return;
|
||||||
} else {
|
|
||||||
recipientFields.style.display = '';
|
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();
|
toggleRecipientFields();
|
||||||
|
|
||||||
// === РАСЧЁТ ИТОГОВОЙ СУММЫ ТОВАРОВ ===
|
// === РАСЧЁТ ИТОГОВОЙ СУММЫ ТОВАРОВ ===
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ urlpatterns = [
|
|||||||
|
|
||||||
# AJAX endpoints
|
# AJAX endpoints
|
||||||
path('api/customer-address-history/', views.get_customer_address_history, name='api-customer-address-history'),
|
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
|
# Wallet payment
|
||||||
path('<int:order_number>/apply-wallet/', views.apply_wallet_payment, name='apply-wallet'),
|
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
|
# Сохраняем форму БЕЗ commit, чтобы не вызывать reset_delivery_cost() до сохранения items
|
||||||
order = form.save(commit=False)
|
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:
|
if order.is_delivery:
|
||||||
address = AddressService.process_address_from_form(order, form.cleaned_data)
|
address = AddressService.process_address_from_form(order, form.cleaned_data)
|
||||||
@@ -211,6 +222,17 @@ def order_update(request, order_number):
|
|||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
order = form.save(commit=False)
|
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:
|
if order.is_delivery:
|
||||||
address = AddressService.process_address_from_form(order, form.cleaned_data)
|
address = AddressService.process_address_from_form(order, form.cleaned_data)
|
||||||
@@ -220,21 +242,11 @@ def order_update(request, order_number):
|
|||||||
address.save()
|
address.save()
|
||||||
order.delivery_address = address
|
order.delivery_address = address
|
||||||
else:
|
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
|
order.delivery_address = None
|
||||||
# Удаляем старый адрес
|
else:
|
||||||
if old_address and not old_address.order:
|
# Если не доставка, очищаем адрес
|
||||||
old_address.delete()
|
order.delivery_address = None
|
||||||
|
|
||||||
order.modified_by = request.user
|
order.modified_by = request.user
|
||||||
order.save()
|
order.save()
|
||||||
@@ -460,8 +472,6 @@ def get_customer_address_history(request):
|
|||||||
'entrance': addr.entrance,
|
'entrance': addr.entrance,
|
||||||
'floor': addr.floor,
|
'floor': addr.floor,
|
||||||
'intercom_code': addr.intercom_code,
|
'intercom_code': addr.intercom_code,
|
||||||
'recipient_name': addr.recipient_name,
|
|
||||||
'recipient_phone': addr.recipient_phone,
|
|
||||||
}
|
}
|
||||||
for addr in addresses
|
for addr in addresses
|
||||||
]
|
]
|
||||||
@@ -479,6 +489,71 @@ def get_customer_address_history(request):
|
|||||||
}, status=500)
|
}, 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
|
@login_required
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ from django.apps import apps
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.core.files.storage import default_storage
|
from django.core.files.storage import default_storage
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from tenants.models import RESERVED_SCHEMA_NAMES
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -258,6 +259,19 @@ def cleanup_temp_media_for_schema(schema_name, ttl_hours=None):
|
|||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
try:
|
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)
|
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:
|
for rel_dir in temp_dirs:
|
||||||
try:
|
try:
|
||||||
# Получаем полный путь с учётом tenant_id
|
# Получаем полный путь с учётом tenant_id
|
||||||
full_dir = default_storage.path(rel_dir)
|
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):
|
if not os.path.isdir(full_dir):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -331,7 +353,9 @@ def cleanup_temp_media_all(ttl_hours=None):
|
|||||||
connection.set_schema('public')
|
connection.set_schema('public')
|
||||||
from tenants.models import Client
|
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)
|
ttl = ttl_hours or getattr(settings, 'TEMP_MEDIA_TTL_HOURS', 24)
|
||||||
|
|
||||||
logger.info(f"[CleanupAll] Scheduling cleanup for {len(schemas)} tenants (TTL: {ttl}h)")
|
logger.info(f"[CleanupAll] Scheduling cleanup for {len(schemas)} tenants (TTL: {ttl}h)")
|
||||||
|
|||||||
Reference in New Issue
Block a user