Добавлен настраиваемый экспорт клиентов с выбором полей и форматов
Реализован полностью новый функционал экспорта клиентов с возможностью
выбора полей, формата файла (CSV/XLSX) и сохранением предпочтений.
Ключевые изменения:
1. CustomerExporter (import_export.py):
- Полностью переписан класс с поддержкой динамического выбора полей
- Добавлена конфигурация AVAILABLE_FIELDS с метаданными полей
- Реализован метод get_available_fields() для фильтрации по ролям
- Новый метод export_to_xlsx() с автоподстройкой ширины столбцов
- Форматирование ContactChannel с переводами строк
- Поддержка фильтрации queryset
2. CustomerExportForm (forms.py):
- Динамическое создание checkbox полей на основе роли пользователя
- Выбор формата файла (CSV/XLSX) через radio buttons
- Валидация выбора хотя бы одного поля
3. View customer_export (views.py):
- КРИТИЧНО: Изменён декоратор с @manager_or_owner_required на @owner_required
- Обработка GET (редирект) и POST запросов
- Применение фильтров CustomerFilter из списка клиентов
- Оптимизация с prefetch_related('contact_channels')
- Сохранение настроек экспорта в session
4. UI изменения:
- Создан шаблон customer_export_modal.html с модальным окном
- Обновлён customer_list.html: кнопка экспорта с проверкой роли
- JavaScript для восстановления сохранённых настроек из session
- Отображение количества экспортируемых клиентов
- Бейдж "Только для владельца" на поле баланса кошелька
Безопасность:
- Экспорт доступен ТОЛЬКО владельцу тенанта (OWNER) и superuser
- Поле "Баланс кошелька" скрыто от менеджеров на уровне формы
- Двойная проверка роли при экспорте баланса
- Кнопка экспорта скрыта в UI для всех кроме owner/superuser
Функциональность:
- Выбор полей: ID, имя, email, телефон, заметки, каналы связи, баланс, дата создания
- Форматы: CSV (с BOM для Excel) и XLSX
- Учёт текущих фильтров и поиска из списка клиентов
- Сохранение предпочтений между экспортами в session
- Исключение системного клиента из экспорта
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -91,3 +91,51 @@ class ContactChannelForm(forms.ModelForm):
|
|||||||
raise ValidationError(f'Такой {type_display} уже существует у другого клиента')
|
raise ValidationError(f'Такой {type_display} уже существует у другого клиента')
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class CustomerExportForm(forms.Form):
|
||||||
|
"""Форма настройки экспорта клиентов"""
|
||||||
|
|
||||||
|
FORMAT_CHOICES = [
|
||||||
|
('csv', 'CSV'),
|
||||||
|
('xlsx', 'Excel (XLSX)'),
|
||||||
|
]
|
||||||
|
|
||||||
|
export_format = forms.ChoiceField(
|
||||||
|
choices=FORMAT_CHOICES,
|
||||||
|
widget=forms.RadioSelect(attrs={'class': 'form-check-input'}),
|
||||||
|
initial='csv',
|
||||||
|
label='Формат файла'
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, user=None, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
# Получаем доступные поля на основе роли пользователя
|
||||||
|
from .services.import_export import CustomerExporter
|
||||||
|
available_fields = CustomerExporter.get_available_fields(user)
|
||||||
|
|
||||||
|
# Динамически создаём checkbox поля
|
||||||
|
for field_key, field_info in available_fields.items():
|
||||||
|
self.fields[f'field_{field_key}'] = forms.BooleanField(
|
||||||
|
required=False,
|
||||||
|
label=field_info['label'],
|
||||||
|
initial=field_key in CustomerExporter.DEFAULT_FIELDS,
|
||||||
|
widget=forms.CheckboxInput(attrs={'class': 'form-check-input'})
|
||||||
|
)
|
||||||
|
|
||||||
|
def clean(self):
|
||||||
|
cleaned_data = super().clean()
|
||||||
|
|
||||||
|
# Собираем выбранные поля
|
||||||
|
selected_fields = [
|
||||||
|
key.replace('field_', '')
|
||||||
|
for key, value in cleaned_data.items()
|
||||||
|
if key.startswith('field_') and value
|
||||||
|
]
|
||||||
|
|
||||||
|
if not selected_fields:
|
||||||
|
raise ValidationError('Выберите хотя бы одно поле для экспорта')
|
||||||
|
|
||||||
|
cleaned_data['selected_fields'] = selected_fields
|
||||||
|
return cleaned_data
|
||||||
@@ -28,56 +28,203 @@ import re
|
|||||||
|
|
||||||
class CustomerExporter:
|
class CustomerExporter:
|
||||||
"""
|
"""
|
||||||
Класс для экспорта клиентов в различные форматы.
|
Класс для экспорта клиентов в различные форматы (CSV/XLSX).
|
||||||
|
Поддерживает выбор полей и фильтрацию по ролям.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@staticmethod
|
# Конфигурация доступных полей с метаданными
|
||||||
def export_to_csv():
|
AVAILABLE_FIELDS = {
|
||||||
|
'id': {'label': 'ID', 'owner_only': False},
|
||||||
|
'name': {'label': 'Имя', 'owner_only': False},
|
||||||
|
'email': {'label': 'Email', 'owner_only': False},
|
||||||
|
'phone': {'label': 'Телефон', 'owner_only': False},
|
||||||
|
'notes': {'label': 'Заметки', 'owner_only': False},
|
||||||
|
'contact_channels': {'label': 'Каналы связи', 'owner_only': False},
|
||||||
|
'wallet_balance': {'label': 'Баланс кошелька', 'owner_only': True},
|
||||||
|
'created_at': {'label': 'Дата создания', 'owner_only': False},
|
||||||
|
}
|
||||||
|
|
||||||
|
DEFAULT_FIELDS = ['id', 'name', 'email', 'phone']
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_available_fields(cls, user):
|
||||||
"""
|
"""
|
||||||
Экспортирует всех клиентов (кроме системного) в CSV файл.
|
Получить поля доступные для пользователя на основе роли.
|
||||||
|
|
||||||
Поля экспорта:
|
Args:
|
||||||
- ID
|
user: Объект пользователя
|
||||||
- Имя
|
|
||||||
- Email
|
|
||||||
- Телефон
|
|
||||||
- Дата создания
|
|
||||||
|
|
||||||
Примечание: Баланс кошелька НЕ экспортируется (требование безопасности).
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
HttpResponse: HTTP ответ с CSV файлом для скачивания
|
dict: Словарь доступных полей с метаданными
|
||||||
|
"""
|
||||||
|
fields = {}
|
||||||
|
is_owner = user.is_superuser or user.is_owner
|
||||||
|
for field_key, field_info in cls.AVAILABLE_FIELDS.items():
|
||||||
|
if field_info['owner_only'] and not is_owner:
|
||||||
|
continue
|
||||||
|
fields[field_key] = field_info
|
||||||
|
return fields
|
||||||
|
|
||||||
|
def __init__(self, queryset, selected_fields, user):
|
||||||
|
"""
|
||||||
|
Инициализация экспортера.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
queryset: QuerySet клиентов (уже отфильтрованный)
|
||||||
|
selected_fields: Список ключей полей для экспорта
|
||||||
|
user: Текущий пользователь (для проверки прав)
|
||||||
|
"""
|
||||||
|
self.queryset = queryset
|
||||||
|
self.selected_fields = selected_fields
|
||||||
|
self.user = user
|
||||||
|
|
||||||
|
def _get_headers(self):
|
||||||
|
"""Генерация заголовков на основе выбранных полей"""
|
||||||
|
return [
|
||||||
|
self.AVAILABLE_FIELDS[field]['label']
|
||||||
|
for field in self.selected_fields
|
||||||
|
]
|
||||||
|
|
||||||
|
def _get_field_value(self, customer, field_key):
|
||||||
|
"""
|
||||||
|
Получить отформатированное значение для конкретного поля.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
customer: Объект Customer
|
||||||
|
field_key: Ключ поля
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Форматированное значение
|
||||||
|
"""
|
||||||
|
if field_key == 'id':
|
||||||
|
return customer.id
|
||||||
|
elif field_key == 'name':
|
||||||
|
return customer.name or ''
|
||||||
|
elif field_key == 'email':
|
||||||
|
return customer.email or ''
|
||||||
|
elif field_key == 'phone':
|
||||||
|
return str(customer.phone) if customer.phone else ''
|
||||||
|
elif field_key == 'notes':
|
||||||
|
return customer.notes or ''
|
||||||
|
elif field_key == 'contact_channels':
|
||||||
|
return self._get_contact_channels_display(customer)
|
||||||
|
elif field_key == 'wallet_balance':
|
||||||
|
# Двойная защита: проверка роли
|
||||||
|
if not (self.user.is_superuser or self.user.is_owner):
|
||||||
|
return 'N/A'
|
||||||
|
return str(customer.wallet_balance)
|
||||||
|
elif field_key == 'created_at':
|
||||||
|
return customer.created_at.strftime('%Y-%m-%d %H:%M:%S')
|
||||||
|
return ''
|
||||||
|
|
||||||
|
def _get_contact_channels_display(self, customer):
|
||||||
|
"""
|
||||||
|
Форматирование каналов связи для экспорта.
|
||||||
|
Объединяет все каналы в одну строку с переводами строк.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
customer: Объект Customer
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: Форматированная строка каналов связи
|
||||||
|
"""
|
||||||
|
channels = customer.contact_channels.all()
|
||||||
|
if not channels:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
from ..models import ContactChannel
|
||||||
|
lines = []
|
||||||
|
for channel in channels:
|
||||||
|
channel_name = dict(ContactChannel.CHANNEL_TYPES).get(
|
||||||
|
channel.channel_type,
|
||||||
|
channel.channel_type
|
||||||
|
)
|
||||||
|
lines.append(f"{channel_name}: {channel.value}")
|
||||||
|
|
||||||
|
return '\n'.join(lines)
|
||||||
|
|
||||||
|
def export_to_csv(self):
|
||||||
|
"""
|
||||||
|
Экспорт в CSV с выбранными полями.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
HttpResponse: CSV файл для скачивания
|
||||||
"""
|
"""
|
||||||
# Создаём HTTP ответ с CSV файлом
|
|
||||||
response = HttpResponse(content_type='text/csv; charset=utf-8')
|
response = HttpResponse(content_type='text/csv; charset=utf-8')
|
||||||
timestamp = timezone.now().strftime("%Y%m%d_%H%M%S")
|
timestamp = timezone.now().strftime("%Y%m%d_%H%M%S")
|
||||||
response['Content-Disposition'] = f'attachment; filename="customers_export_{timestamp}.csv"'
|
response['Content-Disposition'] = f'attachment; filename="customers_export_{timestamp}.csv"'
|
||||||
|
|
||||||
# Добавляем BOM для корректного открытия в Excel
|
# BOM для корректного открытия в Excel
|
||||||
response.write('\ufeff')
|
response.write('\ufeff')
|
||||||
|
|
||||||
writer = csv.writer(response)
|
writer = csv.writer(response)
|
||||||
|
|
||||||
|
# Динамические заголовки
|
||||||
|
writer.writerow(self._get_headers())
|
||||||
|
|
||||||
|
# Данные
|
||||||
|
for customer in self.queryset:
|
||||||
|
row = [
|
||||||
|
self._get_field_value(customer, field)
|
||||||
|
for field in self.selected_fields
|
||||||
|
]
|
||||||
|
writer.writerow(row)
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
def export_to_xlsx(self):
|
||||||
|
"""
|
||||||
|
Экспорт в XLSX используя openpyxl.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
HttpResponse: XLSX файл для скачивания
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from openpyxl import Workbook
|
||||||
|
except ImportError:
|
||||||
|
# Fallback to CSV если openpyxl не установлен
|
||||||
|
return self.export_to_csv()
|
||||||
|
|
||||||
|
wb = Workbook()
|
||||||
|
ws = wb.active
|
||||||
|
ws.title = "Клиенты"
|
||||||
|
|
||||||
# Заголовки
|
# Заголовки
|
||||||
writer.writerow([
|
ws.append(self._get_headers())
|
||||||
'ID',
|
|
||||||
'Имя',
|
|
||||||
'Email',
|
|
||||||
'Телефон',
|
|
||||||
'Дата создания',
|
|
||||||
])
|
|
||||||
|
|
||||||
# Данные (исключаем системного клиента)
|
# Данные
|
||||||
customers = Customer.objects.filter(is_system_customer=False).order_by('-created_at')
|
for customer in self.queryset:
|
||||||
|
row = [
|
||||||
|
self._get_field_value(customer, field)
|
||||||
|
for field in self.selected_fields
|
||||||
|
]
|
||||||
|
ws.append(row)
|
||||||
|
|
||||||
for customer in customers:
|
# Автоподстройка ширины столбцов
|
||||||
writer.writerow([
|
for column in ws.columns:
|
||||||
customer.id,
|
max_length = 0
|
||||||
customer.name or '',
|
column_letter = column[0].column_letter
|
||||||
customer.email or '',
|
for cell in column:
|
||||||
str(customer.phone) if customer.phone else '',
|
try:
|
||||||
customer.created_at.strftime('%Y-%m-%d %H:%M:%S'),
|
if len(str(cell.value)) > max_length:
|
||||||
])
|
max_length = len(cell.value)
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
adjusted_width = min(max_length + 2, 50) # Максимум 50
|
||||||
|
ws.column_dimensions[column_letter].width = adjusted_width
|
||||||
|
|
||||||
|
# Сохранение в BytesIO
|
||||||
|
output = io.BytesIO()
|
||||||
|
wb.save(output)
|
||||||
|
output.seek(0)
|
||||||
|
|
||||||
|
# Создание response
|
||||||
|
response = HttpResponse(
|
||||||
|
output.read(),
|
||||||
|
content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
|
||||||
|
)
|
||||||
|
timestamp = timezone.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
response['Content-Disposition'] = f'attachment; filename="customers_export_{timestamp}.xlsx"'
|
||||||
|
|
||||||
return response
|
return response
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
<!-- Modal for Customer Export Configuration -->
|
||||||
|
<div class="modal fade" id="exportModal" tabindex="-1" aria-labelledby="exportModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-lg">
|
||||||
|
<div class="modal-content">
|
||||||
|
<form method="post" action="{% url 'customers:customer-export' %}?{{ request.GET.urlencode }}" id="exportForm">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<div class="modal-header">
|
||||||
|
<h5 class="modal-title" id="exportModalLabel">
|
||||||
|
<i class="bi bi-download"></i> Настройка экспорта клиентов
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Закрыть"></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-body">
|
||||||
|
<!-- Export info -->
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<i class="bi bi-info-circle"></i>
|
||||||
|
Будет экспортировано клиентов: <strong>{{ total_customers }}</strong>
|
||||||
|
{% if query or filter.form.has_notes.value or filter.form.no_phone.value or filter.form.no_email.value or filter.form.has_contact_channel.value %}
|
||||||
|
<br><small>С учётом текущих фильтров и поиска</small>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Field Selection -->
|
||||||
|
<div class="mb-4">
|
||||||
|
<h6>Выберите поля для экспорта:</h6>
|
||||||
|
<div class="row">
|
||||||
|
{% for field in export_form %}
|
||||||
|
{% if field.name != 'export_format' %}
|
||||||
|
<div class="col-md-6 mb-2">
|
||||||
|
<div class="form-check">
|
||||||
|
{{ field }}
|
||||||
|
<label class="form-check-label" for="{{ field.id_for_label }}">
|
||||||
|
{{ field.label }}
|
||||||
|
{% if 'wallet_balance' in field.name %}
|
||||||
|
<span class="badge bg-warning text-dark">Только для владельца</span>
|
||||||
|
{% endif %}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Format Selection -->
|
||||||
|
<div class="mb-3">
|
||||||
|
<h6>Формат файла:</h6>
|
||||||
|
{% for choice in export_form.export_format %}
|
||||||
|
<div class="form-check form-check-inline">
|
||||||
|
{{ choice.tag }}
|
||||||
|
<label class="form-check-label" for="{{ choice.id_for_label }}">
|
||||||
|
{{ choice.choice_label }}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="text-muted small">
|
||||||
|
<i class="bi bi-lightbulb"></i>
|
||||||
|
Ваш выбор будет сохранён для следующего экспорта
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||||
|
Отмена
|
||||||
|
</button>
|
||||||
|
<button type="submit" class="btn btn-primary">
|
||||||
|
<i class="bi bi-download"></i> Экспортировать
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Pre-select saved preferences from session
|
||||||
|
var preferencesJson = '{{ export_preferences|escapejs }}';
|
||||||
|
if (preferencesJson && preferencesJson !== '{}') {
|
||||||
|
try {
|
||||||
|
var preferences = JSON.parse(preferencesJson.replace(/'/g, '"'));
|
||||||
|
|
||||||
|
// Restore selected fields
|
||||||
|
if (preferences.selected_fields) {
|
||||||
|
preferences.selected_fields.forEach(function(field) {
|
||||||
|
var checkbox = document.getElementById('id_field_' + field);
|
||||||
|
if (checkbox) checkbox.checked = true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restore format selection
|
||||||
|
if (preferences.format) {
|
||||||
|
var radio = document.querySelector('input[name="export_format"][value="' + preferences.format + '"]');
|
||||||
|
if (radio) radio.checked = true;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.log('Could not parse export preferences:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -19,9 +19,11 @@
|
|||||||
<a href="{% url 'customers:customer-import' %}" class="btn btn-outline-success">
|
<a href="{% url 'customers:customer-import' %}" class="btn btn-outline-success">
|
||||||
<i class="bi bi-upload"></i> Импорт
|
<i class="bi bi-upload"></i> Импорт
|
||||||
</a>
|
</a>
|
||||||
<a href="{% url 'customers:customer-export' %}" class="btn btn-outline-info">
|
{% if user.is_owner or user.is_superuser %}
|
||||||
|
<button type="button" class="btn btn-outline-info" data-bs-toggle="modal" data-bs-target="#exportModal">
|
||||||
<i class="bi bi-download"></i> Экспорт
|
<i class="bi bi-download"></i> Экспорт
|
||||||
</a>
|
</button>
|
||||||
|
{% endif %}
|
||||||
<a href="{% url 'customers:customer-create' %}" class="btn btn-primary">
|
<a href="{% url 'customers:customer-create' %}" class="btn btn-primary">
|
||||||
<i class="bi bi-plus-circle"></i> Добавить клиента
|
<i class="bi bi-plus-circle"></i> Добавить клиента
|
||||||
</a>
|
</a>
|
||||||
@@ -207,4 +209,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{% include 'customers/customer_export_modal.html' %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from django.db.models.functions import Greatest, Coalesce
|
|||||||
from django.http import JsonResponse
|
from django.http import JsonResponse
|
||||||
from django.views.decorators.http import require_http_methods
|
from django.views.decorators.http import require_http_methods
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from user_roles.decorators import manager_or_owner_required
|
from user_roles.decorators import manager_or_owner_required, owner_required
|
||||||
import phonenumbers
|
import phonenumbers
|
||||||
import json
|
import json
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
@@ -80,11 +80,18 @@ def customer_list(request):
|
|||||||
page_number = request.GET.get('page')
|
page_number = request.GET.get('page')
|
||||||
page_obj = paginator.get_page(page_number)
|
page_obj = paginator.get_page(page_number)
|
||||||
|
|
||||||
|
# Подготовка формы экспорта и настроек из сессии
|
||||||
|
from .forms import CustomerExportForm
|
||||||
|
export_form = CustomerExportForm(user=request.user)
|
||||||
|
export_preferences = request.session.get('customer_export_preferences', {})
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'page_obj': page_obj,
|
'page_obj': page_obj,
|
||||||
'query': query,
|
'query': query,
|
||||||
'total_customers': paginator.count, # Используем count из paginator, чтобы избежать дублирования SQL запроса
|
'total_customers': paginator.count, # Используем count из paginator, чтобы избежать дублирования SQL запроса
|
||||||
'filter': customer_filter, # Добавляем фильтр в контекст
|
'filter': customer_filter, # Добавляем фильтр в контекст
|
||||||
|
'export_form': export_form, # Форма экспорта для модального окна
|
||||||
|
'export_preferences': export_preferences, # Сохранённые настройки экспорта
|
||||||
}
|
}
|
||||||
return render(request, 'customers/customer_list.html', context)
|
return render(request, 'customers/customer_list.html', context)
|
||||||
|
|
||||||
@@ -886,12 +893,65 @@ def customer_import_download_errors(request):
|
|||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@manager_or_owner_required
|
@owner_required
|
||||||
def customer_export(request):
|
def customer_export(request):
|
||||||
"""
|
"""
|
||||||
Экспорт клиентов в CSV файл.
|
Экспорт клиентов в CSV/XLSX файл.
|
||||||
|
|
||||||
|
GET: Перенаправление на список клиентов
|
||||||
|
POST: Обработка экспорта с выбранными полями и форматом
|
||||||
|
|
||||||
|
Поддерживает фильтрацию - экспортирует только клиентов, соответствующих текущим фильтрам.
|
||||||
|
Доступен только владельцу (OWNER) и superuser.
|
||||||
"""
|
"""
|
||||||
from .services.import_export import CustomerExporter
|
from .services.import_export import CustomerExporter
|
||||||
|
from .forms import CustomerExportForm
|
||||||
|
from .filters import CustomerFilter
|
||||||
|
|
||||||
exporter = CustomerExporter()
|
# Базовый queryset (исключаем системного клиента)
|
||||||
|
queryset = Customer.objects.filter(is_system_customer=False)
|
||||||
|
|
||||||
|
# Применяем фильтры (та же логика что в customer_list)
|
||||||
|
customer_filter = CustomerFilter(request.GET, queryset=queryset)
|
||||||
|
filtered_queryset = customer_filter.qs
|
||||||
|
|
||||||
|
# GET запрос: перенаправление на список клиентов
|
||||||
|
if request.method != 'POST':
|
||||||
|
messages.info(
|
||||||
|
request,
|
||||||
|
'Используйте кнопку "Экспорт" в списке клиентов для настройки экспорта.'
|
||||||
|
)
|
||||||
|
return redirect('customers:customer-list')
|
||||||
|
|
||||||
|
# POST запрос: обработка экспорта
|
||||||
|
form = CustomerExportForm(request.POST, user=request.user)
|
||||||
|
|
||||||
|
if not form.is_valid():
|
||||||
|
messages.error(request, 'Ошибка в настройках экспорта. Выберите хотя бы одно поле.')
|
||||||
|
return redirect('customers:customer-list')
|
||||||
|
|
||||||
|
# Получение конфигурации экспорта
|
||||||
|
selected_fields = form.cleaned_data['selected_fields']
|
||||||
|
export_format = form.cleaned_data['export_format']
|
||||||
|
|
||||||
|
# Сохранение настроек в сессии
|
||||||
|
request.session['customer_export_preferences'] = {
|
||||||
|
'selected_fields': selected_fields,
|
||||||
|
'format': export_format,
|
||||||
|
}
|
||||||
|
|
||||||
|
# Оптимизация запроса (prefetch contact channels)
|
||||||
|
filtered_queryset = filtered_queryset.prefetch_related('contact_channels').order_by('-created_at')
|
||||||
|
|
||||||
|
# Создание экспортера с отфильтрованным queryset
|
||||||
|
exporter = CustomerExporter(
|
||||||
|
queryset=filtered_queryset,
|
||||||
|
selected_fields=selected_fields,
|
||||||
|
user=request.user
|
||||||
|
)
|
||||||
|
|
||||||
|
# Генерация и возврат файла экспорта
|
||||||
|
if export_format == 'xlsx':
|
||||||
|
return exporter.export_to_xlsx()
|
||||||
|
else:
|
||||||
return exporter.export_to_csv()
|
return exporter.export_to_csv()
|
||||||
|
|||||||
Reference in New Issue
Block a user