Добавлен настраиваемый экспорт клиентов с выбором полей и форматов

Реализован полностью новый функционал экспорта клиентов с возможностью
выбора полей, формата файла (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:
2026-01-03 21:12:08 +03:00
parent 0f09702094
commit 95036ed285
5 changed files with 416 additions and 51 deletions

View File

@@ -90,4 +90,52 @@ class ContactChannelForm(forms.ModelForm):
type_display = dict(ContactChannel.CHANNEL_TYPES).get(channel_type, channel_type)
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