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

Реализован полностью новый функционал экспорта клиентов с возможностью
выбора полей, формата файла (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

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

View File

@@ -19,9 +19,11 @@
<a href="{% url 'customers:customer-import' %}" class="btn btn-outline-success">
<i class="bi bi-upload"></i> Импорт
</a>
<a href="{% url 'customers:customer-export' %}" class="btn btn-outline-info">
<i class="bi bi-download"></i> Экспорт
</a>
{% 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> Экспорт
</button>
{% endif %}
<a href="{% url 'customers:customer-create' %}" class="btn btn-primary">
<i class="bi bi-plus-circle"></i> Добавить клиента
</a>
@@ -207,4 +209,7 @@
</div>
</div>
</div>
{% include 'customers/customer_export_modal.html' %}
{% endblock %}