Files
octopus/myproject/customers/forms.py
Andrey Smakotin 95036ed285 Добавлен настраиваемый экспорт клиентов с выбором полей и форматов
Реализован полностью новый функционал экспорта клиентов с возможностью
выбора полей, формата файла (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>
2026-01-03 21:12:08 +03:00

141 lines
5.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from django import forms
from django.core.exceptions import ValidationError
from phonenumber_field.formfields import PhoneNumberField
from phonenumber_field.widgets import PhoneNumberPrefixWidget
from .models import Customer, ContactChannel
class CustomerForm(forms.ModelForm):
phone = PhoneNumberField(
region='BY',
required=False,
help_text='Формат: +375XXXXXXXXX или 80XXXXXXXXX',
widget=forms.TextInput(attrs={'placeholder': '+375XXXXXXXXX'})
)
class Meta:
model = Customer
fields = ['name', 'email', 'phone', 'notes']
exclude = ['is_system_customer']
widgets = {
'notes': forms.Textarea(attrs={'rows': 3}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Ensure phone displays in E.164 format
if self.instance and self.instance.phone:
self.initial['phone'] = str(self.instance.phone)
for field_name, field in self.fields.items():
if field_name == 'notes':
# Textarea already has rows=3 from widget, just add class
field.widget.attrs.update({'class': 'form-control'})
else:
# Regular input fields get form-control class
field.widget.attrs.update({'class': 'form-control'})
def clean_email(self):
"""Нормализует пустые значения email в None"""
email = self.cleaned_data.get('email')
if not email:
return None
return email
def clean_phone(self):
"""Нормализует пустые значения телефона в None"""
phone = self.cleaned_data.get('phone')
if not phone:
return None
return phone
def clean(self):
"""Дополнительная валидация формы"""
cleaned_data = super().clean()
# Защита от редактирования системного клиента
if self.instance and self.instance.pk and self.instance.is_system_customer:
raise ValidationError(
'Системный клиент не может быть изменен. '
'Он необходим для корректной работы системы и создается автоматически.'
)
return cleaned_data
class ContactChannelForm(forms.ModelForm):
"""Форма для добавления/редактирования канала связи"""
class Meta:
model = ContactChannel
fields = ['channel_type', 'value', 'is_primary', 'notes']
widgets = {
'channel_type': forms.Select(attrs={'class': 'form-select'}),
'value': forms.TextInput(attrs={'class': 'form-control', 'placeholder': '@username, номер и т.д.'}),
'notes': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Личный аккаунт, рабочий...'}),
'is_primary': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
}
def clean_value(self):
value = self.cleaned_data.get('value', '').strip()
channel_type = self.cleaned_data.get('channel_type')
if not value:
raise ValidationError('Значение не может быть пустым')
# Проверка уникальности комбинации channel_type + value
qs = ContactChannel.objects.filter(channel_type=channel_type, value=value)
if self.instance.pk:
qs = qs.exclude(pk=self.instance.pk)
if qs.exists():
type_display = dict(ContactChannel.CHANNEL_TYPES).get(channel_type, channel_type)
raise ValidationError(f'Такой {type_display} уже существует у другого клиента')
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