Рабочие изменения: улучшения UI, настройки и бэкенд авторизации
Собрал накопившиеся изменения из рабочей директории: UI улучшения: - customer_detail.html: Расширен интерфейс детальной страницы клиента - order_detail.html: Добавлены элементы отображения деталей заказа - order_list.html: Улучшена визуализация списка заказов Бэкенд: - customers/views.py: Доработаны представления для работы с клиентами - products/views/product_views.py: Минорные правки - user_roles/auth_backend.py: Добавлен кастомный бэкенд авторизации Настройки: - myproject/settings.py: Обновлены конфигурации - .gitignore: Добавлен для игнорирования служебных файлов - requirements.txt: Удален (вероятно заменен на poetry/pipenv) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
79
myproject/.gitignore
vendored
Normal file
79
myproject/.gitignore
vendored
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# Django
|
||||||
|
*.log
|
||||||
|
*.pot
|
||||||
|
*.pyc
|
||||||
|
__pycache__/
|
||||||
|
local_settings.py
|
||||||
|
db.sqlite3
|
||||||
|
db.sqlite3-journal
|
||||||
|
|
||||||
|
# Environment variables (contains secrets!)
|
||||||
|
.env
|
||||||
|
|
||||||
|
# Virtual environment
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
ENV/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
|
|
||||||
|
# Static and media files
|
||||||
|
/staticfiles/
|
||||||
|
/media/
|
||||||
|
|
||||||
|
# Python
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
pip-wheel-metadata/
|
||||||
|
share/python-wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
MANIFEST
|
||||||
|
|
||||||
|
# Testing
|
||||||
|
.pytest_cache/
|
||||||
|
.coverage
|
||||||
|
htmlcov/
|
||||||
|
|
||||||
|
# Migrations (раскомментируйте если не хотите коммитить миграции)
|
||||||
|
# */migrations/*.py
|
||||||
|
# !*/migrations/__init__.py
|
||||||
|
|
||||||
|
# Celery Beat schedule database (автоматически создаётся при запуске celery beat)
|
||||||
|
celerybeat-schedule
|
||||||
|
celerybeat-schedule-shm
|
||||||
|
celerybeat-schedule-wal
|
||||||
|
|
||||||
|
# Documentation files in root (сгенерированные документы)
|
||||||
|
/CELERY_SETUP_GUIDE.md
|
||||||
|
/FINAL_REPORT.md
|
||||||
|
/IMPLEMENTATION_SUMMARY.md
|
||||||
|
/MIGRATION_GUIDE.md
|
||||||
|
/QUICK_START.md
|
||||||
|
/README_CELERY.md
|
||||||
|
/start_celery.bat
|
||||||
|
/start_celery.sh
|
||||||
@@ -70,6 +70,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Алерт о необходимости возврата -->
|
||||||
|
{% if refund_amount > 0 %}
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="alert alert-warning d-flex justify-content-between align-items-center mb-4" role="alert">
|
||||||
|
<div>
|
||||||
|
<h5 class="alert-heading mb-2">
|
||||||
|
<i class="bi bi-exclamation-triangle-fill"></i> Требуется возврат средств
|
||||||
|
</h5>
|
||||||
|
<p class="mb-0">
|
||||||
|
Клиент имеет отменённые заказы с внесённой оплатой.
|
||||||
|
Общая сумма к возврату: <strong>{{ refund_amount|floatformat:2 }} руб.</strong>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span class="badge bg-warning text-dark" style="font-size: 1.2em;">
|
||||||
|
{{ refund_amount|floatformat:2 }} руб.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<!-- Операции с кошельком -->
|
<!-- Операции с кошельком -->
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
@@ -268,12 +290,13 @@
|
|||||||
<th>Сумма</th>
|
<th>Сумма</th>
|
||||||
<th>Оплачено</th>
|
<th>Оплачено</th>
|
||||||
<th>Остаток</th>
|
<th>Остаток</th>
|
||||||
|
<th>Возврат</th>
|
||||||
<th>Действия</th>
|
<th>Действия</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for order in orders_page %}
|
{% for order in orders_page %}
|
||||||
<tr>
|
<tr {% if order.status and order.status.is_negative_end and order.amount_paid > 0 %}class="table-warning"{% endif %}>
|
||||||
<td><strong>#{{ order.order_number }}</strong></td>
|
<td><strong>#{{ order.order_number }}</strong></td>
|
||||||
<td><small>{{ order.created_at|date:"d.m.Y H:i" }}</small></td>
|
<td><small>{{ order.created_at|date:"d.m.Y H:i" }}</small></td>
|
||||||
<td>
|
<td>
|
||||||
@@ -313,12 +336,22 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if order.payment_status == 'paid' %}
|
{% if order.is_paid %}
|
||||||
<span class="badge bg-success">Оплачено</span>
|
<span class="badge bg-success">
|
||||||
{% elif order.payment_status == 'partial' %}
|
<i class="bi bi-check-circle"></i> Оплачено
|
||||||
<span class="badge bg-warning">Частично</span>
|
</span>
|
||||||
|
{% elif order.status and order.status.is_negative_end and order.amount_paid > 0 %}
|
||||||
|
<span class="badge bg-warning text-dark" title="Требуется возврат: {{ order.amount_paid|floatformat:2 }} руб.">
|
||||||
|
<i class="bi bi-exclamation-triangle"></i> Возврат
|
||||||
|
</span>
|
||||||
|
{% elif order.amount_paid > 0 %}
|
||||||
|
<span class="badge bg-warning">
|
||||||
|
<i class="bi bi-exclamation-circle"></i> Частично ({{ order.amount_paid|floatformat:2 }} руб.)
|
||||||
|
</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="badge bg-danger">Не оплачено</span>
|
<span class="badge bg-danger">
|
||||||
|
<i class="bi bi-x-circle"></i> Не оплачено
|
||||||
|
</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td><strong>{{ order.total_amount|floatformat:2 }} руб.</strong></td>
|
<td><strong>{{ order.total_amount|floatformat:2 }} руб.</strong></td>
|
||||||
@@ -330,12 +363,23 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
{% if order.amount_due > 0 %}
|
{% if order.status and order.status.is_negative_end %}
|
||||||
|
<span class="text-muted">—</span>
|
||||||
|
{% elif order.amount_due > 0 %}
|
||||||
<span class="text-danger fw-bold">{{ order.amount_due|floatformat:2 }} руб.</span>
|
<span class="text-danger fw-bold">{{ order.amount_due|floatformat:2 }} руб.</span>
|
||||||
{% else %}
|
{% else %}
|
||||||
<span class="text-success">0.00 руб.</span>
|
<span class="text-success">0.00 руб.</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if order.status and order.status.is_negative_end and order.amount_paid > 0 %}
|
||||||
|
<span class="badge bg-warning text-dark" title="Требуется возврат">
|
||||||
|
<i class="bi bi-exclamation-triangle"></i> {{ order.amount_paid|floatformat:2 }} руб.
|
||||||
|
</span>
|
||||||
|
{% else %}
|
||||||
|
<span class="text-muted">—</span>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<a href="{% url 'orders:order-detail' order.order_number %}" class="btn btn-sm btn-outline-primary">
|
<a href="{% url 'orders:order-detail' order.order_number %}" class="btn btn-sm btn-outline-primary">
|
||||||
<i class="bi bi-eye"></i>
|
<i class="bi bi-eye"></i>
|
||||||
|
|||||||
@@ -87,8 +87,13 @@ def customer_detail(request, pk):
|
|||||||
if customer.is_system_customer:
|
if customer.is_system_customer:
|
||||||
return render(request, 'customers/customer_system.html')
|
return render(request, 'customers/customer_system.html')
|
||||||
|
|
||||||
# Рассчитываем общий долг по активным заказам на стороне БД
|
# Рассчитываем общий долг по заказам на стороне БД
|
||||||
total_debt_result = customer.orders.exclude(payment_status='paid').aggregate(
|
# Долг = все заказы КРОМЕ отмененных и полностью оплаченных
|
||||||
|
# ВКЛЮЧАЕТ завершенные заказы с неполной оплатой!
|
||||||
|
total_debt_result = customer.orders.exclude(
|
||||||
|
Q(status__is_negative_end=True) | # Отмененные → учитываются в refund_amount
|
||||||
|
Q(payment_status='paid') # Полностью оплаченные
|
||||||
|
).aggregate(
|
||||||
total_debt=Coalesce(
|
total_debt=Coalesce(
|
||||||
Sum(Greatest(F('total_amount') - F('amount_paid'), Value(0), output_field=DecimalField())),
|
Sum(Greatest(F('total_amount') - F('amount_paid'), Value(0), output_field=DecimalField())),
|
||||||
Value(0),
|
Value(0),
|
||||||
@@ -97,8 +102,24 @@ def customer_detail(request, pk):
|
|||||||
)
|
)
|
||||||
total_debt = total_debt_result['total_debt'] or Decimal('0')
|
total_debt = total_debt_result['total_debt'] or Decimal('0')
|
||||||
|
|
||||||
# Количество активных заказов
|
# Количество заказов с долгом (с той же логикой)
|
||||||
active_orders_count = customer.orders.exclude(payment_status='paid').count()
|
active_orders_count = customer.orders.exclude(
|
||||||
|
Q(status__is_negative_end=True) |
|
||||||
|
Q(payment_status='paid')
|
||||||
|
).count()
|
||||||
|
|
||||||
|
# Сумма к возврату (отмененные заказы с оплатой)
|
||||||
|
refund_amount_result = customer.orders.filter(
|
||||||
|
status__is_negative_end=True, # Отмененные
|
||||||
|
amount_paid__gt=0 # С оплатой
|
||||||
|
).aggregate(
|
||||||
|
total_refund=Coalesce(
|
||||||
|
Sum('amount_paid'),
|
||||||
|
Value(0),
|
||||||
|
output_field=DecimalField()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
refund_amount = refund_amount_result['total_refund'] or Decimal('0')
|
||||||
|
|
||||||
# История транзакций кошелька (последние 20)
|
# История транзакций кошелька (последние 20)
|
||||||
from .models import WalletTransaction
|
from .models import WalletTransaction
|
||||||
@@ -116,6 +137,7 @@ def customer_detail(request, pk):
|
|||||||
'customer': customer,
|
'customer': customer,
|
||||||
'total_debt': total_debt,
|
'total_debt': total_debt,
|
||||||
'active_orders_count': active_orders_count,
|
'active_orders_count': active_orders_count,
|
||||||
|
'refund_amount': refund_amount,
|
||||||
'wallet_transactions': wallet_transactions,
|
'wallet_transactions': wallet_transactions,
|
||||||
'orders_page': orders_page,
|
'orders_page': orders_page,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,6 +107,18 @@ MIDDLEWARE = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# AUTHENTICATION BACKENDS
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
# Кастомный backend для связи ролей с Django permissions API
|
||||||
|
# ВАЖНО: Этот backend работает с ролями из tenant schema, НЕ трогая public schema!
|
||||||
|
AUTHENTICATION_BACKENDS = [
|
||||||
|
'user_roles.auth_backend.RoleBasedPermissionBackend', # Наш кастомный backend для ролей
|
||||||
|
'django.contrib.auth.backends.ModelBackend', # Стандартный backend (для superuser и т.д.)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# URL CONFIGURATION
|
# URL CONFIGURATION
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|||||||
@@ -329,6 +329,22 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Предупреждение о необходимости возврата -->
|
||||||
|
{% if order.status and order.status.is_negative_end and order.amount_paid > 0 %}
|
||||||
|
<hr>
|
||||||
|
<div class="alert alert-warning mb-0">
|
||||||
|
<h6 class="alert-heading">
|
||||||
|
<i class="bi bi-exclamation-triangle-fill"></i> Требуется возврат
|
||||||
|
</h6>
|
||||||
|
<p class="mb-0">
|
||||||
|
Заказ отменён, но клиент внёс оплату: <strong>{{ order.amount_paid|floatformat:2 }} руб.</strong>
|
||||||
|
</p>
|
||||||
|
<small class="text-muted d-block mt-2">
|
||||||
|
<i class="bi bi-info-circle"></i> Создайте возврат через раздел "История транзакций" ниже
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -153,6 +153,10 @@
|
|||||||
<span class="badge bg-success">
|
<span class="badge bg-success">
|
||||||
<i class="bi bi-check-circle"></i> Оплачен
|
<i class="bi bi-check-circle"></i> Оплачен
|
||||||
</span>
|
</span>
|
||||||
|
{% elif order.status and order.status.is_negative_end and order.amount_paid > 0 %}
|
||||||
|
<span class="badge bg-warning text-dark" title="Требуется возврат: {{ order.amount_paid }} руб.">
|
||||||
|
<i class="bi bi-exclamation-triangle"></i> Возврат
|
||||||
|
</span>
|
||||||
{% elif order.amount_paid > 0 %}
|
{% elif order.amount_paid > 0 %}
|
||||||
<span class="badge bg-warning">
|
<span class="badge bg-warning">
|
||||||
<i class="bi bi-exclamation-circle"></i> Частично ({{ order.amount_paid }} руб.)
|
<i class="bi bi-exclamation-circle"></i> Частично ({{ order.amount_paid }} руб.)
|
||||||
|
|||||||
@@ -366,6 +366,7 @@ class CombinedProductListView(LoginRequiredMixin, ManagerOwnerRequiredMixin, Lis
|
|||||||
context['item_statuses'] = item_statuses
|
context['item_statuses'] = item_statuses
|
||||||
|
|
||||||
# Кнопки действий
|
# Кнопки действий
|
||||||
|
# Проверяем права через has_perm, который использует наш RoleBasedPermissionBackend
|
||||||
action_buttons = []
|
action_buttons = []
|
||||||
|
|
||||||
if self.request.user.has_perm('products.add_product'):
|
if self.request.user.has_perm('products.add_product'):
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
amqp==5.3.1
|
|
||||||
asgiref==3.9.0
|
|
||||||
billiard==4.2.2
|
|
||||||
celery==5.4.0
|
|
||||||
click==8.3.0
|
|
||||||
click-didyoumean==0.3.1
|
|
||||||
click-plugins==1.1.1.2
|
|
||||||
click-repl==0.3.0
|
|
||||||
colorama==0.4.6
|
|
||||||
Django==5.0.10
|
|
||||||
django-celery-results==2.5.1
|
|
||||||
django-environ==0.12.0
|
|
||||||
django-filter==24.3
|
|
||||||
django-nested-admin==4.1.5
|
|
||||||
django-phonenumber-field==8.3.0
|
|
||||||
django-simple-history==3.10.1
|
|
||||||
django-tenants==3.7.0
|
|
||||||
kombu==5.6.0
|
|
||||||
packaging==25.0
|
|
||||||
phonenumbers==9.0.17
|
|
||||||
pillow>=12.0.0
|
|
||||||
pillow-heif>=0.15.0
|
|
||||||
prompt_toolkit==3.0.52
|
|
||||||
psycopg2-binary==2.9.11
|
|
||||||
python-dateutil==2.9.0.post0
|
|
||||||
python-monkey-business==1.1.0
|
|
||||||
redis==5.0.8
|
|
||||||
six==1.17.0
|
|
||||||
sqlparse==0.5.3
|
|
||||||
typing_extensions==4.15.0
|
|
||||||
tzdata==2025.2
|
|
||||||
Unidecode==1.4.0
|
|
||||||
vine==5.1.0
|
|
||||||
wcwidth==0.2.14
|
|
||||||
142
myproject/user_roles/auth_backend.py
Normal file
142
myproject/user_roles/auth_backend.py
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
"""
|
||||||
|
Кастомный backend аутентификации для связывания ролей с Django permissions API.
|
||||||
|
|
||||||
|
ВАЖНО: Этот backend НЕ использует таблицы Django permissions из public schema!
|
||||||
|
Он только эмулирует API has_perm(), читая роли из текущей tenant schema.
|
||||||
|
Это безопасно для мультитенантной архитектуры.
|
||||||
|
"""
|
||||||
|
from django.contrib.auth.backends import ModelBackend
|
||||||
|
from user_roles.services import RoleService
|
||||||
|
from user_roles.models import Role
|
||||||
|
|
||||||
|
|
||||||
|
class RoleBasedPermissionBackend(ModelBackend):
|
||||||
|
"""
|
||||||
|
Backend, который предоставляет права на основе роли пользователя в текущем тенанте.
|
||||||
|
|
||||||
|
Расширяет стандартный ModelBackend, добавляя проверку прав на основе ролей из tenant schema.
|
||||||
|
|
||||||
|
Как это работает:
|
||||||
|
1. Django вызывает user.has_perm('products.add_product')
|
||||||
|
2. Backend проверяет роль пользователя в ТЕКУЩЕЙ tenant schema (через RoleService)
|
||||||
|
3. На основе роли возвращает True/False
|
||||||
|
4. Никакие данные из public schema не используются!
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Маппинг ролей на наборы разрешений
|
||||||
|
# Формат: 'app_label': ['action1', 'action2', ...]
|
||||||
|
# где action - это префикс permission: add_product -> add, change_order -> change
|
||||||
|
ROLE_PERMISSIONS = {
|
||||||
|
Role.OWNER: {
|
||||||
|
# Владелец: полный доступ ко всем модулям
|
||||||
|
'products': ['add', 'change', 'delete', 'view'],
|
||||||
|
'inventory': ['add', 'change', 'delete', 'view'],
|
||||||
|
'orders': ['add', 'change', 'delete', 'view'],
|
||||||
|
'clients': ['add', 'change', 'delete', 'view'],
|
||||||
|
'suppliers': ['add', 'change', 'delete', 'view'],
|
||||||
|
'user_roles': ['add', 'change', 'delete', 'view'],
|
||||||
|
'payments': ['add', 'change', 'delete', 'view'],
|
||||||
|
},
|
||||||
|
Role.MANAGER: {
|
||||||
|
# Менеджер: доступ к основным операционным модулям (без настроек)
|
||||||
|
'products': ['add', 'change', 'delete', 'view'],
|
||||||
|
'inventory': ['add', 'change', 'delete', 'view'],
|
||||||
|
'orders': ['add', 'change', 'delete', 'view'],
|
||||||
|
'clients': ['add', 'change', 'delete', 'view'],
|
||||||
|
'suppliers': ['add', 'change', 'view'], # только просмотр и изменение, без удаления
|
||||||
|
'payments': ['view'],
|
||||||
|
},
|
||||||
|
Role.FLORIST: {
|
||||||
|
# Флорист: работа с заказами и просмотр товаров
|
||||||
|
'products': ['view'],
|
||||||
|
'inventory': ['view', 'change'], # может обновлять остатки при сборке заказов
|
||||||
|
'orders': ['change', 'view'],
|
||||||
|
},
|
||||||
|
Role.COURIER: {
|
||||||
|
# Курьер: только просмотр и обновление статуса заказов
|
||||||
|
'orders': ['change', 'view'],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def has_perm(self, user_obj, perm, obj=None):
|
||||||
|
"""
|
||||||
|
Проверяет, имеет ли пользователь указанное разрешение.
|
||||||
|
|
||||||
|
Формат разрешения: 'app_label.action_modelname'
|
||||||
|
Например: 'products.add_product', 'orders.change_order'
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_obj: Пользователь
|
||||||
|
perm: Строка разрешения в формате 'app.action_model'
|
||||||
|
obj: Опциональный объект для object-level permissions
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True если пользователь имеет разрешение
|
||||||
|
"""
|
||||||
|
# Сначала проверяем стандартные permissions через ModelBackend
|
||||||
|
# (для superuser, staff с Django permissions и т.д.)
|
||||||
|
if super().has_perm(user_obj, perm, obj):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Если пользователь не аутентифицирован, нет доступа
|
||||||
|
if not user_obj.is_authenticated:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Суперпользователь имеет все права
|
||||||
|
if user_obj.is_superuser:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Получаем роль пользователя в текущем тенанте
|
||||||
|
# ВАЖНО: RoleService работает с текущей tenant schema!
|
||||||
|
user_role = RoleService.get_user_role(user_obj)
|
||||||
|
if not user_role:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Парсим разрешение: 'app_label.action_modelname'
|
||||||
|
# Например: 'products.add_product' -> app_label='products', action='add'
|
||||||
|
try:
|
||||||
|
app_label, codename = perm.split('.')
|
||||||
|
# Извлекаем действие из codename (add_product -> add, change_order -> change)
|
||||||
|
action = codename.split('_')[0]
|
||||||
|
except (ValueError, IndexError):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Проверяем, есть ли у роли это разрешение
|
||||||
|
role_perms = self.ROLE_PERMISSIONS.get(user_role.code, {})
|
||||||
|
app_perms = role_perms.get(app_label, [])
|
||||||
|
|
||||||
|
return action in app_perms
|
||||||
|
|
||||||
|
def has_module_perms(self, user_obj, app_label):
|
||||||
|
"""
|
||||||
|
Проверяет, имеет ли пользователь какие-либо разрешения для приложения.
|
||||||
|
|
||||||
|
Используется в Django admin для определения, показывать ли модуль в навигации.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_obj: Пользователь
|
||||||
|
app_label: Название приложения (например, 'products', 'orders')
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True если пользователь имеет хотя бы одно разрешение для приложения
|
||||||
|
"""
|
||||||
|
# Сначала проверяем стандартные permissions через ModelBackend
|
||||||
|
if super().has_module_perms(user_obj, app_label):
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Если пользователь не аутентифицирован, нет доступа
|
||||||
|
if not user_obj.is_authenticated:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Суперпользователь имеет все права
|
||||||
|
if user_obj.is_superuser:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# Получаем роль пользователя в текущем тенанте
|
||||||
|
user_role = RoleService.get_user_role(user_obj)
|
||||||
|
if not user_role:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Проверяем, есть ли у роли какие-либо разрешения для этого приложения
|
||||||
|
role_perms = self.ROLE_PERMISSIONS.get(user_role.code, {})
|
||||||
|
return app_label in role_perms and len(role_perms[app_label]) > 0
|
||||||
Reference in New Issue
Block a user