feat(discounts): добавлен CRUD интерфейс для скидок в настройках
- Добавлена вкладка "Скидки" в страницу настроек - Созданы views для управления скидками и промокодами с проверкой прав: * owner/manager/superuser - полный CRUD * florist - только просмотр * courier - нет доступа - Созданы шаблоны: список скидок, форма, подтверждение удаления - Созданы шаблоны: список промокодов, форма, подтверждение удаления - Добавлены фильтры по типу, области действия, активности - Добавлена пагинация Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
331
myproject/discounts/views.py
Normal file
331
myproject/discounts/views.py
Normal file
@@ -0,0 +1,331 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Views для управления скидками и промокодами во фронтенде.
|
||||
|
||||
Права доступа:
|
||||
- owner/manager/superuser - полный CRUD
|
||||
- florist - только просмотр
|
||||
- courier - нет доступа
|
||||
"""
|
||||
from django.views.generic import ListView, CreateView, UpdateView, DeleteView, TemplateView
|
||||
from django.shortcuts import redirect, get_object_or_404
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.urls import reverse_lazy
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.db.models import Q, Count
|
||||
|
||||
from user_roles.mixins import RoleRequiredMixin
|
||||
from user_roles.services import RoleService
|
||||
|
||||
from .models import Discount, PromoCode
|
||||
|
||||
|
||||
class DiscountAccessMixin(RoleRequiredMixin):
|
||||
"""
|
||||
Миксин для контроля доступа к скидкам.
|
||||
|
||||
- owner/manager/superuser - полный CRUD
|
||||
- florist - только просмотр
|
||||
- courier - нет доступа
|
||||
"""
|
||||
required_roles = ['owner', 'manager', 'florist']
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
# Superuser имеет полный доступ
|
||||
if request.user.is_superuser:
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
# Проверяем базовый доступ к роли
|
||||
if not RoleService.user_has_role(request.user, *self.required_roles):
|
||||
raise PermissionDenied("У вас нет прав для доступа к этой странице")
|
||||
|
||||
# Florist - только просмотр списков
|
||||
if RoleService.user_has_role(request.user, 'florist'):
|
||||
# Разрешаем только ListView
|
||||
if not isinstance(self, ListView):
|
||||
raise PermissionDenied("Флористы могут только просматривать скидки")
|
||||
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
|
||||
class DiscountListView(DiscountAccessMixin, ListView):
|
||||
"""Список скидок"""
|
||||
model = Discount
|
||||
template_name = 'discounts/discount_list.html'
|
||||
context_object_name = 'discounts'
|
||||
paginate_by = 20
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = Discount.objects.select_related(
|
||||
'created_by'
|
||||
).prefetch_related(
|
||||
'products', 'categories'
|
||||
).annotate(
|
||||
promo_count=Count('promo_codes')
|
||||
).order_by('-created_at')
|
||||
|
||||
# Фильтры
|
||||
discount_type = self.request.GET.get('type')
|
||||
scope = self.request.GET.get('scope')
|
||||
is_active = self.request.GET.get('is_active')
|
||||
search = self.request.GET.get('search')
|
||||
|
||||
if discount_type:
|
||||
queryset = queryset.filter(discount_type=discount_type)
|
||||
if scope:
|
||||
queryset = queryset.filter(scope=scope)
|
||||
if is_active:
|
||||
queryset = queryset.filter(is_active=(is_active == 'active'))
|
||||
if search:
|
||||
queryset = queryset.filter(
|
||||
Q(name__icontains=search) | Q(description__icontains=search)
|
||||
)
|
||||
|
||||
return queryset
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['current_filters'] = {
|
||||
'type': self.request.GET.get('type', ''),
|
||||
'scope': self.request.GET.get('scope', ''),
|
||||
'is_active': self.request.GET.get('is_active', ''),
|
||||
'search': self.request.GET.get('search', ''),
|
||||
}
|
||||
context['can_edit'] = self._can_edit()
|
||||
return context
|
||||
|
||||
def _can_edit(self):
|
||||
"""Проверка прав на редактирование"""
|
||||
if self.request.user.is_superuser:
|
||||
return True
|
||||
return RoleService.user_has_role(self.request.user, 'owner', 'manager')
|
||||
|
||||
|
||||
class DiscountCreateView(LoginRequiredMixin, RoleRequiredMixin, CreateView):
|
||||
"""Создание скидки (только owner/manager)"""
|
||||
model = Discount
|
||||
template_name = 'discounts/discount_form.html'
|
||||
fields = [
|
||||
'name', 'description', 'discount_type', 'value', 'scope',
|
||||
'is_auto', 'is_active', 'priority',
|
||||
'start_date', 'end_date',
|
||||
'min_order_amount', 'max_usage_count',
|
||||
'products', 'categories', 'excluded_products',
|
||||
]
|
||||
required_roles = ['owner', 'manager']
|
||||
success_url = reverse_lazy('system_settings:discounts:list')
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.created_by = self.request.user
|
||||
messages.success(self.request, f'Скидка "{form.instance.name}" создана')
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['is_edit'] = False
|
||||
# Передаем товары и категории для формы
|
||||
from products.models import Product, ProductCategory
|
||||
context['all_products'] = Product.objects.filter(is_active=True).order_by('name')
|
||||
context['all_categories'] = ProductCategory.objects.filter(is_active=True).order_by('name')
|
||||
context['selected_products'] = []
|
||||
context['selected_categories'] = []
|
||||
context['excluded_products'] = []
|
||||
return context
|
||||
|
||||
|
||||
class DiscountUpdateView(LoginRequiredMixin, RoleRequiredMixin, UpdateView):
|
||||
"""Редактирование скидки (только owner/manager)"""
|
||||
model = Discount
|
||||
template_name = 'discounts/discount_form.html'
|
||||
fields = [
|
||||
'name', 'description', 'discount_type', 'value', 'scope',
|
||||
'is_auto', 'is_active', 'priority',
|
||||
'start_date', 'end_date',
|
||||
'min_order_amount', 'max_usage_count',
|
||||
'products', 'categories', 'excluded_products',
|
||||
]
|
||||
required_roles = ['owner', 'manager']
|
||||
success_url = reverse_lazy('system_settings:discounts:list')
|
||||
|
||||
def form_valid(self, form):
|
||||
messages.success(self.request, f'Скидка "{form.instance.name}" обновлена')
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['is_edit'] = True
|
||||
# Передаем товары и категории для формы
|
||||
from products.models import Product, ProductCategory
|
||||
context['all_products'] = Product.objects.filter(is_active=True).order_by('name')
|
||||
context['all_categories'] = ProductCategory.objects.filter(is_active=True).order_by('name')
|
||||
context['selected_products'] = list(self.object.products.values_list('id', flat=True))
|
||||
context['selected_categories'] = list(self.object.categories.values_list('id', flat=True))
|
||||
context['excluded_products'] = list(self.object.excluded_products.values_list('id', flat=True))
|
||||
return context
|
||||
|
||||
|
||||
class DiscountDeleteView(LoginRequiredMixin, RoleRequiredMixin, DeleteView):
|
||||
"""Удаление скидки (только owner/manager)"""
|
||||
model = Discount
|
||||
template_name = 'discounts/discount_confirm_delete.html'
|
||||
required_roles = ['owner', 'manager']
|
||||
success_url = reverse_lazy('system_settings:discounts:list')
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
obj = self.get_object()
|
||||
messages.success(self.request, f'Скидка "{obj.name}" удалена')
|
||||
return super().delete(request, *args, **kwargs)
|
||||
|
||||
|
||||
@login_required
|
||||
def discount_toggle(request, pk):
|
||||
"""Переключение активности скидки (только owner/manager)"""
|
||||
if not request.user.is_superuser and not RoleService.user_has_role(request.user, 'owner', 'manager'):
|
||||
raise PermissionDenied("У вас нет прав для этого действия")
|
||||
|
||||
discount = get_object_or_404(Discount, pk=pk)
|
||||
discount.is_active = not discount.is_active
|
||||
discount.save(update_fields=['is_active'])
|
||||
|
||||
status = "активирована" if discount.is_active else "деактивирована"
|
||||
messages.success(request, f'Скидка "{discount.name}" {status}')
|
||||
return redirect('system_settings:discounts:list')
|
||||
|
||||
|
||||
# ============== Промокоды ==============
|
||||
|
||||
class PromoCodeAccessMixin(RoleRequiredMixin):
|
||||
"""Миксин для контроля доступа к промокодам"""
|
||||
required_roles = ['owner', 'manager', 'florist']
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
if request.user.is_superuser:
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
if not RoleService.user_has_role(request.user, *self.required_roles):
|
||||
raise PermissionDenied("У вас нет прав для доступа к этой странице")
|
||||
|
||||
# Florist - только просмотр
|
||||
if RoleService.user_has_role(request.user, 'florist'):
|
||||
if not isinstance(self, ListView):
|
||||
raise PermissionDenied("Флористы могут только просматривать промокоды")
|
||||
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
|
||||
class PromoCodeListView(PromoCodeAccessMixin, ListView):
|
||||
"""Список промокодов"""
|
||||
model = PromoCode
|
||||
template_name = 'discounts/promocode_list.html'
|
||||
context_object_name = 'promocodes'
|
||||
paginate_by = 20
|
||||
|
||||
def get_queryset(self):
|
||||
queryset = PromoCode.objects.select_related(
|
||||
'discount', 'created_by'
|
||||
).order_by('-created_at')
|
||||
|
||||
# Фильтры
|
||||
is_active = self.request.GET.get('is_active')
|
||||
search = self.request.GET.get('search')
|
||||
|
||||
if is_active:
|
||||
queryset = queryset.filter(is_active=(is_active == 'active'))
|
||||
if search:
|
||||
queryset = queryset.filter(code__icontains=search)
|
||||
|
||||
return queryset
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['current_filters'] = {
|
||||
'is_active': self.request.GET.get('is_active', ''),
|
||||
'search': self.request.GET.get('search', ''),
|
||||
}
|
||||
context['can_edit'] = self._can_edit()
|
||||
return context
|
||||
|
||||
def _can_edit(self):
|
||||
if self.request.user.is_superuser:
|
||||
return True
|
||||
return RoleService.user_has_role(self.request.user, 'owner', 'manager')
|
||||
|
||||
|
||||
class PromoCodeCreateView(LoginRequiredMixin, RoleRequiredMixin, CreateView):
|
||||
"""Создание промокода (только owner/manager)"""
|
||||
model = PromoCode
|
||||
template_name = 'discounts/promocode_form.html'
|
||||
fields = [
|
||||
'code', 'discount', 'is_active',
|
||||
'start_date', 'end_date',
|
||||
'max_uses_per_user', 'max_total_uses',
|
||||
]
|
||||
required_roles = ['owner', 'manager']
|
||||
success_url = reverse_lazy('system_settings:discounts:promo-list')
|
||||
|
||||
def form_valid(self, form):
|
||||
form.instance.created_by = self.request.user
|
||||
messages.success(self.request, f'Промокод "{form.instance.code}" создан')
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['is_edit'] = False
|
||||
# Передаем активные скидки для выбора
|
||||
context['all_discounts'] = Discount.objects.filter(is_active=True).order_by('name')
|
||||
return context
|
||||
|
||||
|
||||
class PromoCodeUpdateView(LoginRequiredMixin, RoleRequiredMixin, UpdateView):
|
||||
"""Редактирование промокода (только owner/manager)"""
|
||||
model = PromoCode
|
||||
template_name = 'discounts/promocode_form.html'
|
||||
fields = [
|
||||
'code', 'discount', 'is_active',
|
||||
'start_date', 'end_date',
|
||||
'max_uses_per_user', 'max_total_uses',
|
||||
]
|
||||
required_roles = ['owner', 'manager']
|
||||
success_url = reverse_lazy('system_settings:discounts:promo-list')
|
||||
|
||||
def form_valid(self, form):
|
||||
messages.success(self.request, f'Промокод "{form.instance.code}" обновлён')
|
||||
return super().form_valid(form)
|
||||
|
||||
def get_context_data(self, **kwargs):
|
||||
context = super().get_context_data(**kwargs)
|
||||
context['is_edit'] = True
|
||||
# Передаем все скидки для выбора (включая неактивные, если они выбраны)
|
||||
context['all_discounts'] = Discount.objects.all().order_by('name')
|
||||
return context
|
||||
|
||||
|
||||
class PromoCodeDeleteView(LoginRequiredMixin, RoleRequiredMixin, DeleteView):
|
||||
"""Удаление промокода (только owner/manager)"""
|
||||
model = PromoCode
|
||||
template_name = 'discounts/promocode_confirm_delete.html'
|
||||
required_roles = ['owner', 'manager']
|
||||
success_url = reverse_lazy('system_settings:discounts:promo-list')
|
||||
|
||||
def delete(self, request, *args, **kwargs):
|
||||
obj = self.get_object()
|
||||
messages.success(self.request, f'Промокод "{obj.code}" удалён')
|
||||
return super().delete(request, *args, **kwargs)
|
||||
|
||||
|
||||
@login_required
|
||||
def promocode_toggle(request, pk):
|
||||
"""Переключение активности промокода (только owner/manager)"""
|
||||
if not request.user.is_superuser and not RoleService.user_has_role(request.user, 'owner', 'manager'):
|
||||
raise PermissionDenied("У вас нет прав для этого действия")
|
||||
|
||||
promocode = get_object_or_404(PromoCode, pk=pk)
|
||||
promocode.is_active = not promocode.is_active
|
||||
promocode.save(update_fields=['is_active'])
|
||||
|
||||
status = "активирован" if promocode.is_active else "деактивирован"
|
||||
messages.success(request, f'Промокод "{promocode.code}" {status}')
|
||||
return redirect('system_settings:discounts:promo-list')
|
||||
Reference in New Issue
Block a user