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:
2025-12-23 00:08:41 +03:00
parent 483f150e7a
commit 6669d47cdf
15 changed files with 559 additions and 110 deletions

View File

@@ -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):
"""

View File

@@ -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

View File

@@ -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

View File

@@ -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='Получатель'),
),
]

View 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
),
]

View File

@@ -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',

View File

@@ -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):

View File

@@ -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="Анонимная доставка",

View 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}"

View File

@@ -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

View File

@@ -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">

View File

@@ -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();
// === РАСЧЁТ ИТОГОВОЙ СУММЫ ТОВАРОВ ===

View File

@@ -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'),

View File

@@ -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

View File

@@ -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)")