From b48e6c810da874404049526e355692c7673f6bf6 Mon Sep 17 00:00:00 2001 From: Andrey Smakotin Date: Sun, 11 Jan 2026 01:18:26 +0300 Subject: [PATCH] =?UTF-8?q?feat(discounts):=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=20CRUD=20=D0=B8=D0=BD=D1=82=D0=B5?= =?UTF-8?q?=D1=80=D1=84=D0=B5=D0=B9=D1=81=20=D0=B4=D0=BB=D1=8F=20=D1=81?= =?UTF-8?q?=D0=BA=D0=B8=D0=B4=D0=BE=D0=BA=20=D0=B2=20=D0=BD=D0=B0=D1=81?= =?UTF-8?q?=D1=82=D1=80=D0=BE=D0=B9=D0=BA=D0=B0=D1=85?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Добавлена вкладка "Скидки" в страницу настроек - Созданы views для управления скидками и промокодами с проверкой прав: * owner/manager/superuser - полный CRUD * florist - только просмотр * courier - нет доступа - Созданы шаблоны: список скидок, форма, подтверждение удаления - Созданы шаблоны: список промокодов, форма, подтверждение удаления - Добавлены фильтры по типу, области действия, активности - Добавлена пагинация Co-Authored-By: Claude Opus 4.5 --- .../discounts/discount_confirm_delete.html | 45 +++ .../templates/discounts/discount_form.html | 191 ++++++++++ .../templates/discounts/discount_list.html | 221 ++++++++++++ .../discounts/promocode_confirm_delete.html | 42 +++ .../templates/discounts/promocode_form.html | 107 ++++++ .../templates/discounts/promocode_list.html | 202 +++++++++++ myproject/discounts/urls.py | 21 ++ myproject/discounts/views.py | 331 ++++++++++++++++++ .../system_settings/base_settings.html | 10 +- myproject/system_settings/urls.py | 1 + 10 files changed, 1169 insertions(+), 2 deletions(-) create mode 100644 myproject/discounts/templates/discounts/discount_confirm_delete.html create mode 100644 myproject/discounts/templates/discounts/discount_form.html create mode 100644 myproject/discounts/templates/discounts/discount_list.html create mode 100644 myproject/discounts/templates/discounts/promocode_confirm_delete.html create mode 100644 myproject/discounts/templates/discounts/promocode_form.html create mode 100644 myproject/discounts/templates/discounts/promocode_list.html create mode 100644 myproject/discounts/urls.py create mode 100644 myproject/discounts/views.py diff --git a/myproject/discounts/templates/discounts/discount_confirm_delete.html b/myproject/discounts/templates/discounts/discount_confirm_delete.html new file mode 100644 index 0000000..95569e9 --- /dev/null +++ b/myproject/discounts/templates/discounts/discount_confirm_delete.html @@ -0,0 +1,45 @@ +{% extends "system_settings/base_settings.html" %} + +{% block title %}Удаление скидки{% endblock %} + +{% block settings_content %} +
+
+
+
+

Удаление скидки

+
+
+

Вы уверены, что хотите удалить скидку {{ object.name }}?

+ +
+ + Внимание! Это действие нельзя отменить. + {% if object.promo_codes.count > 0 %} +
Также будут удалены связанные промокоды ({{ object.promo_codes.count }} шт.). + {% endif %} +
+ +
+ Информация о скидке: +
    +
  • Тип: {% if object.discount_type == 'percentage' %}Процент ({{ object.value }}%){% else %}{{ object.value }} руб.{% endif %}
  • +
  • Область: {% if object.scope == 'order' %}На заказ{% elif object.scope == 'product' %}На товары{% else %}На категории{% endif %}
  • +
  • Использований: {{ object.current_usage_count }} раз
  • +
+
+ +
+ {% csrf_token %} +
+ Отмена + +
+
+
+
+
+
+{% endblock %} diff --git a/myproject/discounts/templates/discounts/discount_form.html b/myproject/discounts/templates/discounts/discount_form.html new file mode 100644 index 0000000..7ea29db --- /dev/null +++ b/myproject/discounts/templates/discounts/discount_form.html @@ -0,0 +1,191 @@ +{% extends "system_settings/base_settings.html" %} + +{% block title %}{% if is_edit %}Редактирование скидки{% else %}Создание скидки{% endif %}{% endblock %} + +{% block settings_content %} +
+
+
+
+

+ {% if is_edit %}Редактирование скидки{% else %}Создание скидки{% endif %} +

+ + Назад + +
+
+ {% if messages %} + {% for message in messages %} + + {% endfor %} + {% endif %} + +
+ {% csrf_token %} + + +
Основная информация
+
+
+ + +
+
+ + +
Выше = применяется раньше
+
+
+ +
+ + +
+ + +
Параметры скидки
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+ + +
Скидка будет применяться автоматически при оформлении заказа
+
+
+
+
+ + +
Неактивные скидки не применяются
+
+
+
+ + +
Ограничения
+
+
+ + +
Для скидок на заказ
+
+
+ + +
Оставьте пустым для безлимитного использования
+
+
+ +
+
+ + +
+
+ + +
+
+ + +
Применение к товарам
+
+ + +
Удерживайте Ctrl для выбора нескольких товаров
+
+ +
+ + +
Удерживайте Ctrl для выбора нескольких категорий
+
+ +
+ + +
Эти товары не будут участвовать в акции
+
+ + +
+ Отмена + +
+
+
+
+
+
+{% endblock %} diff --git a/myproject/discounts/templates/discounts/discount_list.html b/myproject/discounts/templates/discounts/discount_list.html new file mode 100644 index 0000000..7a2732a --- /dev/null +++ b/myproject/discounts/templates/discounts/discount_list.html @@ -0,0 +1,221 @@ +{% extends "system_settings/base_settings.html" %} + +{% block title %}Скидки{% endblock %} + +{% block settings_content %} +
+

Скидки

+
+ + Промокоды + + {% if can_edit %} + + Создать скидку + + {% endif %} +
+
+ +{% if messages %} +{% for message in messages %} + +{% endfor %} +{% endif %} + + +
+
+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + + Сбросить + +
+
+
+
+ +
+
+ {% if discounts %} +
+ + + + + + + + + + + + + + + + {% for discount in discounts %} + + + + + + + + + + + + {% endfor %} + +
НазваниеТипЗначениеОбластьАвтоСтатусПромокодыИспользованийДействия
+ {{ discount.name }} + {% if discount.description %} +
{{ discount.description|truncatewords:10 }} + {% endif %} +
+ {% if discount.discount_type == 'percentage' %} + Процент + {% else %} + Фиксированная + {% endif %} + + {% if discount.discount_type == 'percentage' %} + {{ discount.value }}% + {% else %} + {{ discount.value }} руб. + {% endif %} + + {% if discount.scope == 'order' %} + Заказ + {% elif discount.scope == 'product' %} + Товар + {% else %} + Категория + {% endif %} + + {% if discount.is_auto %} + + {% else %} + + {% endif %} + + {% if discount.is_active %} + Активна + {% else %} + Неактивна + {% endif %} + + + {{ discount.promo_count }} + + + {{ discount.current_usage_count }} + {% if discount.max_usage_count %} + / {{ discount.max_usage_count }} + {% endif %} + + {% if can_edit %} + + {% else %} + + {% endif %} +
+
+ + + {% if is_paginated %} + + {% endif %} + + {% else %} +
+

Нет скидок

+ + {% if can_edit %} + + {% endif %} +
+ {% endif %} +
+
+{% endblock %} diff --git a/myproject/discounts/templates/discounts/promocode_confirm_delete.html b/myproject/discounts/templates/discounts/promocode_confirm_delete.html new file mode 100644 index 0000000..673bcf7 --- /dev/null +++ b/myproject/discounts/templates/discounts/promocode_confirm_delete.html @@ -0,0 +1,42 @@ +{% extends "system_settings/base_settings.html" %} + +{% block title %}Удаление промокода{% endblock %} + +{% block settings_content %} +
+
+
+
+

Удаление промокода

+
+
+

Вы уверены, что хотите удалить промокод {{ object.code }}?

+ +
+ + Внимание! Это действие нельзя отменить. +
+ +
+ Информация о промокоде: +
    +
  • Скидка: {{ object.discount.name }}
  • +
  • Значение: {% if object.discount.discount_type == 'percentage' %}{{ object.discount.value }}%{% else %}{{ object.discount.value }} руб.{% endif %}
  • +
  • Использований: {{ object.current_uses }} раз
  • +
+
+ +
+ {% csrf_token %} +
+ Отмена + +
+
+
+
+
+
+{% endblock %} diff --git a/myproject/discounts/templates/discounts/promocode_form.html b/myproject/discounts/templates/discounts/promocode_form.html new file mode 100644 index 0000000..623cd12 --- /dev/null +++ b/myproject/discounts/templates/discounts/promocode_form.html @@ -0,0 +1,107 @@ +{% extends "system_settings/base_settings.html" %} + +{% block title %}{% if is_edit %}Редактирование промокода{% else %}Создание промокода{% endif %}{% endblock %} + +{% block settings_content %} +
+
+
+
+

+ {% if is_edit %}Редактирование промокода{% else %}Создание промокода{% endif %} +

+ + Назад + +
+
+ {% if messages %} + {% for message in messages %} + + {% endfor %} + {% endif %} + +
+ {% csrf_token %} + +
+ + +
Код, который будут вводить покупатели
+
+ +
+ + +
Информация о скидке отобразится при применении промокода
+
+ +
+
+ + +
Оставьте пустым для безлимитного использования
+
+
+ + +
Общий лимит для всех пользователей
+
+
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
Неактивные промокоды не применяются
+
+ +
+ Отмена + +
+
+
+
+
+
+{% endblock %} diff --git a/myproject/discounts/templates/discounts/promocode_list.html b/myproject/discounts/templates/discounts/promocode_list.html new file mode 100644 index 0000000..f3f882c --- /dev/null +++ b/myproject/discounts/templates/discounts/promocode_list.html @@ -0,0 +1,202 @@ +{% extends "system_settings/base_settings.html" %} + +{% block title %}Промокоды{% endblock %} + +{% block settings_content %} +
+

Промокоды

+ +
+ +{% if messages %} +{% for message in messages %} + +{% endfor %} +{% endif %} + + +
+
+
+
+ + +
+
+ + +
+
+ + + Сбросить + +
+
+
+
+ +
+
+ {% if promocodes %} +
+ + + + + + + + + + + + + + + {% for promo in promocodes %} + + + + + + + + + + + {% endfor %} + +
КодСкидкаТипЗначениеИспользованийПериод действияСтатусДействия
+ {{ promo.code }} + + + {{ promo.discount.name }} + + + {% if promo.discount.discount_type == 'percentage' %} + % + {% else %} + руб. + {% endif %} + + {% if promo.discount.discount_type == 'percentage' %} + {{ promo.discount.value }}% + {% else %} + {{ promo.discount.value }} руб. + {% endif %} + + {{ promo.current_uses }} + {% if promo.max_total_uses %} + / {{ promo.max_total_uses }} + {% else %} + / ∞ + {% endif %} + {% if promo.max_uses_per_user %} +
макс. {{ promo.max_uses_per_user }} на пользователя + {% endif %} +
+ + {% if promo.start_date and promo.end_date %} + с {{ promo.start_date|date:"d.m.Y" }}
по {{ promo.end_date|date:"d.m.Y" }} + {% elif promo.start_date %} + с {{ promo.start_date|date:"d.m.Y" }} + {% elif promo.end_date %} + до {{ promo.end_date|date:"d.m.Y" }} + {% else %} + Бессрочный + {% endif %} +
+
+ {% if promo.is_active %} + Активен + {% else %} + Неактивен + {% endif %} + + {% if can_edit %} + + {% else %} + + {% endif %} +
+
+ + + {% if is_paginated %} + + {% endif %} + + {% else %} +
+

Нет промокодов

+ + {% if can_edit %} + + {% endif %} +
+ {% endif %} +
+
+{% endblock %} diff --git a/myproject/discounts/urls.py b/myproject/discounts/urls.py new file mode 100644 index 0000000..bc2365d --- /dev/null +++ b/myproject/discounts/urls.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +from django.urls import path +from . import views + +app_name = 'discounts' + +urlpatterns = [ + # Скидки + path('', views.DiscountListView.as_view(), name='list'), + path('create/', views.DiscountCreateView.as_view(), name='create'), + path('/edit/', views.DiscountUpdateView.as_view(), name='update'), + path('/toggle/', views.discount_toggle, name='toggle'), + path('/delete/', views.DiscountDeleteView.as_view(), name='delete'), + + # Промокоды + path('promo-codes/', views.PromoCodeListView.as_view(), name='promo-list'), + path('promo-codes/create/', views.PromoCodeCreateView.as_view(), name='promo-create'), + path('promo-codes//edit/', views.PromoCodeUpdateView.as_view(), name='promo-update'), + path('promo-codes//toggle/', views.promocode_toggle, name='promo-toggle'), + path('promo-codes//delete/', views.PromoCodeDeleteView.as_view(), name='promo-delete'), +] diff --git a/myproject/discounts/views.py b/myproject/discounts/views.py new file mode 100644 index 0000000..99fd5b3 --- /dev/null +++ b/myproject/discounts/views.py @@ -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') diff --git a/myproject/system_settings/templates/system_settings/base_settings.html b/myproject/system_settings/templates/system_settings/base_settings.html index 76b526a..a270614 100644 --- a/myproject/system_settings/templates/system_settings/base_settings.html +++ b/myproject/system_settings/templates/system_settings/base_settings.html @@ -8,17 +8,23 @@ diff --git a/myproject/system_settings/urls.py b/myproject/system_settings/urls.py index 777be4b..e20a705 100644 --- a/myproject/system_settings/urls.py +++ b/myproject/system_settings/urls.py @@ -7,4 +7,5 @@ app_name = "system_settings" urlpatterns = [ path("", SystemSettingsView.as_view(), name="settings"), path("roles/", include('user_roles.urls', namespace='user_roles')), + path("discounts/", include('discounts.urls', namespace='discounts')), ]