удалил лишнего из заявок и оборудования
This commit is contained in:
parent
46e7059a40
commit
a784212ea1
@ -34,10 +34,6 @@ urlpatterns = [
|
||||
path("notifications/", include("notifications.urls", namespace="notifications")),
|
||||
path("search/", include("search.urls", namespace="search")),
|
||||
path("finance/", include("finance.urls", namespace="finance")),
|
||||
path(
|
||||
"procurement/",
|
||||
include("procurement.urls_frontend", namespace="procurement_frontend")
|
||||
),
|
||||
path("api/", include(("api.urls", "api"), namespace="api")),
|
||||
# Service Worker должен быть в корне для правильного scope
|
||||
path("sw.js", serve_service_worker, name="sw"),
|
||||
|
||||
@ -1,163 +0,0 @@
|
||||
"""
|
||||
Django Forms для модуля закупок.
|
||||
"""
|
||||
from datetime import date
|
||||
|
||||
from django import forms
|
||||
|
||||
from .models import ProcurementRequest, ProcurementItem, Budget
|
||||
|
||||
|
||||
class ProcurementRequestForm(forms.ModelForm):
|
||||
"""Форма создания/редактирования заявки на закупку."""
|
||||
|
||||
class Meta:
|
||||
model = ProcurementRequest
|
||||
fields = ['title', 'description', 'department', 'urgency']
|
||||
widgets = {
|
||||
'title': forms.TextInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'Название заявки',
|
||||
}),
|
||||
'description': forms.Textarea(attrs={
|
||||
'class': 'form-control',
|
||||
'rows': 4,
|
||||
'placeholder': 'Описание и обоснование закупки',
|
||||
}),
|
||||
'department': forms.Select(attrs={
|
||||
'class': 'form-select',
|
||||
}),
|
||||
'urgency': forms.Select(attrs={
|
||||
'class': 'form-select',
|
||||
}),
|
||||
}
|
||||
|
||||
def __init__(self, *args, user=None, **kwargs):
|
||||
"""Инициализация формы с фильтрацией отделов пользователя."""
|
||||
super().__init__(*args, **kwargs)
|
||||
if user:
|
||||
# Ограничиваем выбор отделов теми, в которых состоит юзер
|
||||
user_departments = user.departments.filter(
|
||||
employeedepartment__is_active=True
|
||||
)
|
||||
self.fields['department'].queryset = user_departments
|
||||
|
||||
# Если у пользователя один отдел - выбираем его по умолчанию
|
||||
if user_departments.count() == 1:
|
||||
self.fields['department'].initial = user_departments.first()
|
||||
|
||||
|
||||
class ProcurementItemForm(forms.ModelForm):
|
||||
"""Форма добавления позиции в заявку."""
|
||||
|
||||
class Meta:
|
||||
model = ProcurementItem
|
||||
fields = [
|
||||
'name', 'description', 'quantity', 'unit',
|
||||
'estimated_unit_price', 'supplier_info'
|
||||
]
|
||||
widgets = {
|
||||
'name': forms.TextInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'Наименование',
|
||||
}),
|
||||
'description': forms.Textarea(attrs={
|
||||
'class': 'form-control',
|
||||
'rows': 2,
|
||||
'placeholder': 'Описание позиции',
|
||||
}),
|
||||
'quantity': forms.NumberInput(attrs={
|
||||
'class': 'form-control',
|
||||
'min': 1,
|
||||
}),
|
||||
'unit': forms.TextInput(attrs={
|
||||
'class': 'form-control',
|
||||
'placeholder': 'шт.',
|
||||
}),
|
||||
'estimated_unit_price': forms.NumberInput(attrs={
|
||||
'class': 'form-control',
|
||||
'min': 0.01,
|
||||
'step': '0.01',
|
||||
}),
|
||||
'supplier_info': forms.Textarea(attrs={
|
||||
'class': 'form-control',
|
||||
'rows': 2,
|
||||
'placeholder': 'Ссылки на товар, контакты поставщика',
|
||||
}),
|
||||
}
|
||||
|
||||
|
||||
class BudgetForm(forms.ModelForm):
|
||||
"""Форма создания/редактирования бюджета отдела."""
|
||||
|
||||
QUARTER_CHOICES = [
|
||||
(1, '1 квартал'),
|
||||
(2, '2 квартал'),
|
||||
(3, '3 квартал'),
|
||||
(4, '4 квартал'),
|
||||
]
|
||||
|
||||
class Meta:
|
||||
model = Budget
|
||||
fields = ['department', 'year', 'quarter', 'allocated_amount']
|
||||
widgets = {
|
||||
'department': forms.Select(attrs={
|
||||
'class': 'form-select',
|
||||
}),
|
||||
'year': forms.NumberInput(attrs={
|
||||
'class': 'form-control',
|
||||
'min': 2020,
|
||||
'max': 2100,
|
||||
}),
|
||||
'quarter': forms.Select(attrs={
|
||||
'class': 'form-select',
|
||||
}),
|
||||
'allocated_amount': forms.NumberInput(attrs={
|
||||
'class': 'form-control',
|
||||
'min': 0,
|
||||
'step': '0.01',
|
||||
'placeholder': '0.00',
|
||||
}),
|
||||
}
|
||||
labels = {
|
||||
'department': 'Отдел',
|
||||
'year': 'Год',
|
||||
'quarter': 'Квартал',
|
||||
'allocated_amount': 'Выделенная сумма (₽)',
|
||||
}
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
"""Инициализация формы с текущим годом/кварталом."""
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Устанавливаем текущий год и квартал по умолчанию для новых бюджетов
|
||||
if not self.instance.pk:
|
||||
today = date.today()
|
||||
self.fields['year'].initial = today.year
|
||||
self.fields['quarter'].initial = (today.month - 1) // 3 + 1
|
||||
|
||||
def clean(self):
|
||||
"""Проверка уникальности бюджета."""
|
||||
cleaned_data = super().clean()
|
||||
department = cleaned_data.get('department')
|
||||
year = cleaned_data.get('year')
|
||||
quarter = cleaned_data.get('quarter')
|
||||
|
||||
if department and year and quarter:
|
||||
# Проверяем, нет ли уже такого бюджета
|
||||
existing = Budget.objects.filter(
|
||||
department=department,
|
||||
year=year,
|
||||
quarter=quarter
|
||||
)
|
||||
# Исключаем текущий объект при редактировании
|
||||
if self.instance.pk:
|
||||
existing = existing.exclude(pk=self.instance.pk)
|
||||
|
||||
if existing.exists():
|
||||
raise forms.ValidationError(
|
||||
f'Бюджет для отдела "{department}" на {year} год, '
|
||||
f'{quarter} квартал уже существует.'
|
||||
)
|
||||
|
||||
return cleaned_data
|
||||
@ -1,3 +0,0 @@
|
||||
from django.test import TestCase
|
||||
|
||||
# Create your tests here.
|
||||
@ -1,39 +0,0 @@
|
||||
"""
|
||||
URL конфигурация для HTML страниц модуля закупок (Frontend).
|
||||
"""
|
||||
|
||||
from django.urls import path
|
||||
|
||||
from . import views_frontend as views
|
||||
|
||||
app_name = 'procurement_frontend'
|
||||
|
||||
urlpatterns = [
|
||||
# Дашборд
|
||||
path('', views.dashboard, name='dashboard'),
|
||||
|
||||
# Заявки на закупку (orders - чтобы не путать с заявлениями requests)
|
||||
path('orders/', views.request_list, name='request_list'),
|
||||
path('orders/my/', views.my_requests, name='my_requests'),
|
||||
path(
|
||||
'orders/pending/',
|
||||
views.pending_approvals,
|
||||
name='pending_approvals'
|
||||
),
|
||||
path('orders/create/', views.request_create, name='request_create'),
|
||||
path('orders/<int:pk>/', views.request_detail, name='request_detail'),
|
||||
path('orders/<int:pk>/edit/', views.request_edit, name='request_edit'),
|
||||
|
||||
# Оборудование
|
||||
path('equipment/', views.equipment_list, name='equipment_list'),
|
||||
path(
|
||||
'equipment/<int:pk>/',
|
||||
views.equipment_detail,
|
||||
name='equipment_detail'
|
||||
),
|
||||
|
||||
# Бюджеты
|
||||
path('budgets/', views.budget_list, name='budget_list'),
|
||||
path('budgets/create/', views.budget_create, name='budget_create'),
|
||||
path('budgets/<int:pk>/edit/', views.budget_edit, name='budget_edit'),
|
||||
]
|
||||
@ -1,284 +0,0 @@
|
||||
"""
|
||||
HTML Views для модуля закупок (Frontend).
|
||||
|
||||
Все данные загружаются через API на клиенте.
|
||||
Views только рендерят layout и передают минимальный контекст.
|
||||
"""
|
||||
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.shortcuts import render
|
||||
|
||||
|
||||
@login_required
|
||||
def dashboard(request):
|
||||
"""Главная страница модуля закупок.
|
||||
|
||||
Данные загружаются через API:
|
||||
- /api/procurement/stats/overview/ — статистика
|
||||
- /api/procurement/budgets/my-department/ — бюджет отдела
|
||||
- /api/procurement/requests/?ordering=-created_at&page_size=5 — последние заявки
|
||||
"""
|
||||
return render(request, 'procurement/dashboard.html', {})
|
||||
|
||||
|
||||
@login_required
|
||||
def request_list(request):
|
||||
"""Список заявок на закупку.
|
||||
|
||||
Данные загружаются через API:
|
||||
- /api/procurement/requests/ — с фильтрами
|
||||
- /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."""
|
||||
from django.shortcuts import redirect
|
||||
return redirect('procurement_frontend:request_list')
|
||||
|
||||
|
||||
@login_required
|
||||
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):
|
||||
"""Детальная страница заявки.
|
||||
|
||||
Загружает данные из БД и передаёт в шаблон.
|
||||
|
||||
Права на редактирование (can_edit):
|
||||
- Админы/Staff → любая заявка в DRAFT
|
||||
- Модельные права (change_procurementrequest) → любая заявка в DRAFT
|
||||
- Автор заявки → своя заявка в DRAFT
|
||||
- Начальник отдела → заявки своего отдела в DRAFT
|
||||
|
||||
Права на согласование (can_approve):
|
||||
- Пользователи с активным approval для этой заявки
|
||||
"""
|
||||
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
|
||||
|
||||
# Проверяем права на редактирование (синхронизировано с API)
|
||||
can_edit = False
|
||||
if request_obj.is_editable: # Только DRAFT
|
||||
# Админы могут редактировать
|
||||
if user.is_superuser or user.is_staff:
|
||||
can_edit = True
|
||||
# Модельные права
|
||||
elif user.has_perm('procurement.change_procurementrequest'):
|
||||
can_edit = True
|
||||
# Автор заявки
|
||||
elif request_obj.requestor == user:
|
||||
can_edit = True
|
||||
# Начальник отдела
|
||||
elif request_obj.department.head == user:
|
||||
can_edit = True
|
||||
|
||||
# Проверяем права на согласование
|
||||
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,
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def request_create(request):
|
||||
"""Страница создания заявки.
|
||||
|
||||
Данные загружаются через API:
|
||||
- /api/v1/departments/ — список отделов
|
||||
Создание через:
|
||||
- POST /api/procurement/requests/
|
||||
"""
|
||||
return render(request, 'procurement/request_form.html', {
|
||||
'page_title': 'Новая заявка',
|
||||
'request_id': None,
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def request_edit(request, pk):
|
||||
"""Страница редактирования заявки.
|
||||
|
||||
Данные загружаются через API:
|
||||
- /api/v1/procurement/requests/{pk}/ — данные заявки
|
||||
- /api/v1/departments/ — список отделов
|
||||
Обновление через:
|
||||
- PATCH /api/v1/procurement/requests/{pk}/
|
||||
"""
|
||||
return render(request, 'procurement/request_form.html', {
|
||||
'page_title': 'Редактирование заявки',
|
||||
'request_id': pk,
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def equipment_list(request):
|
||||
"""Список оборудования.
|
||||
|
||||
Данные загружаются через API:
|
||||
- /api/procurement/equipment/ — список с фильтрами
|
||||
- /api/procurement/equipment-categories/ — категории для фильтра
|
||||
- /api/v1/departments/ — отделы для фильтра
|
||||
- /api/procurement/equipment/create-options/ — при создании
|
||||
"""
|
||||
return render(request, 'procurement/equipment_list.html', {})
|
||||
|
||||
|
||||
@login_required
|
||||
def equipment_detail(request, pk):
|
||||
"""Детальная страница оборудования.
|
||||
|
||||
Загружает данные из БД и передаёт в шаблон.
|
||||
|
||||
Права на управление (can_manage):
|
||||
- Админы/Staff → любое оборудование
|
||||
- Начальник отдела → оборудование своего отдела
|
||||
- Ответственный за оборудование → своё оборудование
|
||||
"""
|
||||
from django.shortcuts import get_object_or_404
|
||||
from procurement.models import Equipment
|
||||
|
||||
equipment = get_object_or_404(
|
||||
Equipment.objects.select_related(
|
||||
'department',
|
||||
'department__head',
|
||||
'category',
|
||||
'responsible_person',
|
||||
'procurement_item',
|
||||
'procurement_item__request'
|
||||
).prefetch_related(
|
||||
'maintenance_history',
|
||||
'transfer_logs__from_department',
|
||||
'transfer_logs__to_department',
|
||||
'transfer_logs__from_person',
|
||||
'transfer_logs__to_person'
|
||||
),
|
||||
pk=pk
|
||||
)
|
||||
|
||||
user = request.user
|
||||
|
||||
# Проверяем права на управление
|
||||
can_manage = False
|
||||
if user.is_superuser or user.is_staff:
|
||||
can_manage = True
|
||||
elif equipment.department and equipment.department.head == user:
|
||||
can_manage = True
|
||||
elif equipment.responsible_person == user:
|
||||
can_manage = True
|
||||
|
||||
return render(request, 'procurement/equipment_detail.html', {
|
||||
'equipment': equipment,
|
||||
'can_manage': can_manage,
|
||||
})
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# БЮДЖЕТЫ
|
||||
# =============================================================================
|
||||
|
||||
@login_required
|
||||
def budget_list(request):
|
||||
"""Список бюджетов.
|
||||
|
||||
Данные загружаются через API:
|
||||
- /api/procurement/budgets/ — список с фильтрами
|
||||
- /api/v1/departments/ — отделы для фильтра
|
||||
"""
|
||||
return render(request, 'procurement/budget_list.html', {})
|
||||
|
||||
|
||||
@login_required
|
||||
def budget_create(request):
|
||||
"""Создание бюджета.
|
||||
|
||||
Данные загружаются через API:
|
||||
- /api/v1/departments/ — список отделов
|
||||
Создание через:
|
||||
- POST /api/procurement/budgets/
|
||||
"""
|
||||
return render(request, 'procurement/budget_form.html', {
|
||||
'budget_id': None,
|
||||
})
|
||||
|
||||
|
||||
@login_required
|
||||
def budget_edit(request, pk):
|
||||
"""Редактирование бюджета.
|
||||
|
||||
Данные загружаются через API:
|
||||
- /api/procurement/budgets/{pk}/ — текущие данные
|
||||
- /api/v1/departments/ — список отделов
|
||||
Обновление через:
|
||||
- PATCH /api/procurement/budgets/{pk}/
|
||||
"""
|
||||
return render(request, 'procurement/budget_form.html', {
|
||||
'budget_id': pk,
|
||||
})
|
||||
Loading…
x
Reference in New Issue
Block a user