From b787df45f68e756a752c0050881c47122390e7b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B3=D0=BE=D1=80=D1=8C=20=D0=9D=D0=B0=D0=B4=D0=B5?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Wed, 10 Dec 2025 16:37:14 +0300 Subject: [PATCH] =?UTF-8?q?refactor:=20=D1=81=D0=B8=D0=BD=D1=85=D1=80?= =?UTF-8?q?=D0=BE=D0=BD=D0=B8=D0=B7=D0=B8=D1=80=D0=BE=D0=B2=D0=B0=D0=BD?= =?UTF-8?q?=D1=8B=20=D1=81=D1=82=D0=B0=D1=82=D1=83=D1=81=D0=BD=D1=8B=D0=B5?= =?UTF-8?q?=20=D0=B1=D0=B5=D0=B9=D0=B4=D0=B6=D0=B8=20=D0=B2=D0=BE=20=D0=B2?= =?UTF-8?q?=D1=81=D0=B5=D1=85=20=D1=88=D0=B0=D0=B1=D0=BB=D0=BE=D0=BD=D0=B0?= =?UTF-8?q?=D1=85=20=D0=B7=D0=B0=D0=BA=D1=83=D0=BF=D0=BE=D0=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Обновлены иконки статусов в dashboard.html и request_list.html - Все статусы теперь используют одинаковые иконки и стили - Добавлены функции getStatusBadgeHTML() для динамического рендеринга - Исправлена синтаксическая ошибка в loadRequests() - Статусы: draft, pending, approved, rejected, in_progress, completed, cancelled --- backend/api/v1/procurement/__init__.py | 1 + .../{ => api/v1}/procurement/permissions.py | 4 +- .../{ => api/v1}/procurement/serializers.py | 23 +- backend/{ => api/v1}/procurement/urls.py | 5 +- backend/{ => api/v1}/procurement/views.py | 96 ++-- backend/api/v1/urls.py | 5 + backend/eusrr_backend/urls.py | 6 +- backend/procurement/urls_frontend.py | 12 +- backend/procurement/views_frontend.py | 68 ++- backend/static/css/components/procurement.css | 4 +- backend/templates/includes/sidebar.html | 4 +- backend/templates/procurement/_filters.html | 12 + .../templates/procurement/_status_badge.html | 10 +- .../templates/procurement/budget_form.html | 6 +- .../templates/procurement/budget_list.html | 4 +- backend/templates/procurement/dashboard.html | 256 +++++----- .../procurement/equipment_detail.html | 49 +- .../templates/procurement/equipment_list.html | 470 ++++++++++++------ .../includes/equipment_create_modal.html | 4 +- .../includes/equipment_create_modal_js.html | 6 +- .../templates/procurement/request_detail.html | 211 +++++--- .../templates/procurement/request_list.html | 355 +++++++++---- 22 files changed, 1094 insertions(+), 517 deletions(-) create mode 100644 backend/api/v1/procurement/__init__.py rename backend/{ => api/v1}/procurement/permissions.py (99%) rename backend/{ => api/v1}/procurement/serializers.py (97%) rename backend/{ => api/v1}/procurement/urls.py (92%) rename backend/{ => api/v1}/procurement/views.py (94%) diff --git a/backend/api/v1/procurement/__init__.py b/backend/api/v1/procurement/__init__.py new file mode 100644 index 00000000..84a1e3b2 --- /dev/null +++ b/backend/api/v1/procurement/__init__.py @@ -0,0 +1 @@ +# API модуля закупок diff --git a/backend/procurement/permissions.py b/backend/api/v1/procurement/permissions.py similarity index 99% rename from backend/procurement/permissions.py rename to backend/api/v1/procurement/permissions.py index 8c6571dd..03bad9d1 100644 --- a/backend/procurement/permissions.py +++ b/backend/api/v1/procurement/permissions.py @@ -6,7 +6,7 @@ from rest_framework import permissions from api.v1.permissions import has_dept_perm from employees.constants import DeptPerm -from employees.models import Department, Employee, EmployeeDepartment +from employees.models import Department, EmployeeDepartment class IsDepartmentHead(permissions.BasePermission): @@ -56,6 +56,8 @@ class CanCreateProcurementRequest(permissions.BasePermission): """Проверка права на создание заявки на закупку.""" def has_permission(self, request, view): + from employees.models import Employee + if not request.user or not request.user.is_authenticated: return False diff --git a/backend/procurement/serializers.py b/backend/api/v1/procurement/serializers.py similarity index 97% rename from backend/procurement/serializers.py rename to backend/api/v1/procurement/serializers.py index 71b16772..49592dbb 100644 --- a/backend/procurement/serializers.py +++ b/backend/api/v1/procurement/serializers.py @@ -4,7 +4,7 @@ from rest_framework import serializers -from .models import ( +from procurement.models import ( Approval, Budget, Equipment, @@ -344,6 +344,11 @@ class EquipmentListSerializer(serializers.ModelSerializer): source='category.name', read_only=True ) + category_icon = serializers.CharField( + source='category.icon', + read_only=True, + default='bi-box-seam' + ) department_name = serializers.CharField( source='department.name', read_only=True @@ -366,6 +371,7 @@ class EquipmentListSerializer(serializers.ModelSerializer): 'inventory_number', 'category', 'category_name', + 'category_icon', 'status', 'status_display', 'department', @@ -380,6 +386,7 @@ class EquipmentListSerializer(serializers.ModelSerializer): 'id', 'inventory_number', # Генерируется автоматически 'category_name', + 'category_icon', 'department_name', 'responsible_name', 'status_display', @@ -444,13 +451,15 @@ class EquipmentListSerializer(serializers.ModelSerializer): ).select_related('department', 'role') for link in user_dept_links: - if has_dept_perm(user, link.department_id, DeptPerm.MANAGE_EQUIPMENT): + if has_dept_perm( + user, link.department_id, DeptPerm.MANAGE_EQUIPMENT + ): return 'scoped' return None def _get_user_allowed_departments(self, user, perm_level): - """Возвращает отделы, в которых пользователь может создать оборудование.""" + """Возвращает отделы, где пользователь может создать оборудование.""" from employees.models import Department, EmployeeDepartment if perm_level == 'full': @@ -507,7 +516,9 @@ class EquipmentListSerializer(serializers.ModelSerializer): elif perm_level == 'dept_head': # Начальник — только свои отделы - allowed_depts = self._get_user_allowed_departments(user, perm_level) + allowed_depts = self._get_user_allowed_departments( + user, perm_level + ) allowed_ids = [d.id for d in allowed_depts] if department and department.id not in allowed_ids: @@ -528,7 +539,9 @@ class EquipmentListSerializer(serializers.ModelSerializer): elif perm_level == 'scoped': # Скоуп-право — только свой отдел, ответственный = начальник - allowed_depts = self._get_user_allowed_departments(user, perm_level) + allowed_depts = self._get_user_allowed_departments( + user, perm_level + ) allowed_ids = [d.id for d in allowed_depts] if department and department.id not in allowed_ids: diff --git a/backend/procurement/urls.py b/backend/api/v1/procurement/urls.py similarity index 92% rename from backend/procurement/urls.py rename to backend/api/v1/procurement/urls.py index fbaf1097..73b5c11f 100644 --- a/backend/procurement/urls.py +++ b/backend/api/v1/procurement/urls.py @@ -2,7 +2,6 @@ URL конфигурация для API модуля закупок. """ -from django.urls import include, path from rest_framework.routers import DefaultRouter from .views import ( @@ -60,6 +59,4 @@ router.register( basename='stats' ) -urlpatterns = [ - path('', include(router.urls)), -] +urlpatterns = router.urls diff --git a/backend/procurement/views.py b/backend/api/v1/procurement/views.py similarity index 94% rename from backend/procurement/views.py rename to backend/api/v1/procurement/views.py index 3c574783..4d9de8bf 100644 --- a/backend/procurement/views.py +++ b/backend/api/v1/procurement/views.py @@ -12,8 +12,8 @@ from rest_framework.decorators import action from rest_framework.response import Response from employees.models import Department -from .constants import ApprovalStatus, ProcurementStatus, EquipmentStatus -from .models import ( +from procurement.constants import ApprovalStatus, ProcurementStatus, EquipmentStatus +from procurement.models import ( Approval, Budget, Equipment, @@ -24,7 +24,7 @@ from .models import ( ProcurementRequest, Supplier, ) -from .services import QRCodeGenerator +from procurement.services import QRCodeGenerator from .permissions import ( CanApproveProcurementRequest, CanManageBudget, @@ -34,6 +34,7 @@ from .permissions import ( ) from .serializers import ( BudgetSerializer, + BudgetDetailSerializer, EquipmentCategorySerializer, EquipmentDetailSerializer, EquipmentListSerializer, @@ -84,26 +85,66 @@ class ProcurementRequestViewSet(viewsets.ModelViewSet): return [permission() for permission in permission_classes] def get_queryset(self): - """Фильтровать заявки в зависимости от роли пользователя.""" + """Фильтровать заявки в зависимости от роли пользователя и scope.""" + from datetime import timedelta + from django.utils import timezone + queryset = super().get_queryset() user = self.request.user + scope = self.request.query_params.get('scope', None) + period = self.request.query_params.get('period', None) - # Админы и пользователи с модельными правами видят все - if user.is_superuser or user.is_staff: - return queryset + # Обработка scope параметра + if scope == 'mine': + # Только мои заявки (где я автор) + queryset = queryset.filter(requestor=user) + elif scope == 'department': + # Заявки моего отдела + queryset = queryset.filter(department__in=user.departments.all()) + elif scope == 'my_work': + # Заявки, которые я взял в работу (где я исполнитель) + queryset = queryset.filter(executor=user) + elif scope == 'available': + # Доступные заявки - со статусом "Согласовано" + queryset = queryset.filter(status='approved') + elif scope == 'all': + # Все заявки - применяем стандартную логику прав + pass + else: + # Если scope не указан, применяем стандартную логику + # Админы и пользователи с модельными правами видят все + if user.is_superuser or user.is_staff: + pass + # Пользователи с любыми модельными правами на заявки видят все + elif (user.has_perm('procurement.view_procurementrequest') or + user.has_perm('procurement.change_procurementrequest') or + user.has_perm('procurement.delete_procurementrequest')): + pass + else: + # Показываем: свои заявки + заявки отдела + где я approver + queryset = queryset.filter( + Q(requestor=user) | + Q(department__in=user.departments.all()) | + Q(approvals__approver=user) + ) - # Пользователи с любыми модельными правами на заявки видят все - if (user.has_perm('procurement.view_procurementrequest') or - user.has_perm('procurement.change_procurementrequest') or - user.has_perm('procurement.delete_procurementrequest')): - return queryset + # Фильтрация по периоду + if period: + now = timezone.now() + if period == 'today': + start_date = now.replace(hour=0, minute=0, second=0, microsecond=0) + queryset = queryset.filter(created_at__gte=start_date) + elif period == 'week': + start_date = now - timedelta(days=7) + queryset = queryset.filter(created_at__gte=start_date) + elif period == 'month': + start_date = now - timedelta(days=30) + queryset = queryset.filter(created_at__gte=start_date) + elif period == 'quarter': + start_date = now - timedelta(days=90) + queryset = queryset.filter(created_at__gte=start_date) - # Показываем: свои заявки + заявки отдела + где я approver - return queryset.filter( - Q(requestor=user) | - Q(department__in=user.departments.all()) | - Q(approvals__approver=user) - ).distinct() + return queryset.distinct() def _check_budget_alert(self, procurement_request): """Проверяет бюджет и отправляет алерт если низкий остаток.""" @@ -248,7 +289,7 @@ class ProcurementRequestViewSet(viewsets.ModelViewSet): f'ожидает вашего согласования. ' f'Сумма: {procurement_request.total_cost}₽' ), - action_url=f'/procurement/requests/{procurement_request.id}/', + action_url=f'/procurement/orders/{procurement_request.id}/', send_immediately=True, ) @@ -289,7 +330,7 @@ class ProcurementRequestViewSet(viewsets.ModelViewSet): f'{request.user.get_full_name()} одобрил ' f'заявку "{procurement_request.title}".' ), - action_url=f'/procurement/requests/{procurement_request.id}/', + action_url=f'/procurement/orders/{procurement_request.id}/', send_immediately=True, ) @@ -312,7 +353,7 @@ class ProcurementRequestViewSet(viewsets.ModelViewSet): f'Ваша заявка "{procurement_request.title}" ' f'была полностью одобрена. Можно приступать к закупке.' ), - action_url=f'/procurement/requests/{procurement_request.id}/', + action_url=f'/procurement/orders/{procurement_request.id}/', send_immediately=True, ) @@ -361,7 +402,7 @@ class ProcurementRequestViewSet(viewsets.ModelViewSet): f'заявку "{procurement_request.title}". ' f'Причина: {approval.comment}' ), - action_url=f'/procurement/requests/{procurement_request.id}/', + action_url=f'/procurement/orders/{procurement_request.id}/', send_immediately=True, ) @@ -472,7 +513,7 @@ class ProcurementRequestViewSet(viewsets.ModelViewSet): f'взята в работу пользователем ' f'{request.user.get_full_name()}.' ), - action_url=f'/procurement/requests/{procurement_request.id}/', + action_url=f'/procurement/orders/{procurement_request.id}/', send_immediately=True, ) @@ -489,7 +530,7 @@ class ProcurementRequestViewSet(viewsets.ModelViewSet): f'взята в работу пользователем ' f'{request.user.get_full_name()}.' ), - action_url=f'/procurement/requests/{procurement_request.id}/', + action_url=f'/procurement/orders/{procurement_request.id}/', send_immediately=True, ) @@ -535,7 +576,7 @@ class ProcurementRequestViewSet(viewsets.ModelViewSet): f'Заявка "{procurement_request.title}" ' f'успешно завершена.' ), - action_url=f'/procurement/requests/{procurement_request.id}/', + action_url=f'/procurement/orders/{procurement_request.id}/', send_immediately=True, ) @@ -618,7 +659,7 @@ class ProcurementRequestViewSet(viewsets.ModelViewSet): f'была отменена автором. ' f'Причина: {reason or "не указана"}' ), - action_url=f'/procurement/requests/{procurement_request.id}/', + action_url=f'/procurement/orders/{procurement_request.id}/', send_immediately=True, ) @@ -860,6 +901,7 @@ class ProcurementItemViewSet(viewsets.ModelViewSet): } }, status=status.HTTP_200_OK) + class EquipmentCategoryViewSet(viewsets.ModelViewSet): """ViewSet для категорий оборудования.""" @@ -1334,7 +1376,6 @@ class BudgetViewSet(viewsets.ModelViewSet): def my_department(self, request): """Получить бюджет текущего квартала для отдела пользователя.""" from django.utils import timezone - from .serializers import BudgetDetailSerializer user = request.user now = timezone.now() @@ -1518,4 +1559,3 @@ class ProcurementStatsViewSet(viewsets.ViewSet): }) return Response(result) - diff --git a/backend/api/v1/urls.py b/backend/api/v1/urls.py index d9aeefd2..58e6e83b 100644 --- a/backend/api/v1/urls.py +++ b/backend/api/v1/urls.py @@ -159,5 +159,10 @@ urlpatterns = [ get_poll_results, name="poll_results" ), + # Procurement API + path( + "procurement/", + include("api.v1.procurement.urls", namespace="procurement") + ), path("", include(router.urls)), ] diff --git a/backend/eusrr_backend/urls.py b/backend/eusrr_backend/urls.py index 309795d7..84c2b9d1 100644 --- a/backend/eusrr_backend/urls.py +++ b/backend/eusrr_backend/urls.py @@ -15,12 +15,8 @@ urlpatterns = [ path("finance/", include("finance.urls", namespace="finance")), path( "procurement/", - include("procurement.urls_frontend", namespace="procurement") + include("procurement.urls_frontend", namespace="procurement_frontend") ), path("api/", include(("api.urls", "api"), namespace="api")), - path( - "api/procurement/", - include("procurement.urls", namespace="procurement_api") - ), path("", include("feed.urls_front", namespace="feed")), ] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) diff --git a/backend/procurement/urls_frontend.py b/backend/procurement/urls_frontend.py index 646d92d1..9f321963 100644 --- a/backend/procurement/urls_frontend.py +++ b/backend/procurement/urls_frontend.py @@ -12,16 +12,16 @@ urlpatterns = [ # Дашборд path('', views.dashboard, name='dashboard'), - # Заявки на закупку - path('requests/', views.request_list, name='request_list'), - path('requests/my/', views.my_requests, name='my_requests'), + # Заявки на закупку (orders - чтобы не путать с заявлениями requests) + path('orders/', views.request_list, name='request_list'), + path('orders/my/', views.my_requests, name='my_requests'), path( - 'requests/pending/', + 'orders/pending/', views.pending_approvals, name='pending_approvals' ), - path('requests/create/', views.request_create, name='request_create'), - path('requests//', views.request_detail, name='request_detail'), + path('orders/create/', views.request_create, name='request_create'), + path('orders//', views.request_detail, name='request_detail'), # Оборудование path('equipment/', views.equipment_list, name='equipment_list'), diff --git a/backend/procurement/views_frontend.py b/backend/procurement/views_frontend.py index abd35c07..f3d00bd6 100644 --- a/backend/procurement/views_frontend.py +++ b/backend/procurement/views_frontend.py @@ -30,13 +30,30 @@ def request_list(request): - /api/v1/departments/ — для фильтра по отделам """ scope = request.GET.get('scope', 'mine') + user = request.user + + # Читаем фильтры из GET параметров + filters = { + 'status': request.GET.get('status', ''), + 'urgency': request.GET.get('urgency', ''), + 'period': request.GET.get('period', ''), + } + + # Определяем доступные представления + can_view_department = user.departments.exists() + can_view_all = ( + user.is_staff or user.is_superuser or + user.has_perm('procurement.view_procurementrequest') or + user.has_perm('procurement.change_procurementrequest') + ) return render(request, 'procurement/request_list.html', { 'scope': scope, 'show_tabs': True, + 'can_view_department': can_view_department, + 'can_view_all': can_view_all, + 'filters': filters, }) - - @login_required def my_requests(request): """Мои заявки — редирект на request_list.""" @@ -51,24 +68,63 @@ def pending_approvals(request): Данные загружаются через API: - /api/procurement/requests/pending_approvals/ """ + user = request.user + + # Определяем доступные представления + can_view_department = user.departments.exists() + can_view_all = ( + user.is_staff or user.is_superuser or + user.has_perm('procurement.view_procurementrequest') or + user.has_perm('procurement.change_procurementrequest') + ) + return render(request, 'procurement/request_list.html', { 'scope': 'pending', 'page_title': 'На согласовании', 'show_tabs': True, + 'can_view_department': can_view_department, + 'can_view_all': can_view_all, }) +@login_required @login_required def request_detail(request, pk): """Детальная страница заявки. - Данные загружаются через API: - - /api/procurement/requests/{pk}/ - - /api/procurement/equipment/create-options/ — при создании оборудования - - /api/procurement/equipment-categories/ — категории + Загружает данные из БД и передаёт в шаблон. """ + from django.shortcuts import get_object_or_404 + from procurement.models import ProcurementRequest + + request_obj = get_object_or_404( + ProcurementRequest.objects.select_related( + 'department', 'requestor', 'executor' + ).prefetch_related('items', 'approvals__approver'), + pk=pk + ) + + user = request.user + + # Проверяем права на редактирование + can_edit = ( + request_obj.requestor == user and + request_obj.status == 'draft' + ) + + # Проверяем права на согласование + can_approve = ( + request_obj.approvals.filter( + approver=user, + status='pending' + ).exists() + ) + return render(request, 'procurement/request_detail.html', { 'request_id': pk, + 'request_obj': request_obj, + 'can_edit': can_edit, + 'can_approve': can_approve, }) diff --git a/backend/static/css/components/procurement.css b/backend/static/css/components/procurement.css index 2d1fa163..92046561 100644 --- a/backend/static/css/components/procurement.css +++ b/backend/static/css/components/procurement.css @@ -235,12 +235,12 @@ color: var(--bs-danger); } -.proc-status.ordered { +.proc-status.in_progress { background: color-mix(in srgb, var(--bs-info) 15%, transparent); color: var(--bs-info); } -.proc-status.delivered { +.proc-status.completed { background: color-mix(in srgb, var(--bs-primary) 15%, transparent); color: var(--bs-primary); } diff --git a/backend/templates/includes/sidebar.html b/backend/templates/includes/sidebar.html index f0c0037d..93296535 100644 --- a/backend/templates/includes/sidebar.html +++ b/backend/templates/includes/sidebar.html @@ -33,8 +33,8 @@ Отделы + class="nav-link {% if request.resolver_match.app_name == 'requests' %}active{% endif %}" + {% if request.resolver_match.app_name == 'requests' %}aria-current="page"{% endif %}> Заявления diff --git a/backend/templates/procurement/_filters.html b/backend/templates/procurement/_filters.html index b6007dd1..558401d5 100644 --- a/backend/templates/procurement/_filters.html +++ b/backend/templates/procurement/_filters.html @@ -54,18 +54,30 @@ const urgency = document.getElementById('filterUrgency').value; const period = document.getElementById('filterPeriod').value; + // Сохраняем scope если он есть + const scope = params.get('scope'); + if (status) params.set('status', status); else params.delete('status'); if (urgency) params.set('urgency', urgency); else params.delete('urgency'); if (period) params.set('period', period); else params.delete('period'); + // Восстанавливаем scope + if (scope) params.set('scope', scope); + window.location.search = params.toString(); }); document.getElementById('resetFilters')?.addEventListener('click', () => { const params = new URLSearchParams(window.location.search); + const scope = params.get('scope'); + params.delete('status'); params.delete('urgency'); params.delete('period'); + + // Восстанавливаем scope + if (scope) params.set('scope', scope); + window.location.search = params.toString(); }); diff --git a/backend/templates/procurement/_status_badge.html b/backend/templates/procurement/_status_badge.html index 63649fa3..ab134839 100644 --- a/backend/templates/procurement/_status_badge.html +++ b/backend/templates/procurement/_status_badge.html @@ -17,13 +17,13 @@ Отклонено -{% elif status == 'ordered' %} - +{% elif status == 'in_progress' %} + Заказано -{% elif status == 'delivered' %} - - Получено +{% elif status == 'completed' %} + + Завершено {% elif status == 'cancelled' %} diff --git a/backend/templates/procurement/budget_form.html b/backend/templates/procurement/budget_form.html index e6164f73..fb1c39e1 100644 --- a/backend/templates/procurement/budget_form.html +++ b/backend/templates/procurement/budget_form.html @@ -224,7 +224,7 @@ // === ЗАГРУЗКА БЮДЖЕТА === async function loadBudget() { try { - const resp = await fetch(`/api/procurement/budgets/${budgetId}/`); + const resp = await fetch(`/api/v1/procurement/budgets/${budgetId}/`); if (!resp.ok) { if (resp.status === 404) { showError('Бюджет не найден'); @@ -307,8 +307,8 @@ }; const url = budgetId - ? `/api/procurement/budgets/${budgetId}/` - : '/api/procurement/budgets/'; + ? `/api/v1/procurement/budgets/${budgetId}/` + : '/api/v1/procurement/budgets/'; const method = budgetId ? 'PATCH' : 'POST'; diff --git a/backend/templates/procurement/budget_list.html b/backend/templates/procurement/budget_list.html index 39273bd3..4f497a46 100644 --- a/backend/templates/procurement/budget_list.html +++ b/backend/templates/procurement/budget_list.html @@ -300,7 +300,7 @@ if (currentFilters.quarter) params.set('quarter', currentFilters.quarter); if (currentFilters.department) params.set('department', currentFilters.department); - const resp = await fetch(`/api/procurement/budgets/?${params}`); + const resp = await fetch(`/api/v1/procurement/budgets/?${params}`); if (!resp.ok) throw new Error('Ошибка загрузки'); const data = await resp.json(); @@ -515,7 +515,7 @@ } try { - const resp = await fetch('/api/procurement/budgets/create_next_quarter/', { + const resp = await fetch('/api/v1/procurement/budgets/create_next_quarter/', { method: 'POST', headers: { 'X-CSRFToken': getCsrfToken(), diff --git a/backend/templates/procurement/dashboard.html b/backend/templates/procurement/dashboard.html index 9699e168..37fdc25f 100644 --- a/backend/templates/procurement/dashboard.html +++ b/backend/templates/procurement/dashboard.html @@ -20,7 +20,7 @@
-
{{ stats.pending|default:0 }}
+
0
На согласовании
@@ -30,7 +30,7 @@
-
{{ stats.approved|default:0 }}
+
0
Одобрено
@@ -40,13 +40,7 @@
-
- {% if budget %} - {{ budget.remaining_percent|floatformat:0 }}% - {% else %} - — - {% endif %} -
+
—
Бюджет отдела
@@ -56,7 +50,7 @@
-
{{ stats.equipment|default:0 }}
+
0
Оборудование
@@ -94,8 +88,7 @@ - {% if can_approve %} -
+ - {% endif %}
- {# ===== МОЙ БЮДЖЕТ (если есть) ===== #} - {% if budget %} -
-
-
- Бюджет отдела на {{ budget.quarter }} кв. {{ budget.year }} -
-
-
-
-
-
{{ budget.allocated_amount|floatformat:0 }} ₽
-
Выделено
-
-
-
{{ budget.spent_amount|floatformat:0 }} ₽
-
Потрачено
-
-
-
{{ budget.reserved_amount|floatformat:0 }} ₽
-
Зарезервировано
-
-
-
{{ budget.available_amount|floatformat:0 }} ₽
-
Доступно
-
-
- -
- {% with percent=budget.spent_percent %} -
- {% endwith %} -
-
- - Использовано {{ budget.spent_percent|floatformat:0 }}% бюджета - -
-
-
- {% endif %} - {# ===== ПОСЛЕДНИЕ ЗАЯВКИ ===== #}
@@ -160,59 +109,11 @@ Все заявки
-
- {% if recent_requests %} -
-
-
-
Заявка
-
Отдел
-
Сумма
-
Статус
-
Дата
-
-
- - {% for request in recent_requests %} -
-
- {% if request.urgency == 'critical' %} - - {% elif request.urgency == 'high' %} - - {% elif request.urgency == 'medium' %} - - {% else %} - - {% endif %} -
- -
- -
{{ request.description|truncatechars:50 }}
-
- -
{{ request.department.name }}
- -
{{ request.estimated_cost|floatformat:0 }} ₽
- -
- {% include "procurement/_status_badge.html" with status=request.status %} -
- -
- {{ request.created_at|date:"d.m.Y" }} -
-
- {% endfor %} - {% else %} -
- -

Заявок пока нет

-
- {% endif %} +
+
+
+

Загрузка...

+
@@ -244,11 +145,8 @@
- - {% for dept in departments %} - - {% endfor %}
@@ -311,6 +209,132 @@ {% block extra_js %} -{% endblock %} +{% endblock %} \ No newline at end of file